From 5ce7cce6a2bf680cc90a810e696b8f5e287b82e6 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 14 Aug 2020 10:42:25 +0200 Subject: [PATCH 1/2] First release 0.0.1 --- .idea/codeStyles/Project.xml | 116 ++ .idea/jarRepositories.xml | 30 + .idea/misc.xml | 9 + .idea/runConfigurations.xml | 12 + CHANGES.md | 9 + README.md | 25 +- build.gradle | 48 + gradle.properties | 23 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 55190 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 172 ++ gradlew.bat | 84 + matrix-sdk-android/.gitignore | 1 + matrix-sdk-android/build.gradle | 209 +++ matrix-sdk-android/lint.xml | 33 + matrix-sdk-android/proguard-rules.pro | 82 + .../matrix/android/sdk/InstrumentedTest.kt | 37 + .../android/sdk/LiveDataTestObserver.java | 211 +++ .../android/sdk/MainThreadExecutor.java | 32 + .../sdk/OkReplayRuleChainNoActivity.kt | 32 + .../sdk/SingleThreadCoroutineDispatcher.kt | 25 + .../sdk/account/AccountCreationTest.kt | 61 + .../android/sdk/account/ChangePasswordTest.kt | 60 + .../sdk/account/DeactivateAccountTest.kt | 88 + .../java/org/matrix/android/sdk/api/Matrix.kt | 104 ++ .../android/sdk/common/CommonTestHelper.kt | 383 +++++ .../android/sdk/common/CryptoTestData.kt | 31 + .../android/sdk/common/CryptoTestHelper.kt | 424 +++++ .../sdk/common/MockOkHttpInterceptor.kt | 90 + .../android/sdk/common/SessionTestParams.kt | 19 + .../android/sdk/common/TestAssertUtil.kt | 75 + .../android/sdk/common/TestConstants.kt | 44 + .../android/sdk/common/TestMatrixCallback.kt | 48 + .../android/sdk/common/TestMatrixComponent.kt | 38 + .../matrix/android/sdk/common/TestModule.kt | 27 + .../android/sdk/common/TestNetworkModule.kt | 39 + .../crypto/AttachmentEncryptionTest.kt | 148 ++ .../sdk/internal/crypto/CryptoStoreHelper.kt | 47 + .../sdk/internal/crypto/CryptoStoreTest.kt | 130 ++ .../internal/crypto/ExportEncryptionTest.kt | 208 +++ .../sdk/internal/crypto/UnwedgingTest.kt | 247 +++ .../crypto/crosssigning/ExtensionsKtTest.kt | 38 + .../crypto/crosssigning/XSigningTest.kt | 161 ++ .../crypto/gossiping/KeyShareTests.kt | 299 ++++ .../crypto/gossiping/WithHeldTests.kt | 245 +++ .../keysbackup/KeysBackupPasswordTest.kt | 180 ++ .../keysbackup/KeysBackupScenarioData.kt | 35 + .../crypto/keysbackup/KeysBackupTest.kt | 1099 ++++++++++++ .../keysbackup/KeysBackupTestConstants.kt | 24 + .../crypto/keysbackup/KeysBackupTestHelper.kt | 182 ++ .../keysbackup/PrepareKeysBackupDataResult.kt | 22 + .../crypto/keysbackup/StateObserver.kt | 104 ++ .../sdk/internal/crypto/ssss/QuadSTests.kt | 363 ++++ .../internal/crypto/verification/SASTest.kt | 629 +++++++ .../crypto/verification/qrcode/HexParser.kt | 35 + .../crypto/verification/qrcode/QrCodeTest.kt | 249 +++ .../verification/qrcode/SharedSecretTest.kt | 46 + .../verification/qrcode/VerificationTest.kt | 232 +++ .../session/room/send/MarkdownParserTest.kt | 278 +++ .../internal/util/JsonCanonicalizerTest.kt | 153 ++ .../session/room/timeline/ChunkEntityTest.kt | 154 ++ .../timeline/FakeGetContextOfEventTask.kt | 35 + .../room/timeline/FakePaginationTask.kt | 31 + .../room/timeline/FakeTokenChunkEvent.kt | 26 + .../session/room/timeline/RoomDataHelper.kt | 69 + .../TimelineBackToPreviousLastForwardTest.kt | 183 ++ .../timeline/TimelineForwardPaginationTest.kt | 190 +++ .../TimelinePreviousLastForwardTest.kt | 241 +++ .../sdk/session/room/timeline/TimelineTest.kt | 84 + .../sdk/internal/database/RealmDebugTools.kt | 68 + .../interceptors/CurlLoggingInterceptor.kt | 108 ++ .../interceptors/FormattedJsonHttpLogger.kt | 74 + .../src/main/AndroidManifest.xml | 26 + .../src/main/assets/postMessageAPI.js | 54 + .../java/org/matrix/android/sdk/api/Matrix.kt | 105 ++ .../matrix/android/sdk/api/MatrixCallback.kt | 46 + .../android/sdk/api/MatrixConfiguration.kt | 48 + .../matrix/android/sdk/api/MatrixPatterns.kt | 150 ++ .../sdk/api/auth/AuthenticationService.kt | 105 ++ .../matrix/android/sdk/api/auth/Constants.kt | 38 + .../android/sdk/api/auth/data/Credentials.kt | 64 + .../sdk/api/auth/data/DiscoveryInformation.kt | 41 + .../auth/data/HomeServerConnectionConfig.kt | 251 +++ .../sdk/api/auth/data/LoginFlowResult.kt | 29 + .../sdk/api/auth/data/LoginFlowTypes.kt | 32 + .../sdk/api/auth/data/SessionParams.kt | 69 + .../android/sdk/api/auth/data/WellKnown.kt | 68 + .../sdk/api/auth/data/WellKnownBaseConfig.kt | 35 + .../android/sdk/api/auth/login/LoginWizard.kt | 55 + .../api/auth/registration/RegisterThreePid.kt | 23 + .../auth/registration/RegistrationResult.kt | 31 + .../auth/registration/RegistrationWizard.kt | 47 + .../sdk/api/auth/registration/Stage.kt | 42 + .../sdk/api/auth/wellknown/WellknownResult.kt | 56 + .../api/comparators/DatedObjectComparators.kt | 41 + .../matrix/android/sdk/api/crypto/Emojis.kt | 28 + .../android/sdk/api/crypto/MXCryptoConfig.kt | 36 + .../api/crypto/RoomEncryptionTrustLevel.kt | 32 + .../android/sdk/api/extensions/Booleans.kt | 22 + .../sdk/api/extensions/MatrixSdkExtensions.kt | 37 + .../android/sdk/api/extensions/Strings.kt | 25 + .../matrix/android/sdk/api/extensions/Try.kt | 31 + .../android/sdk/api/failure/Extensions.kt | 60 + .../matrix/android/sdk/api/failure/Failure.kt | 50 + .../android/sdk/api/failure/GlobalError.kt | 27 + .../android/sdk/api/failure/MatrixError.kt | 144 ++ .../android/sdk/api/interfaces/DatedObject.kt | 26 + .../sdk/api/legacy/LegacySessionImporter.kt | 27 + .../sdk/api/listeners/ProgressListener.kt | 30 + .../sdk/api/listeners/StepProgressListener.kt | 35 + .../sdk/api/permalinks/MatrixLinkify.kt | 62 + .../sdk/api/permalinks/MatrixPermalinkSpan.kt | 39 + .../sdk/api/permalinks/PermalinkData.kt | 35 + .../sdk/api/permalinks/PermalinkFactory.kt | 101 ++ .../sdk/api/permalinks/PermalinkParser.kt | 81 + .../android/sdk/api/pushrules/Action.kt | 129 ++ .../android/sdk/api/pushrules/Condition.kt | 49 + .../sdk/api/pushrules/ConditionResolver.kt | 37 + .../pushrules/ContainsDisplayNameCondition.kt | 75 + .../sdk/api/pushrules/EventMatchCondition.kt | 104 ++ .../sdk/api/pushrules/PushRuleService.kt | 54 + .../api/pushrules/RoomMemberCountCondition.kt | 74 + .../android/sdk/api/pushrules/RuleIds.kt | 48 + .../android/sdk/api/pushrules/RuleScope.kt | 21 + .../android/sdk/api/pushrules/RuleSetKey.kt | 34 + .../SenderNotificationPermissionCondition.kt | 45 + .../pushrules/rest/GetPushRulesResponse.kt | 39 + .../sdk/api/pushrules/rest/PushCondition.kt | 96 ++ .../sdk/api/pushrules/rest/PushRule.kt | 176 ++ .../android/sdk/api/pushrules/rest/RuleSet.kt | 82 + .../android/sdk/api/query/QueryStringValue.kt | 36 + .../api/session/InitialSyncProgressService.kt | 33 + .../matrix/android/sdk/api/session/Session.kt | 230 +++ .../sdk/api/session/account/AccountService.kt | 51 + .../session/accountdata/AccountDataService.kt | 53 + .../accountdata/UserAccountDataEvent.kt | 32 + .../accountdata/UserAccountDataTypes.kt | 31 + .../sdk/api/session/cache/CacheService.kt | 31 + .../api/session/call/CallSignalingService.kt | 37 + .../android/sdk/api/session/call/CallState.kt | 45 + .../sdk/api/session/call/CallsListener.kt | 44 + .../android/sdk/api/session/call/EglUtils.kt | 56 + .../android/sdk/api/session/call/MxCall.kt | 76 + .../api/session/call/TurnServerResponse.kt | 48 + .../session/content/ContentAttachmentData.kt | 47 + .../content/ContentUploadStateTracker.kt | 41 + .../api/session/content/ContentUrlResolver.kt | 53 + .../sdk/api/session/crypto/CryptoService.kt | 154 ++ .../sdk/api/session/crypto/MXCryptoError.kt | 95 ++ .../crosssigning/CrossSigningService.kt | 86 + .../CrossSigningSsssSecretConstants.kt | 26 + .../crypto/crosssigning/MXCrossSigningInfo.kt | 39 + .../crypto/keysbackup/KeysBackupService.kt | 223 +++ .../crypto/keysbackup/KeysBackupState.kt | 76 + .../keysbackup/KeysBackupStateListener.kt | 27 + .../keyshare/GossipingRequestListener.kt | 47 + .../session/crypto/verification/CancelCode.kt | 37 + .../verification/EmojiRepresentation.kt | 26 + .../IncomingSasVerificationTransaction.kt | 35 + .../OutgoingSasVerificationTransaction.kt | 33 + .../PendingVerificationRequest.kt | 81 + .../QrCodeVerificationTransaction.kt | 41 + .../session/crypto/verification/SasMode.kt | 23 + .../SasVerificationTransaction.kt | 37 + .../ValidVerificationInfoReady.kt | 24 + .../ValidVerificationInfoRequest.kt | 25 + .../crypto/verification/VerificationMethod.kt | 30 + .../verification/VerificationService.kt | 145 ++ .../verification/VerificationTransaction.kt | 39 + .../verification/VerificationTxState.kt | 56 + .../events/model/AggregatedAnnotation.kt | 43 + .../events/model/AggregatedRelations.kt | 54 + .../model/DefaultUnsignedRelationInfo.kt | 27 + .../sdk/api/session/events/model/Event.kt | 260 +++ .../sdk/api/session/events/model/EventType.kt | 96 ++ .../sdk/api/session/events/model/LocalEcho.kt | 29 + .../session/events/model/RelationChunkInfo.kt | 36 + .../api/session/events/model/RelationType.kt | 31 + .../api/session/events/model/UnsignedData.kt | 49 + .../events/model/UnsignedRelationInfo.kt | 22 + .../file/ContentDownloadStateTracker.kt | 36 + .../sdk/api/session/file/FileService.kt | 90 + .../api/session/file/MatrixSDKFileProvider.kt | 31 + .../android/sdk/api/session/group/Group.kt | 36 + .../sdk/api/session/group/GroupService.kt | 53 + .../session/group/GroupSummaryQueryParams.kt | 45 + .../api/session/group/model/GroupSummary.kt | 34 + .../homeserver/HomeServerCapabilities.kt | 46 + .../HomeServerCapabilitiesService.kt | 29 + .../sdk/api/session/identity/FoundThreePid.kt | 23 + .../api/session/identity/IdentityService.kt | 110 ++ .../session/identity/IdentityServiceError.kt | 30 + .../identity/IdentityServiceListener.kt | 22 + .../sdk/api/session/identity/SharedState.kt | 24 + .../sdk/api/session/identity/ThreePid.kt | 41 + .../IntegrationManagerConfig.kt | 46 + .../IntegrationManagerService.kt | 117 ++ .../sdk/api/session/profile/ProfileService.kt | 86 + .../android/sdk/api/session/pushers/Pusher.kt | 43 + .../sdk/api/session/pushers/PushersService.kt | 84 + .../android/sdk/api/session/room/Room.kt | 72 + .../api/session/room/RoomDirectoryService.kt | 43 + .../sdk/api/session/room/RoomService.kt | 118 ++ .../session/room/RoomSummaryQueryParams.kt | 52 + .../api/session/room/call/RoomCallService.kt | 28 + .../session/room/crypto/RoomCryptoService.kt | 36 + .../session/room/failure/CreateRoomFailure.kt | 25 + .../session/room/failure/JoinRoomFailure.kt | 25 + .../room/members/ChangeMembershipState.kt | 34 + .../session/room/members/MembershipService.kt | 107 ++ .../room/members/RoomMemberQueryParams.kt | 51 + .../room/model/EditAggregatedSummary.kt | 27 + .../room/model/EventAnnotationsSummary.kt | 25 + .../sdk/api/session/room/model/Invite.kt | 31 + .../sdk/api/session/room/model/Membership.kt | 63 + .../model/PollResponseAggregatedSummary.kt | 30 + .../session/room/model/PollSummaryContent.kt | 49 + .../session/room/model/PowerLevelsContent.kt | 53 + .../room/model/ReactionAggregatedSummary.kt | 27 + .../sdk/api/session/room/model/ReadReceipt.kt | 25 + .../room/model/ReferencesAggregatedContent.kt | 33 + .../room/model/ReferencesAggregatedSummary.kt | 30 + .../session/room/model/RoomAliasesContent.kt | 29 + .../session/room/model/RoomAvatarContent.kt | 29 + .../room/model/RoomCanonicalAliasContent.kt | 29 + .../room/model/RoomDirectoryVisibility.kt | 27 + .../room/model/RoomGuestAccessContent.kt | 39 + .../room/model/RoomHistoryVisibility.kt | 48 + .../model/RoomHistoryVisibilityContent.kt | 26 + .../api/session/room/model/RoomJoinRules.kt | 41 + .../room/model/RoomJoinRulesContent.kt | 30 + .../session/room/model/RoomMemberContent.kt | 39 + .../session/room/model/RoomMemberSummary.kt | 28 + .../api/session/room/model/RoomNameContent.kt | 29 + .../sdk/api/session/room/model/RoomSummary.kt | 76 + .../room/model/RoomThirdPartyInviteContent.kt | 67 + .../session/room/model/RoomTopicContent.kt | 29 + .../sdk/api/session/room/model/Signed.kt | 26 + .../api/session/room/model/VersioningState.kt | 24 + .../room/model/call/CallAnswerContent.kt | 53 + .../room/model/call/CallCandidatesContent.kt | 58 + .../room/model/call/CallHangupContent.kt | 52 + .../room/model/call/CallInviteContent.kt | 64 + .../api/session/room/model/call/SdpType.kt | 30 + .../room/model/create/CreateRoomParams.kt | 118 ++ .../room/model/create/CreateRoomPreset.kt | 33 + .../session/room/model/create/Predecessor.kt | 29 + .../room/model/create/RoomCreateContent.kt | 31 + .../session/room/model/message/AudioInfo.kt | 39 + .../session/room/model/message/FileInfo.kt | 50 + .../session/room/model/message/ImageInfo.kt | 60 + .../room/model/message/LocationInfo.kt | 40 + .../room/model/message/MessageAudioContent.kt | 59 + .../room/model/message/MessageContent.kt | 28 + .../MessageContentWithFormattedBody.kt | 36 + .../model/message/MessageDefaultContent.kt | 31 + .../room/model/message/MessageEmoteContent.kt | 49 + .../room/model/message/MessageFileContent.kt | 73 + .../room/model/message/MessageFormat.kt | 22 + .../room/model/message/MessageImageContent.kt | 59 + .../model/message/MessageImageInfoContent.kt | 26 + .../model/message/MessageLocationContent.kt | 50 + .../model/message/MessageNoticeContent.kt | 49 + .../model/message/MessageOptionsContent.kt | 41 + .../message/MessagePollResponseContent.kt | 34 + .../model/message/MessageRelationContent.kt | 27 + .../model/message/MessageStickerContent.kt | 59 + .../room/model/message/MessageTextContent.kt | 49 + .../session/room/model/message/MessageType.kt | 36 + .../MessageVerificationAcceptContent.kt | 63 + .../MessageVerificationCancelContent.kt | 51 + .../message/MessageVerificationDoneContent.kt | 47 + .../message/MessageVerificationKeyContent.kt | 53 + .../message/MessageVerificationMacContent.kt | 51 + .../MessageVerificationReadyContent.kt | 51 + .../MessageVerificationRequestContent.kt | 43 + .../MessageVerificationStartContent.kt | 46 + .../room/model/message/MessageVideoContent.kt | 58 + .../message/MessageWithAttachmentContent.kt | 44 + .../session/room/model/message/OptionItem.kt | 30 + .../room/model/message/ThumbnailInfo.kt | 44 + .../session/room/model/message/VideoInfo.kt | 65 + .../room/model/relation/ReactionContent.kt | 26 + .../room/model/relation/ReactionInfo.kt | 31 + .../room/model/relation/RelationContent.kt | 28 + .../model/relation/RelationDefaultContent.kt | 28 + .../room/model/relation/RelationService.kt | 125 ++ .../room/model/relation/ReplyToContent.kt | 26 + .../room/model/roomdirectory/PublicRoom.kt | 94 + .../model/roomdirectory/PublicRoomsFilter.kt | 32 + .../model/roomdirectory/PublicRoomsParams.kt | 58 + .../roomdirectory/PublicRoomsResponse.kt | 51 + .../sdk/api/session/room/model/tag/RoomTag.kt | 30 + .../session/room/model/tag/RoomTagContent.kt | 26 + .../room/model/thirdparty/FieldType.kt | 37 + .../model/thirdparty/RoomDirectoryData.kt | 55 + .../model/thirdparty/ThirdPartyProtocol.kt | 63 + .../thirdparty/ThirdPartyProtocolInstance.kt | 60 + .../model/tombstone/RoomTombstoneContent.kt | 36 + .../notification/RoomNotificationState.kt | 43 + .../room/notification/RoomPushRuleService.kt | 29 + .../room/powerlevels/PowerLevelsHelper.kt | 127 ++ .../sdk/api/session/room/powerlevels/Role.kt | 47 + .../sdk/api/session/room/read/ReadService.kt | 71 + .../room/reporting/ReportingService.kt | 33 + .../sdk/api/session/room/send/DraftService.kt | 41 + .../api/session/room/send/MatrixItemSpan.kt | 28 + .../sdk/api/session/room/send/SendService.kt | 133 ++ .../sdk/api/session/room/send/SendState.kt | 49 + .../sdk/api/session/room/send/UserDraft.kt | 39 + .../sdk/api/session/room/sender/SenderInfo.kt | 35 + .../api/session/room/state/StateService.kt | 71 + .../sdk/api/session/room/tags/TagsService.kt | 36 + .../sdk/api/session/room/timeline/Timeline.kt | 137 ++ .../session/room/timeline/TimelineEvent.kt | 147 ++ .../session/room/timeline/TimelineService.kt | 45 + .../session/room/timeline/TimelineSettings.kt | 52 + .../api/session/room/typing/TypingService.kt | 39 + .../session/room/uploads/GetUploadsResult.kt | 27 + .../api/session/room/uploads/UploadEvent.kt | 33 + .../session/room/uploads/UploadsService.kt | 36 + .../securestorage/EncryptedSecretContent.kt | 50 + .../session/securestorage/IntegrityResult.kt | 23 + .../session/securestorage/KeyInfoResult.kt | 25 + .../api/session/securestorage/KeySigner.kt | 26 + .../securestorage/SecretStorageKeyContent.kt | 104 ++ .../securestorage/SecureStorageService.kt | 28 + .../securestorage/SharedSecretStorageError.kt | 35 + .../SharedSecretStorageService.kt | 144 ++ .../securestorage/SsssKeyCreationInfo.kt | 25 + .../api/session/securestorage/SsssKeySpec.kt | 67 + .../sdk/api/session/signout/SignOutService.kt | 48 + .../sdk/api/session/sync/FilterService.kt | 34 + .../android/sdk/api/session/sync/SyncState.kt | 28 + .../sdk/api/session/terms/GetTermsResponse.kt | 25 + .../sdk/api/session/terms/TermsService.kt | 38 + .../api/session/typing/TypingUsersTracker.kt | 32 + .../sdk/api/session/user/UserService.kt | 84 + .../sdk/api/session/user/model/User.kt | 36 + .../widgets/WidgetManagementFailure.kt | 26 + .../session/widgets/WidgetPostAPIMediator.kt | 96 ++ .../sdk/api/session/widgets/WidgetService.kt | 125 ++ .../api/session/widgets/WidgetURLFormatter.kt | 37 + .../sdk/api/session/widgets/model/Widget.kt | 36 + .../session/widgets/model/WidgetContent.kt | 45 + .../api/session/widgets/model/WidgetType.kt | 70 + .../matrix/android/sdk/api/util/Cancelable.kt | 34 + .../android/sdk/api/util/CancelableBag.kt | 25 + .../android/sdk/api/util/ContentUtils.kt | 48 + .../sdk/api/util/MatrixCallbackDelegate.kt | 25 + .../matrix/android/sdk/api/util/MatrixItem.kt | 160 ++ .../matrix/android/sdk/api/util/Optional.kt | 58 + .../org/matrix/android/sdk/api/util/Types.kt | 27 + .../android/sdk/internal/SessionManager.kt | 62 + .../android/sdk/internal/auth/AuthAPI.kt | 114 ++ .../android/sdk/internal/auth/AuthModule.kt | 84 + .../auth/DefaultAuthenticationService.kt | 369 ++++ .../sdk/internal/auth/PendingSessionStore.kt | 32 + .../sdk/internal/auth/SessionCreator.kt | 70 + .../sdk/internal/auth/SessionParamsStore.kt | 40 + .../data/InteractiveAuthenticationFlow.kt | 34 + .../internal/auth/data/LoginFlowResponse.kt | 39 + .../sdk/internal/auth/data/LoginParams.kt | 22 + .../internal/auth/data/PasswordLoginParams.kt | 100 ++ .../sdk/internal/auth/data/RiotConfig.kt | 29 + .../sdk/internal/auth/data/ThreePidMedium.kt | 23 + .../internal/auth/data/TokenLoginParams.kt | 28 + .../internal/auth/db/AuthRealmMigration.kt | 85 + .../sdk/internal/auth/db/AuthRealmModule.kt | 30 + .../internal/auth/db/PendingSessionData.kt | 51 + .../internal/auth/db/PendingSessionEntity.kt | 30 + .../internal/auth/db/PendingSessionMapper.kt | 70 + .../auth/db/RealmPendingSessionStore.kt | 62 + .../auth/db/RealmSessionParamsStore.kt | 144 ++ .../internal/auth/db/SessionParamsEntity.kt | 31 + .../internal/auth/db/SessionParamsMapper.kt | 60 + .../internal/auth/login/DefaultLoginWizard.kt | 148 ++ .../internal/auth/login/DirectLoginTask.kt | 88 + .../internal/auth/login/ResetPasswordData.kt | 30 + .../auth/login/ResetPasswordMailConfirmed.kt | 46 + .../AddThreePidRegistrationParams.kt | 102 ++ .../AddThreePidRegistrationResponse.kt | 55 + .../internal/auth/registration/AuthParams.kt | 103 ++ .../registration/DefaultRegistrationWizard.kt | 247 +++ .../LocalizedFlowDataLoginTerms.kt | 32 + .../registration/RegisterAddThreePidTask.kt | 49 + .../auth/registration/RegisterTask.kt | 48 + .../registration/RegistrationFlowResponse.kt | 100 ++ .../auth/registration/RegistrationParams.kt | 48 + .../auth/registration/SuccessResult.kt | 30 + .../auth/registration/ThreePidData.kt | 55 + .../auth/registration/ValidateCodeTask.kt | 40 + .../auth/registration/ValidationCodeBody.kt | 36 + .../auth/version/HomeServerVersion.kt | 61 + .../sdk/internal/auth/version/Versions.kt | 110 ++ .../crypto/CancelGossipRequestWorker.kt | 119 ++ .../sdk/internal/crypto/CryptoConstants.kt | 44 + .../sdk/internal/crypto/CryptoModule.kt | 260 +++ .../internal/crypto/DefaultCryptoService.kt | 1360 +++++++++++++++ .../sdk/internal/crypto/DeviceListManager.kt | 547 ++++++ .../internal/crypto/GossipingRequestState.kt | 46 + .../internal/crypto/GossipingWorkManager.kt | 58 + .../crypto/IncomingGossipingRequestManager.kt | 434 +++++ .../crypto/IncomingRequestCancellation.kt | 63 + .../internal/crypto/IncomingRoomKeyRequest.kt | 85 + .../crypto/IncomingSecretShareRequest.kt | 83 + .../crypto/IncomingShareRequestCommon.kt | 37 + .../sdk/internal/crypto/MXCryptoAlgorithms.kt | 59 + .../crypto/MXEventDecryptionResult.kt | 49 + .../crypto/MXMegolmExportEncryption.kt | 350 ++++ .../sdk/internal/crypto/MXOlmDevice.kt | 779 +++++++++ .../sdk/internal/crypto/MegolmSessionData.kt | 74 + .../sdk/internal/crypto/MyDeviceInfoHolder.kt | 81 + .../sdk/internal/crypto/NewSessionListener.kt | 21 + .../sdk/internal/crypto/ObjectSigner.kt | 53 + .../internal/crypto/OneTimeKeysUploader.kt | 169 ++ .../crypto/OutgoingGossipingRequest.kt | 26 + .../crypto/OutgoingGossipingRequestManager.kt | 163 ++ .../internal/crypto/OutgoingRoomKeyRequest.kt | 61 + .../internal/crypto/OutgoingSecretRequest.kt | 39 + .../internal/crypto/RoomDecryptorProvider.kt | 105 ++ .../internal/crypto/RoomEncryptorsStore.kt | 41 + .../crypto/SendGossipRequestWorker.kt | 149 ++ .../sdk/internal/crypto/SendGossipWorker.kt | 142 ++ .../EnsureOlmSessionsForDevicesAction.kt | 144 ++ .../EnsureOlmSessionsForUsersAction.kt | 49 + .../actions/MegolmSessionDataImporter.kt | 108 ++ .../crypto/actions/MessageEncrypter.kt | 86 + .../actions/SetDeviceVerificationAction.kt | 55 + .../crypto/algorithms/IMXDecrypting.kt | 76 + .../crypto/algorithms/IMXEncrypting.kt | 67 + .../crypto/algorithms/IMXWithHeldExtension.kt | 24 + .../algorithms/megolm/MXMegolmDecryption.kt | 388 +++++ .../megolm/MXMegolmDecryptionFactory.kt | 58 + .../algorithms/megolm/MXMegolmEncryption.kt | 440 +++++ .../megolm/MXMegolmEncryptionFactory.kt | 59 + .../megolm/MXOutboundSessionInfo.kt | 75 + .../algorithms/megolm/SharedWithHelper.kt | 39 + .../crypto/algorithms/olm/MXOlmDecryption.kt | 219 +++ .../algorithms/olm/MXOlmDecryptionFactory.kt | 32 + .../crypto/algorithms/olm/MXOlmEncryption.kt | 91 + .../algorithms/olm/MXOlmEncryptionFactory.kt | 44 + .../algorithms/olm/OlmDecryptionResult.kt | 49 + .../sdk/internal/crypto/api/CryptoApi.kt | 159 ++ .../crypto/attachments/ElementToDecrypt.kt | 46 + .../crypto/attachments/EncryptionResult.kt | 28 + .../attachments/MXEncryptedAttachments.kt | 214 +++ .../crypto/crosssigning/ComputeTrustTask.kt | 94 + .../DefaultCrossSigningService.kt | 747 ++++++++ .../crypto/crosssigning/DeviceTrustLevel.kt | 26 + .../crypto/crosssigning/DeviceTrustResult.kt | 32 + .../crypto/crosssigning/Extensions.kt | 51 + .../SessionToCryptoRoomMembersUpdate.kt | 27 + .../crypto/crosssigning/ShieldTrustUpdater.kt | 127 ++ .../crypto/crosssigning/UserTrustResult.kt | 36 + .../keysbackup/DefaultKeysBackupService.kt | 1454 ++++++++++++++++ .../crypto/keysbackup/KeysBackupPassword.kt | 152 ++ .../keysbackup/KeysBackupStateManager.kt | 70 + .../crypto/keysbackup/api/RoomKeysApi.kt | 193 +++ .../keysbackup/model/KeyBackupVersionTrust.kt | 37 + .../model/KeyBackupVersionTrustSignature.kt | 36 + .../model/KeysBackupVersionTrust.kt | 34 + .../model/KeysBackupVersionTrustSignature.kt | 42 + .../keysbackup/model/MegolmBackupAuthData.kt | 77 + .../model/MegolmBackupCreationInfo.kt | 38 + .../keysbackup/model/rest/BackupKeysResult.kt | 31 + .../model/rest/CreateKeysBackupVersionBody.kt | 38 + .../keysbackup/model/rest/KeyBackupData.kt | 60 + .../model/rest/KeysAlgorithmAndData.kt | 61 + .../keysbackup/model/rest/KeysBackupData.kt | 31 + .../keysbackup/model/rest/KeysVersion.kt | 23 + .../model/rest/KeysVersionResult.kt | 50 + .../model/rest/RoomKeysBackupData.kt | 31 + .../model/rest/UpdateKeysBackupVersionBody.kt | 42 + .../tasks/CreateKeysBackupVersionTask.kt | 40 + .../keysbackup/tasks/DeleteBackupTask.kt | 42 + .../tasks/DeleteRoomSessionDataTask.kt | 47 + .../tasks/DeleteRoomSessionsDataTask.kt | 45 + .../tasks/DeleteSessionsDataTask.kt | 42 + .../tasks/GetKeysBackupLastVersionTask.kt | 39 + .../tasks/GetKeysBackupVersionTask.kt | 39 + .../tasks/GetRoomSessionDataTask.kt | 48 + .../tasks/GetRoomSessionsDataTask.kt | 46 + .../keysbackup/tasks/GetSessionsDataTask.kt | 43 + .../tasks/StoreRoomSessionDataTask.kt | 51 + .../tasks/StoreRoomSessionsDataTask.kt | 49 + .../keysbackup/tasks/StoreSessionsDataTask.kt | 47 + .../tasks/UpdateKeysBackupVersionTask.kt | 44 + .../internal/crypto/keysbackup/util/Base58.kt | 87 + .../crypto/keysbackup/util/RecoveryKey.kt | 120 ++ .../crypto/model/CryptoCrossSigningKey.kt | 109 ++ .../internal/crypto/model/CryptoDeviceInfo.kt | 84 + .../sdk/internal/crypto/model/CryptoInfo.kt | 33 + .../internal/crypto/model/CryptoInfoMapper.kt | 65 + .../crypto/model/ImportRoomKeysResult.kt | 21 + .../sdk/internal/crypto/model/MXDeviceInfo.kt | 180 ++ .../model/MXEncryptEventContentResult.kt | 31 + .../sdk/internal/crypto/model/MXKey.kt | 123 ++ .../crypto/model/MXOlmSessionResult.kt | 31 + .../crypto/model/MXQueuedEncryption.kt | 35 + .../crypto/model/MXUsersDevicesMap.kt | 132 ++ .../model/OlmInboundGroupSessionWrapper.kt | 153 ++ .../model/OlmInboundGroupSessionWrapper2.kt | 158 ++ .../crypto/model/OlmSessionWrapper.kt | 37 + .../model/event/EncryptedEventContent.kt | 61 + .../model/event/EncryptionEventContent.kt | 44 + .../crypto/model/event/NewDeviceContent.kt | 31 + .../crypto/model/event/OlmEventContent.kt | 38 + .../crypto/model/event/OlmPayloadContent.kt | 61 + .../crypto/model/event/RoomKeyContent.kt | 43 + .../model/event/RoomKeyWithHeldContent.kt | 101 ++ .../model/event/SecretSendEventContent.kt | 29 + .../crypto/model/rest/DeleteDeviceParams.kt | 29 + .../internal/crypto/model/rest/DeviceInfo.kt | 63 + .../internal/crypto/model/rest/DeviceKeys.kt | 56 + .../model/rest/DeviceKeysWithUnsigned.kt | 62 + .../crypto/model/rest/DevicesListResponse.kt | 29 + .../crypto/model/rest/DummyContent.kt | 23 + .../model/rest/EncryptedBodyFileInfo.kt | 30 + .../crypto/model/rest/EncryptedFileInfo.kt | 90 + .../crypto/model/rest/EncryptedFileKey.kt | 80 + .../crypto/model/rest/EncryptedMessage.kt | 33 + .../model/rest/ForwardedRoomKeyContent.kt | 73 + .../model/rest/GossipingToDeviceObject.kt | 45 + .../crypto/model/rest/KeyChangesResponse.kt | 34 + .../model/rest/KeyVerificationAccept.kt | 88 + .../model/rest/KeyVerificationCancel.kt | 57 + .../crypto/model/rest/KeyVerificationDone.kt | 32 + .../crypto/model/rest/KeyVerificationKey.kt | 48 + .../crypto/model/rest/KeyVerificationMac.kt | 42 + .../crypto/model/rest/KeyVerificationReady.kt | 34 + .../model/rest/KeyVerificationRequest.kt | 35 + .../crypto/model/rest/KeyVerificationStart.kt | 45 + .../crypto/model/rest/KeysClaimBody.kt | 39 + .../crypto/model/rest/KeysClaimResponse.kt | 34 + .../crypto/model/rest/KeysQueryBody.kt | 49 + .../crypto/model/rest/KeysQueryResponse.kt | 56 + .../crypto/model/rest/KeysUploadBody.kt | 45 + .../crypto/model/rest/KeysUploadResponse.kt | 53 + .../internal/crypto/model/rest/RestKeyInfo.kt | 59 + .../crypto/model/rest/RoomKeyRequestBody.kt | 50 + .../crypto/model/rest/RoomKeyShareRequest.kt | 39 + .../crypto/model/rest/SecretShareRequest.kt | 38 + .../crypto/model/rest/SendToDeviceBody.kt | 28 + .../crypto/model/rest/SendToDeviceObject.kt | 20 + .../model/rest/ShareRequestCancellation.kt | 36 + .../model/rest/SignatureUploadResponse.kt | 49 + .../crypto/model/rest/UnsignedDeviceInfo.kt | 30 + .../crypto/model/rest/UpdateDeviceInfoBody.kt | 30 + .../model/rest/UploadSignatureQueryBuilder.kt | 60 + .../model/rest/UploadSigningKeysBody.kt | 35 + .../crypto/model/rest/UserPasswordAuth.kt | 43 + .../model/rest/VerificationMethodValues.kt | 36 + .../WarnOnUnknownDeviceRepository.kt | 45 + .../DefaultSharedSecretStorageService.kt | 447 +++++ .../internal/crypto/store/IMXCryptoStore.kt | 440 +++++ .../internal/crypto/store/PrivateKeysInfo.kt | 26 + .../crypto/store/SavedKeyBackupKeyInfo.kt | 23 + .../sdk/internal/crypto/store/db/Helper.kt | 105 ++ .../crypto/store/db/RealmCryptoStore.kt | 1513 +++++++++++++++++ .../store/db/RealmCryptoStoreMigration.kt | 458 +++++ .../crypto/store/db/RealmCryptoStoreModule.kt | 60 + .../crypto/store/db/SafeObjectInputStream.kt | 45 + .../store/db/mapper/CrossSigningKeysMapper.kt | 86 + .../store/db/model/CrossSigningInfoEntity.kt | 59 + .../crypto/store/db/model/CryptoMapper.kt | 107 ++ .../store/db/model/CryptoMetadataEntity.kt | 61 + .../crypto/store/db/model/CryptoRoomEntity.kt | 31 + .../crypto/store/db/model/DeviceInfoEntity.kt | 50 + .../store/db/model/GossipingEventEntity.kt | 89 + .../model/IncomingGossipingRequestEntity.kt | 90 + .../crypto/store/db/model/KeyInfoEntity.kt | 33 + .../store/db/model/KeysBackupDataEntity.kt | 31 + .../db/model/MyDeviceLastSeenInfoEntity.kt | 35 + .../db/model/OlmInboundGroupSessionEntity.kt | 54 + .../crypto/store/db/model/OlmSessionEntity.kt | 45 + .../model/OutgoingGossipingRequestEntity.kt | 105 ++ .../store/db/model/SharedSessionEntity.kt | 38 + .../crypto/store/db/model/TrustLevelEntity.kt | 30 + .../crypto/store/db/model/UserEntity.kt | 32 + .../store/db/model/WithHeldSessionEntity.kt | 49 + .../db/query/CrossSigningInfoEntityQueries.kt | 37 + .../store/db/query/CryptoRoomEntityQueries.kt | 40 + .../store/db/query/DeviceInfoEntityQueries.kt | 40 + .../store/db/query/SharedSessionQueries.kt | 58 + .../store/db/query/UserEntitiesQueries.kt | 44 + .../store/db/query/WithHeldSessionQueries.kt | 42 + .../ClaimOneTimeKeysForUsersDeviceTask.kt | 65 + .../internal/crypto/tasks/DeleteDeviceTask.kt | 51 + .../tasks/DeleteDeviceWithUserPasswordTask.kt | 58 + .../crypto/tasks/DownloadKeysForUsersTask.kt | 54 + .../internal/crypto/tasks/EncryptEventTask.kt | 81 + .../crypto/tasks/GetDeviceInfoTask.kt | 41 + .../internal/crypto/tasks/GetDevicesTask.kt | 39 + .../crypto/tasks/GetKeyChangesTask.kt | 46 + .../tasks/InitializeCrossSigningTask.kt | 173 ++ .../internal/crypto/tasks/SendEventTask.kt | 80 + .../internal/crypto/tasks/SendToDeviceTask.kt | 60 + .../tasks/SendVerificationMessageTask.kt | 80 + .../crypto/tasks/SetDeviceNameTask.kt | 49 + .../internal/crypto/tasks/UploadKeysTask.kt | 57 + .../crypto/tasks/UploadSignaturesTask.kt | 53 + .../crypto/tasks/UploadSigningKeysTask.kt | 98 ++ .../sdk/internal/crypto/tools/HkdfSha256.kt | 103 ++ .../sdk/internal/crypto/tools/Tools.kt | 59 + ...comingSASDefaultVerificationTransaction.kt | 265 +++ ...tgoingSASDefaultVerificationTransaction.kt | 257 +++ .../DefaultVerificationService.kt | 1479 ++++++++++++++++ .../DefaultVerificationTransaction.kt | 113 ++ .../SASDefaultVerificationTransaction.kt | 421 +++++ .../SendVerificationMessageWorker.kt | 92 + .../crypto/verification/VerificationEmoji.kt | 89 + .../crypto/verification/VerificationInfo.kt | 34 + .../verification/VerificationInfoAccept.kt | 82 + .../verification/VerificationInfoCancel.kt | 46 + .../verification/VerificationInfoDone.kt | 27 + .../verification/VerificationInfoKey.kt | 46 + .../verification/VerificationInfoMac.kt | 54 + .../verification/VerificationInfoReady.kt | 55 + .../verification/VerificationInfoRequest.kt | 53 + .../verification/VerificationInfoStart.kt | 125 ++ .../VerificationMessageProcessor.kt | 169 ++ .../verification/VerificationTransport.kt | 97 ++ .../VerificationTransportRoomMessage.kt | 405 +++++ .../VerificationTransportToDevice.kt | 261 +++ .../DefaultQrCodeVerificationTransaction.kt | 284 ++++ .../crypto/verification/qrcode/Extensions.kt | 128 ++ .../crypto/verification/qrcode/QrCodeData.kt | 103 ++ .../verification/qrcode/SharedSecret.kt | 30 + .../sdk/internal/database/AsyncTransaction.kt | 46 + .../sdk/internal/database/DBConstants.kt | 23 + .../sdk/internal/database/DatabaseCleaner.kt | 101 ++ .../database/EventInsertLiveObserver.kt | 115 ++ .../sdk/internal/database/RealmKeysUtils.kt | 128 ++ .../database/RealmLiveEntityObserver.kt | 77 + .../sdk/internal/database/RealmQueryLatch.kt | 61 + .../database/RealmSessionStoreMigration.kt | 55 + .../SessionRealmConfigurationFactory.kt | 101 ++ .../database/helper/ChunkEntityHelper.kt | 199 +++ .../database/helper/RoomEntityHelper.kt | 32 + .../helper/TimelineEventEntityHelper.kt | 40 + .../database/mapper/AccountDataMapper.kt | 36 + .../internal/database/mapper/ContentMapper.kt | 40 + .../internal/database/mapper/DraftMapper.kt | 46 + .../mapper/EventAnnotationsSummaryMapper.kt | 110 ++ .../internal/database/mapper/EventMapper.kt | 108 ++ .../database/mapper/GroupSummaryMapper.kt | 40 + .../mapper/HomeServerCapabilitiesMapper.kt | 37 + .../database/mapper/IsUselessResolver.kt | 39 + ...llResponseAggregatedSummaryEntityMapper.kt | 51 + .../database/mapper/PushConditionMapper.kt | 42 + .../database/mapper/PushRulesMapper.kt | 102 ++ .../internal/database/mapper/PushersMapper.kt | 61 + .../mapper/ReadReceiptsSummaryMapper.kt | 45 + .../mapper/RoomMemberSummaryMapper.kt | 37 + .../database/mapper/RoomSummaryMapper.kt | 70 + .../database/mapper/TimelineEventMapper.kt | 59 + .../internal/database/mapper/UserMapper.kt | 36 + .../database/model/BreadcrumbsEntity.kt | 28 + .../internal/database/model/ChunkEntity.kt | 46 + .../database/model/CurrentStateEventEntity.kt | 31 + .../internal/database/model/DraftEntity.kt | 34 + .../model/EditAggregatedSummaryEntity.kt | 34 + .../model/EventAnnotationsSummaryEntity.kt | 34 + .../internal/database/model/EventEntity.kt | 69 + .../database/model/EventInsertEntity.kt | 38 + .../database/model/EventInsertType.java | 25 + .../internal/database/model/FilterEntity.kt | 37 + .../internal/database/model/GroupEntity.kt | 41 + .../database/model/GroupSummaryEntity.kt | 43 + .../model/HomeServerCapabilitiesEntity.kt | 33 + .../database/model/IgnoredUserEntity.kt | 25 + .../PollResponseAggregatedSummaryEntity.kt | 41 + .../database/model/PushConditionEntity.kt | 29 + .../internal/database/model/PushRuleEntity.kt | 43 + .../database/model/PushRulesEntity.kt | 38 + .../database/model/PusherDataEntity.kt | 26 + .../internal/database/model/PusherEntity.kt | 57 + .../model/ReactionAggregatedSummaryEntity.kt | 42 + .../database/model/ReadMarkerEntity.kt | 30 + .../database/model/ReadReceiptEntity.kt | 35 + .../model/ReadReceiptsSummaryEntity.kt | 37 + .../ReferencesAggregatedSummaryEntity.kt | 32 + .../sdk/internal/database/model/RoomEntity.kt | 41 + .../database/model/RoomMemberSummaryEntity.kt | 44 + .../database/model/RoomSummaryEntity.kt | 92 + .../internal/database/model/RoomTagEntity.kt | 28 + .../database/model/ScalarTokenEntity.kt | 29 + .../database/model/SessionRealmModule.kt | 64 + .../sdk/internal/database/model/SyncEntity.kt | 25 + .../database/model/TimelineEventEntity.kt | 42 + .../database/model/UserAccountDataEntity.kt | 35 + .../database/model/UserDraftsEntity.kt | 36 + .../sdk/internal/database/model/UserEntity.kt | 29 + .../database/model/UserThreePidEntity.kt | 26 + ...WellknownIntegrationManagerConfigEntity.kt | 30 + .../database/query/BreadcrumbsEntityQuery.kt | 31 + .../database/query/ChunkEntityQueries.kt | 70 + .../query/CurrentStateEventEntityQueries.kt | 53 + .../EventAnnotationsSummaryEntityQuery.kt | 54 + .../database/query/EventEntityQueries.kt | 86 + .../database/query/FilterEntityQueries.kt | 43 + .../database/query/GroupEntityQueries.kt | 35 + .../query/GroupSummaryEntityQueries.kt | 42 + .../query/HomeServerCapabilitiesQueries.kt | 37 + .../internal/database/query/PushersQueries.kt | 54 + .../database/query/ReadMarkerEntityQueries.kt | 34 + .../internal/database/query/ReadQueries.kt | 78 + .../query/ReadReceiptEntityQueries.kt | 56 + .../query/ReadReceiptsSummaryEntityQueries.kt | 34 + ...eferencesAggregatedSummaryEntityQueries.kt | 36 + .../database/query/RoomEntityQueries.kt | 43 + .../database/query/RoomMemberEntityQueries.kt | 35 + .../query/RoomSummaryEntityQueries.kt | 64 + .../database/query/ScalarTokenQuery.kt | 30 + .../query/TimelineEventEntityQueries.kt | 105 ++ .../database/query/TimelineEventFilter.kt | 45 + .../database/query/UserDraftsEntityQueries.kt | 33 + .../database/query/UserEntityQueries.kt | 30 + .../android/sdk/internal/di/AuthQualifiers.kt | 40 + .../android/sdk/internal/di/DbQualifiers.kt | 36 + .../android/sdk/internal/di/FileQualifiers.kt | 36 + .../sdk/internal/di/MatrixComponent.kt | 85 + .../android/sdk/internal/di/MatrixModule.kt | 74 + .../android/sdk/internal/di/MatrixScope.kt | 28 + .../android/sdk/internal/di/MoshiProvider.kt | 63 + .../android/sdk/internal/di/NetworkModule.kt | 101 ++ .../android/sdk/internal/di/NoOpTestModule.kt | 32 + .../android/sdk/internal/di/SerializeNulls.kt | 40 + .../di/SessionAssistedInjectModule.kt | 25 + .../sdk/internal/di/StringQualifiers.kt | 48 + .../sdk/internal/di/WorkManagerProvider.kt | 75 + .../internal/eventbus/EventBusTimberLogger.kt | 32 + .../sdk/internal/extensions/LiveData.kt | 30 + .../sdk/internal/extensions/Primitives.kt | 23 + .../internal/extensions/RealmExtensions.kt | 24 + .../android/sdk/internal/extensions/Result.kt | 26 + .../android/sdk/internal/extensions/Try.kt | 46 + .../legacy/DefaultLegacySessionImporter.kt | 227 +++ .../sdk/internal/legacy/riot/Credentials.java | 113 ++ .../sdk/internal/legacy/riot/Fingerprint.java | 97 ++ .../riot/HomeServerConnectionConfig.java | 677 ++++++++ .../internal/legacy/riot/LoginStorage.java | 207 +++ .../sdk/internal/legacy/riot/WellKnown.kt | 97 ++ .../legacy/riot/WellKnownBaseConfig.kt | 40 + .../legacy/riot/WellKnownManagerConfig.kt | 26 + .../legacy/riot/WellKnownPreferredConfig.kt | 40 + .../network/AccessTokenInterceptor.kt | 38 + .../sdk/internal/network/HttpHeaders.kt | 24 + .../network/NetworkCallbackStrategy.kt | 88 + .../network/NetworkConnectivityChecker.kt | 110 ++ .../sdk/internal/network/NetworkConstants.kt | 36 + .../internal/network/NetworkInfoReceiver.kt | 40 + .../internal/network/ProgressRequestBody.kt | 71 + .../android/sdk/internal/network/Request.kt | 77 + .../internal/network/RetrofitExtensions.kt | 102 ++ .../sdk/internal/network/RetrofitFactory.kt | 58 + .../internal/network/TimeOutInterceptor.kt | 57 + .../internal/network/UnitConverterFactory.kt | 36 + .../sdk/internal/network/UserAgentHolder.kt | 87 + .../internal/network/UserAgentInterceptor.kt | 38 + .../network/httpclient/OkHttpClientUtil.kt | 54 + .../network/parsing/ForceToBoolean.kt | 50 + .../parsing/RuntimeJsonAdapterFactory.java | 170 ++ .../network/parsing/UriMoshiAdapter.kt | 35 + .../sdk/internal/network/ssl/CertUtil.kt | 262 +++ .../sdk/internal/network/ssl/Fingerprint.kt | 85 + .../network/ssl/PinnedTrustManager.kt | 84 + .../network/ssl/PinnedTrustManagerApi24.kt | 156 ++ .../network/ssl/PinnedTrustManagerProvider.kt | 42 + .../internal/network/ssl/TLSSocketFactory.kt | 121 ++ .../ssl/UnrecognizedCertificateException.kt | 31 + .../network/token/AccessTokenProvider.kt | 22 + .../token/HomeserverAccessTokenProvider.kt | 29 + .../internal/query/QueryEnumListProcessor.kt | 34 + .../query/QueryStringValueProcessor.kt | 44 + .../internal/session/DefaultFileService.kt | 256 +++ .../DefaultInitialSyncProgressService.kt | 137 ++ .../sdk/internal/session/DefaultSession.kt | 279 +++ .../session/EventInsertLiveProcessor.kt | 29 + .../sdk/internal/session/SessionComponent.kt | 139 ++ .../session/SessionLifecycleObserver.kt | 50 + .../sdk/internal/session/SessionListeners.kt | 47 + .../sdk/internal/session/SessionModule.kt | 361 ++++ .../sdk/internal/session/SessionScope.kt | 25 + .../sdk/internal/session/TestInterceptor.kt | 24 + .../internal/session/account/AccountAPI.kt | 41 + .../internal/session/account/AccountModule.kt | 48 + .../session/account/ChangePasswordParams.kt | 43 + .../session/account/ChangePasswordTask.kt | 63 + .../account/DeactivateAccountParams.kt | 41 + .../session/account/DeactivateAccountTask.kt | 57 + .../session/account/DefaultAccountService.kt | 46 + .../sdk/internal/session/cache/CacheModule.kt | 42 + .../internal/session/cache/ClearCacheTask.kt | 34 + .../session/cache/DefaultCacheService.kt | 39 + .../session/call/CallEventProcessor.kt | 70 + .../sdk/internal/session/call/CallModule.kt | 45 + .../call/DefaultCallSignalingService.kt | 237 +++ .../session/call/GetTurnServerTask.kt | 38 + .../sdk/internal/session/call/VoipApi.kt | 29 + .../internal/session/call/model/MxCallImpl.kt | 151 ++ .../session/cleanup/CleanupSession.kt | 88 + .../internal/session/content/ContentModule.kt | 38 + .../session/content/ContentUploadResponse.kt | 26 + .../DefaultContentUploadStateTracker.kt | 99 ++ .../content/DefaultContentUrlResolver.kt | 76 + .../internal/session/content/FileUploader.kt | 107 ++ .../session/content/ThumbnailExtractor.kt | 73 + .../session/content/UploadContentWorker.kt | 347 ++++ .../DefaultContentDownloadStateTracker.kt | 86 + .../download/DownloadProgressInterceptor.kt | 50 + .../session/download/ProgressResponseBody.kt | 63 + .../session/filter/DefaultFilterRepository.kt | 91 + .../session/filter/DefaultFilterService.kt | 34 + .../session/filter/DefaultSaveFilterTask.kt | 70 + .../internal/session/filter/EventFilter.kt | 60 + .../sdk/internal/session/filter/Filter.kt | 59 + .../sdk/internal/session/filter/FilterApi.kt | 49 + .../internal/session/filter/FilterFactory.kt | 84 + .../internal/session/filter/FilterModule.kt | 48 + .../session/filter/FilterRepository.kt | 41 + .../internal/session/filter/FilterResponse.kt | 34 + .../sdk/internal/session/filter/FilterUtil.kt | 113 ++ .../session/filter/RoomEventFilter.kt | 87 + .../sdk/internal/session/filter/RoomFilter.kt | 71 + .../session/group/DefaultGetGroupDataTask.kt | 111 ++ .../internal/session/group/DefaultGroup.kt | 36 + .../session/group/DefaultGroupService.kt | 75 + .../session/group/GetGroupDataWorker.kt | 58 + .../sdk/internal/session/group/GroupAPI.kt | 53 + .../internal/session/group/GroupFactory.kt | 41 + .../sdk/internal/session/group/GroupModule.kt | 48 + .../session/group/model/GroupProfile.kt | 50 + .../internal/session/group/model/GroupRoom.kt | 36 + .../session/group/model/GroupRooms.kt | 29 + .../group/model/GroupSummaryResponse.kt | 47 + .../group/model/GroupSummaryRoomsSection.kt | 35 + .../session/group/model/GroupSummaryUser.kt | 38 + .../group/model/GroupSummaryUsersSection.kt | 36 + .../internal/session/group/model/GroupUser.kt | 30 + .../session/group/model/GroupUsers.kt | 27 + .../session/homeserver/CapabilitiesAPI.kt | 50 + .../DefaultGetHomeServerCapabilitiesTask.kt | 130 ++ .../DefaultHomeServerCapabilitiesService.kt | 40 + .../homeserver/GetCapabilitiesResult.kt | 59 + .../homeserver/GetUploadCapabilitiesResult.kt | 31 + .../HomeServerCapabilitiesModule.kt | 42 + .../session/homeserver/HomeServerPinger.kt | 50 + .../identity/DefaultIdentityService.kt | 333 ++++ .../session/identity/EnsureIdentityToken.kt | 60 + .../internal/session/identity/IdentityAPI.kt | 100 ++ .../identity/IdentityAccessTokenProvider.kt | 28 + .../session/identity/IdentityApiProvider.kt | 27 + .../session/identity/IdentityAuthAPI.kt | 58 + .../identity/IdentityBulkLookupTask.kt | 134 ++ .../identity/IdentityDisconnectTask.kt | 50 + .../session/identity/IdentityModule.kt | 102 ++ .../session/identity/IdentityPingTask.kt | 53 + .../session/identity/IdentityRegisterTask.kt | 40 + .../IdentityRequestTokenForBindingTask.kt | 88 + .../IdentitySubmitTokenForBindingTask.kt | 62 + .../session/identity/IdentityTaskHelper.kt | 35 + .../session/identity/data/IdentityData.kt | 25 + .../identity/data/IdentityPendingBinding.kt | 27 + .../session/identity/data/IdentityStore.kt | 45 + .../session/identity/db/IdentityDataEntity.kt | 31 + .../identity/db/IdentityDataEntityQuery.kt | 63 + .../session/identity/db/IdentityMapper.kt | 41 + .../db/IdentityPendingBindingEntity.kt | 38 + .../db/IdentityPendingBindingEntityQuery.kt | 43 + .../identity/db/IdentityRealmModule.kt | 30 + .../session/identity/db/RealmIdentityStore.kt | 92 + .../identity/model/IdentityAccountResponse.kt | 30 + .../model/IdentityHashDetailResponse.kt | 46 + .../identity/model/IdentityLookUpParams.kt | 46 + .../identity/model/IdentityLookUpResponse.kt | 34 + .../model/IdentityRegisterResponse.kt | 30 + .../model/IdentityRequestOwnershipParams.kt | 41 + .../model/IdentityRequestTokenBody.kt | 83 + .../model/IdentityRequestTokenResponse.kt | 32 + .../AllowedWidgetsContent.kt | 40 + .../DefaultIntegrationManagerService.kt | 67 + .../integrationmanager/IntegrationManager.kt | 292 ++++ .../IntegrationManagerConfigExtractor.kt | 47 + .../IntegrationManagerModule.kt | 29 + .../IntegrationManagerWidgetData.kt | 26 + .../IntegrationProvisioningContent.kt | 26 + .../notification/DefaultPushRuleService.kt | 224 +++ .../notification/ProcessEventForPushTask.kt | 107 ++ .../session/openid/GetOpenIdTokenTask.kt | 38 + .../sdk/internal/session/openid/OpenIdAPI.kt | 40 + .../internal/session/openid/OpenIdModule.kt | 39 + .../openid/RequestOpenIdTokenResponse.kt | 49 + .../profile/AccountThreePidsResponse.kt | 29 + .../session/profile/BindThreePidBody.kt | 47 + .../session/profile/BindThreePidsTask.kt | 60 + .../session/profile/DefaultProfileService.kt | 141 ++ .../session/profile/GetProfileInfoTask.kt | 41 + .../internal/session/profile/ProfileAPI.kt | 74 + .../internal/session/profile/ProfileModule.kt | 61 + .../profile/RefreshUserThreePidsTask.kt | 63 + .../session/profile/SetAvatarUrlBody.kt | 29 + .../session/profile/SetAvatarUrlTask.kt | 44 + .../session/profile/SetDisplayNameBody.kt | 29 + .../session/profile/SetDisplayNameTask.kt | 44 + .../session/profile/ThirdPartyIdentifier.kt | 58 + .../session/profile/UnbindThreePidBody.kt | 42 + .../session/profile/UnbindThreePidResponse.kt | 28 + .../session/profile/UnbindThreePidsTask.kt | 52 + .../session/pushers/AddHttpPusherWorker.kt | 109 ++ .../session/pushers/AddPushRuleTask.kt | 43 + .../pushers/DefaultConditionResolver.kt | 67 + .../session/pushers/DefaultPushersService.kt | 116 ++ .../session/pushers/GetPushRulesTask.kt | 45 + .../session/pushers/GetPushersResponse.kt | 26 + .../session/pushers/GetPushersTask.kt | 54 + .../internal/session/pushers/JsonPusher.kt | 116 ++ .../session/pushers/JsonPusherData.kt | 37 + .../internal/session/pushers/PushRulesApi.kt | 86 + .../internal/session/pushers/PushersAPI.kt | 43 + .../internal/session/pushers/PushersModule.kt | 90 + .../session/pushers/RemovePushRuleTask.kt | 43 + .../session/pushers/RemovePusherTask.kt | 73 + .../session/pushers/SavePushRulesTask.kt | 82 + .../pushers/UpdatePushRuleActionsTask.kt | 56 + .../pushers/UpdatePushRuleEnableStatusTask.kt | 42 + .../sdk/internal/session/room/DefaultRoom.kt | 126 ++ .../room/DefaultRoomDirectoryService.kt | 53 + .../session/room/DefaultRoomService.kt | 122 ++ .../EventRelationsAggregationProcessor.kt | 573 +++++++ .../sdk/internal/session/room/RoomAPI.kt | 383 +++++ .../session/room/RoomAvatarResolver.kt | 58 + .../sdk/internal/session/room/RoomFactory.kt | 87 + .../sdk/internal/session/room/RoomGetter.kt | 68 + .../sdk/internal/session/room/RoomModule.kt | 207 +++ .../session/room/alias/AddRoomAliasBody.kt | 29 + .../session/room/alias/AddRoomAliasTask.kt | 48 + .../room/alias/GetRoomIdByAliasTask.kt | 60 + .../room/alias/RoomAliasDescription.kt | 34 + .../room/call/DefaultRoomCallService.kt | 39 + .../session/room/create/CreateRoomBody.kt | 116 ++ .../room/create/CreateRoomBodyBuilder.kt | 146 ++ .../session/room/create/CreateRoomResponse.kt | 31 + .../session/room/create/CreateRoomTask.kt | 115 ++ .../room/create/RoomCreateEventProcessor.kt | 47 + .../room/directory/GetPublicRoomTask.kt | 45 + .../directory/GetThirdPartyProtocolsTask.kt | 39 + .../session/room/draft/DefaultDraftService.kt | 61 + .../session/room/draft/DraftRepository.kt | 158 ++ .../membership/DefaultMembershipService.kt | 185 ++ .../room/membership/LoadRoomMembersTask.kt | 98 ++ .../RoomChangeMembershipStateDataSource.kt | 68 + .../membership/RoomDisplayNameResolver.kt | 137 ++ .../membership/RoomMemberEntityFactory.kt | 37 + .../room/membership/RoomMemberEventHandler.kt | 51 + .../room/membership/RoomMemberHelper.kt | 119 ++ .../room/membership/RoomMembersResponse.kt | 27 + .../membership/admin/MembershipAdminTask.kt | 53 + .../room/membership/admin/UserIdAndReason.kt | 26 + .../room/membership/joining/InviteBody.kt | 27 + .../room/membership/joining/InviteTask.kt | 47 + .../room/membership/joining/JoinRoomTask.kt | 82 + .../room/membership/leaving/LeaveRoomTask.kt | 80 + .../membership/threepid/InviteThreePidTask.kt | 66 + .../membership/threepid/ThreePidInviteBody.kt | 42 + .../DefaultRoomPushRuleService.kt | 74 + .../session/room/notification/RoomPushRule.kt | 26 + .../room/notification/RoomPushRuleMapper.kt | 106 ++ .../SetRoomNotificationStateTask.kt | 56 + .../room/prune/RedactionEventProcessor.kt | 126 ++ .../session/room/read/DefaultReadService.kt | 127 ++ .../session/room/read/FullyReadContent.kt | 26 + .../session/room/read/MarkAllRoomsReadTask.kt | 36 + .../session/room/read/SetReadMarkersTask.kt | 132 ++ .../room/relation/DefaultRelationService.kt | 240 +++ .../room/relation/FetchEditHistoryTask.kt | 54 + .../relation/FindReactionEventForUndoTask.kt | 73 + .../room/relation/RelationsResponse.kt | 29 + .../room/relation/SendRelationWorker.kt | 102 ++ .../room/relation/UpdateQuickReactionTask.kt | 88 + .../room/reporting/DefaultReportingService.kt | 47 + .../room/reporting/ReportContentBody.kt | 34 + .../room/reporting/ReportContentTask.kt | 45 + .../session/room/send/DefaultSendService.kt | 323 ++++ .../session/room/send/EncryptEventWorker.kt | 137 ++ .../room/send/LocalEchoEventFactory.kt | 493 ++++++ .../session/room/send/LocalEchoRepository.kt | 190 +++ .../session/room/send/MarkdownParser.kt | 77 + .../MultipleEventSendingDispatcherWorker.kt | 113 ++ .../internal/session/room/send/NoMerger.kt | 29 + .../session/room/send/RedactEventWorker.kt | 94 + .../session/room/send/RoomEventSender.kt | 73 + .../session/room/send/SendEventWorker.kt | 115 ++ .../session/room/send/SendResponse.kt | 29 + .../internal/session/room/send/TextContent.kt | 50 + .../room/send/pills/MentionLinkSpec.kt | 26 + .../send/pills/MentionLinkSpecComparator.kt | 33 + .../session/room/send/pills/TextPillsUtils.kt | 113 ++ .../session/room/state/DefaultStateService.kt | 144 ++ .../session/room/state/SendStateTask.kt | 59 + .../room/state/StateEventDataSource.kt | 85 + .../room/summary/RoomSummaryDataSource.kt | 111 ++ .../room/summary/RoomSummaryUpdater.kt | 193 +++ .../session/room/tags/AddTagToRoomTask.kt | 54 + .../session/room/tags/DefaultTagsService.kt | 57 + .../room/tags/DeleteTagFromRoomTask.kt | 50 + .../sdk/internal/session/room/tags/TagBody.kt | 30 + .../timeline/DefaultGetContextOfEventTask.kt | 50 + .../room/timeline/DefaultPaginationTask.kt | 52 + .../session/room/timeline/DefaultTimeline.kt | 798 +++++++++ .../room/timeline/DefaultTimelineService.kt | 110 ++ .../room/timeline/EventContextResponse.kt | 37 + .../timeline/FetchTokenAndPaginateTask.kt | 78 + .../session/room/timeline/GetEventTask.kt | 44 + .../room/timeline/PaginationDirection.kt | 39 + .../room/timeline/PaginationResponse.kt | 30 + .../room/timeline/TimelineEventDecryptor.kt | 145 ++ .../timeline/TimelineHiddenReadReceipts.kt | 177 ++ .../timeline/TimelineSendEventWorkCommon.kt | 90 + .../session/room/timeline/TokenChunkEvent.kt | 29 + .../room/timeline/TokenChunkEventPersistor.kt | 264 +++ .../tombstone/RoomTombstoneEventProcessor.kt | 50 + .../room/typing/DefaultTypingService.kt | 119 ++ .../session/room/typing/SendTypingTask.kt | 56 + .../session/room/typing/TypingBody.kt | 31 + .../session/room/typing/TypingEventContent.kt | 27 + .../room/uploads/DefaultUploadsService.kt | 49 + .../session/room/uploads/GetUploadsTask.kt | 137 ++ .../DefaultSecureStorageService.kt | 34 + .../securestorage/SecretStoringUtils.kt | 572 +++++++ .../session/signout/DefaultSignOutService.kt | 61 + .../session/signout/SignInAgainTask.kt | 60 + .../internal/session/signout/SignOutAPI.kt | 46 + .../internal/session/signout/SignOutModule.kt | 48 + .../internal/session/signout/SignOutTask.kt | 73 + .../session/sync/CryptoSyncHandler.kt | 89 + .../internal/session/sync/GroupSyncHandler.kt | 99 ++ .../session/sync/ReadReceiptHandler.kt | 114 ++ .../session/sync/RoomFullyReadHandler.kt | 43 + .../internal/session/sync/RoomSyncHandler.kt | 430 +++++ .../internal/session/sync/RoomTagHandler.kt | 43 + .../session/sync/RoomTypingUsersHandler.kt | 44 + .../sdk/internal/session/sync/SyncAPI.kt | 35 + .../sdk/internal/session/sync/SyncModule.kt | 41 + .../session/sync/SyncResponseHandler.kt | 168 ++ .../sdk/internal/session/sync/SyncTask.kt | 87 + .../session/sync/SyncTaskSequencer.kt | 25 + .../internal/session/sync/SyncTokenStore.kt | 38 + .../sync/UserAccountDataSyncHandler.kt | 223 +++ .../internal/session/sync/job/SyncService.kt | 163 ++ .../internal/session/sync/job/SyncThread.kt | 220 +++ .../internal/session/sync/job/SyncWorker.kt | 116 ++ .../internal/session/sync/model/DeviceInfo.kt | 51 + .../session/sync/model/DeviceListResponse.kt | 30 + .../DeviceOneTimeKeysCountSyncResponse.kt | 26 + .../session/sync/model/DevicesListResponse.kt | 27 + .../session/sync/model/GroupSyncProfile.kt | 34 + .../session/sync/model/GroupsSyncResponse.kt | 39 + .../session/sync/model/InvitedGroupSync.kt | 34 + .../session/sync/model/InvitedRoomSync.kt | 34 + .../sync/model/PresenceSyncResponse.kt | 31 + .../session/sync/model/RoomInviteState.kt | 31 + .../session/sync/model/RoomResponse.kt | 58 + .../internal/session/sync/model/RoomSync.kt | 55 + .../session/sync/model/RoomSyncAccountData.kt | 30 + .../session/sync/model/RoomSyncEphemeral.kt | 31 + .../session/sync/model/RoomSyncState.kt | 32 + .../session/sync/model/RoomSyncSummary.kt | 48 + .../session/sync/model/RoomSyncTimeline.kt | 42 + .../sync/model/RoomSyncUnreadNotifications.kt | 42 + .../session/sync/model/RoomsSyncResponse.kt | 39 + .../session/sync/model/SyncResponse.kt | 68 + .../sync/model/ToDeviceSyncResponse.kt | 31 + .../session/sync/model/TokensChunkResponse.kt | 26 + .../model/accountdata/AcceptedTermsContent.kt | 26 + .../model/accountdata/BreadcrumbsContent.kt | 26 + .../accountdata/DirectMessagesContent.kt | 20 + .../accountdata/IdentityServerContent.kt | 26 + .../model/accountdata/IgnoredUsersContent.kt | 39 + .../model/accountdata/UserAccountDataSync.kt | 27 + .../internal/session/terms/AcceptTermsBody.kt | 30 + .../session/terms/DefaultTermsService.kt | 120 ++ .../sdk/internal/session/terms/TermsAPI.kt | 42 + .../sdk/internal/session/terms/TermsModule.kt | 47 + .../internal/session/terms/TermsResponse.kt | 56 + .../typing/DefaultTypingUsersTracker.kt | 43 + .../session/user/DefaultUserService.kt | 87 + .../internal/session/user/SearchUserAPI.kt | 36 + .../internal/session/user/UserDataSource.kt | 121 ++ .../session/user/UserEntityFactory.kt | 32 + .../sdk/internal/session/user/UserModule.kt | 60 + .../sdk/internal/session/user/UserStore.kt | 38 + .../user/accountdata/AccountDataAPI.kt | 39 + .../user/accountdata/AccountDataContent.kt | 23 + .../user/accountdata/AccountDataDataSource.kt | 68 + .../user/accountdata/AccountDataModule.kt | 46 + .../accountdata/DefaultAccountDataService.kt | 80 + .../user/accountdata/DirectChatsHelper.kt | 41 + .../user/accountdata/SaveBreadcrumbsTask.kt | 70 + .../user/accountdata/SaveIgnoredUsersTask.kt | 48 + .../user/accountdata/UpdateBreadcrumbsTask.kt | 68 + .../accountdata/UpdateIgnoredUserIdsTask.kt | 74 + .../accountdata/UpdateUserAccountDataTask.kt | 112 ++ .../internal/session/user/model/SearchUser.kt | 28 + .../session/user/model/SearchUserTask.kt | 49 + .../session/user/model/SearchUsersParams.kt | 32 + .../session/user/model/SearchUsersResponse.kt | 30 + .../session/widgets/CreateWidgetTask.kt | 65 + .../widgets/DefaultWidgetPostAPIMediator.kt | 184 ++ .../session/widgets/DefaultWidgetService.kt | 97 ++ .../widgets/DefaultWidgetURLFormatter.kt | 115 ++ .../session/widgets/RegisterWidgetResponse.kt | 26 + .../internal/session/widgets/WidgetManager.kt | 207 +++ .../internal/session/widgets/WidgetModule.kt | 56 + .../widgets/WidgetPostMessageAPIProvider.kt | 45 + .../internal/session/widgets/WidgetsAPI.kt | 40 + .../session/widgets/WidgetsAPIProvider.kt | 39 + .../widgets/helper/UserAccountWidgets.kt | 34 + .../session/widgets/helper/WidgetFactory.kt | 89 + .../widgets/token/GetScalarTokenTask.kt | 93 + .../session/widgets/token/ScalarTokenStore.kt | 50 + .../sdk/internal/task/ConfigurableTask.kt | 72 + .../sdk/internal/task/CoroutineSequencer.kt | 44 + .../sdk/internal/task/CoroutineToCallback.kt | 43 + .../matrix/android/sdk/internal/task/Task.kt | 23 + .../android/sdk/internal/task/TaskExecutor.kt | 68 + .../android/sdk/internal/task/TaskThread.kt | 27 + .../util/BackgroundDetectionObserver.kt | 65 + .../sdk/internal/util/CancelableCoroutine.kt | 37 + .../sdk/internal/util/CancelableWork.kt | 30 + .../android/sdk/internal/util/CompatUtil.kt | 328 ++++ .../android/sdk/internal/util/Debouncer.kt | 42 + .../android/sdk/internal/util/Exhaustive.kt | 21 + .../android/sdk/internal/util/FileSaver.kt | 33 + .../android/sdk/internal/util/Handler.kt | 30 + .../matrix/android/sdk/internal/util/Hash.kt | 34 + .../sdk/internal/util/JsonCanonicalizer.kt | 96 ++ .../sdk/internal/util/LiveDataUtils.kt | 52 + .../util/MatrixCoroutineDispatchers.kt | 28 + .../android/sdk/internal/util/Monarchy.kt | 50 + .../sdk/internal/util/SecretKeyAndVersion.kt | 33 + .../sdk/internal/util/StringProvider.kt | 63 + .../android/sdk/internal/util/StringUtils.kt | 54 + .../internal/util/SuspendMatrixCallback.kt | 36 + .../android/sdk/internal/util/UrlUtils.kt | 51 + .../internal/wellknown/GetWellknownTask.kt | 222 +++ .../sdk/internal/wellknown/WellKnownAPI.kt | 27 + .../sdk/internal/wellknown/WellknownModule.kt | 28 + .../internal/worker/AlwaysSuccessfulWorker.kt | 29 + .../internal/worker/DelegateWorkerFactory.kt | 27 + .../android/sdk/internal/worker/Extensions.kt | 31 + .../internal/worker/MatrixWorkerFactory.kt | 42 + .../internal/worker/SessionWorkerParams.kt | 32 + .../android/sdk/internal/worker/Worker.kt | 26 + .../internal/worker/WorkerParamsFactory.kt | 44 + .../androidsdk/crypto/data/MXDeviceInfo.java | 85 + .../data/MXOlmInboundGroupSession2.java | 53 + .../res/drawable/ic_verification_airplane.xml | 18 + .../res/drawable/ic_verification_anchor.xml | 12 + .../res/drawable/ic_verification_apple.xml | 15 + .../res/drawable/ic_verification_ball.xml | 15 + .../res/drawable/ic_verification_banana.xml | 36 + .../res/drawable/ic_verification_bell.xml | 15 + .../res/drawable/ic_verification_bicycle.xml | 27 + .../res/drawable/ic_verification_book.xml | 24 + .../drawable/ic_verification_butterfly.xml | 39 + .../res/drawable/ic_verification_cactus.xml | 51 + .../res/drawable/ic_verification_cake.xml | 42 + .../main/res/drawable/ic_verification_cat.xml | 36 + .../res/drawable/ic_verification_clock.xml | 27 + .../res/drawable/ic_verification_cloud.xml | 12 + .../res/drawable/ic_verification_corn.xml | 18 + .../main/res/drawable/ic_verification_dog.xml | 45 + .../res/drawable/ic_verification_elephant.xml | 15 + .../res/drawable/ic_verification_fire.xml | 12 + .../res/drawable/ic_verification_fish.xml | 24 + .../res/drawable/ic_verification_flag.xml | 24 + .../res/drawable/ic_verification_flower.xml | 15 + .../res/drawable/ic_verification_folder.xml | 12 + .../res/drawable/ic_verification_gift.xml | 21 + .../res/drawable/ic_verification_glasses.xml | 12 + .../res/drawable/ic_verification_globe.xml | 15 + .../res/drawable/ic_verification_guitar.xml | 51 + .../res/drawable/ic_verification_hammer.xml | 12 + .../main/res/drawable/ic_verification_hat.xml | 15 + .../drawable/ic_verification_headphone.xml | 15 + .../res/drawable/ic_verification_heart.xml | 9 + .../res/drawable/ic_verification_horse.xml | 33 + .../drawable/ic_verification_hourglass.xml | 15 + .../main/res/drawable/ic_verification_key.xml | 9 + .../drawable/ic_verification_light_bulb.xml | 21 + .../res/drawable/ic_verification_lion.xml | 54 + .../res/drawable/ic_verification_lock.xml | 12 + .../res/drawable/ic_verification_moon.xml | 12 + .../res/drawable/ic_verification_mushroom.xml | 24 + .../res/drawable/ic_verification_octopus.xml | 24 + .../res/drawable/ic_verification_panda.xml | 42 + .../drawable/ic_verification_paperclip.xml | 9 + .../res/drawable/ic_verification_pencil.xml | 24 + .../res/drawable/ic_verification_penguin.xml | 21 + .../res/drawable/ic_verification_phone.xml | 15 + .../main/res/drawable/ic_verification_pig.xml | 21 + .../main/res/drawable/ic_verification_pin.xml | 18 + .../res/drawable/ic_verification_pizza.xml | 21 + .../res/drawable/ic_verification_rabbit.xml | 27 + .../res/drawable/ic_verification_robot.xml | 48 + .../res/drawable/ic_verification_rocket.xml | 24 + .../res/drawable/ic_verification_rooster.xml | 18 + .../res/drawable/ic_verification_santa.xml | 42 + .../res/drawable/ic_verification_scissors.xml | 21 + .../res/drawable/ic_verification_smiley.xml | 18 + .../drawable/ic_verification_strawberry.xml | 15 + .../drawable/ic_verification_thumbs_up.xml | 12 + .../res/drawable/ic_verification_train.xml | 72 + .../res/drawable/ic_verification_tree.xml | 54 + .../res/drawable/ic_verification_trophy.xml | 18 + .../res/drawable/ic_verification_trumpet.xml | 21 + .../res/drawable/ic_verification_turtle.xml | 21 + .../res/drawable/ic_verification_umbrella.xml | 18 + .../res/drawable/ic_verification_unicorn.xml | 30 + .../res/drawable/ic_verification_wrench.xml | 9 + .../src/main/res/values-ar/strings.xml | 82 + .../src/main/res/values-az/strings.xml | 182 ++ .../src/main/res/values-bg/strings.xml | 207 +++ .../src/main/res/values-bn-rIN/strings.xml | 295 ++++ .../src/main/res/values-bs/strings.xml | 7 + .../src/main/res/values-ca/strings.xml | 79 + .../src/main/res/values-cs/strings.xml | 171 ++ .../src/main/res/values-da/strings.xml | 75 + .../src/main/res/values-de/strings.xml | 306 ++++ .../src/main/res/values-el/strings.xml | 76 + .../src/main/res/values-en-rGB/strings.xml | 5 + .../src/main/res/values-eo/strings.xml | 205 +++ .../src/main/res/values-es-rMX/strings.xml | 96 ++ .../src/main/res/values-es/strings.xml | 215 +++ .../src/main/res/values-et/strings.xml | 302 ++++ .../src/main/res/values-eu/strings.xml | 207 +++ .../src/main/res/values-fa/strings.xml | 206 +++ .../src/main/res/values-fi/strings.xml | 207 +++ .../src/main/res/values-fr/strings.xml | 207 +++ .../src/main/res/values-gl/strings.xml | 68 + .../src/main/res/values-hu/strings.xml | 206 +++ .../src/main/res/values-id/strings.xml | 12 + .../src/main/res/values-in/strings.xml | 12 + .../src/main/res/values-is/strings.xml | 76 + .../src/main/res/values-it/strings.xml | 303 ++++ .../src/main/res/values-ja/strings.xml | 74 + .../src/main/res/values-ko/strings.xml | 167 ++ .../src/main/res/values-lt/strings.xml | 8 + .../src/main/res/values-lv/strings.xml | 75 + .../src/main/res/values-nl/strings.xml | 215 +++ .../src/main/res/values-nn/strings.xml | 150 ++ .../src/main/res/values-pl/strings.xml | 169 ++ .../src/main/res/values-pt-rBR/strings.xml | 309 ++++ .../src/main/res/values-pt/strings.xml | 87 + .../src/main/res/values-ru/strings.xml | 320 ++++ .../src/main/res/values-sk/strings.xml | 202 +++ .../src/main/res/values-sq/strings.xml | 205 +++ .../src/main/res/values-te/strings.xml | 71 + .../src/main/res/values-th/strings.xml | 4 + .../src/main/res/values-uk/strings.xml | 84 + .../src/main/res/values-vls/strings.xml | 169 ++ .../src/main/res/values-zh-rCN/strings.xml | 296 ++++ .../src/main/res/values-zh-rTW/strings.xml | 297 ++++ .../src/main/res/values/strings.xml | 368 ++++ .../main/res/xml/network_security_config.xml | 16 + .../src/main/res/xml/sdk_provider_paths.xml | 6 + .../interceptors/CurlLoggingInterceptor.kt | 36 + .../interceptors/FormattedJsonHttpLogger.kt | 30 + .../android/sdk/test/shared/TestRules.kt | 27 + .../java/org/matrix/android/sdk/MatrixTest.kt | 26 + .../sdk/api/auth/data/VersionsKtTest.kt | 57 + .../sdk/api/pushrules/PushRuleActionsTest.kt | 82 + .../api/pushrules/PushrulesConditionTest.kt | 218 +++ .../crypto/keysbackup/util/Base58Test.kt | 53 + .../crypto/keysbackup/util/RecoveryKeyTest.kt | 84 + .../internal/crypto/store/db/HelperTest.kt | 38 + .../verification/qrcode/BinaryStringTest.kt | 53 + .../internal/task/CoroutineSequencersTest.kt | 130 ++ settings.gradle | 1 + tools/import_from_element.sh | 49 + 1282 files changed, 101124 insertions(+), 1 deletion(-) create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/jarRepositories.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/runConfigurations.xml create mode 100644 CHANGES.md create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 matrix-sdk-android/.gitignore create mode 100644 matrix-sdk-android/build.gradle create mode 100644 matrix-sdk-android/lint.xml create mode 100644 matrix-sdk-android/proguard-rules.pro create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/InstrumentedTest.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/LiveDataTestObserver.java create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/MainThreadExecutor.java create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/OkReplayRuleChainNoActivity.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/SingleThreadCoroutineDispatcher.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/AccountCreationTest.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/ChangePasswordTest.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/Matrix.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestData.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/MockOkHttpInterceptor.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/SessionTestParams.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestAssertUtil.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestConstants.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixCallback.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixComponent.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestModule.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestNetworkModule.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/AttachmentEncryptionTest.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreHelper.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreTest.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ExportEncryptionTest.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/ExtensionsKtTest.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupPasswordTest.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupScenarioData.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestConstants.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/PrepareKeysBackupDataResult.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/StateObserver.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/HexParser.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeTest.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/SharedSecretTest.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/util/JsonCanonicalizerTest.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/ChunkEntityTest.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/FakeGetContextOfEventTask.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/FakePaginationTask.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/FakeTokenChunkEvent.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/RoomDataHelper.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineBackToPreviousLastForwardTest.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelinePreviousLastForwardTest.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineTest.kt create mode 100644 matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/database/RealmDebugTools.kt create mode 100644 matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/CurlLoggingInterceptor.kt create mode 100644 matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt create mode 100644 matrix-sdk-android/src/main/AndroidManifest.xml create mode 100755 matrix-sdk-android/src/main/assets/postMessageAPI.js create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixCallback.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/Constants.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/Credentials.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/DiscoveryInformation.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/HomeServerConnectionConfig.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowTypes.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SessionParams.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/WellKnown.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/WellKnownBaseConfig.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegisterThreePid.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationResult.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationWizard.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/Stage.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/wellknown/WellknownResult.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/comparators/DatedObjectComparators.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/Emojis.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/MXCryptoConfig.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/RoomEncryptionTrustLevel.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/Booleans.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/MatrixSdkExtensions.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/Strings.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/Try.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Failure.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/GlobalError.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/MatrixError.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/interfaces/DatedObject.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/legacy/LegacySessionImporter.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/listeners/ProgressListener.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/listeners/StepProgressListener.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/permalinks/MatrixLinkify.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/permalinks/MatrixPermalinkSpan.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/permalinks/PermalinkData.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/permalinks/PermalinkFactory.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/permalinks/PermalinkParser.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/Action.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/Condition.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/ConditionResolver.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/ContainsDisplayNameCondition.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/EventMatchCondition.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/RoomMemberCountCondition.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/RuleIds.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/RuleScope.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/RuleSetKey.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/SenderNotificationPermissionCondition.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/GetPushRulesResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/PushCondition.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/PushRule.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/RuleSet.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/QueryStringValue.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/InitialSyncProgressService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/account/AccountService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/AccountDataService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/UserAccountDataEvent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/UserAccountDataTypes.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/cache/CacheService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallSignalingService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallState.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallsListener.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/EglUtils.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/TurnServerResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentAttachmentData.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUploadStateTracker.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUrlResolver.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/MXCryptoError.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CrossSigningService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CrossSigningSsssSecretConstants.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/MXCrossSigningInfo.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupState.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupStateListener.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keyshare/GossipingRequestListener.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/CancelCode.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/EmojiRepresentation.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/IncomingSasVerificationTransaction.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/OutgoingSasVerificationTransaction.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/PendingVerificationRequest.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/QrCodeVerificationTransaction.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/SasMode.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/SasVerificationTransaction.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/ValidVerificationInfoReady.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/ValidVerificationInfoRequest.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationMethod.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationTransaction.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationTxState.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedAnnotation.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/DefaultUnsignedRelationInfo.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/LocalEcho.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationChunkInfo.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationType.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/UnsignedData.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/UnsignedRelationInfo.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/ContentDownloadStateTracker.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/MatrixSDKFileProvider.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/Group.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/GroupService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/GroupSummaryQueryParams.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/model/GroupSummary.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilitiesService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/FoundThreePid.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityServiceError.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityServiceListener.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/SharedState.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/ThreePid.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/integrationmanager/IntegrationManagerConfig.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/integrationmanager/IntegrationManagerService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/Pusher.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomDirectoryService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/call/RoomCallService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/crypto/RoomCryptoService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/failure/CreateRoomFailure.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/failure/JoinRoomFailure.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/ChangeMembershipState.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/MembershipService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/RoomMemberQueryParams.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/EditAggregatedSummary.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/EventAnnotationsSummary.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/Invite.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/Membership.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PollResponseAggregatedSummary.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PollSummaryContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PowerLevelsContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReactionAggregatedSummary.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReadReceipt.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReferencesAggregatedContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReferencesAggregatedSummary.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomAliasesContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomAvatarContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomCanonicalAliasContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomDirectoryVisibility.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomGuestAccessContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomHistoryVisibility.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomHistoryVisibilityContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRules.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRulesContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomMemberContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomMemberSummary.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomNameContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomThirdPartyInviteContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomTopicContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/Signed.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/VersioningState.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallAnswerContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallCandidatesContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallHangupContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallInviteContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/SdpType.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomPreset.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/Predecessor.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/RoomCreateContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/AudioInfo.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/FileInfo.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/ImageInfo.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationInfo.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageAudioContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageContentWithFormattedBody.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageDefaultContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageEmoteContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageFileContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageFormat.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageImageContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageImageInfoContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageNoticeContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageOptionsContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollResponseContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageRelationContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageStickerContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageTextContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationAcceptContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationCancelContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationDoneContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationKeyContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationMacContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationReadyContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationRequestContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationStartContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVideoContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageWithAttachmentContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/OptionItem.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/ThumbnailInfo.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/VideoInfo.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReactionContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReactionInfo.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationDefaultContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReplyToContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/roomdirectory/PublicRoom.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/roomdirectory/PublicRoomsFilter.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/roomdirectory/PublicRoomsParams.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/roomdirectory/PublicRoomsResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/tag/RoomTag.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/tag/RoomTagContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/thirdparty/FieldType.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/thirdparty/RoomDirectoryData.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/thirdparty/ThirdPartyProtocol.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/thirdparty/ThirdPartyProtocolInstance.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/tombstone/RoomTombstoneContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/notification/RoomNotificationState.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/notification/RoomPushRuleService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/PowerLevelsHelper.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/Role.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/reporting/ReportingService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/DraftService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/MatrixItemSpan.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendState.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/UserDraft.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/sender/SenderInfo.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/tags/TagsService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineSettings.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/typing/TypingService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/uploads/GetUploadsResult.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/uploads/UploadEvent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/uploads/UploadsService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/EncryptedSecretContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/IntegrityResult.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/KeyInfoResult.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/KeySigner.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SecretStorageKeyContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SecureStorageService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SharedSecretStorageError.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SharedSecretStorageService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SsssKeyCreationInfo.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SsssKeySpec.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/signout/SignOutService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/FilterService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/SyncState.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/terms/GetTermsResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/terms/TermsService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/typing/TypingUsersTracker.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/UserService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/model/User.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetManagementFailure.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetPostAPIMediator.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetURLFormatter.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/Widget.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetType.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/Cancelable.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/CancelableBag.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/ContentUtils.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixCallbackDelegate.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/Optional.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/Types.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/SessionManager.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthAPI.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthModule.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/PendingSessionStore.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/SessionCreator.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/SessionParamsStore.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/InteractiveAuthenticationFlow.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginParams.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/PasswordLoginParams.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/RiotConfig.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/ThreePidMedium.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/TokenLoginParams.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/AuthRealmMigration.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/AuthRealmModule.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/PendingSessionData.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/PendingSessionEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/PendingSessionMapper.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/RealmPendingSessionStore.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/RealmSessionParamsStore.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/SessionParamsEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/SessionParamsMapper.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DirectLoginTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/ResetPasswordData.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/ResetPasswordMailConfirmed.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/AddThreePidRegistrationParams.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/AddThreePidRegistrationResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/AuthParams.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/LocalizedFlowDataLoginTerms.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegisterAddThreePidTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegisterTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegistrationFlowResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegistrationParams.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/SuccessResult.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/ThreePidData.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/ValidateCodeTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/ValidationCodeBody.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/HomeServerVersion.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CancelGossipRequestWorker.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoConstants.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt create mode 100755 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt create mode 100755 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GossipingRequestState.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GossipingWorkManager.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt create mode 100755 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingRequestCancellation.kt create mode 100755 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingRoomKeyRequest.kt create mode 100755 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingSecretShareRequest.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingShareRequestCommon.kt create mode 100755 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXCryptoAlgorithms.kt create mode 100755 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXEventDecryptionResult.kt create mode 100755 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXMegolmExportEncryption.kt create mode 100755 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MegolmSessionData.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MyDeviceInfoHolder.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/NewSessionListener.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/ObjectSigner.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequest.kt create mode 100755 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequestManager.kt create mode 100755 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingRoomKeyRequest.kt create mode 100755 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingSecretRequest.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomDecryptorProvider.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomEncryptorsStore.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipRequestWorker.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipWorker.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MessageEncrypter.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/SetDeviceVerificationAction.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXEncrypting.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXWithHeldExtension.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/SharedWithHelper.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryptionFactory.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryption.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryptionFactory.kt create mode 100755 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/OlmDecryptionResult.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/api/CryptoApi.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/ElementToDecrypt.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/EncryptionResult.kt create mode 100755 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/ComputeTrustTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DeviceTrustLevel.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DeviceTrustResult.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/Extensions.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/SessionToCryptoRoomMembersUpdate.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/ShieldTrustUpdater.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UserTrustResult.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupPassword.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupStateManager.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/api/RoomKeysApi.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/KeyBackupVersionTrust.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/KeyBackupVersionTrustSignature.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/KeysBackupVersionTrust.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/KeysBackupVersionTrustSignature.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/MegolmBackupAuthData.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/MegolmBackupCreationInfo.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/BackupKeysResult.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/CreateKeysBackupVersionBody.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeyBackupData.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysAlgorithmAndData.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysBackupData.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysVersion.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysVersionResult.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/RoomKeysBackupData.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/UpdateKeysBackupVersionBody.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/CreateKeysBackupVersionTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteBackupTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteRoomSessionDataTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteRoomSessionsDataTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteSessionsDataTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetKeysBackupLastVersionTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetKeysBackupVersionTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetRoomSessionDataTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetRoomSessionsDataTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetSessionsDataTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreRoomSessionDataTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreRoomSessionsDataTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreSessionsDataTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/UpdateKeysBackupVersionTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/Base58.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/RecoveryKey.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoCrossSigningKey.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoDeviceInfo.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoInfo.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoInfoMapper.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/ImportRoomKeysResult.kt create mode 100755 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXDeviceInfo.kt create mode 100755 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXEncryptEventContentResult.kt create mode 100755 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXKey.kt create mode 100755 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXOlmSessionResult.kt create mode 100755 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXQueuedEncryption.kt create mode 100755 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXUsersDevicesMap.kt create mode 100755 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmInboundGroupSessionWrapper.kt create mode 100755 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmInboundGroupSessionWrapper2.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmSessionWrapper.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/EncryptedEventContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/EncryptionEventContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/NewDeviceContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/OlmEventContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/OlmPayloadContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/RoomKeyContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/RoomKeyWithHeldContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/SecretSendEventContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDeviceParams.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeviceInfo.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeviceKeys.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeviceKeysWithUnsigned.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DevicesListResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DummyContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedBodyFileInfo.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedFileInfo.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedFileKey.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedMessage.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/ForwardedRoomKeyContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/GossipingToDeviceObject.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyChangesResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationAccept.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationCancel.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationDone.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationKey.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationMac.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationReady.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationRequest.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationStart.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysClaimBody.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysClaimResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysQueryBody.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysQueryResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysUploadBody.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysUploadResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/RestKeyInfo.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/RoomKeyRequestBody.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/RoomKeyShareRequest.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/SecretShareRequest.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/SendToDeviceBody.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/SendToDeviceObject.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/ShareRequestCancellation.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/SignatureUploadResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UnsignedDeviceInfo.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UpdateDeviceInfoBody.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UploadSignatureQueryBuilder.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UploadSigningKeysBody.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UserPasswordAuth.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/VerificationMethodValues.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/repository/WarnOnUnknownDeviceRepository.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/PrivateKeysInfo.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/SavedKeyBackupKeyInfo.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/Helper.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreModule.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/SafeObjectInputStream.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/CrossSigningKeysMapper.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CrossSigningInfoEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMapper.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMetadataEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoRoomEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/DeviceInfoEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/GossipingEventEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/IncomingGossipingRequestEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/KeyInfoEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/KeysBackupDataEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/MyDeviceLastSeenInfoEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OlmSessionEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingGossipingRequestEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/SharedSessionEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/TrustLevelEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/UserEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/WithHeldSessionEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/CrossSigningInfoEntityQueries.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/CryptoRoomEntityQueries.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/DeviceInfoEntityQueries.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/SharedSessionQueries.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/UserEntitiesQueries.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/WithHeldSessionQueries.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/ClaimOneTimeKeysForUsersDeviceTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DownloadKeysForUsersTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/GetDeviceInfoTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/GetDevicesTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/GetKeyChangesTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendToDeviceTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SetDeviceNameTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadKeysTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSignaturesTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSigningKeysTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tools/HkdfSha256.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tools/Tools.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationTransaction.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SendVerificationMessageWorker.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationEmoji.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfo.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoAccept.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoCancel.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoDone.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoKey.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoMac.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoReady.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoRequest.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoStart.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransport.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDevice.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/Extensions.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeData.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/SharedSecret.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/DBConstants.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/DatabaseCleaner.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmKeysUtils.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmLiveEntityObserver.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmQueryLatch.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/SessionRealmConfigurationFactory.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/RoomEntityHelper.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/TimelineEventEntityHelper.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/AccountDataMapper.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ContentMapper.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/DraftMapper.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/GroupSummaryMapper.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/IsUselessResolver.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PollResponseAggregatedSummaryEntityMapper.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PushConditionMapper.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PushRulesMapper.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PushersMapper.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ReadReceiptsSummaryMapper.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomMemberSummaryMapper.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/TimelineEventMapper.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/UserMapper.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/BreadcrumbsEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/CurrentStateEventEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/DraftEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EditAggregatedSummaryEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventAnnotationsSummaryEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventInsertEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventInsertType.java create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/FilterEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/GroupEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/GroupSummaryEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/IgnoredUserEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollResponseAggregatedSummaryEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PushConditionEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PushRuleEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PushRulesEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PusherDataEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PusherEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReactionAggregatedSummaryEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReadMarkerEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReadReceiptEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReadReceiptsSummaryEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReferencesAggregatedSummaryEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomMemberSummaryEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomTagEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ScalarTokenEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SyncEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/UserAccountDataEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/UserDraftsEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/UserEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/UserThreePidEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/WellknownIntegrationManagerConfigEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/BreadcrumbsEntityQuery.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/CurrentStateEventEntityQueries.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/FilterEntityQueries.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/GroupEntityQueries.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/GroupSummaryEntityQueries.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/HomeServerCapabilitiesQueries.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/PushersQueries.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadMarkerEntityQueries.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptsSummaryEntityQueries.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReferencesAggregatedSummaryEntityQueries.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomEntityQueries.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomMemberEntityQueries.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomSummaryEntityQueries.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ScalarTokenQuery.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventFilter.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/UserDraftsEntityQueries.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/UserEntityQueries.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/AuthQualifiers.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/DbQualifiers.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/FileQualifiers.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixModule.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixScope.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MoshiProvider.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NoOpTestModule.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/SerializeNulls.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/SessionAssistedInjectModule.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/StringQualifiers.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/eventbus/EventBusTimberLogger.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/LiveData.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Primitives.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/RealmExtensions.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Result.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Try.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/DefaultLegacySessionImporter.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/Credentials.java create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/Fingerprint.java create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/HomeServerConnectionConfig.java create mode 100755 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/LoginStorage.java create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnown.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnownBaseConfig.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnownManagerConfig.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnownPreferredConfig.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/AccessTokenInterceptor.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/HttpHeaders.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkCallbackStrategy.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConnectivityChecker.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkInfoReceiver.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ProgressRequestBody.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitExtensions.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitFactory.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/TimeOutInterceptor.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UnitConverterFactory.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UserAgentHolder.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UserAgentInterceptor.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/httpclient/OkHttpClientUtil.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/ForceToBoolean.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/RuntimeJsonAdapterFactory.java create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/UriMoshiAdapter.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/CertUtil.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/Fingerprint.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManager.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManagerApi24.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManagerProvider.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/TLSSocketFactory.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/UnrecognizedCertificateException.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/token/AccessTokenProvider.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/token/HomeserverAccessTokenProvider.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryEnumListProcessor.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryStringValueProcessor.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultInitialSyncProgressService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/EventInsertLiveProcessor.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionLifecycleObserver.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionListeners.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionScope.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/TestInterceptor.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/AccountAPI.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/AccountModule.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/ChangePasswordParams.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/ChangePasswordTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountParams.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DefaultAccountService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/CacheModule.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/ClearCacheTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/DefaultCacheService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallModule.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/DefaultCallSignalingService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/GetTurnServerTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/VoipApi.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cleanup/CleanupSession.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ContentModule.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ContentUploadResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUploadStateTracker.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUrlResolver.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ThumbnailExtractor.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/download/DefaultContentDownloadStateTracker.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/download/DownloadProgressInterceptor.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/download/ProgressResponseBody.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterRepository.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultSaveFilterTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/EventFilter.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/Filter.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterApi.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterModule.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterRepository.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterUtil.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomFilter.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGetGroupDataTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGroup.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGroupService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GetGroupDataWorker.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GroupAPI.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GroupFactory.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GroupModule.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupProfile.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupRoom.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupRooms.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryRoomsSection.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryUser.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryUsersSection.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupUser.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupUsers.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/CapabilitiesAPI.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultHomeServerCapabilitiesService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetUploadCapabilitiesResult.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/HomeServerCapabilitiesModule.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/HomeServerPinger.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/EnsureIdentityToken.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAPI.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAccessTokenProvider.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityApiProvider.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAuthAPI.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityDisconnectTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityModule.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityPingTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityRegisterTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityRequestTokenForBindingTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentitySubmitTokenForBindingTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityTaskHelper.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/data/IdentityData.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/data/IdentityPendingBinding.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/data/IdentityStore.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityDataEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityDataEntityQuery.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityMapper.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityPendingBindingEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityPendingBindingEntityQuery.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityRealmModule.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/RealmIdentityStore.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityAccountResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityHashDetailResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityLookUpParams.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityLookUpResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityRegisterResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityRequestOwnershipParams.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityRequestTokenBody.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityRequestTokenResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/AllowedWidgetsContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/DefaultIntegrationManagerService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManager.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManagerConfigExtractor.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManagerModule.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManagerWidgetData.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationProvisioningContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/GetOpenIdTokenTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/OpenIdAPI.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/OpenIdModule.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/RequestOpenIdTokenResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AccountThreePidsResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/BindThreePidBody.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/BindThreePidsTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/GetProfileInfoTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileAPI.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileModule.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/RefreshUserThreePidsTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/SetAvatarUrlBody.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/SetAvatarUrlTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/SetDisplayNameBody.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/SetDisplayNameTask.kt create mode 100755 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ThirdPartyIdentifier.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/UnbindThreePidBody.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/UnbindThreePidResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/UnbindThreePidsTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddHttpPusherWorker.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddPushRuleTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultConditionResolver.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/GetPushRulesTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/GetPushersResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/GetPushersTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/JsonPusher.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/JsonPusherData.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushRulesApi.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushersAPI.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushersModule.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/RemovePushRuleTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/RemovePusherTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/SavePushRulesTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/UpdatePushRuleActionsTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/UpdatePushRuleEnableStatusTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomDirectoryService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAvatarResolver.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomGetter.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/AddRoomAliasBody.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/AddRoomAliasTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomIdByAliasTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasDescription.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/call/DefaultRoomCallService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBody.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/RoomCreateEventProcessor.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/directory/GetPublicRoomTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/directory/GetThirdPartyProtocolsTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DefaultDraftService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DraftRepository.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomChangeMembershipStateDataSource.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEntityFactory.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEventHandler.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberHelper.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMembersResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/admin/MembershipAdminTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/admin/UserIdAndReason.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/InviteBody.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/InviteTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/leaving/LeaveRoomTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/threepid/InviteThreePidTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/threepid/ThreePidInviteBody.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/DefaultRoomPushRuleService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/RoomPushRule.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/RoomPushRuleMapper.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/SetRoomNotificationStateTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/FullyReadContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/MarkAllRoomsReadTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/FetchEditHistoryTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/FindReactionEventForUndoTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/RelationsResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/SendRelationWorker.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/UpdateQuickReactionTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/DefaultReportingService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/ReportContentBody.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/ReportContentTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/EncryptEventWorker.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParser.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/NoMerger.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RedactEventWorker.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RoomEventSender.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/TextContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/MentionLinkSpec.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/MentionLinkSpecComparator.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/SendStateTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/StateEventDataSource.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/AddTagToRoomTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/DefaultTagsService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/DeleteTagFromRoomTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/TagBody.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultGetContextOfEventTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultPaginationTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/EventContextResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/FetchTokenAndPaginateTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationDirection.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineHiddenReadReceipts.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineSendEventWorkCommon.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEvent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tombstone/RoomTombstoneEventProcessor.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/DefaultTypingService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/SendTypingTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/TypingBody.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/TypingEventContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/DefaultUploadsService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/GetUploadsTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/DefaultSecureStorageService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/DefaultSignOutService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignInAgainTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutAPI.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutModule.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/CryptoSyncHandler.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/GroupSyncHandler.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/ReadReceiptHandler.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomFullyReadHandler.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomTagHandler.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomTypingUsersHandler.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncAPI.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncModule.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTaskSequencer.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTokenStore.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/UserAccountDataSyncHandler.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DeviceInfo.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DeviceListResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DeviceOneTimeKeysCountSyncResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DevicesListResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/GroupSyncProfile.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/GroupsSyncResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/InvitedGroupSync.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/InvitedRoomSync.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/PresenceSyncResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomInviteState.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSync.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncAccountData.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncEphemeral.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncState.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncSummary.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncTimeline.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncUnreadNotifications.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomsSyncResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/SyncResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/ToDeviceSyncResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/TokensChunkResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/AcceptedTermsContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/BreadcrumbsContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/DirectMessagesContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/IdentityServerContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/IgnoredUsersContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/UserAccountDataSync.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/AcceptTermsBody.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/TermsAPI.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/TermsModule.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/TermsResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/typing/DefaultTypingUsersTracker.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/DefaultUserService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/SearchUserAPI.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserDataSource.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserEntityFactory.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserModule.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserStore.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataAPI.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataDataSource.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataModule.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultAccountDataService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DirectChatsHelper.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/SaveBreadcrumbsTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/SaveIgnoredUsersTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UpdateBreadcrumbsTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UpdateIgnoredUserIdsTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UpdateUserAccountDataTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/model/SearchUser.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/model/SearchUserTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/model/SearchUsersParams.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/model/SearchUsersResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/CreateWidgetTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetPostAPIMediator.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetURLFormatter.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/RegisterWidgetResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetModule.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetPostMessageAPIProvider.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetsAPI.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetsAPIProvider.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/UserAccountWidgets.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/WidgetFactory.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/token/GetScalarTokenTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/token/ScalarTokenStore.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/ConfigurableTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/CoroutineSequencer.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/CoroutineToCallback.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/Task.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/TaskExecutor.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/TaskThread.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/BackgroundDetectionObserver.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/CancelableCoroutine.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/CancelableWork.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/CompatUtil.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Debouncer.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Exhaustive.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/FileSaver.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Handler.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Hash.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/JsonCanonicalizer.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/LiveDataUtils.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/MatrixCoroutineDispatchers.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Monarchy.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/SecretKeyAndVersion.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringProvider.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/SuspendMatrixCallback.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/UrlUtils.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/GetWellknownTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/WellKnownAPI.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/WellknownModule.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/AlwaysSuccessfulWorker.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/DelegateWorkerFactory.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/Extensions.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/MatrixWorkerFactory.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/SessionWorkerParams.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/Worker.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/WorkerParamsFactory.kt create mode 100755 matrix-sdk-android/src/main/java/org/matrix/androidsdk/crypto/data/MXDeviceInfo.java create mode 100755 matrix-sdk-android/src/main/java/org/matrix/androidsdk/crypto/data/MXOlmInboundGroupSession2.java create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_airplane.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_anchor.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_apple.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_ball.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_banana.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_bell.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_bicycle.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_book.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_butterfly.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_cactus.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_cake.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_cat.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_clock.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_cloud.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_corn.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_dog.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_elephant.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_fire.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_fish.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_flag.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_flower.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_folder.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_gift.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_glasses.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_globe.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_guitar.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_hammer.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_hat.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_headphone.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_heart.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_horse.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_hourglass.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_key.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_light_bulb.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_lion.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_lock.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_moon.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_mushroom.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_octopus.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_panda.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_paperclip.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_pencil.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_penguin.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_phone.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_pig.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_pin.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_pizza.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_rabbit.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_robot.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_rocket.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_rooster.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_santa.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_scissors.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_smiley.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_strawberry.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_thumbs_up.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_train.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_tree.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_trophy.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_trumpet.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_turtle.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_umbrella.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_unicorn.xml create mode 100644 matrix-sdk-android/src/main/res/drawable/ic_verification_wrench.xml create mode 100644 matrix-sdk-android/src/main/res/values-ar/strings.xml create mode 100644 matrix-sdk-android/src/main/res/values-az/strings.xml create mode 100644 matrix-sdk-android/src/main/res/values-bg/strings.xml create mode 100644 matrix-sdk-android/src/main/res/values-bn-rIN/strings.xml create mode 100644 matrix-sdk-android/src/main/res/values-bs/strings.xml create mode 100644 matrix-sdk-android/src/main/res/values-ca/strings.xml create mode 100644 matrix-sdk-android/src/main/res/values-cs/strings.xml create mode 100644 matrix-sdk-android/src/main/res/values-da/strings.xml create mode 100644 matrix-sdk-android/src/main/res/values-de/strings.xml create mode 100644 matrix-sdk-android/src/main/res/values-el/strings.xml create mode 100644 matrix-sdk-android/src/main/res/values-en-rGB/strings.xml create mode 100644 matrix-sdk-android/src/main/res/values-eo/strings.xml create mode 100644 matrix-sdk-android/src/main/res/values-es-rMX/strings.xml create mode 100644 matrix-sdk-android/src/main/res/values-es/strings.xml create mode 100644 matrix-sdk-android/src/main/res/values-et/strings.xml create mode 100644 matrix-sdk-android/src/main/res/values-eu/strings.xml create mode 100644 matrix-sdk-android/src/main/res/values-fa/strings.xml create mode 100644 matrix-sdk-android/src/main/res/values-fi/strings.xml create mode 100644 matrix-sdk-android/src/main/res/values-fr/strings.xml create mode 100644 matrix-sdk-android/src/main/res/values-gl/strings.xml create mode 100644 matrix-sdk-android/src/main/res/values-hu/strings.xml create mode 100644 matrix-sdk-android/src/main/res/values-id/strings.xml create mode 100644 matrix-sdk-android/src/main/res/values-in/strings.xml create mode 100644 matrix-sdk-android/src/main/res/values-is/strings.xml create mode 100644 matrix-sdk-android/src/main/res/values-it/strings.xml create mode 100644 matrix-sdk-android/src/main/res/values-ja/strings.xml create mode 100644 matrix-sdk-android/src/main/res/values-ko/strings.xml create mode 100644 matrix-sdk-android/src/main/res/values-lt/strings.xml create mode 100644 matrix-sdk-android/src/main/res/values-lv/strings.xml create mode 100644 matrix-sdk-android/src/main/res/values-nl/strings.xml create mode 100644 matrix-sdk-android/src/main/res/values-nn/strings.xml create mode 100644 matrix-sdk-android/src/main/res/values-pl/strings.xml create mode 100644 matrix-sdk-android/src/main/res/values-pt-rBR/strings.xml create mode 100644 matrix-sdk-android/src/main/res/values-pt/strings.xml create mode 100644 matrix-sdk-android/src/main/res/values-ru/strings.xml create mode 100644 matrix-sdk-android/src/main/res/values-sk/strings.xml create mode 100644 matrix-sdk-android/src/main/res/values-sq/strings.xml create mode 100644 matrix-sdk-android/src/main/res/values-te/strings.xml create mode 100644 matrix-sdk-android/src/main/res/values-th/strings.xml create mode 100644 matrix-sdk-android/src/main/res/values-uk/strings.xml create mode 100644 matrix-sdk-android/src/main/res/values-vls/strings.xml create mode 100644 matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml create mode 100644 matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml create mode 100644 matrix-sdk-android/src/main/res/values/strings.xml create mode 100644 matrix-sdk-android/src/main/res/xml/network_security_config.xml create mode 100644 matrix-sdk-android/src/main/res/xml/sdk_provider_paths.xml create mode 100644 matrix-sdk-android/src/release/java/org/matrix/android/sdk/internal/network/interceptors/CurlLoggingInterceptor.kt create mode 100644 matrix-sdk-android/src/release/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt create mode 100644 matrix-sdk-android/src/sharedTest/java/org/matrix/android/sdk/test/shared/TestRules.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/MatrixTest.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/auth/data/VersionsKtTest.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/pushrules/PushRuleActionsTest.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/pushrules/PushrulesConditionTest.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/Base58Test.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/RecoveryKeyTest.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/store/db/HelperTest.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/BinaryStringTest.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/task/CoroutineSequencersTest.kt create mode 100644 settings.gradle create mode 100755 tools/import_from_element.sh diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000000..681f41ae2a --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,116 @@ + + + + + + + +
+ + + + xmlns:android + + ^$ + + + +
+
+ + + + xmlns:.* + + ^$ + + + BY_NAME + +
+
+ + + + .*:id + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:name + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + name + + ^$ + + + +
+
+ + + + style + + ^$ + + + +
+
+ + + + .* + + ^$ + + + BY_NAME + +
+
+ + + + .* + + http://schemas.android.com/apk/res/android + + + ANDROID_ATTRIBUTE_ORDER + +
+
+ + + + .* + + .* + + + BY_NAME + +
+
+
+
+
+
\ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000000..eb2873e7ed --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000000..7bfef59df1 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000000..7f68460d8b --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 0000000000..040b60d1a4 --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,9 @@ +Please refer to the Changelog of Element Android: https://github.com/vector-im/element-android/blob/master/CHANGES.md + +Changes in Matrix-SDK 0.0.1 (2020-08-14) +=================================================== + +This is the first release of the Matrix SDK. + +This first release has been created from the develop branch of Element Android ([at this commit](https://github.com/vector-im/element-android/commit/5a3894036cb34d00177603e69c5b15431212152d)). +Next releases will be done from master branch of Element Android and the version name will be the same between the SDK and Element Android diff --git a/README.md b/README.md index 4ae2d0a11c..9e7074ee5a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,25 @@ # matrix-android-sdk2 -Matrix SDK for Android, extracted from the Element Android application + +Matrix SDK for Android, extracted from the Element Android application. + +The SDK is still in beta, and replaces the [legacy Matrix Android SDK](https://github.com/matrix-org/matrix-android-sdk) provided by Matrix.org + +## About + +This repository contains the matrix-android-sdk extracted from the project [Element Android](https://github.com/vector-im/element-android) + +Please open any issue in the Element Android project [Create an issue](https://github.com/vector-im/element-android/issues/new/choose) + +## How to integrate the SDK in your application + +To integrate the SDK to your application, add the following gradle dependency to the build.gradle of your application module: + +> implementation 'com.github.matrix-org:matrix-android-sdk2:v0.0.1' + +You need to add Jitpack as a repository in your main build.gradle file. Please follow instructions here: https://jitpack.io/ + +## Migrate from legacy SDK + +Sadly there is no official documentation on how to migrate from the old SDK to the new one. Because the new SDK API is totally new, we guess that there is no easy way to handle a migration. + +We advice that new applications uses this new SDK. \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000000..44384b9f7d --- /dev/null +++ b/build.gradle @@ -0,0 +1,48 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + ext.kotlin_version = '1.3.72' + repositories { + google() + jcenter() + maven { + url "https://plugins.gradle.org/m2/" + } + } + dependencies { + // Warning: 3.6.3 leads to infinite gradle builds. Stick to 3.5.3 for the moment + classpath 'com.android.tools.build:gradle:3.5.3' + classpath "com.airbnb.okreplay:gradle-plugin:1.5.0" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + // For olm library. This has to be declared first, to ensure that Olm library is not downloaded from another repo + maven { + url 'https://jitpack.io' + content { + // Use this repo only for olm library + includeGroupByRegex "org\\.matrix\\.gitlab\\.matrix-org" + // And monarchy + includeGroupByRegex "com\\.github\\.Zhuinden" + } + } + google() + jcenter() + } + + tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { + // Warnings are potential errors, so stop ignoring them + kotlinOptions.allWarningsAsErrors = true + } + +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000000..99fd9d64fd --- /dev/null +++ b/gradle.properties @@ -0,0 +1,23 @@ +# 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. +android.enableJetifier=true +android.useAndroidX=true +org.gradle.jvmargs=-Xmx2048m +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + + +vector.debugPrivateData=false +vector.httpLogLevel=NONE + +# Note: to debug, you can put and uncomment the following lines in the file ~/.gradle/gradle.properties to override the value above +#vector.debugPrivateData=true +#vector.httpLogLevel=BODY diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..87b738cbd051603d91cc39de6cb000dd98fe6b02 GIT binary patch literal 55190 zcmafaW0WS*vSoFbZQHhO+s0S6%`V%vZQJa!ZQHKus_B{g-pt%P_q|ywBQt-*Stldc z$+IJ3?^KWm27v+sf`9-50uuadKtMnL*BJ;1^6ynvR7H?hQcjE>7)art9Bu0Pcm@7C z@c%WG|JzYkP)<@zR9S^iR_sA`azaL$mTnGKnwDyMa;8yL_0^>Ba^)phg0L5rOPTbm7g*YIRLg-2^{qe^`rb!2KqS zk~5wEJtTdD?)3+}=eby3x6%i)sb+m??NHC^u=tcG8p$TzB<;FL(WrZGV&cDQb?O0GMe6PBV=V z?tTO*5_HTW$xea!nkc~Cnx#cL_rrUGWPRa6l+A{aiMY=<0@8y5OC#UcGeE#I>nWh}`#M#kIn-$A;q@u-p71b#hcSItS!IPw?>8 zvzb|?@Ahb22L(O4#2Sre&l9H(@TGT>#Py)D&eW-LNb!=S;I`ZQ{w;MaHW z#to!~TVLgho_Pm%zq@o{K3Xq?I|MVuVSl^QHnT~sHlrVxgsqD-+YD?Nz9@HA<;x2AQjxP)r6Femg+LJ-*)k%EZ}TTRw->5xOY z9#zKJqjZgC47@AFdk1$W+KhTQJKn7e>A&?@-YOy!v_(}GyV@9G#I?bsuto4JEp;5|N{orxi_?vTI4UF0HYcA( zKyGZ4<7Fk?&LZMQb6k10N%E*$gr#T&HsY4SPQ?yerqRz5c?5P$@6dlD6UQwZJ*Je9 z7n-@7!(OVdU-mg@5$D+R%gt82Lt%&n6Yr4=|q>XT%&^z_D*f*ug8N6w$`woqeS-+#RAOfSY&Rz z?1qYa5xi(7eTCrzCFJfCxc%j{J}6#)3^*VRKF;w+`|1n;Xaojr2DI{!<3CaP`#tXs z*`pBQ5k@JLKuCmovFDqh_`Q;+^@t_;SDm29 zCNSdWXbV?9;D4VcoV`FZ9Ggrr$i<&#Dx3W=8>bSQIU_%vf)#(M2Kd3=rN@^d=QAtC zI-iQ;;GMk|&A++W5#hK28W(YqN%?!yuW8(|Cf`@FOW5QbX|`97fxmV;uXvPCqxBD zJ9iI37iV)5TW1R+fV16y;6}2tt~|0J3U4E=wQh@sx{c_eu)t=4Yoz|%Vp<#)Qlh1V z0@C2ZtlT>5gdB6W)_bhXtcZS)`9A!uIOa`K04$5>3&8An+i9BD&GvZZ=7#^r=BN=k za+=Go;qr(M)B~KYAz|<^O3LJON}$Q6Yuqn8qu~+UkUKK~&iM%pB!BO49L+?AL7N7o z(OpM(C-EY753=G=WwJHE`h*lNLMNP^c^bBk@5MyP5{v7x>GNWH>QSgTe5 z!*GPkQ(lcbEs~)4ovCu!Zt&$${9$u(<4@9%@{U<-ksAqB?6F`bQ;o-mvjr)Jn7F&j$@`il1Mf+-HdBs<-`1FahTxmPMMI)@OtI&^mtijW6zGZ67O$UOv1Jj z;a3gmw~t|LjPkW3!EZ=)lLUhFzvO;Yvj9g`8hm%6u`;cuek_b-c$wS_0M4-N<@3l|88 z@V{Sd|M;4+H6guqMm4|v=C6B7mlpP(+It%0E;W`dxMOf9!jYwWj3*MRk`KpS_jx4c z=hrKBkFK;gq@;wUV2eqE3R$M+iUc+UD0iEl#-rECK+XmH9hLKrC={j@uF=f3UiceB zU5l$FF7#RKjx+6!JHMG5-!@zI-eG=a-!Bs^AFKqN_M26%cIIcSs61R$yuq@5a3c3& z4%zLs!g}+C5%`ja?F`?5-og0lv-;(^e<`r~p$x%&*89_Aye1N)9LNVk?9BwY$Y$$F^!JQAjBJvywXAesj7lTZ)rXuxv(FFNZVknJha99lN=^h`J2> zl5=~(tKwvHHvh|9-41@OV`c;Ws--PE%{7d2sLNbDp;A6_Ka6epzOSFdqb zBa0m3j~bT*q1lslHsHqaHIP%DF&-XMpCRL(v;MV#*>mB^&)a=HfLI7efblG z(@hzN`|n+oH9;qBklb=d^S0joHCsArnR1-h{*dIUThik>ot^!6YCNjg;J_i3h6Rl0ji)* zo(tQ~>xB!rUJ(nZjCA^%X;)H{@>uhR5|xBDA=d21p@iJ!cH?+%U|VSh2S4@gv`^)^ zNKD6YlVo$%b4W^}Rw>P1YJ|fTb$_(7C;hH+ z1XAMPb6*p^h8)e5nNPKfeAO}Ik+ZN_`NrADeeJOq4Ak;sD~ zTe77no{Ztdox56Xi4UE6S7wRVxJzWxKj;B%v7|FZ3cV9MdfFp7lWCi+W{}UqekdpH zdO#eoOuB3Fu!DU`ErfeoZWJbWtRXUeBzi zBTF-AI7yMC^ntG+8%mn(I6Dw}3xK8v#Ly{3w3_E?J4(Q5JBq~I>u3!CNp~Ekk&YH` z#383VO4O42NNtcGkr*K<+wYZ>@|sP?`AQcs5oqX@-EIqgK@Pmp5~p6O6qy4ml~N{D z{=jQ7k(9!CM3N3Vt|u@%ssTw~r~Z(}QvlROAkQQ?r8OQ3F0D$aGLh zny+uGnH5muJ<67Z=8uilKvGuANrg@s3Vu_lU2ajb?rIhuOd^E@l!Kl0hYIxOP1B~Q zggUmXbh$bKL~YQ#!4fos9UUVG#}HN$lIkM<1OkU@r>$7DYYe37cXYwfK@vrHwm;pg zbh(hEU|8{*d$q7LUm+x&`S@VbW*&p-sWrplWnRM|I{P;I;%U`WmYUCeJhYc|>5?&& zj}@n}w~Oo=l}iwvi7K6)osqa;M8>fRe}>^;bLBrgA;r^ZGgY@IC^ioRmnE&H4)UV5 zO{7egQ7sBAdoqGsso5q4R(4$4Tjm&&C|7Huz&5B0wXoJzZzNc5Bt)=SOI|H}+fbit z-PiF5(NHSy>4HPMrNc@SuEMDuKYMQ--G+qeUPqO_9mOsg%1EHpqoX^yNd~~kbo`cH zlV0iAkBFTn;rVb>EK^V6?T~t~3vm;csx+lUh_%ROFPy0(omy7+_wYjN!VRDtwDu^h4n|xpAMsLepm% zggvs;v8+isCW`>BckRz1MQ=l>K6k^DdT`~sDXTWQ<~+JtY;I~I>8XsAq3yXgxe>`O zZdF*{9@Z|YtS$QrVaB!8&`&^W->_O&-JXn1n&~}o3Z7FL1QE5R*W2W@=u|w~7%EeC1aRfGtJWxImfY-D3t!!nBkWM> zafu>^Lz-ONgT6ExjV4WhN!v~u{lt2-QBN&UxwnvdH|I%LS|J-D;o>@@sA62@&yew0 z)58~JSZP!(lX;da!3`d)D1+;K9!lyNlkF|n(UduR-%g>#{`pvrD^ClddhJyfL7C-(x+J+9&7EsC~^O`&}V%)Ut8^O_7YAXPDpzv8ir4 zl`d)(;imc6r16k_d^)PJZ+QPxxVJS5e^4wX9D=V2zH&wW0-p&OJe=}rX`*->XT=;_qI&)=WHkYnZx6bLoUh_)n-A}SF_ z9z7agNTM5W6}}ui=&Qs@pO5$zHsOWIbd_&%j^Ok5PJ3yUWQw*i4*iKO)_er2CDUME ztt+{Egod~W-fn^aLe)aBz)MOc_?i-stTj}~iFk7u^-gGSbU;Iem06SDP=AEw9SzuF zeZ|hKCG3MV(z_PJg0(JbqTRf4T{NUt%kz&}4S`)0I%}ZrG!jgW2GwP=WTtkWS?DOs znI9LY!dK+1_H0h+i-_~URb^M;4&AMrEO_UlDV8o?E>^3x%ZJyh$JuDMrtYL8|G3If zPf2_Qb_W+V?$#O; zydKFv*%O;Y@o_T_UAYuaqx1isMKZ^32JtgeceA$0Z@Ck0;lHbS%N5)zzAW9iz; z8tTKeK7&qw!8XVz-+pz>z-BeIzr*#r0nB^cntjQ9@Y-N0=e&ZK72vlzX>f3RT@i7@ z=z`m7jNk!9%^xD0ug%ptZnM>F;Qu$rlwo}vRGBIymPL)L|x}nan3uFUw(&N z24gdkcb7!Q56{0<+zu zEtc5WzG2xf%1<@vo$ZsuOK{v9gx^0`gw>@h>ZMLy*h+6ueoie{D#}}` zK2@6Xxq(uZaLFC%M!2}FX}ab%GQ8A0QJ?&!vaI8Gv=vMhd);6kGguDmtuOElru()) zuRk&Z{?Vp!G~F<1#s&6io1`poBqpRHyM^p;7!+L??_DzJ8s9mYFMQ0^%_3ft7g{PD zZd}8E4EV}D!>F?bzcX=2hHR_P`Xy6?FOK)mCj)Ym4s2hh z0OlOdQa@I;^-3bhB6mpw*X5=0kJv8?#XP~9){G-+0ST@1Roz1qi8PhIXp1D$XNqVG zMl>WxwT+K`SdO1RCt4FWTNy3!i?N>*-lbnn#OxFJrswgD7HjuKpWh*o@QvgF&j+CT z{55~ZsUeR1aB}lv#s_7~+9dCix!5(KR#c?K?e2B%P$fvrsZxy@GP#R#jwL{y#Ld$} z7sF>QT6m|}?V;msb?Nlohj7a5W_D$y+4O6eI;Zt$jVGymlzLKscqer9#+p2$0It&u zWY!dCeM6^B^Z;ddEmhi?8`scl=Lhi7W%2|pT6X6^%-=q90DS(hQ-%c+E*ywPvmoF(KqDoW4!*gmQIklm zk#!GLqv|cs(JRF3G?=AYY19{w@~`G3pa z@xR9S-Hquh*&5Yas*VI};(%9%PADn`kzm zeWMJVW=>>wap*9|R7n#!&&J>gq04>DTCMtj{P^d12|2wXTEKvSf?$AvnE!peqV7i4 zE>0G%CSn%WCW1yre?yi9*aFP{GvZ|R4JT}M%x_%Hztz2qw?&28l&qW<6?c6ym{f$d z5YCF+k#yEbjCN|AGi~-NcCG8MCF1!MXBFL{#7q z)HO+WW173?kuI}^Xat;Q^gb4Hi0RGyB}%|~j8>`6X4CPo+|okMbKy9PHkr58V4bX6<&ERU)QlF8%%huUz&f+dwTN|tk+C&&o@Q1RtG`}6&6;ncQuAcfHoxd5AgD7`s zXynq41Y`zRSiOY@*;&1%1z>oNcWTV|)sjLg1X8ijg1Y zbIGL0X*Sd}EXSQ2BXCKbJmlckY(@EWn~Ut2lYeuw1wg?hhj@K?XB@V_ZP`fyL~Yd3n3SyHU-RwMBr6t-QWE5TinN9VD4XVPU; zonIIR!&pGqrLQK)=#kj40Im%V@ij0&Dh0*s!lnTw+D`Dt-xmk-jmpJv$1-E-vfYL4 zqKr#}Gm}~GPE+&$PI@4ag@=M}NYi7Y&HW82Q`@Y=W&PE31D110@yy(1vddLt`P%N^ z>Yz195A%tnt~tvsSR2{m!~7HUc@x<&`lGX1nYeQUE(%sphTi>JsVqSw8xql*Ys@9B z>RIOH*rFi*C`ohwXjyeRBDt8p)-u{O+KWP;$4gg||%*u{$~yEj+Al zE(hAQRQ1k7MkCq9s4^N3ep*$h^L%2Vq?f?{+cicpS8lo)$Cb69b98au+m2J_e7nYwID0@`M9XIo1H~|eZFc8Hl!qly612ADCVpU zY8^*RTMX(CgehD{9v|^9vZ6Rab`VeZ2m*gOR)Mw~73QEBiktViBhR!_&3l$|be|d6 zupC`{g89Y|V3uxl2!6CM(RNpdtynaiJ~*DqSTq9Mh`ohZnb%^3G{k;6%n18$4nAqR zjPOrP#-^Y9;iw{J@XH9=g5J+yEVh|e=4UeY<^65`%gWtdQ=-aqSgtywM(1nKXh`R4 zzPP&7r)kv_uC7X9n=h=!Zrf<>X=B5f<9~Q>h#jYRD#CT7D~@6@RGNyO-#0iq0uHV1 zPJr2O4d_xLmg2^TmG7|dpfJ?GGa`0|YE+`2Rata9!?$j#e9KfGYuLL(*^z z!SxFA`$qm)q-YKh)WRJZ@S+-sD_1E$V?;(?^+F3tVcK6 z2fE=8hV*2mgiAbefU^uvcM?&+Y&E}vG=Iz!%jBF7iv){lyC`)*yyS~D8k+Mx|N3bm zI~L~Z$=W9&`x)JnO;8c>3LSDw!fzN#X3qi|0`sXY4?cz{*#xz!kvZ9bO=K3XbN z5KrgN=&(JbXH{Wsu9EdmQ-W`i!JWEmfI;yVTT^a-8Ch#D8xf2dtyi?7p z%#)W3n*a#ndFpd{qN|+9Jz++AJQO#-Y7Z6%*%oyEP5zs}d&kKIr`FVEY z;S}@d?UU=tCdw~EJ{b}=9x}S2iv!!8<$?d7VKDA8h{oeD#S-$DV)-vPdGY@x08n)@ zag?yLF_E#evvRTj4^CcrLvBL=fft&@HOhZ6Ng4`8ijt&h2y}fOTC~7GfJi4vpomA5 zOcOM)o_I9BKz}I`q)fu+Qnfy*W`|mY%LO>eF^a z;$)?T4F-(X#Q-m}!-k8L_rNPf`Mr<9IWu)f&dvt=EL+ESYmCvErd@8B9hd)afc(ZL94S z?rp#h&{7Ah5IJftK4VjATklo7@hm?8BX*~oBiz)jyc9FuRw!-V;Uo>p!CWpLaIQyt zAs5WN)1CCeux-qiGdmbIk8LR`gM+Qg=&Ve}w?zA6+sTL)abU=-cvU`3E?p5$Hpkxw znu0N659qR=IKnde*AEz_7z2pdi_Bh-sb3b=PdGO1Pdf_q2;+*Cx9YN7p_>rl``knY zRn%aVkcv1(W;`Mtp_DNOIECtgq%ufk-mu_<+Fu3Q17Tq4Rr(oeq)Yqk_CHA7LR@7@ zIZIDxxhS&=F2IQfusQ+Nsr%*zFK7S4g!U0y@3H^Yln|i;0a5+?RPG;ZSp6Tul>ezM z`40+516&719qT)mW|ArDSENle5hE2e8qY+zfeZoy12u&xoMgcP)4=&P-1Ib*-bAy` zlT?>w&B|ei-rCXO;sxo7*G;!)_p#%PAM-?m$JP(R%x1Hfas@KeaG%LO?R=lmkXc_MKZW}3f%KZ*rAN?HYvbu2L$ zRt_uv7~-IejlD1x;_AhwGXjB94Q=%+PbxuYzta*jw?S&%|qb=(JfJ?&6P=R7X zV%HP_!@-zO*zS}46g=J}#AMJ}rtWBr21e6hOn&tEmaM%hALH7nlm2@LP4rZ>2 zebe5aH@k!e?ij4Zwak#30|}>;`bquDQK*xmR=zc6vj0yuyC6+U=LusGnO3ZKFRpen z#pwzh!<+WBVp-!$MAc<0i~I%fW=8IO6K}bJ<-Scq>e+)951R~HKB?Mx2H}pxPHE@} zvqpq5j81_jtb_WneAvp<5kgdPKm|u2BdQx9%EzcCN&U{l+kbkhmV<1}yCTDv%&K^> zg;KCjwh*R1f_`6`si$h6`jyIKT7rTv5#k~x$mUyIw)_>Vr)D4fwIs@}{FSX|5GB1l z4vv;@oS@>Bu7~{KgUa_8eg#Lk6IDT2IY$41$*06{>>V;Bwa(-@N;ex4;D`(QK*b}{ z{#4$Hmt)FLqERgKz=3zXiV<{YX6V)lvYBr3V>N6ajeI~~hGR5Oe>W9r@sg)Na(a4- zxm%|1OKPN6^%JaD^^O~HbLSu=f`1px>RawOxLr+1b2^28U*2#h*W^=lSpSY4(@*^l z{!@9RSLG8Me&RJYLi|?$c!B0fP=4xAM4rerxX{xy{&i6=AqXueQAIBqO+pmuxy8Ib z4X^}r!NN3-upC6B#lt7&x0J;)nb9O~xjJMemm$_fHuP{DgtlU3xiW0UesTzS30L+U zQzDI3p&3dpONhd5I8-fGk^}@unluzu%nJ$9pzoO~Kk!>dLxw@M)M9?pNH1CQhvA`z zV;uacUtnBTdvT`M$1cm9`JrT3BMW!MNVBy%?@ZX%;(%(vqQAz<7I!hlDe|J3cn9=} zF7B;V4xE{Ss76s$W~%*$JviK?w8^vqCp#_G^jN0j>~Xq#Zru26e#l3H^{GCLEXI#n z?n~F-Lv#hU(bZS`EI9(xGV*jT=8R?CaK)t8oHc9XJ;UPY0Hz$XWt#QyLBaaz5+}xM zXk(!L_*PTt7gwWH*HLWC$h3Ho!SQ-(I||nn_iEC{WT3S{3V{8IN6tZ1C+DiFM{xlI zeMMk{o5;I6UvaC)@WKp9D+o?2Vd@4)Ue-nYci()hCCsKR`VD;hr9=vA!cgGL%3k^b(jADGyPi2TKr(JNh8mzlIR>n(F_hgiV(3@Ds(tjbNM7GoZ;T|3 zWzs8S`5PrA!9){jBJuX4y`f<4;>9*&NY=2Sq2Bp`M2(fox7ZhIDe!BaQUb@P(ub9D zlP8!p(AN&CwW!V&>H?yPFMJ)d5x#HKfwx;nS{Rr@oHqpktOg)%F+%1#tsPtq7zI$r zBo-Kflhq-=7_eW9B2OQv=@?|y0CKN77)N;z@tcg;heyW{wlpJ1t`Ap!O0`Xz{YHqO zI1${8Hag^r!kA<2_~bYtM=<1YzQ#GGP+q?3T7zYbIjN6Ee^V^b&9en$8FI*NIFg9G zPG$OXjT0Ku?%L7fat8Mqbl1`azf1ltmKTa(HH$Dqlav|rU{zP;Tbnk-XkGFQ6d+gi z-PXh?_kEJl+K98&OrmzgPIijB4!Pozbxd0H1;Usy!;V>Yn6&pu*zW8aYx`SC!$*ti zSn+G9p=~w6V(fZZHc>m|PPfjK6IN4(o=IFu?pC?+`UZAUTw!e`052{P=8vqT^(VeG z=psASIhCv28Y(;7;TuYAe>}BPk5Qg=8$?wZj9lj>h2kwEfF_CpK=+O6Rq9pLn4W)# zeXCKCpi~jsfqw7Taa0;!B5_C;B}e56W1s8@p*)SPzA;Fd$Slsn^=!_&!mRHV*Lmt| zBGIDPuR>CgS4%cQ4wKdEyO&Z>2aHmja;Pz+n|7(#l%^2ZLCix%>@_mbnyPEbyrHaz z>j^4SIv;ZXF-Ftzz>*t4wyq)ng8%0d;(Z_ExZ-cxwei=8{(br-`JYO(f23Wae_MqE z3@{Mlf^%M5G1SIN&en1*| zH~ANY1h3&WNsBy$G9{T=`kcxI#-X|>zLX2r*^-FUF+m0{k)n#GTG_mhG&fJfLj~K& zU~~6othMlvMm9<*SUD2?RD+R17|Z4mgR$L*R3;nBbo&Vm@39&3xIg;^aSxHS>}gwR zmzs?h8oPnNVgET&dx5^7APYx6Vv6eou07Zveyd+^V6_LzI$>ic+pxD_8s~ zC<}ucul>UH<@$KM zT4oI=62M%7qQO{}re-jTFqo9Z;rJKD5!X5$iwUsh*+kcHVhID08MB5cQD4TBWB(rI zuWc%CA}}v|iH=9gQ?D$1#Gu!y3o~p7416n54&Hif`U-cV?VrUMJyEqo_NC4#{puzU zzXEE@UppeeRlS9W*^N$zS`SBBi<@tT+<%3l@KhOy^%MWB9(A#*J~DQ;+MK*$rxo6f zcx3$3mcx{tly!q(p2DQrxcih|)0do_ZY77pyHGE#Q(0k*t!HUmmMcYFq%l$-o6%lS zDb49W-E?rQ#Hl``C3YTEdGZjFi3R<>t)+NAda(r~f1cT5jY}s7-2^&Kvo&2DLTPYP zhVVo-HLwo*vl83mtQ9)PR#VBg)FN}+*8c-p8j`LnNUU*Olm1O1Qqe62D#$CF#?HrM zy(zkX|1oF}Z=T#3XMLWDrm(|m+{1&BMxHY7X@hM_+cV$5-t!8HT(dJi6m9{ja53Yw z3f^`yb6Q;(e|#JQIz~B*=!-GbQ4nNL-NL z@^NWF_#w-Cox@h62;r^;Y`NX8cs?l^LU;5IWE~yvU8TqIHij!X8ydbLlT0gwmzS9} z@5BccG?vO;rvCs$mse1*ANi-cYE6Iauz$Fbn3#|ToAt5v7IlYnt6RMQEYLldva{~s zvr>1L##zmeoYgvIXJ#>bbuCVuEv2ZvZ8I~PQUN3wjP0UC)!U+wn|&`V*8?)` zMSCuvnuGec>QL+i1nCPGDAm@XSMIo?A9~C?g2&G8aNKjWd2pDX{qZ?04+2 zeyLw}iEd4vkCAWwa$ zbrHlEf3hfN7^1g~aW^XwldSmx1v~1z(s=1az4-wl} z`mM+G95*N*&1EP#u3}*KwNrPIgw8Kpp((rdEOO;bT1;6ea~>>sK+?!;{hpJ3rR<6UJb`O8P4@{XGgV%63_fs%cG8L zk9Fszbdo4tS$g0IWP1>t@0)E%-&9yj%Q!fiL2vcuL;90fPm}M==<>}Q)&sp@STFCY z^p!RzmN+uXGdtPJj1Y-khNyCb6Y$Vs>eZyW zPaOV=HY_T@FwAlleZCFYl@5X<<7%5DoO(7S%Lbl55?{2vIr_;SXBCbPZ(up;pC6Wx={AZL?shYOuFxLx1*>62;2rP}g`UT5+BHg(ju z&7n5QSvSyXbioB9CJTB#x;pexicV|9oaOpiJ9VK6EvKhl4^Vsa(p6cIi$*Zr0UxQ z;$MPOZnNae2Duuce~7|2MCfhNg*hZ9{+8H3?ts9C8#xGaM&sN;2lriYkn9W>&Gry! z3b(Xx1x*FhQkD-~V+s~KBfr4M_#0{`=Yrh90yj}Ph~)Nx;1Y^8<418tu!$1<3?T*~ z7Dl0P3Uok-7w0MPFQexNG1P5;y~E8zEvE49>$(f|XWtkW2Mj`udPn)pb%} zrA%wRFp*xvDgC767w!9`0vx1=q!)w!G+9(-w&p*a@WXg{?T&%;qaVcHo>7ca%KX$B z^7|KBPo<2;kM{2mRnF8vKm`9qGV%|I{y!pKm8B(q^2V;;x2r!1VJ^Zz8bWa)!-7a8 zSRf@dqEPlsj!7}oNvFFAA)75})vTJUwQ03hD$I*j6_5xbtd_JkE2`IJD_fQ;a$EkO z{fQ{~e%PKgPJsD&PyEvDmg+Qf&p*-qu!#;1k2r_(H72{^(Z)htgh@F?VIgK#_&eS- z$~(qInec>)XIkv@+{o6^DJLpAb>!d}l1DK^(l%#OdD9tKK6#|_R?-%0V!`<9Hj z3w3chDwG*SFte@>Iqwq`J4M&{aHXzyigT620+Vf$X?3RFfeTcvx_e+(&Q*z)t>c0e zpZH$1Z3X%{^_vylHVOWT6tno=l&$3 z9^eQ@TwU#%WMQaFvaYp_we%_2-9=o{+ck zF{cKJCOjpW&qKQquyp2BXCAP920dcrZ}T1@piukx_NY;%2W>@Wca%=Ch~x5Oj58Hv z;D-_ALOZBF(Mqbcqjd}P3iDbek#Dwzu`WRs`;hRIr*n0PV7vT+%Io(t}8KZ zpp?uc2eW!v28ipep0XNDPZt7H2HJ6oey|J3z!ng#1H~x_k%35P+Cp%mqXJ~cV0xdd z^4m5^K_dQ^Sg?$P`))ccV=O>C{Ds(C2WxX$LMC5vy=*44pP&)X5DOPYfqE${)hDg< z3hcG%U%HZ39=`#Ko4Uctg&@PQLf>?0^D|4J(_1*TFMOMB!Vv1_mnOq$BzXQdOGqgy zOp#LBZ!c>bPjY1NTXksZmbAl0A^Y&(%a3W-k>bE&>K?px5Cm%AT2E<&)Y?O*?d80d zgI5l~&Mve;iXm88Q+Fw7{+`PtN4G7~mJWR^z7XmYQ>uoiV!{tL)hp|= zS(M)813PM`d<501>{NqaPo6BZ^T{KBaqEVH(2^Vjeq zgeMeMpd*1tE@@);hGjuoVzF>Cj;5dNNwh40CnU+0DSKb~GEMb_# zT8Z&gz%SkHq6!;_6dQFYE`+b`v4NT7&@P>cA1Z1xmXy<2htaDhm@XXMp!g($ zw(7iFoH2}WR`UjqjaqOQ$ecNt@c|K1H1kyBArTTjLp%-M`4nzOhkfE#}dOpcd;b#suq8cPJ&bf5`6Tq>ND(l zib{VrPZ>{KuaIg}Y$W>A+nrvMg+l4)-@2jpAQ5h(Tii%Ni^-UPVg{<1KGU2EIUNGaXcEkOedJOusFT9X3%Pz$R+-+W+LlRaY-a$5r?4V zbPzgQl22IPG+N*iBRDH%l{Zh$fv9$RN1sU@Hp3m=M}{rX%y#;4(x1KR2yCO7Pzo>rw(67E{^{yUR`91nX^&MxY@FwmJJbyPAoWZ9Z zcBS$r)&ogYBn{DOtD~tIVJUiq|1foX^*F~O4hlLp-g;Y2wKLLM=?(r3GDqsPmUo*? zwKMEi*%f)C_@?(&&hk>;m07F$X7&i?DEK|jdRK=CaaNu-)pX>n3}@%byPKVkpLzBq z{+Py&!`MZ^4@-;iY`I4#6G@aWMv{^2VTH7|WF^u?3vsB|jU3LgdX$}=v7#EHRN(im zI(3q-eU$s~r=S#EWqa_2!G?b~ z<&brq1vvUTJH380=gcNntZw%7UT8tLAr-W49;9y^=>TDaTC|cKA<(gah#2M|l~j)w zY8goo28gj$n&zcNgqX1Qn6=<8?R0`FVO)g4&QtJAbW3G#D)uNeac-7cH5W#6i!%BH z=}9}-f+FrtEkkrQ?nkoMQ1o-9_b+&=&C2^h!&mWFga#MCrm85hW;)1pDt;-uvQG^D zntSB?XA*0%TIhtWDS!KcI}kp3LT>!(Nlc(lQN?k^bS8Q^GGMfo}^|%7s;#r+pybl@?KA++|FJ zr%se9(B|g*ERQU96az%@4gYrxRRxaM2*b}jNsG|0dQi;Rw{0WM0E>rko!{QYAJJKY z)|sX0N$!8d9E|kND~v|f>3YE|uiAnqbkMn)hu$if4kUkzKqoNoh8v|S>VY1EKmgO} zR$0UU2o)4i4yc1inx3}brso+sio{)gfbLaEgLahj8(_Z#4R-v) zglqwI%`dsY+589a8$Mu7#7_%kN*ekHupQ#48DIN^uhDxblDg3R1yXMr^NmkR z7J_NWCY~fhg}h!_aXJ#?wsZF$q`JH>JWQ9`jbZzOBpS`}-A$Vgkq7+|=lPx9H7QZG z8i8guMN+yc4*H*ANr$Q-3I{FQ-^;8ezWS2b8rERp9TMOLBxiG9J*g5=?h)mIm3#CGi4JSq1ohFrcrxx@`**K5%T}qbaCGldV!t zVeM)!U3vbf5FOy;(h08JnhSGxm)8Kqxr9PsMeWi=b8b|m_&^@#A3lL;bVKTBx+0v8 zLZeWAxJ~N27lsOT2b|qyp$(CqzqgW@tyy?CgwOe~^i;ZH zlL``i4r!>i#EGBNxV_P@KpYFQLz4Bdq{#zA&sc)*@7Mxsh9u%e6Ke`?5Yz1jkTdND zR8!u_yw_$weBOU}24(&^Bm|(dSJ(v(cBct}87a^X(v>nVLIr%%D8r|&)mi+iBc;B;x;rKq zd8*X`r?SZsTNCPQqoFOrUz8nZO?225Z#z(B!4mEp#ZJBzwd7jW1!`sg*?hPMJ$o`T zR?KrN6OZA1H{9pA;p0cSSu;@6->8aJm1rrO-yDJ7)lxuk#npUk7WNER1Wwnpy%u zF=t6iHzWU(L&=vVSSc^&D_eYP3TM?HN!Tgq$SYC;pSIPWW;zeNm7Pgub#yZ@7WPw#f#Kl)W4%B>)+8%gpfoH1qZ;kZ*RqfXYeGXJ_ zk>2otbp+1By`x^1V!>6k5v8NAK@T;89$`hE0{Pc@Q$KhG0jOoKk--Qx!vS~lAiypV zCIJ&6B@24`!TxhJ4_QS*S5;;Pk#!f(qIR7*(c3dN*POKtQe)QvR{O2@QsM%ujEAWEm) z+PM=G9hSR>gQ`Bv2(k}RAv2+$7qq(mU`fQ+&}*i%-RtSUAha>70?G!>?w%F(b4k!$ zvm;E!)2`I?etmSUFW7WflJ@8Nx`m_vE2HF#)_BiD#FaNT|IY@!uUbd4v$wTglIbIX zblRy5=wp)VQzsn0_;KdM%g<8@>#;E?vypTf=F?3f@SSdZ;XpX~J@l1;p#}_veWHp>@Iq_T z@^7|h;EivPYv1&u0~l9(a~>dV9Uw10QqB6Dzu1G~-l{*7IktljpK<_L8m0|7VV_!S zRiE{u97(%R-<8oYJ{molUd>vlGaE-C|^<`hppdDz<7OS13$#J zZ+)(*rZIDSt^Q$}CRk0?pqT5PN5TT`Ya{q(BUg#&nAsg6apPMhLTno!SRq1e60fl6GvpnwDD4N> z9B=RrufY8+g3_`@PRg+(+gs2(bd;5#{uTZk96CWz#{=&h9+!{_m60xJxC%r&gd_N! z>h5UzVX%_7@CUeAA1XFg_AF%(uS&^1WD*VPS^jcC!M2v@RHZML;e(H-=(4(3O&bX- zI6>usJOS+?W&^S&DL{l|>51ZvCXUKlH2XKJPXnHjs*oMkNM#ZDLx!oaM5(%^)5XaP zk6&+P16sA>vyFe9v`Cp5qnbE#r#ltR5E+O3!WnKn`56Grs2;sqr3r# zp@Zp<^q`5iq8OqOlJ`pIuyK@3zPz&iJ0Jcc`hDQ1bqos2;}O|$i#}e@ua*x5VCSx zJAp}+?Hz++tm9dh3Fvm_bO6mQo38al#>^O0g)Lh^&l82+&x)*<n7^Sw-AJo9tEzZDwyJ7L^i7|BGqHu+ea6(&7jKpBq>~V z8CJxurD)WZ{5D0?s|KMi=e7A^JVNM6sdwg@1Eg_+Bw=9j&=+KO1PG|y(mP1@5~x>d z=@c{EWU_jTSjiJl)d(>`qEJ;@iOBm}alq8;OK;p(1AdH$)I9qHNmxxUArdzBW0t+Qeyl)m3?D09770g z)hzXEOy>2_{?o%2B%k%z4d23!pZcoxyW1Ik{|m7Q1>fm4`wsRrl)~h z_=Z*zYL+EG@DV1{6@5@(Ndu!Q$l_6Qlfoz@79q)Kmsf~J7t1)tl#`MD<;1&CAA zH8;i+oBm89dTTDl{aH`cmTPTt@^K-%*sV+t4X9q0Z{A~vEEa!&rRRr=0Rbz4NFCJr zLg2u=0QK@w9XGE=6(-JgeP}G#WG|R&tfHRA3a9*zh5wNTBAD;@YYGx%#E4{C#Wlfo z%-JuW9=FA_T6mR2-Vugk1uGZvJbFvVVWT@QOWz$;?u6+CbyQsbK$>O1APk|xgnh_8 zc)s@Mw7#0^wP6qTtyNq2G#s?5j~REyoU6^lT7dpX{T-rhZWHD%dik*=EA7bIJgOVf_Ga!yC8V^tkTOEHe+JK@Fh|$kfNxO^= z#lpV^(ZQ-3!^_BhV>aXY~GC9{8%1lOJ}6vzXDvPhC>JrtXwFBC+!3a*Z-%#9}i z#<5&0LLIa{q!rEIFSFc9)>{-_2^qbOg5;_A9 ztQ))C6#hxSA{f9R3Eh^`_f${pBJNe~pIQ`tZVR^wyp}=gLK}e5_vG@w+-mp#Fu>e| z*?qBp5CQ5zu+Fi}xAs)YY1;bKG!htqR~)DB$ILN6GaChoiy%Bq@i+1ZnANC0U&D z_4k$=YP47ng+0NhuEt}6C;9-JDd8i5S>`Ml==9wHDQFOsAlmtrVwurYDw_)Ihfk35 zJDBbe!*LUpg%4n>BExWz>KIQ9vexUu^d!7rc_kg#Bf= z7TLz|l*y*3d2vi@c|pX*@ybf!+Xk|2*z$@F4K#MT8Dt4zM_EcFmNp31#7qT6(@GG? zdd;sSY9HHuDb=w&|K%sm`bYX#%UHKY%R`3aLMO?{T#EI@FNNFNO>p@?W*i0z(g2dt z{=9Ofh80Oxv&)i35AQN>TPMjR^UID-T7H5A?GI{MD_VeXZ%;uo41dVm=uT&ne2h0i zv*xI%9vPtdEK@~1&V%p1sFc2AA`9?H)gPnRdlO~URx!fiSV)j?Tf5=5F>hnO=$d$x zzaIfr*wiIc!U1K*$JO@)gP4%xp!<*DvJSv7p}(uTLUb=MSb@7_yO+IsCj^`PsxEl& zIxsi}s3L?t+p+3FXYqujGhGwTx^WXgJ1}a@Yq5mwP0PvGEr*qu7@R$9j>@-q1rz5T zriz;B^(ex?=3Th6h;7U`8u2sDlfS{0YyydK=*>-(NOm9>S_{U|eg(J~C7O zIe{|LK=Y`hXiF_%jOM8Haw3UtaE{hWdzo3BbD6ud7br4cODBtN(~Hl+odP0SSWPw;I&^m)yLw+nd#}3#z}?UIcX3=SssI}`QwY=% zAEXTODk|MqTx}2DVG<|~(CxgLyi*A{m>M@1h^wiC)4Hy>1K7@|Z&_VPJsaQoS8=ex zDL&+AZdQa>ylxhT_Q$q=60D5&%pi6+qlY3$3c(~rsITX?>b;({FhU!7HOOhSP7>bmTkC8KM%!LRGI^~y3Ug+gh!QM=+NZXznM)?L3G=4=IMvFgX3BAlyJ z`~jjA;2z+65D$j5xbv9=IWQ^&-K3Yh`vC(1Qz2h2`o$>Cej@XRGff!it$n{@WEJ^N z41qk%Wm=}mA*iwCqU_6}Id!SQd13aFER3unXaJJXIsSnxvG2(hSCP{i&QH$tL&TPx zDYJsuk+%laN&OvKb-FHK$R4dy%M7hSB*yj#-nJy?S9tVoxAuDei{s}@+pNT!vLOIC z8g`-QQW8FKp3cPsX%{)0B+x+OhZ1=L7F-jizt|{+f1Ga7%+!BXqjCjH&x|3%?UbN# zh?$I1^YokvG$qFz5ySK+Ja5=mkR&p{F}ev**rWdKMko+Gj^?Or=UH?SCg#0F(&a_y zXOh}dPv0D9l0RVedq1~jCNV=8?vZfU-Xi|nkeE->;ohG3U7z+^0+HV17~-_Mv#mV` zzvwUJJ15v5wwKPv-)i@dsEo@#WEO9zie7mdRAbgL2kjbW4&lk$vxkbq=w5mGKZK6@ zjXWctDkCRx58NJD_Q7e}HX`SiV)TZMJ}~zY6P1(LWo`;yDynY_5_L?N-P`>ALfmyl z8C$a~FDkcwtzK9m$tof>(`Vu3#6r#+v8RGy#1D2)F;vnsiL&P-c^PO)^B-4VeJteLlT@25sPa z%W~q5>YMjj!mhN})p$47VA^v$Jo6_s{!y?}`+h+VM_SN`!11`|;C;B};B&Z<@%FOG z_YQVN+zFF|q5zKab&e4GH|B;sBbKimHt;K@tCH+S{7Ry~88`si7}S)1E{21nldiu5 z_4>;XTJa~Yd$m4A9{Qbd)KUAm7XNbZ4xHbg3a8-+1uf*$1PegabbmCzgC~1WB2F(W zYj5XhVos!X!QHuZXCatkRsdEsSCc+D2?*S7a+(v%toqyxhjz|`zdrUvsxQS{J>?c& zvx*rHw^8b|v^7wq8KWVofj&VUitbm*a&RU_ln#ZFA^3AKEf<#T%8I!Lg3XEsdH(A5 zlgh&M_XEoal)i#0tcq8c%Gs6`xu;vvP2u)D9p!&XNt z!TdF_H~;`g@fNXkO-*t<9~;iEv?)Nee%hVe!aW`N%$cFJ(Dy9+Xk*odyFj72T!(b%Vo5zvCGZ%3tkt$@Wcx8BWEkefI1-~C_3y*LjlQ5%WEz9WD8i^ z2MV$BHD$gdPJV4IaV)G9CIFwiV=ca0cfXdTdK7oRf@lgyPx;_7*RRFk=?@EOb9Gcz zg~VZrzo*Snp&EE{$CWr)JZW)Gr;{B2ka6B!&?aknM-FENcl%45#y?oq9QY z3^1Y5yn&^D67Da4lI}ljDcphaEZw2;tlYuzq?uB4b9Mt6!KTW&ptxd^vF;NbX=00T z@nE1lIBGgjqs?ES#P{ZfRb6f!At51vk%<0X%d_~NL5b8UyfQMPDtfU@>ijA0NP3UU zh{lCf`Wu7cX!go`kUG`1K=7NN@SRGjUKuo<^;@GS!%iDXbJs`o6e`v3O8-+7vRkFm z)nEa$sD#-v)*Jb>&Me+YIW3PsR1)h=-Su)))>-`aRcFJG-8icomO4J@60 zw10l}BYxi{eL+Uu0xJYk-Vc~BcR49Qyyq!7)PR27D`cqGrik=?k1Of>gY7q@&d&Ds zt7&WixP`9~jjHO`Cog~RA4Q%uMg+$z^Gt&vn+d3&>Ux{_c zm|bc;k|GKbhZLr-%p_f%dq$eiZ;n^NxoS-Nu*^Nx5vm46)*)=-Bf<;X#?`YC4tLK; z?;u?shFbXeks+dJ?^o$l#tg*1NA?(1iFff@I&j^<74S!o;SWR^Xi);DM%8XiWpLi0 zQE2dL9^a36|L5qC5+&Pf0%>l&qQ&)OU4vjd)%I6{|H+pw<0(a``9w(gKD&+o$8hOC zNAiShtc}e~ob2`gyVZx59y<6Fpl*$J41VJ-H*e-yECWaDMmPQi-N8XI3 z%iI@ljc+d}_okL1CGWffeaejlxWFVDWu%e=>H)XeZ|4{HlbgC-Uvof4ISYQzZ0Um> z#Ov{k1c*VoN^f(gfiueuag)`TbjL$XVq$)aCUBL_M`5>0>6Ska^*Knk__pw{0I>jA zzh}Kzg{@PNi)fcAk7jMAdi-_RO%x#LQszDMS@_>iFoB+zJ0Q#CQJzFGa8;pHFdi`^ zxnTC`G$7Rctm3G8t8!SY`GwFi4gF|+dAk7rh^rA{NXzc%39+xSYM~($L(pJ(8Zjs* zYdN_R^%~LiGHm9|ElV4kVZGA*T$o@YY4qpJOxGHlUi*S*A(MrgQ{&xoZQo+#PuYRs zv3a$*qoe9gBqbN|y|eaH=w^LE{>kpL!;$wRahY(hhzRY;d33W)m*dfem@)>pR54Qy z ze;^F?mwdU?K+=fBabokSls^6_6At#1Sh7W*y?r6Ss*dmZP{n;VB^LDxM1QWh;@H0J z!4S*_5j_;+@-NpO1KfQd&;C7T`9ak;X8DTRz$hDNcjG}xAfg%gwZSb^zhE~O);NMO zn2$fl7Evn%=Lk!*xsM#(y$mjukN?A&mzEw3W5>_o+6oh62kq=4-`e3B^$rG=XG}Kd zK$blh(%!9;@d@3& zGFO60j1Vf54S}+XD?%*uk7wW$f`4U3F*p7@I4Jg7f`Il}2H<{j5h?$DDe%wG7jZQL zI{mj?t?Hu>$|2UrPr5&QyK2l3mas?zzOk0DV30HgOQ|~xLXDQ8M3o#;CNKO8RK+M; zsOi%)js-MU>9H4%Q)#K_me}8OQC1u;f4!LO%|5toa1|u5Q@#mYy8nE9IXmR}b#sZK z3sD395q}*TDJJA9Er7N`y=w*S&tA;mv-)Sx4(k$fJBxXva0_;$G6!9bGBw13c_Uws zXks4u(8JA@0O9g5f?#V~qR5*u5aIe2HQO^)RW9TTcJk28l`Syl>Q#ZveEE4Em+{?%iz6=V3b>rCm9F zPQQm@-(hfNdo2%n?B)u_&Qh7^^@U>0qMBngH8}H|v+Ejg*Dd(Y#|jgJ-A zQ_bQscil%eY}8oN7ZL+2r|qv+iJY?*l)&3W_55T3GU;?@Om*(M`u0DXAsQ7HSl56> z4P!*(%&wRCb?a4HH&n;lAmr4rS=kMZb74Akha2U~Ktni>>cD$6jpugjULq)D?ea%b zk;UW0pAI~TH59P+o}*c5Ei5L-9OE;OIBt>^(;xw`>cN2`({Rzg71qrNaE=cAH^$wP zNrK9Glp^3a%m+ilQj0SnGq`okjzmE7<3I{JLD6Jn^+oas=h*4>Wvy=KXqVBa;K&ri z4(SVmMXPG}0-UTwa2-MJ=MTfM3K)b~DzSVq8+v-a0&Dsv>4B65{dBhD;(d44CaHSM zb!0ne(*<^Q%|nuaL`Gb3D4AvyO8wyygm=1;9#u5x*k0$UOwx?QxR*6Od8>+ujfyo0 zJ}>2FgW_iv(dBK2OWC-Y=Tw!UwIeOAOUUC;h95&S1hn$G#if+d;*dWL#j#YWswrz_ zMlV=z+zjZJ%SlDhxf)vv@`%~$Afd)T+MS1>ZE7V$Rj#;J*<9Ld=PrK0?qrazRJWx) z(BTLF@Wk279nh|G%ZY7_lK7=&j;x`bMND=zgh_>>-o@6%8_#Bz!FnF*onB@_k|YCF z?vu!s6#h9bL3@tPn$1;#k5=7#s*L;FLK#=M89K^|$3LICYWIbd^qguQp02w5>8p-H z+@J&+pP_^iF4Xu>`D>DcCnl8BUwwOlq6`XkjHNpi@B?OOd`4{dL?kH%lt78(-L}eah8?36zw9d-dI6D{$s{f=M7)1 zRH1M*-82}DoFF^Mi$r}bTB5r6y9>8hjL54%KfyHxn$LkW=AZ(WkHWR;tIWWr@+;^^ zVomjAWT)$+rn%g`LHB6ZSO@M3KBA? z+W7ThSBgpk`jZHZUrp`F;*%6M5kLWy6AW#T{jFHTiKXP9ITrMlEdti7@&AT_a-BA!jc(Kt zWk>IdY-2Zbz?U1)tk#n_Lsl?W;0q`;z|t9*g-xE!(}#$fScX2VkjSiboKWE~afu5d z2B@9mvT=o2fB_>Mnie=TDJB+l`GMKCy%2+NcFsbpv<9jS@$X37K_-Y!cvF5NEY`#p z3sWEc<7$E*X*fp+MqsOyMXO=<2>o8)E(T?#4KVQgt=qa%5FfUG_LE`n)PihCz2=iNUt7im)s@;mOc9SR&{`4s9Q6)U31mn?}Y?$k3kU z#h??JEgH-HGt`~%)1ZBhT9~uRi8br&;a5Y3K_Bl1G)-y(ytx?ok9S*Tz#5Vb=P~xH z^5*t_R2It95=!XDE6X{MjLYn4Eszj9Y91T2SFz@eYlx9Z9*hWaS$^5r7=W5|>sY8}mS(>e9Ez2qI1~wtlA$yv2e-Hjn&K*P z2zWSrC~_8Wrxxf#%QAL&f8iH2%R)E~IrQLgWFg8>`Vnyo?E=uiALoRP&qT{V2{$79 z%9R?*kW-7b#|}*~P#cA@q=V|+RC9=I;aK7Pju$K-n`EoGV^-8Mk=-?@$?O37evGKn z3NEgpo_4{s>=FB}sqx21d3*=gKq-Zk)U+bM%Q_}0`XGkYh*+jRaP+aDnRv#Zz*n$pGp zEU9omuYVXH{AEx>=kk}h2iKt!yqX=EHN)LF}z1j zJx((`CesN1HxTFZ7yrvA2jTPmKYVij>45{ZH2YtsHuGzIRotIFj?(8T@ZWUv{_%AI zgMZlB03C&FtgJqv9%(acqt9N)`4jy4PtYgnhqev!r$GTIOvLF5aZ{tW5MN@9BDGu* zBJzwW3sEJ~Oy8is`l6Ly3an7RPtRr^1Iu(D!B!0O241Xua>Jee;Rc7tWvj!%#yX#m z&pU*?=rTVD7pF6va1D@u@b#V@bShFr3 zMyMbNCZwT)E-%L-{%$3?n}>EN>ai7b$zR_>=l59mW;tfKj^oG)>_TGCJ#HbLBsNy$ zqAqPagZ3uQ(Gsv_-VrZmG&hHaOD#RB#6J8&sL=^iMFB=gH5AIJ+w@sTf7xa&Cnl}@ zxrtzoNq>t?=(+8bS)s2p3>jW}tye0z2aY_Dh@(18-vdfvn;D?sv<>UgL{Ti08$1Q+ zZI3q}yMA^LK=d?YVg({|v?d1|R?5 zL0S3fw)BZazRNNX|7P4rh7!+3tCG~O8l+m?H} z(CB>8(9LtKYIu3ohJ-9ecgk+L&!FX~Wuim&;v$>M4 zUfvn<=Eok(63Ubc>mZrd8d7(>8bG>J?PtOHih_xRYFu1Hg{t;%+hXu2#x%a%qzcab zv$X!ccoj)exoOnaco_jbGw7KryOtuf(SaR-VJ0nAe(1*AA}#QV1lMhGtzD>RoUZ;WA?~!K{8%chYn?ttlz17UpDLlhTkGcVfHY6R<2r4E{mU zq-}D?+*2gAkQYAKrk*rB%4WFC-B!eZZLg4(tR#@kUQHIzEqV48$9=Q(~J_0 zy1%LSCbkoOhRO!J+Oh#;bGuXe;~(bIE*!J@i<%_IcB7wjhB5iF#jBn5+u~fEECN2* z!QFh!m<(>%49H12Y33+?$JxKV3xW{xSs=gxkxW-@Xds^|O1`AmorDKrE8N2-@ospk z=Au%h=f!`_X|G^A;XWL}-_L@D6A~*4Yf!5RTTm$!t8y&fp5_oqvBjW{FufS`!)5m% z2g(=9Ap6Y2y(9OYOWuUVGp-K=6kqQ)kM0P^TQT{X{V$*sN$wbFb-DaUuJF*!?EJPl zJev!UsOB^UHZ2KppYTELh+kqDw+5dPFv&&;;C~=u$Mt+Ywga!8YkL2~@g67}3wAQP zrx^RaXb1(c7vwU8a2se75X(cX^$M{FH4AHS7d2}heqqg4F0!1|Na>UtAdT%3JnS!B)&zelTEj$^b0>Oyfw=P-y-Wd^#dEFRUN*C{!`aJIHi<_YA2?piC%^ zj!p}+ZnBrM?ErAM+D97B*7L8U$K zo(IR-&LF(85p+fuct9~VTSdRjs`d-m|6G;&PoWvC&s8z`TotPSoksp;RsL4VL@CHf z_3|Tn%`ObgRhLmr60<;ya-5wbh&t z#ycN_)3P_KZN5CRyG%LRO4`Ot)3vY#dNX9!f!`_>1%4Q`81E*2BRg~A-VcN7pcX#j zrbl@7`V%n z6J53(m?KRzKb)v?iCuYWbH*l6M77dY4keS!%>}*8n!@ROE4!|7mQ+YS4dff1JJC(t z6Fnuf^=dajqHpH1=|pb(po9Fr8it^;2dEk|Ro=$fxqK$^Yix{G($0m-{RCFQJ~LqUnO7jJcjr zl*N*!6WU;wtF=dLCWzD6kW;y)LEo=4wSXQDIcq5WttgE#%@*m><@H;~Q&GniA-$in z`sjWFLgychS1kIJmPtd-w6%iKkj&dGhtB%0)pyy0M<4HZ@ZY0PWLAd7FCrj&i|NRh?>hZj*&FYnyu%Ur`JdiTu&+n z78d3n)Rl6q&NwVj_jcr#s5G^d?VtV8bkkYco5lV0LiT+t8}98LW>d)|v|V3++zLbHC(NC@X#Hx?21J0M*gP2V`Yd^DYvVIr{C zSc4V)hZKf|OMSm%FVqSRC!phWSyuUAu%0fredf#TDR$|hMZihJ__F!)Nkh6z)d=NC z3q4V*K3JTetxCPgB2_)rhOSWhuXzu+%&>}*ARxUaDeRy{$xK(AC0I=9%X7dmc6?lZNqe-iM(`?Xn3x2Ov>sej6YVQJ9Q42>?4lil?X zew-S>tm{=@QC-zLtg*nh5mQojYnvVzf3!4TpXPuobW_*xYJs;9AokrXcs!Ay z;HK>#;G$*TPN2M!WxdH>oDY6k4A6S>BM0Nimf#LfboKxJXVBC=RBuO&g-=+@O-#0m zh*aPG16zY^tzQLNAF7L(IpGPa+mDsCeAK3k=IL6^LcE8l0o&)k@?dz!79yxUquQIe($zm5DG z5RdXTv)AjHaOPv6z%99mPsa#8OD@9=URvHoJ1hYnV2bG*2XYBgB!-GEoP&8fLmWGg z9NG^xl5D&3L^io&3iYweV*qhc=m+r7C#Jppo$Ygg;jO2yaFU8+F*RmPL` zYxfGKla_--I}YUT353k}nF1zt2NO?+kofR8Efl$Bb^&llgq+HV_UYJUH7M5IoN0sT z4;wDA0gs55ZI|FmJ0}^Pc}{Ji-|#jdR$`!s)Di4^g3b_Qr<*Qu2rz}R6!B^;`Lj3sKWzjMYjexX)-;f5Y+HfkctE{PstO-BZan0zdXPQ=V8 zS8cBhnQyy4oN?J~oK0zl!#S|v6h-nx5to7WkdEk0HKBm;?kcNO*A+u=%f~l&aY*+J z>%^Dz`EQ6!+SEX$>?d(~|MNWU-}JTrk}&`IR|Ske(G^iMdk04)Cxd@}{1=P0U*%L5 zMFH_$R+HUGGv|ju2Z>5x(-aIbVJLcH1S+(E#MNe9g;VZX{5f%_|Kv7|UY-CM(>vf= z!4m?QS+AL+rUyfGJ;~uJGp4{WhOOc%2ybVP68@QTwI(8kDuYf?#^xv zBmOHCZU8O(x)=GVFn%tg@TVW1)qJJ_bU}4e7i>&V?r zh-03>d3DFj&@}6t1y3*yOzllYQ++BO-q!)zsk`D(z||)y&}o%sZ-tUF>0KsiYKFg6 zTONq)P+uL5Vm0w{D5Gms^>H1qa&Z##*X31=58*r%Z@Ko=IMXX{;aiMUp-!$As3{sq z0EEk02MOsgGm7$}E%H1ys2$yftNbB%1rdo@?6~0!a8Ym*1f;jIgfcYEF(I_^+;Xdr z2a>&oc^dF3pm(UNpazXgVzuF<2|zdPGjrNUKpdb$HOgNp*V56XqH`~$c~oSiqx;8_ zEz3fHoU*aJUbFJ&?W)sZB3qOSS;OIZ=n-*#q{?PCXi?Mq4aY@=XvlNQdA;yVC0Vy+ z{Zk6OO!lMYWd`T#bS8FV(`%flEA9El;~WjZKU1YmZpG#49`ku`oV{Bdtvzyz3{k&7 zlG>ik>eL1P93F zd&!aXluU_qV1~sBQf$F%sM4kTfGx5MxO0zJy<#5Z&qzNfull=k1_CZivd-WAuIQf> zBT3&WR|VD|=nKelnp3Q@A~^d_jN3@$x2$f@E~e<$dk$L@06Paw$);l*ewndzL~LuU zq`>vfKb*+=uw`}NsM}~oY}gW%XFwy&A>bi{7s>@(cu4NM;!%ieP$8r6&6jfoq756W z$Y<`J*d7nK4`6t`sZ;l%Oen|+pk|Ry2`p9lri5VD!Gq`U#Ms}pgX3ylAFr8(?1#&dxrtJgB>VqrlWZf61(r`&zMXsV~l{UGjI7R@*NiMJLUoK*kY&gY9kC@^}Fj* zd^l6_t}%Ku<0PY71%zQL`@}L}48M!@=r)Q^Ie5AWhv%#l+Rhu6fRpvv$28TH;N7Cl z%I^4ffBqx@Pxpq|rTJV)$CnxUPOIn`u278s9#ukn>PL25VMv2mff)-RXV&r`Dwid7}TEZxXX1q(h{R6v6X z&x{S_tW%f)BHc!jHNbnrDRjGB@cam{i#zZK*_*xlW@-R3VDmp)<$}S%t*@VmYX;1h zFWmpXt@1xJlc15Yjs2&e%)d`fimRfi?+fS^BoTcrsew%e@T^}wyVv6NGDyMGHSKIQ zC>qFr4GY?#S#pq!%IM_AOf`#}tPoMn7JP8dHXm(v3UTq!aOfEXNRtEJ^4ED@jx%le zvUoUs-d|2(zBsrN0wE(Pj^g5wx{1YPg9FL1)V1JupsVaXNzq4fX+R!oVX+q3tG?L= z>=s38J_!$eSzy0m?om6Wv|ZCbYVHDH*J1_Ndajoh&?L7h&(CVii&rmLu+FcI;1qd_ zHDb3Vk=(`WV?Uq;<0NccEh0s`mBXcEtmwt6oN99RQt7MNER3`{snV$qBTp={Hn!zz z1gkYi#^;P8s!tQl(Y>|lvz{5$uiXsitTD^1YgCp+1%IMIRLiSP`sJru0oY-p!FPbI)!6{XM%)(_Dolh1;$HlghB-&e><;zU&pc=ujpa-(+S&Jj zX1n4T#DJDuG7NP;F5TkoG#qjjZ8NdXxF0l58RK?XO7?faM5*Z17stidTP|a%_N z^e$D?@~q#Pf+708cLSWCK|toT1YSHfXVIs9Dnh5R(}(I;7KhKB7RD>f%;H2X?Z9eR z{lUMuO~ffT!^ew= z7u13>STI4tZpCQ?yb9;tSM-(EGb?iW$a1eBy4-PVejgMXFIV_Ha^XB|F}zK_gzdhM z!)($XfrFHPf&uyFQf$EpcAfk83}91Y`JFJOiQ;v5ca?)a!IxOi36tGkPk4S6EW~eq z>WiK`Vu3D1DaZ}515nl6>;3#xo{GQp1(=uTXl1~ z4gdWxr-8a$L*_G^UVd&bqW_nzMM&SlNW$8|$lAfo@zb+P>2q?=+T^qNwblP*RsN?N zdZE%^Zs;yAwero1qaoqMp~|KL=&npffh981>2om!fseU(CtJ=bW7c6l{U5(07*e0~ zJRbid6?&psp)ilmYYR3ZIg;t;6?*>hoZ3uq7dvyyq-yq$zH$yyImjfhpQb@WKENSP zl;KPCE+KXzU5!)mu12~;2trrLfs&nlEVOndh9&!SAOdeYd}ugwpE-9OF|yQs(w@C9 zoXVX`LP~V>%$<(%~tE*bsq(EFm zU5z{H@Fs^>nm%m%wZs*hRl=KD%4W3|(@j!nJr{Mmkl`e_uR9fZ-E{JY7#s6i()WXB0g-b`R{2r@K{2h3T+a>82>722+$RM*?W5;Bmo6$X3+Ieg9&^TU(*F$Q3 zT572!;vJeBr-)x?cP;^w1zoAM`nWYVz^<6N>SkgG3s4MrNtzQO|A?odKurb6DGZffo>DP_)S0$#gGQ_vw@a9JDXs2}hV&c>$ zUT0;1@cY5kozKOcbN6)n5v)l#>nLFL_x?2NQgurQH(KH@gGe>F|$&@ zq@2A!EXcIsDdzf@cWqElI5~t z4cL9gg7{%~4@`ANXnVAi=JvSsj95-7V& zME3o-%9~2?cvlH#twW~99=-$C=+b5^Yv}Zh4;Mg-!LS zw>gqc=}CzS9>v5C?#re>JsRY!w|Mtv#%O3%Ydn=S9cQarqkZwaM4z(gL~1&oJZ;t; zA5+g3O6itCsu93!G1J_J%Icku>b3O6qBW$1Ej_oUWc@MI)| zQ~eyS-EAAnVZp}CQnvG0N>Kc$h^1DRJkE7xZqJ0>p<>9*apXgBMI-v87E0+PeJ-K& z#(8>P_W^h_kBkI;&e_{~!M+TXt@z8Po*!L^8XBn{of)knd-xp{heZh~@EunB2W)gd zAVTw6ZZasTi>((qpBFh(r4)k zz&@Mc@ZcI-4d639AfcOgHOU+YtpZ)rC%Bc5gw5o~+E-i+bMm(A6!uE>=>1M;V!Wl4 z<#~muol$FsY_qQC{JDc8b=$l6Y_@_!$av^08`czSm!Xan{l$@GO-zPq1s>WF)G=wv zDD8j~Ht1pFj)*-b7h>W)@O&m&VyYci&}K|0_Z*w`L>1jnGfCf@6p}Ef*?wdficVe_ zmPRUZ(C+YJU+hIj@_#IiM7+$4kH#VS5tM!Ksz01siPc-WUe9Y3|pb4u2qnn zRavJiRpa zq?tr&YV?yKt<@-kAFl3s&Kq#jag$hN+Y%%kX_ytvpCsElgFoN3SsZLC>0f|m#&Jhu zp7c1dV$55$+k78FI2q!FT}r|}cIV;zp~#6X2&}22$t6cHx_95FL~T~1XW21VFuatb zpM@6w>c^SJ>Pq6{L&f9()uy)TAWf;6LyHH3BUiJ8A4}od)9sriz~e7}l7Vr0e%(=>KG1Jay zW0azuWC`(|B?<6;R)2}aU`r@mt_#W2VrO{LcX$Hg9f4H#XpOsAOX02x^w9+xnLVAt z^~hv2guE-DElBG+`+`>PwXn5kuP_ZiOO3QuwoEr)ky;o$n7hFoh}Aq0@Ar<8`H!n} zspCC^EB=6>$q*gf&M2wj@zzfBl(w_@0;h^*fC#PW9!-kT-dt*e7^)OIU{Uw%U4d#g zL&o>6`hKQUps|G4F_5AuFU4wI)(%9(av7-u40(IaI|%ir@~w9-rLs&efOR@oQy)}{ z&T#Qf`!|52W0d+>G!h~5A}7VJky`C3^fkJzt3|M&xW~x-8rSi-uz=qBsgODqbl(W#f{Ew#ui(K)(Hr&xqZs` zfrK^2)tF#|U=K|_U@|r=M_Hb;qj1GJG=O=d`~#AFAccecIaq3U`(Ds1*f*TIs=IGL zp_vlaRUtFNK8(k;JEu&|i_m39c(HblQkF8g#l|?hPaUzH2kAAF1>>Yykva0;U@&oRV8w?5yEK??A0SBgh?@Pd zJg{O~4xURt7!a;$rz9%IMHQeEZHR8KgFQixarg+MfmM_OeX#~#&?mx44qe!wt`~dd zqyt^~ML>V>2Do$huU<7}EF2wy9^kJJSm6HoAD*sRz%a|aJWz_n6?bz99h)jNMp}3k ztPVbos1$lC1nX_OK0~h>=F&v^IfgBF{#BIi&HTL}O7H-t4+wwa)kf3AE2-Dx@#mTA z!0f`>vz+d3AF$NH_-JqkuK1C+5>yns0G;r5ApsU|a-w9^j4c+FS{#+7- zH%skr+TJ~W_8CK_j$T1b;$ql_+;q6W|D^BNK*A+W5XQBbJy|)(IDA=L9d>t1`KX2b zOX(Ffv*m?e>! zS3lc>XC@IqPf1g-%^4XyGl*1v0NWnwZTW?z4Y6sncXkaA{?NYna3(n@(+n+#sYm}A zGQS;*Li$4R(Ff{obl3#6pUsA0fKuWurQo$mWXMNPV5K66V!XYOyc})^>889Hg3I<{V^Lj9($B4Zu$xRr=89-lDz9x`+I8q(vEAimx1K{sTbs|5x7S zZ+7o$;9&9>@3K;5-DVzGw=kp7ez%1*kxhGytdLS>Q)=xUWv3k_x(IsS8we39Tijvr z`GKk>gkZTHSht;5q%fh9z?vk%sWO}KR04G9^jleJ^@ovWrob7{1xy7V=;S~dDVt%S za$Q#Th%6g1(hiP>hDe}7lcuI94K-2~Q0R3A1nsb7Y*Z!DtQ(Ic<0;TDKvc6%1kBdJ z$hF!{uALB0pa?B^TC}#N5gZ|CKjy|BnT$7eaKj;f>Alqdb_FA3yjZ4CCvm)D&ibL) zZRi91HC!TIAUl<|`rK_6avGh`!)TKk=j|8*W|!vb9>HLv^E%t$`@r@piI(6V8pqDG zBON7~=cf1ZWF6jc{qkKm;oYBtUpIdau6s+<-o^5qNi-p%L%xAtn9OktFd{@EjVAT% z#?-MJ5}Q9QiK_jYYWs+;I4&!N^(mb!%4zx7qO6oCEDn=8oL6#*9XIJ&iJ30O`0vsFy|fEVkw}*jd&B6!IYi+~Y)qv6QlM&V9g0 zh)@^BVDB|P&#X{31>G*nAT}Mz-j~zd>L{v{9AxrxKFw8j;ccQ$NE0PZCc(7fEt1xd z`(oR2!gX6}R+Z77VkDz^{I)@%&HQT5q+1xlf*3R^U8q%;IT8-B53&}dNA7GW`Ki&= z$lrdH zDCu;j$GxW<&v_4Te7=AE2J0u1NM_7Hl9$u{z(8#%8vvrx2P#R7AwnY|?#LbWmROa; zOJzU_*^+n(+k;Jd{e~So9>OF>fPx$Hb$?~K1ul2xr>>o@**n^6IMu8+o3rDp(X$cC z`wQt9qIS>yjA$K~bg{M%kJ00A)U4L+#*@$8UlS#lN3YA{R{7{-zu#n1>0@(#^eb_% zY|q}2)jOEM8t~9p$X5fpT7BZQ1bND#^Uyaa{mNcFWL|MoYb@>y`d{VwmsF&haoJuS2W7azZU0{tu#Jj_-^QRc35tjW~ae&zhKk!wD}#xR1WHu z_7Fys#bp&R?VXy$WYa$~!dMxt2@*(>@xS}5f-@6eoT%rwH zv_6}M?+piNE;BqaKzm1kK@?fTy$4k5cqYdN8x-<(o6KelwvkTqC3VW5HEnr+WGQlF zs`lcYEm=HPpmM4;Ich7A3a5Mb3YyQs7(Tuz-k4O0*-YGvl+2&V(B&L1F8qfR0@vQM-rF<2h-l9T12eL}3LnNAVyY_z51xVr$%@VQ-lS~wf3mnHc zoM({3Z<3+PpTFCRn_Y6cbxu9v>_>eTN0>hHPl_NQQuaK^Mhrv zX{q#80ot;ptt3#js3>kD&uNs{G0mQp>jyc0GG?=9wb33hm z`y2jL=J)T1JD7eX3xa4h$bG}2ev=?7f>-JmCj6){Upo&$k{2WA=%f;KB;X5e;JF3IjQBa4e-Gp~xv- z|In&Rad7LjJVz*q*+splCj|{7=kvQLw0F@$vPuw4m^z=B^7=A4asK_`%lEf_oIJ-O z{L)zi4bd#&g0w{p1$#I&@bz3QXu%Y)j46HAJKWVfRRB*oXo4lIy7BcVl4hRs<%&iQ zr|)Z^LUJ>qn>{6y`JdabfNNFPX7#3`x|uw+z@h<`x{J4&NlDjnknMf(VW_nKWT!Jh zo1iWBqT6^BR-{T=4Ybe+?6zxP_;A5Uo{}Xel%*=|zRGm1)pR43K39SZ=%{MDCS2d$~}PE-xPw4ZK6)H;Zc&0D5p!vjCn0wCe&rVIhchR9ql!p2`g0b@JsC^J#n_r*4lZ~u0UHKwo(HaHUJDHf^gdJhTdTW z3i7Zp_`xyKC&AI^#~JMVZj^9WsW}UR#nc#o+ifY<4`M+?Y9NTBT~p`ONtAFf8(ltr*ER-Ig!yRs2xke#NN zkyFcaQKYv>L8mQdrL+#rjgVY>Z2_$bIUz(kaqL}cYENh-2S6BQK-a(VNDa_UewSW` zMgHi<3`f!eHsyL6*^e^W7#l?V|42CfAjsgyiJsA`yNfAMB*lAsJj^K3EcCzm1KT zDU2+A5~X%ax-JJ@&7>m`T;;}(-e%gcYQtj}?ic<*gkv)X2-QJI5I0tA2`*zZRX(;6 zJ0dYfMbQ+{9Rn3T@Iu4+imx3Y%bcf2{uT4j-msZ~eO)5Z_T7NC|Nr3)|NWjomhv=E zXaVin)MY)`1QtDyO7mUCjG{5+o1jD_anyKn73uflH*ASA8rm+S=gIfgJ);>Zx*hNG z!)8DDCNOrbR#9M7Ud_1kf6BP)x^p(|_VWCJ+(WGDbYmnMLWc?O4zz#eiP3{NfP1UV z(n3vc-axE&vko^f+4nkF=XK-mnHHQ7>w05$Q}iv(kJc4O3TEvuIDM<=U9@`~WdKN* zp4e4R1ncR_kghW}>aE$@OOc~*aH5OOwB5U*Z)%{LRlhtHuigxH8KuDwvq5{3Zg{Vr zrd@)KPwVKFP2{rXho(>MTZZfkr$*alm_lltPob4N4MmhEkv`J(9NZFzA>q0Ch;!Ut zi@jS_=0%HAlN+$-IZGPi_6$)ap>Z{XQGt&@ZaJ(es!Po5*3}>R4x66WZNsjE4BVgn z>}xm=V?F#tx#e+pimNPH?Md5hV7>0pAg$K!?mpt@pXg6UW9c?gvzlNe0 z3QtIWmw$0raJkjQcbv-7Ri&eX6Ks@@EZ&53N|g7HU<;V1pkc&$3D#8k!coJ=^{=vf z-pCP;vr2#A+i#6VA?!hs6A4P@mN62XYY$#W9;MwNia~89i`=1GoFESI+%Mbrmwg*0 zbBq4^bA^XT#1MAOum)L&ARDXJ6S#G>&*72f50M1r5JAnM1p7GFIv$Kf9eVR(u$KLt z9&hQ{t^i16zL1c(tRa~?qr?lbSN;1k;%;p*#gw_BwHJRjcYPTj6>y-rw*dFTnEs95 z`%-AoPL!P16{=#RI0 zUb6#`KR|v^?6uNnY`zglZ#Wd|{*rZ(x&Hk8N6ob6mpX~e^qu5kxvh$2TLJA$M=rx zc!#ot+sS+-!O<0KR6+Lx&~zgEhCsbFY{i_DQCihspM?e z-V}HemMAvFzXR#fV~a=Xf-;tJ1edd}Mry@^=9BxON;dYr8vDEK<<{ zW~rg(ZspxuC&aJo$GTM!9_sXu(EaQJNkV9AC(ob#uA=b4*!Uf}B*@TK=*dBvKKPAF z%14J$S)s-ws9~qKsf>DseEW(ssVQ9__YNg}r9GGx3AJiZR@w_QBlGP>yYh0lQCBtf zx+G;mP+cMAg&b^7J!`SiBwC81M_r0X9kAr2y$0(Lf1gZK#>i!cbww(hn$;fLIxRf? z!AtkSZc-h76KGSGz%48Oe`8ZBHkSXeVb!TJt_VC>$m<#}(Z}!(3h631ltKb3CDMw^fTRy%Ia!b&at`^g7Ew-%WLT9(#V0OP9CE?uj62s>`GI3NA z!`$U+i<`;IQyNBkou4|-7^9^ylac-Xu!M+V5p5l0Ve?J0wTSV+$gYtoc=+Ve*OJUJ z$+uIGALW?}+M!J9+M&#bT=Hz@{R2o>NtNGu1yS({pyteyb>*sg4N`KAD?`u3F#C1y z2K4FKOAPASGZTep54PqyCG(h3?kqQQAxDSW@>T2d!n;9C8NGS;3A8YMRcL>b=<<%M zMiWf$jY;`Ojq5S{kA!?28o)v$;)5bTL<4eM-_^h4)F#eeC2Dj*S`$jl^yn#NjJOYT zx%yC5Ww@eX*zsM)P(5#wRd=0+3~&3pdIH7CxF_2iZSw@>kCyd z%M}$1p((Bidw4XNtk&`BTkU{-PG)SXIZ)yQ!Iol6u8l*SQ1^%zC72FP zLvG>_Z0SReMvB%)1@+et0S{<3hV@^SY3V~5IY(KUtTR{*^xJ^2NN{sIMD9Mr9$~(C$GLNlSpzS=fsbw-DtHb_T|{s z9OR|sx!{?F``H!gVUltY7l~dx^a(2;OUV^)7 z%@hg`8+r&xIxmzZ;Q&v0X%9P)U0SE@r@(lKP%TO(>6I_iF{?PX(bez6v8Gp!W_nd5 z<8)`1jcT)ImNZp-9rr4_1MQ|!?#8sJQx{`~7)QZ75I=DPAFD9Mt{zqFrcrXCU9MG8 zEuGcy;nZ?J#M3!3DWW?Zqv~dnN6ijlIjPfJx(#S0cs;Z=jDjKY|$w2s4*Xa1Iz953sN2Lt!Vmk|%ZwOOqj`sA--5Hiaq8!C%LV zvWZ=bxeRV(&%BffMJ_F~~*FdcjhRVNUXu)MS(S#67rDe%Ler=GS+WysC1I2=Bmbh3s6wdS}o$0 zz%H08#SPFY9JPdL6blGD$D-AaYi;X!#zqib`(XX*i<*eh+2UEPzU4}V4RlC3{<>-~ zadGA8lSm>b7Z!q;D_f9DT4i)Q_}ByElGl*Cy~zX%IzHp)@g-itZB6xM70psn z;AY8II99e6P2drgtTG5>`^|7qg`9MTp%T~|1N3tBqV}2zgow3TFAH{XPor0%=HrkXnKyxyozHlJ6 zd3}OWkl?H$l#yZqOzZbMI+lDLoH48;s10!m1!K87g;t}^+A3f3e&w{EYhVPR0Km*- zh5-ku$Z|Ss{2?4pGm(Rz!0OQb^_*N`)rW{z)^Cw_`a(_L9j=&HEJl(!4rQy1IS)>- zeTIr>hOii`gc(fgYF(cs$R8l@q{mJzpoB5`5r>|sG zBpsY}RkY(g5`bj~D>(;F8v*DyjX(#nVLSs>)XneWI&%Wo>a0u#4A?N<1SK4D}&V1oN)76 z%S>a2n3n>G`YY1>0Hvn&AMtMuI_?`5?4y3w2Hnq4Qa2YH5 zxKdfM;k467djL31Y$0kd9FCPbU=pHBp@zaIi`Xkd80;%&66zvSqsq6%aY)jZacfvw ztkWE{ZV6V2WL9e}Dvz|!d96KqVkJU@5ryp#rReeWu>mSrOJxY^tWC9wd0)$+lZc%{ zY=c4#%OSyQJvQUuy^u}s8DN8|8T%TajOuaY^)R-&8s@r9D`(Ic4NmEu)fg1f!u`xUb;9t#rM z>}cY=648@d5(9A;J)d{a^*ORdVtJrZ77!g~^lZ9@)|-ojvW#>)Jhe8$7W3mhmQh@S zU=CSO+1gSsQ+Tv=x-BD}*py_Ox@;%#hPb&tqXqyUW9jV+fonnuCyVw=?HR>dAB~Fg z^vl*~y*4|)WUW*9RC%~O1gHW~*tJb^a-j;ae2LRNo|0S2`RX>MYqGKB^_ng7YRc@! zFxg1X!VsvXkNuv^3mI`F2=x6$(pZdw=jfYt1ja3FY7a41T07FPdCqFhU6%o|Yb6Z4 zpBGa=(ao3vvhUv#*S{li|EyujXQPUV;0sa5!0Ut)>tPWyC9e0_9(=v*z`TV5OUCcx zT=w=^8#5u~7<}8Mepqln4lDv*-~g^VoV{(+*4w(q{At6d^E-Usa2`JXty++Oh~on^ z;;WHkJsk2jvh#N|?(2PLl+g!M0#z_A;(#Uy=TzL&{Ei5G9#V{JbhKV$Qmkm%5tn!CMA? z@hM=b@2DZWTQ6>&F6WCq6;~~WALiS#@{|I+ucCmD6|tBf&e;$_)%JL8$oIQ%!|Xih1v4A$=7xNO zZVz$G8;G5)rxyD+M0$20L$4yukA_D+)xmK3DMTH3Q+$N&L%qB)XwYx&s1gkh=%qGCCPwnwhbT4p%*3R)I}S#w7HK3W^E%4w z2+7ctHPx3Q97MFYB48HfD!xKKb(U^K_4)Bz(5dvwyl*R?)k;uHEYVi|{^rvh)w7}t z`tnH{v9nlVHj2ign|1an_wz0vO)*`3RaJc#;(W-Q6!P&>+@#fptCgtUSn4!@b7tW0&pE2Qj@7}f#ugu4*C)8_}AMRuz^WG zc)XDcOPQjRaGptRD^57B83B-2NKRo!j6TBAJntJPHNQG;^Oz}zt5F^kId~miK3J@l ztc-IKp6qL!?u~q?qfGP0I~$5gvq#-0;R(oLU@sYayr*QH95fnrYA*E|n%&FP@Cz`a zSdJ~(c@O^>qaO`m9IQ8sd8!L<+)GPJDrL7{4{ko2gWOZel^3!($Gjt|B&$4dtfTmBmC>V`R&&6$wpgvdmns zxcmfS%9_ZoN>F~azvLFtA(9Q5HYT#A(byGkESnt{$Tu<73$W~reB4&KF^JBsoqJ6b zS?$D7DoUgzLO-?P`V?5_ub$nf1p0mF?I)StvPomT{uYjy!w&z$t~j&en=F~hw|O(1 zlV9$arQmKTc$L)Kupwz_zA~deT+-0WX6NzFPh&d+ly*3$%#?Ca9Z9lOJsGVoQ&1HNg+)tJ_sw)%oo*DK)iU~n zvL``LqTe=r=7SwZ@LB)9|3QB5`0(B9r(iR}0nUwJss-v=dXnwMRQFYSRK1blS#^g(3@z{`=8_CGDm!LESTWig zzm1{?AG&7`uYJ;PoFO$o8RWuYsV26V{>D-iYTnvq7igWx9@w$EC*FV^vpvDl@i9yp zPIqiX@hEZF4VqzI3Y)CHhR`xKN8poL&~ak|wgbE4zR%Dm(a@?bw%(7(!^>CM!^4@J z6Z)KhoQP;WBq_Z_&<@i2t2&xq>N>b;Np2rX?yK|-!14iE2T}E|jC+=wYe~`y38g3J z8QGZquvqBaG!vw&VtdXWX5*i5*% zJP~7h{?&E|<#l{klGPaun`IgAJ4;RlbRqgJz5rmHF>MtJHbfqyyZi53?Lhj=(Ku#& z__ubmZIxzSq3F90Xur!1)Vqe6b@!ueHA!93H~jdHmaS5Q^CULso}^poy)0Op6!{^9 zWyCyyIrdBP4fkliZ%*g+J-A!6VFSRF6Liu6G^^=W>cn81>4&7(c7(6vCGSAJ zQZ|S3mb|^Wf=yJ(h~rq`iiW~|n#$+KcblIR<@|lDtm!&NBzSG-1;7#YaU+-@=xIm4 zE}edTYd~e&_%+`dIqqgFntL-FxL3!m4yTNt<(^Vt9c6F(`?9`u>$oNxoKB29<}9FE zgf)VK!*F}nW?}l95%RRk8N4^Rf8)Xf;drT4<|lUDLPj^NPMrBPL;MX&0oGCsS za3}vWcF(IPx&W6{s%zwX{UxHX2&xLGfT{d9bWP!g;Lg#etpuno$}tHoG<4Kd*=kpU z;4%y(<^yj(UlG%l-7E9z_Kh2KoQ19qT3CR@Ghr>BAgr3Vniz3LmpC4g=g|A3968yD2KD$P7v$ zx9Q8`2&qH3&y-iv0#0+jur@}k`6C%7fKbCr|tHX2&O%r?rBpg`YNy~2m+ z*L7dP$RANzVUsG_Lb>=__``6vA*xpUecuGsL+AW?BeSwyoQfDlXe8R1*R1M{0#M?M zF+m19`3<`gM{+GpgW^=UmuK*yMh3}x)7P738wL8r@(Na6%ULPgbPVTa6gh5Q(SR0f znr6kdRpe^(LVM;6Rt(Z@Lsz3EX*ry6(WZ?w>#ZRelx)N%sE+MN>5G|Z8{%@b&D+Ov zPU{shc9}%;G7l;qbonIb_1m^Qc8ez}gTC-k02G8Rl?7={9zBz8uRX2{XJQ{vZhs67avlRn| zgRtWl0Lhjet&!YC47GIm%1gdq%T24_^@!W3pCywc89X4I5pnBCZDn(%!$lOGvS*`0!AoMtqxNPFgaMR zwoW$p;8l6v%a)vaNsesED3f}$%(>zICnoE|5JwP&+0XI}JxPccd+D^gx`g`=GsUc0 z9Uad|C+_@_0%JmcObGnS@3+J^0P!tg+fUZ_w#4rk#TlJYPXJiO>SBxzs9(J;XV9d{ zmTQE1(K8EYaz9p^XLbdWudyIPJlGPo0U*)fAh-jnbfm@SYD_2+?|DJ-^P+ojG{2{6 z>HJtedEjO@j_tqZ4;Zq1t5*5cWm~W?HGP!@_f6m#btM@46cEMhhK{(yI&jG)fwL1W z^n_?o@G8a-jYt!}$H*;{0#z8lANlo!9b@!c5K8<(#lPlpE!z86Yq#>WT&2} z;;G1$pD%iNoj#Z=&kij5&V1KHIhN-h<;{HC5wD)PvkF>CzlQOEx_0;-TJ*!#&{Wzt zKcvq^SZIdop}y~iouNqtU7K7+?eIz-v_rfNM>t#i+dD$s_`M;sjGubTdP)WI*uL@xPOLHt#~T<@Yz>xt50ZoTw;a(a}lNiDN-J${gOdE zx?8LOA|tv{Mb}=TTR=LcqMqbCJkKj+@;4Mu)Cu0{`~ohix6E$g&tff)aHeUAQQ%M? zIN4uSUTzC1iMEWL*W-in1y)C`E+R8j?4_?X4&2Zv5?QdkNMz(k} zw##^Ikx`#_s>i&CO_mu@vJJ*|3ePRDl5pq$9V^>D;g0R%l>lw;ttyM6Sy`NBF{)Lr zSk)V>mZr96+aHY%vTLLt%vO-+juw6^SO_ zYGJaGeWX6W(TOQx=5oTGXOFqMMU*uZyt>MR-Y`vxW#^&)H zk0!F8f*@v6NO@Z*@Qo)+hlX40EWcj~j9dGrLaq%1;DE_%#lffXCcJ;!ZyyyZTz74Q zb2WSly6sX{`gQeToQsi1-()5EJ1nJ*kXGD`xpXr~?F#V^sxE3qSOwRSaC9x9oa~jJ zTG9`E|q zC5Qs1xh}jzb5UPYF`3N9YuMnI7xsZ41P;?@c|%w zl=OxLr6sMGR+`LStLvh)g?fA5p|xbUD;yFAMQg&!PEDYxVYDfA>oTY;CFt`cg?Li1 z0b})!9Rvw&j#*&+D2))kXLL z0+j=?7?#~_}N-qdEIP>DQaZh#F(#e0WNLzwUAj@r694VJ8?Dr5_io2X49XYsG^ zREt0$HiNI~6VV!ycvao+0v7uT$_ilKCvsC+VDNg7yG1X+eNe^3D^S==F3ByiW0T^F zH6EsH^}Uj^VPIE&m)xlmOScYR(w750>hclqH~~dM2+;%GDXT`u4zG!p((*`Hwx41M z4KB+`hfT(YA%W)Ve(n+Gu9kuXWKzxg{1ff^xNQw>w%L-)RySTk9kAS92(X0Shg^Q? zx1YXg_TLC^?h6!4mBqZ9pKhXByu|u~gF%`%`vdoaGBN3^j4l!4x?Bw4Jd)Z4^di}! zXlG1;hFvc>H?bmmu1E7Vx=%vahd!P1#ZGJOJYNbaek^$DHt`EOE|Hlij+hX>ocQFSLVu|wz`|KVl@Oa;m2k6b*mNK2Vo{~l9>Qa3@B7G7#k?)aLx;w6U ze8bBq%vF?5v>#TspEoaII!N}sRT~>bh-VWJ7Q*1qsz%|G)CFmnttbq$Ogb{~YK_=! z{{0vhlW@g!$>|}$&4E3@k`KPElW6x#tSX&dfle>o!irek$NAbDzdd2pVeNzk4&qgJ zXvNF0$R96~g0x+R1igR=Xu&X_Hc5;!Ze&C)eUTB$9wW&?$&o8Yxhm5s(S`;?{> z*F?9Gr0|!OiKA>Rq-ae=_okB6&yMR?!JDer{@iQgIn=cGxs-u^!8Q$+N&pfg2WM&Z zulHu=Uh~U>fS{=Nm0x>ACvG*4R`Dx^kJ65&Vvfj`rSCV$5>c04N26Rt2S?*kh3JKq z9(3}5T?*x*AP(X2Ukftym0XOvg~r6Ms$2x&R&#}Sz23aMGU&7sU-cFvE3Eq`NBJe84VoftWF#v7PDAp`@V zRFCS24_k~;@~R*L)eCx@Q9EYmM)Sn}HLbVMyxx%{XnMBDc-YZ<(DXDBYUt8$u5Zh} zBK~=M9cG$?_m_M61YG+#|9Vef7LfbH>(C21&aC)x$^Lg}fa#SF){RX|?-xZjSOrn# z2ZAwUF)$VB<&S;R3FhNSQOV~8w%A`V9dWyLiy zgt7G=Z4t|zU3!dh5|s(@XyS|waBr$>@=^Dspmem8)@L`Ns{xl%rGdX!R(BiC5C7Vo zXetb$oC_iXS}2x_Hy}T(hUUNbO47Q@+^4Q`h>(R-;OxCyW#eoOeC51jzxnM1yxBrp zz6}z`(=cngs6X05e79o_B7@3K|Qpe3n38Py_~ zpi?^rj!`pq!7PHGliC$`-8A^Ib?2qgJJCW+(&TfOnFGJ+@-<<~`7BR0f4oSINBq&R z2CM`0%WLg_Duw^1SPwj-{?BUl2Y=M4e+7yL1{C&&f&zjF06#xf>VdLozgNye(BNgSD`=fFbBy0HIosLl@JwCQl^s;eTnc( z3!r8G=K>zb`|bLLI0N|eFJk%s)B>oJ^M@AQzqR;HUjLsOqW<0v>1ksT_#24*U@R3HJu*A^#1o#P3%3_jq>icD@<`tqU6ICEgZrME(xX#?i^Z z%Id$_uyQGlFD-CcaiRtRdGn|K`Lq5L-rx7`vYYGH7I=eLfHRozPiUtSe~Tt;IN2^gCXmf2#D~g2@9bhzK}3nphhG%d?V7+Zq{I2?Gt*!NSn_r~dd$ zqkUOg{U=MI?Ehx@`(X%rQB?LP=CjJ*V!rec{#0W2WshH$X#9zep!K)tzZoge*LYd5 z@g?-j5_mtMp>_WW`p*UNUZTFN{_+#m*bJzt{hvAdkF{W40{#L3w6gzPztnsA_4?&0 z(+>pv!zB16rR-(nm(^c>Z(its{ny677vT8sF564^mlZvJ!h65}OW%Hn|2OXbOQM%b z{6C54Z2v;^hyMQ;UH+HwFD2!F!VlQ}6Z{L0_9g5~CH0@Mqz?ZC`^QkhOU#$Lx<4`B zyZsa9uPF!rZDo8ZVfzzR#raQ>5|)k~_Ef*wDqG^76o)j!C4 zykvT*o$!-MBko@?{b~*Zf2*YMlImrK`cEp|#D7f%Twm<|C|dWD \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# 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"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# 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 + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# 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" + which java >/dev/null 2>&1 || 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 + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000000..6d57edc706 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@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=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@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" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="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! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/matrix-sdk-android/.gitignore b/matrix-sdk-android/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/matrix-sdk-android/.gitignore @@ -0,0 +1 @@ +/build diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle new file mode 100644 index 0000000000..a96e0690dc --- /dev/null +++ b/matrix-sdk-android/build.gradle @@ -0,0 +1,209 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'realm-android' +apply plugin: 'okreplay' + +buildscript { + repositories { + jcenter() + } + dependencies { + classpath "io.realm:realm-gradle-plugin:6.1.0" + } +} + +androidExtensions { + experimental = true +} + +android { + compileSdkVersion 29 + testOptions.unitTests.includeAndroidResources = true + + defaultConfig { + minSdkVersion 21 + targetSdkVersion 29 + versionCode 1 + versionName "0.0.1" + // Multidex is useful for tests + multiDexEnabled true + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + // The following argument makes the Android Test Orchestrator run its + // "pm clear" command after each test invocation. This command ensures + // that the app's state is completely cleared between tests. + testInstrumentationRunnerArguments clearPackageData: 'true' + + buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" + resValue "string", "git_sdk_revision", "\"${gitRevision()}\"" + resValue "string", "git_sdk_revision_unix_date", "\"${gitRevisionUnixDate()}\"" + resValue "string", "git_sdk_revision_date", "\"${gitRevisionDate()}\"" + + defaultConfig { + consumerProguardFiles 'proguard-rules.pro' + } + } + + testOptions { + execution 'ANDROIDX_TEST_ORCHESTRATOR' + } + + buildTypes { + debug { + // Set to true to log privacy or sensible data, such as token + buildConfigField "boolean", "LOG_PRIVATE_DATA", project.property("vector.debugPrivateData") + // Set to BODY instead of NONE to enable logging + buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level." + project.property("vector.httpLogLevel") + } + + release { + buildConfigField "boolean", "LOG_PRIVATE_DATA", "false" + buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level.NONE" + } + } + + adbOptions { + installOptions "-g" + } + + lintOptions { + lintConfig file("lint.xml") + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } + + sourceSets { + androidTest { + java.srcDirs += "src/sharedTest/java" + } + test { + java.srcDirs += "src/sharedTest/java" + } + } +} + +static def gitRevision() { + def cmd = "git rev-parse --short=8 HEAD" + return cmd.execute().text.trim() +} + +static def gitRevisionUnixDate() { + def cmd = "git show -s --format=%ct HEAD^{commit}" + return cmd.execute().text.trim() +} + +static def gitRevisionDate() { + def cmd = "git show -s --format=%ci HEAD^{commit}" + return cmd.execute().text.trim() +} + +dependencies { + + def arrow_version = "0.8.2" + def moshi_version = '1.8.0' + def lifecycle_version = '2.2.0' + def arch_version = '2.1.0' + def coroutines_version = "1.3.2" + def markwon_version = '3.1.0' + def daggerVersion = '2.25.4' + def work_version = '2.3.3' + def retrofit_version = '2.6.2' + + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" + + implementation "androidx.appcompat:appcompat:1.1.0" + implementation "androidx.core:core-ktx:1.3.0" + + implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" + + // Network + implementation "com.squareup.retrofit2:retrofit:$retrofit_version" + implementation "com.squareup.retrofit2:converter-moshi:$retrofit_version" + implementation "com.squareup.retrofit2:converter-scalars:$retrofit_version" + implementation 'com.squareup.okhttp3:okhttp:4.2.2' + implementation 'com.squareup.okhttp3:logging-interceptor:4.2.2' + implementation "com.squareup.moshi:moshi-adapters:$moshi_version" + kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version" + + implementation "ru.noties.markwon:core:$markwon_version" + + // Image + implementation 'androidx.exifinterface:exifinterface:1.3.0-alpha01' + implementation 'id.zelory:compressor:3.0.0' + + // Database + implementation 'com.github.Zhuinden:realm-monarchy:0.5.1' + kapt 'dk.ilios:realmfieldnameshelper:1.1.1' + + // Work + implementation "androidx.work:work-runtime-ktx:$work_version" + + // FP + implementation "io.arrow-kt:arrow-core:$arrow_version" + implementation "io.arrow-kt:arrow-instances-core:$arrow_version" + + // olm lib is now hosted by jitpack: https://jitpack.io/#org.matrix.gitlab.matrix-org/olm + implementation 'org.matrix.gitlab.matrix-org:olm:3.1.2' + + // DI + implementation "com.google.dagger:dagger:$daggerVersion" + kapt "com.google.dagger:dagger-compiler:$daggerVersion" + compileOnly 'com.squareup.inject:assisted-inject-annotations-dagger2:0.5.0' + kapt 'com.squareup.inject:assisted-inject-processor-dagger2:0.5.0' + + // Logging + implementation 'com.jakewharton.timber:timber:4.7.1' + implementation 'com.facebook.stetho:stetho-okhttp3:1.5.1' + + // Bus + implementation 'org.greenrobot:eventbus:3.1.1' + + // Phone number https://github.com/google/libphonenumber + implementation 'com.googlecode.libphonenumber:libphonenumber:8.10.23' + + // Web RTC + // TODO meant for development purposes only. See http://webrtc.github.io/webrtc-org/native-code/android/ + implementation 'org.webrtc:google-webrtc:1.0.+' + + debugImplementation 'com.airbnb.okreplay:okreplay:1.5.0' + releaseImplementation 'com.airbnb.okreplay:noop:1.5.0' + androidTestImplementation 'com.airbnb.okreplay:espresso:1.5.0' + + testImplementation 'junit:junit:4.12' + testImplementation 'org.robolectric:robolectric:4.3' + //testImplementation 'org.robolectric:shadows-support-v4:3.0' + // Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281 + testImplementation 'io.mockk:mockk:1.9.2.kotlin12' + testImplementation 'org.amshove.kluent:kluent-android:1.44' + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" + // Plant Timber tree for test + testImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1' + + kaptAndroidTest "com.google.dagger:dagger-compiler:$daggerVersion" + androidTestImplementation 'androidx.test:core:1.2.0' + androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation 'androidx.test:rules:1.2.0' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + androidTestImplementation 'org.amshove.kluent:kluent-android:1.44' + // Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281 + androidTestImplementation 'io.mockk:mockk-android:1.9.2.kotlin12' + androidTestImplementation "androidx.arch.core:core-testing:$arch_version" + androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" + // Plant Timber tree for test + androidTestImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1' + + androidTestUtil 'androidx.test:orchestrator:1.2.0' +} diff --git a/matrix-sdk-android/lint.xml b/matrix-sdk-android/lint.xml new file mode 100644 index 0000000000..3e4078d7d9 --- /dev/null +++ b/matrix-sdk-android/lint.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/matrix-sdk-android/proguard-rules.pro b/matrix-sdk-android/proguard-rules.pro new file mode 100644 index 0000000000..fa860d8049 --- /dev/null +++ b/matrix-sdk-android/proguard-rules.pro @@ -0,0 +1,82 @@ +# 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 + + +### EVENT BUS ### + +-keepattributes *Annotation* +-keepclassmembers class * { + @org.greenrobot.eventbus.Subscribe ; +} +-keep enum org.greenrobot.eventbus.ThreadMode { *; } + +### MOSHI ### + +# JSR 305 annotations are for embedding nullability information. + +-dontwarn javax.annotation.** + +-keepclasseswithmembers class * { + @com.squareup.moshi.* ; +} + +-keep @com.squareup.moshi.JsonQualifier interface * + +# Enum field names are used by the integrated EnumJsonAdapter. +# values() is synthesized by the Kotlin compiler and is used by EnumJsonAdapter indirectly +# Annotate enums with @JsonClass(generateAdapter = false) to use them with Moshi. +-keepclassmembers @com.squareup.moshi.JsonClass class * extends java.lang.Enum { + ; + **[] values(); +} + +-keep class kotlin.reflect.jvm.internal.impl.builtins.BuiltInsLoaderImpl + +-keepclassmembers class kotlin.Metadata { + public ; +} + +### OKHTTP for Android Studio ### +-keep class okhttp3.Headers { *; } +-keep interface okhttp3.Interceptor.* { *; } + +### OLM JNI ### +-keep class org.matrix.olm.** { *; } + +### Webrtc +-keep class org.webrtc.** { *; } + +### Serializable persisted classes +# https://www.guardsquare.com/en/products/proguard/manual/examples#serializable +-keepnames class * implements java.io.Serializable + +-keepclassmembers class * implements java.io.Serializable { + static final long serialVersionUID; + private static final java.io.ObjectStreamField[] serialPersistentFields; + !static !transient ; + !private ; + !private ; + private void writeObject(java.io.ObjectOutputStream); + private void readObject(java.io.ObjectInputStream); + java.lang.Object writeReplace(); + java.lang.Object readResolve(); +} \ No newline at end of file diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/InstrumentedTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/InstrumentedTest.kt new file mode 100644 index 0000000000..cb6e624bce --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/InstrumentedTest.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import org.matrix.android.sdk.test.shared.createTimberTestRule +import org.junit.Rule +import java.io.File + +interface InstrumentedTest { + + @Rule + fun timberTestRule() = createTimberTestRule() + + fun context(): Context { + return ApplicationProvider.getApplicationContext() + } + + fun cacheDir(): File { + return context().cacheDir + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/LiveDataTestObserver.java b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/LiveDataTestObserver.java new file mode 100644 index 0000000000..a09a655008 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/LiveDataTestObserver.java @@ -0,0 +1,211 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk; + +import androidx.annotation.Nullable; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.Observer; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +public final class LiveDataTestObserver implements Observer { + private final List valueHistory = new ArrayList<>(); + private final List> childObservers = new ArrayList<>(); + + @Deprecated // will be removed in version 1.0 + private final LiveData observedLiveData; + + private CountDownLatch valueLatch = new CountDownLatch(1); + + private LiveDataTestObserver(LiveData observedLiveData) { + this.observedLiveData = observedLiveData; + } + + @Override + public void onChanged(@Nullable T value) { + valueHistory.add(value); + valueLatch.countDown(); + for (Observer childObserver : childObservers) { + childObserver.onChanged(value); + } + } + + public T value() { + assertHasValue(); + return valueHistory.get(valueHistory.size() - 1); + } + + public List valueHistory() { + return Collections.unmodifiableList(valueHistory); + } + + /** + * Disposes and removes observer from observed live data. + * + * @return This Observer + * @deprecated Please use {@link LiveData#removeObserver(Observer)} instead, will be removed in 1.0 + */ + @Deprecated + public LiveDataTestObserver dispose() { + observedLiveData.removeObserver(this); + return this; + } + + public LiveDataTestObserver assertHasValue() { + if (valueHistory.isEmpty()) { + throw fail("Observer never received any value"); + } + + return this; + } + + public LiveDataTestObserver assertNoValue() { + if (!valueHistory.isEmpty()) { + throw fail("Expected no value, but received: " + value()); + } + + return this; + } + + public LiveDataTestObserver assertHistorySize(int expectedSize) { + int size = valueHistory.size(); + if (size != expectedSize) { + throw fail("History size differ; Expected: " + expectedSize + ", Actual: " + size); + } + return this; + } + + public LiveDataTestObserver assertValue(T expected) { + T value = value(); + + if (expected == null && value == null) { + return this; + } + + if (!value.equals(expected)) { + throw fail("Expected: " + valueAndClass(expected) + ", Actual: " + valueAndClass(value)); + } + + return this; + } + + public LiveDataTestObserver assertValue(Function valuePredicate) { + T value = value(); + + if (!valuePredicate.apply(value)) { + throw fail("Value not present"); + } + + return this; + } + + public LiveDataTestObserver assertNever(Function valuePredicate) { + int size = valueHistory.size(); + for (int valueIndex = 0; valueIndex < size; valueIndex++) { + T value = this.valueHistory.get(valueIndex); + if (valuePredicate.apply(value)) { + throw fail("Value at position " + valueIndex + " matches predicate " + + valuePredicate.toString() + ", which was not expected."); + } + } + + return this; + } + + /** + * Awaits until this TestObserver has any value. + *

+ * If this TestObserver has already value then this method returns immediately. + * + * @return this + * @throws InterruptedException if the current thread is interrupted while waiting + */ + public LiveDataTestObserver awaitValue() throws InterruptedException { + valueLatch.await(); + return this; + } + + /** + * Awaits the specified amount of time or until this TestObserver has any value. + *

+ * If this TestObserver has already value then this method returns immediately. + * + * @return this + * @throws InterruptedException if the current thread is interrupted while waiting + */ + public LiveDataTestObserver awaitValue(long timeout, TimeUnit timeUnit) throws InterruptedException { + valueLatch.await(timeout, timeUnit); + return this; + } + + /** + * Awaits until this TestObserver receives next value. + *

+ * If this TestObserver has already value then it awaits for another one. + * + * @return this + * @throws InterruptedException if the current thread is interrupted while waiting + */ + public LiveDataTestObserver awaitNextValue() throws InterruptedException { + return withNewLatch().awaitValue(); + } + + + /** + * Awaits the specified amount of time or until this TestObserver receives next value. + *

+ * If this TestObserver has already value then it awaits for another one. + * + * @return this + * @throws InterruptedException if the current thread is interrupted while waiting + */ + public LiveDataTestObserver awaitNextValue(long timeout, TimeUnit timeUnit) throws InterruptedException { + return withNewLatch().awaitValue(timeout, timeUnit); + } + + private LiveDataTestObserver withNewLatch() { + valueLatch = new CountDownLatch(1); + return this; + } + + private AssertionError fail(String message) { + return new AssertionError(message); + } + + private static String valueAndClass(Object value) { + if (value != null) { + return value + " (class: " + value.getClass().getSimpleName() + ")"; + } + return "null"; + } + + public static LiveDataTestObserver create() { + return new LiveDataTestObserver<>(new MutableLiveData()); + } + + public static LiveDataTestObserver test(LiveData liveData) { + LiveDataTestObserver observer = new LiveDataTestObserver<>(liveData); + liveData.observeForever(observer); + return observer; + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/MainThreadExecutor.java b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/MainThreadExecutor.java new file mode 100644 index 0000000000..d26782d538 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/MainThreadExecutor.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk; + +import android.os.Handler; +import android.os.Looper; + +import java.util.concurrent.Executor; + +public class MainThreadExecutor implements Executor { + + private final Handler handler = new Handler(Looper.getMainLooper()); + + @Override + public void execute(Runnable runnable) { + handler.post(runnable); + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/OkReplayRuleChainNoActivity.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/OkReplayRuleChainNoActivity.kt new file mode 100644 index 0000000000..372ef95be8 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/OkReplayRuleChainNoActivity.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2019 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk + +import okreplay.OkReplayConfig +import okreplay.PermissionRule +import okreplay.RecorderRule +import org.junit.rules.RuleChain +import org.junit.rules.TestRule + +class OkReplayRuleChainNoActivity( + private val configuration: OkReplayConfig) { + + fun get(): TestRule { + return RuleChain.outerRule(PermissionRule(configuration)) + .around(RecorderRule(configuration)) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/SingleThreadCoroutineDispatcher.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/SingleThreadCoroutineDispatcher.kt new file mode 100644 index 0000000000..4316b09b89 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/SingleThreadCoroutineDispatcher.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk + +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.asCoroutineDispatcher +import java.util.concurrent.Executors + +internal val testCoroutineDispatchers = MatrixCoroutineDispatchers(Main, Main, Main, Main, + Executors.newSingleThreadExecutor().asCoroutineDispatcher()) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/AccountCreationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/AccountCreationTest.kt new file mode 100644 index 0000000000..cbb5af5911 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/AccountCreationTest.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2020 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.account + +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.SessionTestParams +import org.matrix.android.sdk.common.TestConstants +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class AccountCreationTest : InstrumentedTest { + + private val commonTestHelper = CommonTestHelper(context()) + private val cryptoTestHelper = CryptoTestHelper(commonTestHelper) + + @Test + fun createAccountTest() { + val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true)) + + commonTestHelper.signOutAndClose(session) + } + + @Test + fun createAccountAndLoginAgainTest() { + val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true)) + + // Log again to the same account + val session2 = commonTestHelper.logIntoAccount(session.myUserId, SessionTestParams(withInitialSync = true)) + + commonTestHelper.signOutAndClose(session) + commonTestHelper.signOutAndClose(session2) + } + + @Test + fun simpleE2eTest() { + val res = cryptoTestHelper.doE2ETestWithAliceInARoom() + + res.cleanUp(commonTestHelper) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/ChangePasswordTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/ChangePasswordTest.kt new file mode 100644 index 0000000000..e2140328e6 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/ChangePasswordTest.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.account + +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.failure.isInvalidPassword +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.SessionTestParams +import org.matrix.android.sdk.common.TestConstants +import org.amshove.kluent.shouldBeTrue +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class ChangePasswordTest : InstrumentedTest { + + private val commonTestHelper = CommonTestHelper(context()) + + companion object { + private const val NEW_PASSWORD = "this is a new password" + } + + @Test + fun changePasswordTest() { + val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = false)) + + // Change password + commonTestHelper.doSync { + session.changePassword(TestConstants.PASSWORD, NEW_PASSWORD, it) + } + + // Try to login with the previous password, it will fail + val throwable = commonTestHelper.logAccountWithError(session.myUserId, TestConstants.PASSWORD) + throwable.isInvalidPassword().shouldBeTrue() + + // Try to login with the new password, should work + val session2 = commonTestHelper.logIntoAccount(session.myUserId, NEW_PASSWORD, SessionTestParams(withInitialSync = false)) + + commonTestHelper.signOutAndClose(session) + commonTestHelper.signOutAndClose(session2) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt new file mode 100644 index 0000000000..36d09fb497 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.account + +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.auth.data.LoginFlowResult +import org.matrix.android.sdk.api.auth.registration.RegistrationResult +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.MatrixError +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.SessionTestParams +import org.matrix.android.sdk.common.TestConstants +import org.matrix.android.sdk.common.TestMatrixCallback +import org.junit.Assert.assertTrue +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class DeactivateAccountTest : InstrumentedTest { + + private val commonTestHelper = CommonTestHelper(context()) + + @Test + fun deactivateAccountTest() { + val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = false)) + + // Deactivate the account + commonTestHelper.doSync { + session.deactivateAccount(TestConstants.PASSWORD, false, it) + } + + // Try to login on the previous account, it will fail (M_USER_DEACTIVATED) + val throwable = commonTestHelper.logAccountWithError(session.myUserId, TestConstants.PASSWORD) + + // Test the error + assertTrue(throwable is Failure.ServerError + && throwable.error.code == MatrixError.M_USER_DEACTIVATED + && throwable.error.message == "This account has been deactivated") + + // Try to create an account with the deactivate account user id, it will fail (M_USER_IN_USE) + val hs = commonTestHelper.createHomeServerConfig() + + commonTestHelper.doSync { + commonTestHelper.matrix.authenticationService.getLoginFlow(hs, it) + } + + var accountCreationError: Throwable? = null + commonTestHelper.waitWithLatch { + commonTestHelper.matrix.authenticationService + .getRegistrationWizard() + .createAccount(session.myUserId.substringAfter("@").substringBefore(":"), + TestConstants.PASSWORD, + null, + object : TestMatrixCallback(it, false) { + override fun onFailure(failure: Throwable) { + accountCreationError = failure + super.onFailure(failure) + } + }) + } + + // Test the error + accountCreationError.let { + assertTrue(it is Failure.ServerError + && it.error.code == MatrixError.M_USER_IN_USE) + } + + // No need to close the session, it has been deactivated + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/Matrix.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/Matrix.kt new file mode 100644 index 0000000000..df359f2adc --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/Matrix.kt @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.api + +import android.content.Context +import androidx.lifecycle.ProcessLifecycleOwner +import androidx.work.Configuration +import androidx.work.WorkManager +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.BuildConfig +import org.matrix.android.sdk.api.auth.AuthenticationService +import org.matrix.android.sdk.common.DaggerTestMatrixComponent +import org.matrix.android.sdk.api.legacy.LegacySessionImporter +import org.matrix.android.sdk.internal.SessionManager +import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt +import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments +import org.matrix.android.sdk.internal.network.UserAgentHolder +import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver +import org.matrix.olm.OlmManager +import java.io.InputStream +import java.util.concurrent.Executors +import java.util.concurrent.atomic.AtomicBoolean +import javax.inject.Inject + +/** + * This is the main entry point to the matrix sdk. + * To get the singleton instance, use getInstance static method. + */ +class Matrix private constructor(context: Context, matrixConfiguration: MatrixConfiguration) { + + @Inject internal lateinit var legacySessionImporter: LegacySessionImporter + @Inject internal lateinit var authenticationService: AuthenticationService + @Inject internal lateinit var userAgentHolder: UserAgentHolder + @Inject internal lateinit var backgroundDetectionObserver: BackgroundDetectionObserver + @Inject internal lateinit var olmManager: OlmManager + @Inject internal lateinit var sessionManager: SessionManager + + init { + Monarchy.init(context) + DaggerTestMatrixComponent.factory().create(context, matrixConfiguration).inject(this) + if (context.applicationContext !is Configuration.Provider) { + WorkManager.initialize(context, Configuration.Builder().setExecutor(Executors.newCachedThreadPool()).build()) + } + ProcessLifecycleOwner.get().lifecycle.addObserver(backgroundDetectionObserver) + } + + fun getUserAgent() = userAgentHolder.userAgent + + fun authenticationService(): AuthenticationService { + return authenticationService + } + + fun legacySessionImporter(): LegacySessionImporter { + return legacySessionImporter + } + + companion object { + + private lateinit var instance: Matrix + private val isInit = AtomicBoolean(false) + + fun initialize(context: Context, matrixConfiguration: MatrixConfiguration) { + if (isInit.compareAndSet(false, true)) { + instance = Matrix(context.applicationContext, matrixConfiguration) + } + } + + fun getInstance(context: Context): Matrix { + if (isInit.compareAndSet(false, true)) { + val appContext = context.applicationContext + if (appContext is MatrixConfiguration.Provider) { + val matrixConfiguration = (appContext as MatrixConfiguration.Provider).providesMatrixConfiguration() + instance = Matrix(appContext, matrixConfiguration) + } else { + throw IllegalStateException("Matrix is not initialized properly." + + " You should call Matrix.initialize or let your application implements MatrixConfiguration.Provider.") + } + } + return instance + } + + fun getSdkVersion(): String { + return BuildConfig.VERSION_NAME + " (" + BuildConfig.GIT_SDK_REVISION + ")" + } + + fun decryptStream(inputStream: InputStream?, elementToDecrypt: ElementToDecrypt): InputStream? { + return MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt) + } + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt new file mode 100644 index 0000000000..fdbfa57b5c --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt @@ -0,0 +1,383 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2018 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.common + +import android.content.Context +import android.net.Uri +import androidx.lifecycle.Observer +import org.matrix.android.sdk.api.Matrix +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import org.matrix.android.sdk.api.auth.data.LoginFlowResult +import org.matrix.android.sdk.api.auth.registration.RegistrationResult +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.Room +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.api.session.room.timeline.Timeline +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings +import org.matrix.android.sdk.api.session.sync.SyncState +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import java.util.ArrayList +import java.util.UUID +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +/** + * This class exposes methods to be used in common cases + * Registration, login, Sync, Sending messages... + */ +class CommonTestHelper(context: Context) { + + val matrix: Matrix + + fun getTestInterceptor(session: Session): MockOkHttpInterceptor? = TestNetworkModule.interceptorForSession(session.sessionId) as? MockOkHttpInterceptor + + init { + Matrix.initialize(context, MatrixConfiguration("TestFlavor")) + matrix = Matrix.getInstance(context) + } + + fun createAccount(userNamePrefix: String, testParams: SessionTestParams): Session { + return createAccount(userNamePrefix, TestConstants.PASSWORD, testParams) + } + + fun logIntoAccount(userId: String, testParams: SessionTestParams): Session { + return logIntoAccount(userId, TestConstants.PASSWORD, testParams) + } + + /** + * Create a Home server configuration, with Http connection allowed for test + */ + fun createHomeServerConfig(): HomeServerConnectionConfig { + return HomeServerConnectionConfig.Builder() + .withHomeServerUri(Uri.parse(TestConstants.TESTS_HOME_SERVER_URL)) + .build() + } + + /** + * This methods init the event stream and check for initial sync + * + * @param session the session to sync + */ + fun syncSession(session: Session) { + val lock = CountDownLatch(1) + + GlobalScope.launch(Dispatchers.Main) { session.open() } + + session.startSync(true) + + val syncLiveData = runBlocking(Dispatchers.Main) { + session.getSyncStateLive() + } + val syncObserver = object : Observer { + override fun onChanged(t: SyncState?) { + if (session.hasAlreadySynced()) { + lock.countDown() + syncLiveData.removeObserver(this) + } + } + } + GlobalScope.launch(Dispatchers.Main) { syncLiveData.observeForever(syncObserver) } + + await(lock) + } + + /** + * Sends text messages in a room + * + * @param room the room where to send the messages + * @param message the message to send + * @param nbOfMessages the number of time the message will be sent + */ + fun sendTextMessage(room: Room, message: String, nbOfMessages: Int): List { + val timeline = room.createTimeline(null, TimelineSettings(10)) + val sentEvents = ArrayList(nbOfMessages) + val latch = CountDownLatch(1) + val timelineListener = object : Timeline.Listener { + override fun onTimelineFailure(throwable: Throwable) { + } + + override fun onNewTimelineEvents(eventIds: List) { + // noop + } + + override fun onTimelineUpdated(snapshot: List) { + val newMessages = snapshot + .filter { it.root.sendState == SendState.SYNCED } + .filter { it.root.getClearType() == EventType.MESSAGE } + .filter { it.root.getClearContent().toModel()?.body?.startsWith(message) == true } + + if (newMessages.size == nbOfMessages) { + sentEvents.addAll(newMessages) + // Remove listener now, if not at the next update sendEvents could change + timeline.removeListener(this) + latch.countDown() + } + } + } + timeline.start() + timeline.addListener(timelineListener) + for (i in 0 until nbOfMessages) { + room.sendTextMessage(message + " #" + (i + 1)) + } + // Wait 3 second more per message + await(latch, timeout = TestConstants.timeOutMillis + 3_000L * nbOfMessages) + timeline.dispose() + + // Check that all events has been created + assertEquals("Message number do not match $sentEvents", nbOfMessages.toLong(), sentEvents.size.toLong()) + + return sentEvents + } + + // PRIVATE METHODS ***************************************************************************** + + /** + * Creates a unique account + * + * @param userNamePrefix the user name prefix + * @param password the password + * @param testParams test params about the session + * @return the session associated with the newly created account + */ + private fun createAccount(userNamePrefix: String, + password: String, + testParams: SessionTestParams): Session { + val session = createAccountAndSync( + userNamePrefix + "_" + System.currentTimeMillis() + UUID.randomUUID(), + password, + testParams + ) + assertNotNull(session) + return session + } + + /** + * Logs into an existing account + * + * @param userId the userId to log in + * @param password the password to log in + * @param testParams test params about the session + * @return the session associated with the existing account + */ + fun logIntoAccount(userId: String, + password: String, + testParams: SessionTestParams): Session { + val session = logAccountAndSync(userId, password, testParams) + assertNotNull(session) + return session + } + + /** + * Create an account and a dedicated session + * + * @param userName the account username + * @param password the password + * @param sessionTestParams parameters for the test + */ + private fun createAccountAndSync(userName: String, + password: String, + sessionTestParams: SessionTestParams): Session { + val hs = createHomeServerConfig() + + doSync { + matrix.authenticationService + .getLoginFlow(hs, it) + } + + doSync { + matrix.authenticationService + .getRegistrationWizard() + .createAccount(userName, password, null, it) + } + + // Preform dummy step + val registrationResult = doSync { + matrix.authenticationService + .getRegistrationWizard() + .dummy(it) + } + + assertTrue(registrationResult is RegistrationResult.Success) + val session = (registrationResult as RegistrationResult.Success).session + if (sessionTestParams.withInitialSync) { + syncSession(session) + } + + return session + } + + /** + * Start an account login + * + * @param userName the account username + * @param password the password + * @param sessionTestParams session test params + */ + private fun logAccountAndSync(userName: String, + password: String, + sessionTestParams: SessionTestParams): Session { + val hs = createHomeServerConfig() + + doSync { + matrix.authenticationService + .getLoginFlow(hs, it) + } + + val session = doSync { + matrix.authenticationService + .getLoginWizard() + .login(userName, password, "myDevice", it) + } + + if (sessionTestParams.withInitialSync) { + syncSession(session) + } + + return session + } + + /** + * Log into the account and expect an error + * + * @param userName the account username + * @param password the password + */ + fun logAccountWithError(userName: String, + password: String): Throwable { + val hs = createHomeServerConfig() + + doSync { + matrix.authenticationService + .getLoginFlow(hs, it) + } + + var requestFailure: Throwable? = null + waitWithLatch { latch -> + matrix.authenticationService + .getLoginWizard() + .login(userName, password, "myDevice", object : TestMatrixCallback(latch, onlySuccessful = false) { + override fun onFailure(failure: Throwable) { + requestFailure = failure + super.onFailure(failure) + } + }) + } + + assertNotNull(requestFailure) + return requestFailure!! + } + + fun createEventListener(latch: CountDownLatch, predicate: (List) -> Boolean): Timeline.Listener { + return object : Timeline.Listener { + override fun onTimelineFailure(throwable: Throwable) { + // noop + } + + override fun onNewTimelineEvents(eventIds: List) { + // noop + } + + override fun onTimelineUpdated(snapshot: List) { + if (predicate(snapshot)) { + latch.countDown() + } + } + } + } + + /** + * Await for a latch and ensure the result is true + * + * @param latch + * @throws InterruptedException + */ + fun await(latch: CountDownLatch, timeout: Long? = TestConstants.timeOutMillis) { + assertTrue(latch.await(timeout ?: TestConstants.timeOutMillis, TimeUnit.MILLISECONDS)) + } + + fun retryPeriodicallyWithLatch(latch: CountDownLatch, condition: (() -> Boolean)) { + GlobalScope.launch { + while (true) { + delay(1000) + if (condition()) { + latch.countDown() + return@launch + } + } + } + } + + fun waitWithLatch(timeout: Long? = TestConstants.timeOutMillis, block: (CountDownLatch) -> Unit) { + val latch = CountDownLatch(1) + block(latch) + await(latch, timeout) + } + + // Transform a method with a MatrixCallback to a synchronous method + inline fun doSync(block: (MatrixCallback) -> Unit): T { + val lock = CountDownLatch(1) + var result: T? = null + + val callback = object : TestMatrixCallback(lock) { + override fun onSuccess(data: T) { + result = data + super.onSuccess(data) + } + } + + block.invoke(callback) + + await(lock) + + assertNotNull(result) + return result!! + } + + /** + * Clear all provided sessions + */ + fun Iterable.signOutAndClose() = forEach { signOutAndClose(it) } + + fun signOutAndClose(session: Session) { + doSync { session.signOut(true, it) } + session.close() + } +} + +fun List.checkSendOrder(baseTextMessage: String, numberOfMessages: Int, startIndex: Int): Boolean { + return drop(startIndex) + .take(numberOfMessages) + .foldRightIndexed(true) { index, timelineEvent, acc -> + val body = timelineEvent.root.content.toModel()?.body + val currentMessageSuffix = numberOfMessages - index + acc && (body == null || body.startsWith(baseTextMessage) && body.endsWith("#$currentMessageSuffix")) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestData.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestData.kt new file mode 100644 index 0000000000..283ddd6fde --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestData.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2018 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.common + +import org.matrix.android.sdk.api.session.Session + +data class CryptoTestData(val firstSession: Session, + val roomId: String, + val secondSession: Session? = null, + val thirdSession: Session? = null) { + + fun cleanUp(testHelper: CommonTestHelper) { + testHelper.signOutAndClose(firstSession) + secondSession?.let { testHelper.signOutAndClose(it) } + thirdSession?.let { testHelper.signOutAndClose(it) } + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt new file mode 100644 index 0000000000..9765d35bc5 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt @@ -0,0 +1,424 @@ +/* + * Copyright 2018 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.common + +import android.os.SystemClock +import android.util.Log +import androidx.lifecycle.Observer +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction +import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction +import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod +import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.Room +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams +import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams +import org.matrix.android.sdk.api.session.room.timeline.Timeline +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings +import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP +import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupAuthData +import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo +import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import java.util.UUID +import java.util.concurrent.CountDownLatch + +class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { + + private val messagesFromAlice: List = listOf("0 - Hello I'm Alice!", "4 - Go!") + private val messagesFromBob: List = listOf("1 - Hello I'm Bob!", "2 - Isn't life grand?", "3 - Let's go to the opera.") + + private val defaultSessionParams = SessionTestParams(true) + + /** + * @return alice session + */ + fun doE2ETestWithAliceInARoom(encryptedRoom: Boolean = true): CryptoTestData { + val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams) + + val roomId = mTestHelper.doSync { + aliceSession.createRoom(CreateRoomParams().apply { name = "MyRoom" }, it) + } + + if (encryptedRoom) { + val room = aliceSession.getRoom(roomId)!! + + mTestHelper.doSync { + room.enableEncryption(callback = it) + } + } + + return CryptoTestData(aliceSession, roomId) + } + + /** + * @return alice and bob sessions + */ + fun doE2ETestWithAliceAndBobInARoom(encryptedRoom: Boolean = true): CryptoTestData { + val cryptoTestData = doE2ETestWithAliceInARoom(encryptedRoom) + val aliceSession = cryptoTestData.firstSession + val aliceRoomId = cryptoTestData.roomId + + val aliceRoom = aliceSession.getRoom(aliceRoomId)!! + + val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, defaultSessionParams) + + val lock1 = CountDownLatch(1) + + val bobRoomSummariesLive = runBlocking(Dispatchers.Main) { + bobSession.getRoomSummariesLive(roomSummaryQueryParams { }) + } + + val newRoomObserver = object : Observer> { + override fun onChanged(t: List?) { + if (t?.isNotEmpty() == true) { + lock1.countDown() + bobRoomSummariesLive.removeObserver(this) + } + } + } + + GlobalScope.launch(Dispatchers.Main) { + bobRoomSummariesLive.observeForever(newRoomObserver) + } + + mTestHelper.doSync { + aliceRoom.invite(bobSession.myUserId, callback = it) + } + + mTestHelper.await(lock1) + + val lock = CountDownLatch(1) + + val roomJoinedObserver = object : Observer> { + override fun onChanged(t: List?) { + if (bobSession.getRoom(aliceRoomId) + ?.getRoomMember(aliceSession.myUserId) + ?.membership == Membership.JOIN) { + lock.countDown() + bobRoomSummariesLive.removeObserver(this) + } + } + } + + GlobalScope.launch(Dispatchers.Main) { + bobRoomSummariesLive.observeForever(roomJoinedObserver) + } + + mTestHelper.doSync { bobSession.joinRoom(aliceRoomId, callback = it) } + + mTestHelper.await(lock) + + // Ensure bob can send messages to the room +// val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!! +// assertNotNull(roomFromBobPOV.powerLevels) +// assertTrue(roomFromBobPOV.powerLevels.maySendMessage(bobSession.myUserId)) + + return CryptoTestData(aliceSession, aliceRoomId, bobSession) + } + + /** + * @return Alice, Bob and Sam session + */ + fun doE2ETestWithAliceAndBobAndSamInARoom(): CryptoTestData { + val cryptoTestData = doE2ETestWithAliceAndBobInARoom() + val aliceSession = cryptoTestData.firstSession + val aliceRoomId = cryptoTestData.roomId + + val room = aliceSession.getRoom(aliceRoomId)!! + + val samSession = createSamAccountAndInviteToTheRoom(room) + + // wait the initial sync + SystemClock.sleep(1000) + + return CryptoTestData(aliceSession, aliceRoomId, cryptoTestData.secondSession, samSession) + } + + /** + * Create Sam account and invite him in the room. He will accept the invitation + * @Return Sam session + */ + fun createSamAccountAndInviteToTheRoom(room: Room): Session { + val samSession = mTestHelper.createAccount(TestConstants.USER_SAM, defaultSessionParams) + + mTestHelper.doSync { + room.invite(samSession.myUserId, null, it) + } + + mTestHelper.doSync { + samSession.joinRoom(room.roomId, null, emptyList(), it) + } + + return samSession + } + + /** + * @return Alice and Bob sessions + */ + fun doE2ETestWithAliceAndBobInARoomWithEncryptedMessages(): CryptoTestData { + val cryptoTestData = doE2ETestWithAliceAndBobInARoom() + val aliceSession = cryptoTestData.firstSession + val aliceRoomId = cryptoTestData.roomId + val bobSession = cryptoTestData.secondSession!! + + bobSession.cryptoService().setWarnOnUnknownDevices(false) + + aliceSession.cryptoService().setWarnOnUnknownDevices(false) + + val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!! + val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!! + + val lock = CountDownLatch(1) + + val bobEventsListener = object : Timeline.Listener { + override fun onTimelineFailure(throwable: Throwable) { + // noop + } + + override fun onNewTimelineEvents(eventIds: List) { + // noop + } + + override fun onTimelineUpdated(snapshot: List) { + val messages = snapshot.filter { it.root.getClearType() == EventType.MESSAGE } + .groupBy { it.root.senderId!! } + + // Alice has sent 2 messages and Bob has sent 3 messages + if (messages[aliceSession.myUserId]?.size == 2 && messages[bobSession.myUserId]?.size == 3) { + lock.countDown() + } + } + } + + val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(20)) + bobTimeline.start() + bobTimeline.addListener(bobEventsListener) + + // Alice sends a message + roomFromAlicePOV.sendTextMessage(messagesFromAlice[0]) + + // Bob send 3 messages + roomFromBobPOV.sendTextMessage(messagesFromBob[0]) + roomFromBobPOV.sendTextMessage(messagesFromBob[1]) + roomFromBobPOV.sendTextMessage(messagesFromBob[2]) + + // Alice sends a message + roomFromAlicePOV.sendTextMessage(messagesFromAlice[1]) + + mTestHelper.await(lock) + + bobTimeline.removeListener(bobEventsListener) + bobTimeline.dispose() + + return cryptoTestData + } + + fun checkEncryptedEvent(event: Event, roomId: String, clearMessage: String, senderSession: Session) { + assertEquals(EventType.ENCRYPTED, event.type) + assertNotNull(event.content) + + val eventWireContent = event.content.toContent() + assertNotNull(eventWireContent) + + assertNull(eventWireContent["body"]) + assertEquals(MXCRYPTO_ALGORITHM_MEGOLM, eventWireContent["algorithm"]) + + assertNotNull(eventWireContent["ciphertext"]) + assertNotNull(eventWireContent["session_id"]) + assertNotNull(eventWireContent["sender_key"]) + + assertEquals(senderSession.sessionParams.deviceId, eventWireContent["device_id"]) + + assertNotNull(event.eventId) + assertEquals(roomId, event.roomId) + assertEquals(EventType.MESSAGE, event.getClearType()) + // TODO assertTrue(event.getAge() < 10000) + + val eventContent = event.toContent() + assertNotNull(eventContent) + assertEquals(clearMessage, eventContent["body"]) + assertEquals(senderSession.myUserId, event.senderId) + } + + fun createFakeMegolmBackupAuthData(): MegolmBackupAuthData { + return MegolmBackupAuthData( + publicKey = "abcdefg", + signatures = mapOf("something" to mapOf("ed25519:something" to "hijklmnop")) + ) + } + + fun createFakeMegolmBackupCreationInfo(): MegolmBackupCreationInfo { + return MegolmBackupCreationInfo( + algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP, + authData = createFakeMegolmBackupAuthData() + ) + } + + fun createDM(alice: Session, bob: Session): String { + val roomId = mTestHelper.doSync { + alice.createRoom( + CreateRoomParams().apply { + invitedUserIds.add(bob.myUserId) + setDirectMessage() + enableEncryptionIfInvitedUsersSupportIt = true + }, + it + ) + } + + mTestHelper.waitWithLatch { latch -> + val bobRoomSummariesLive = runBlocking(Dispatchers.Main) { + bob.getRoomSummariesLive(roomSummaryQueryParams { }) + } + + val newRoomObserver = object : Observer> { + override fun onChanged(t: List?) { + val indexOfFirst = t?.indexOfFirst { it.roomId == roomId } ?: -1 + if (indexOfFirst != -1) { + latch.countDown() + bobRoomSummariesLive.removeObserver(this) + } + } + } + + GlobalScope.launch(Dispatchers.Main) { + bobRoomSummariesLive.observeForever(newRoomObserver) + } + } + + mTestHelper.waitWithLatch { latch -> + val bobRoomSummariesLive = runBlocking(Dispatchers.Main) { + bob.getRoomSummariesLive(roomSummaryQueryParams { }) + } + + val newRoomObserver = object : Observer> { + override fun onChanged(t: List?) { + if (bob.getRoom(roomId) + ?.getRoomMember(bob.myUserId) + ?.membership == Membership.JOIN) { + latch.countDown() + bobRoomSummariesLive.removeObserver(this) + } + } + } + + GlobalScope.launch(Dispatchers.Main) { + bobRoomSummariesLive.observeForever(newRoomObserver) + } + + mTestHelper.doSync { bob.joinRoom(roomId, callback = it) } + } + + return roomId + } + + fun initializeCrossSigning(session: Session) { + mTestHelper.doSync { + session.cryptoService().crossSigningService() + .initializeCrossSigning(UserPasswordAuth( + user = session.myUserId, + password = TestConstants.PASSWORD + ), it) + } + } + + fun verifySASCrossSign(alice: Session, bob: Session, roomId: String) { + assertTrue(alice.cryptoService().crossSigningService().canCrossSign()) + assertTrue(bob.cryptoService().crossSigningService().canCrossSign()) + + val requestID = UUID.randomUUID().toString() + val aliceVerificationService = alice.cryptoService().verificationService() + val bobVerificationService = bob.cryptoService().verificationService() + + aliceVerificationService.beginKeyVerificationInDMs( + VerificationMethod.SAS, + requestID, + roomId, + bob.myUserId, + bob.sessionParams.credentials.deviceId!!, + null) + + // we should reach SHOW SAS on both + var alicePovTx: OutgoingSasVerificationTransaction? = null + var bobPovTx: IncomingSasVerificationTransaction? = null + + // wait for alice to get the ready + mTestHelper.waitWithLatch { + mTestHelper.retryPeriodicallyWithLatch(it) { + bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID) as? IncomingSasVerificationTransaction + Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}") + if (bobPovTx?.state == VerificationTxState.OnStarted) { + bobPovTx?.performAccept() + true + } else { + false + } + } + } + + mTestHelper.waitWithLatch { + mTestHelper.retryPeriodicallyWithLatch(it) { + alicePovTx = aliceVerificationService.getExistingTransaction(bob.myUserId, requestID) as? OutgoingSasVerificationTransaction + Log.v("TEST", "== alicePovTx is ${alicePovTx?.uxState}") + alicePovTx?.state == VerificationTxState.ShortCodeReady + } + } + // wait for alice to get the ready + mTestHelper.waitWithLatch { + mTestHelper.retryPeriodicallyWithLatch(it) { + bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID) as? IncomingSasVerificationTransaction + Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}") + if (bobPovTx?.state == VerificationTxState.OnStarted) { + bobPovTx?.performAccept() + } + bobPovTx?.state == VerificationTxState.ShortCodeReady + } + } + + assertEquals("SAS code do not match", alicePovTx!!.getDecimalCodeRepresentation(), bobPovTx!!.getDecimalCodeRepresentation()) + + bobPovTx!!.userHasVerifiedShortCode() + alicePovTx!!.userHasVerifiedShortCode() + + mTestHelper.waitWithLatch { + mTestHelper.retryPeriodicallyWithLatch(it) { + alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId) + } + } + + mTestHelper.waitWithLatch { + mTestHelper.retryPeriodicallyWithLatch(it) { + alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId) + } + } + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/MockOkHttpInterceptor.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/MockOkHttpInterceptor.kt new file mode 100644 index 0000000000..a9bd9403d2 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/MockOkHttpInterceptor.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2019 New Vector Ltd + * + * 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. + */ +package org.matrix.android.sdk.common + +import org.matrix.android.sdk.internal.session.TestInterceptor +import okhttp3.Interceptor +import okhttp3.Protocol +import okhttp3.Request +import okhttp3.Response +import okhttp3.ResponseBody.Companion.toResponseBody +import javax.net.ssl.HttpsURLConnection + +/** + * Allows to intercept network requests for test purpose by + * - re-writing the response + * - changing the response code (200/404/etc..). + * - Test delays.. + * + * Basic usage: + * + * val mockInterceptor = MockOkHttpInterceptor() + * mockInterceptor.addRule(MockOkHttpInterceptor.SimpleRule(".well-known/matrix/client", 200, "{}")) + * + * RestHttpClientFactoryProvider.defaultProvider = RestClientHttpClientFactory(mockInterceptor) + * AutoDiscovery().findClientConfig("matrix.org", ) + * + */ +class MockOkHttpInterceptor : TestInterceptor { + + private var rules: ArrayList = ArrayList() + + fun addRule(rule: Rule) { + rules.add(rule) + } + + fun clearRules() { + rules.clear() + } + + override var sessionId: String? = null + + override fun intercept(chain: Interceptor.Chain): Response { + val originalRequest = chain.request() + + rules.forEach { rule -> + if (originalRequest.url.toString().contains(rule.match)) { + rule.process(originalRequest)?.let { + return it + } + } + } + + return chain.proceed(originalRequest) + } + + abstract class Rule(val match: String) { + abstract fun process(originalRequest: Request): Response? + } + + /** + * Simple rule that reply with the given body for any request that matches the match param + */ + class SimpleRule(match: String, + private val code: Int = HttpsURLConnection.HTTP_OK, + private val body: String = "{}") : Rule(match) { + + override fun process(originalRequest: Request): Response? { + return Response.Builder() + .protocol(Protocol.HTTP_1_1) + .request(originalRequest) + .message("mocked answer") + .body(body.toResponseBody(null)) + .code(code) + .build() + } + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/SessionTestParams.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/SessionTestParams.kt new file mode 100644 index 0000000000..287cafcdfd --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/SessionTestParams.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2018 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.common + +data class SessionTestParams @JvmOverloads constructor(val withInitialSync: Boolean = false) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestAssertUtil.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestAssertUtil.kt new file mode 100644 index 0000000000..d972ad621c --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestAssertUtil.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2018 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.common + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.fail + +/** + * Compare two lists and their content + */ +fun assertListEquals(list1: List?, list2: List?) { + if (list1 == null) { + assertNull(list2) + } else { + assertNotNull(list2) + + assertEquals("List sizes must match", list1.size, list2!!.size) + + for (i in list1.indices) { + assertEquals("Elements at index $i are not equal", list1[i], list2[i]) + } + } +} + +/** + * Compare two maps and their content + */ +fun assertDictEquals(dict1: Map?, dict2: Map?) { + if (dict1 == null) { + assertNull(dict2) + } else { + assertNotNull(dict2) + + assertEquals("Map sizes must match", dict1.size, dict2!!.size) + + for (i in dict1.keys) { + assertEquals("Values for key $i are not equal", dict1[i], dict2[i]) + } + } +} + +/** + * Compare two byte arrays content. + * Note that if the arrays have not the same size, it also fails. + */ +fun assertByteArrayNotEqual(a1: ByteArray, a2: ByteArray) { + if (a1.size != a2.size) { + fail("Arrays have not the same size.") + } + + for (index in a1.indices) { + if (a1[index] != a2[index]) { + // Difference found! + return + } + } + + fail("Arrays are equals.") +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestConstants.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestConstants.kt new file mode 100644 index 0000000000..cbfc9bbbf6 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestConstants.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2018 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.common + +import android.os.Debug + +object TestConstants { + + const val TESTS_HOME_SERVER_URL = "http://10.0.2.2:8080" + + // Time out to use when waiting for server response. 20s + private const val AWAIT_TIME_OUT_MILLIS = 20_000 + + // Time out to use when waiting for server response, when the debugger is connected. 10 minutes + private const val AWAIT_TIME_OUT_WITH_DEBUGGER_MILLIS = 10 * 60_000 + + const val USER_ALICE = "Alice" + const val USER_BOB = "Bob" + const val USER_SAM = "Sam" + + const val PASSWORD = "password" + + val timeOutMillis: Long + get() = if (Debug.isDebuggerConnected()) { + // Wait more + AWAIT_TIME_OUT_WITH_DEBUGGER_MILLIS.toLong() + } else { + AWAIT_TIME_OUT_MILLIS.toLong() + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixCallback.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixCallback.kt new file mode 100644 index 0000000000..800c6ae7e0 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixCallback.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2018 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.common + +import androidx.annotation.CallSuper +import org.matrix.android.sdk.api.MatrixCallback +import org.junit.Assert.fail +import timber.log.Timber +import java.util.concurrent.CountDownLatch + +/** + * Simple implementation of MatrixCallback, which count down the CountDownLatch on each API callback + * @param onlySuccessful true to fail if an error occurs. This is the default behavior + * @param + */ +open class TestMatrixCallback(private val countDownLatch: CountDownLatch, + private val onlySuccessful: Boolean = true) : MatrixCallback { + + @CallSuper + override fun onSuccess(data: T) { + countDownLatch.countDown() + } + + @CallSuper + override fun onFailure(failure: Throwable) { + Timber.e(failure, "TestApiCallback") + + if (onlySuccessful) { + fail("onFailure " + failure.localizedMessage) + } + + countDownLatch.countDown() + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixComponent.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixComponent.kt new file mode 100644 index 0000000000..50290e1d63 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixComponent.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.common + +import android.content.Context +import dagger.BindsInstance +import dagger.Component +import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.internal.auth.AuthModule +import org.matrix.android.sdk.internal.di.MatrixComponent +import org.matrix.android.sdk.internal.di.MatrixModule +import org.matrix.android.sdk.internal.di.MatrixScope +import org.matrix.android.sdk.internal.di.NetworkModule + +@Component(modules = [TestModule::class, MatrixModule::class, NetworkModule::class, AuthModule::class, TestNetworkModule::class]) +@MatrixScope +internal interface TestMatrixComponent : MatrixComponent { + + @Component.Factory + interface Factory { + fun create(@BindsInstance context: Context, + @BindsInstance matrixConfiguration: MatrixConfiguration): TestMatrixComponent + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestModule.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestModule.kt new file mode 100644 index 0000000000..c3b11d65cc --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestModule.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.common + +import dagger.Binds +import dagger.Module +import org.matrix.android.sdk.internal.di.MatrixComponent + +@Module +internal abstract class TestModule { + @Binds + abstract fun providesMatrixComponent(testMatrixComponent: TestMatrixComponent): MatrixComponent +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestNetworkModule.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestNetworkModule.kt new file mode 100644 index 0000000000..80467d91f4 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestNetworkModule.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.common + +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.internal.session.MockHttpInterceptor +import org.matrix.android.sdk.internal.session.TestInterceptor + +@Module +internal object TestNetworkModule { + + val interceptors = ArrayList() + + fun interceptorForSession(sessionId: String): TestInterceptor? = interceptors.firstOrNull { it.sessionId == sessionId } + + @Provides + @JvmStatic + @MockHttpInterceptor + fun providesTestInterceptor(): TestInterceptor? { + return MockOkHttpInterceptor().also { + interceptors.add(it) + } + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/AttachmentEncryptionTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/AttachmentEncryptionTest.kt new file mode 100644 index 0000000000..05dbc40e1e --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/AttachmentEncryptionTest.kt @@ -0,0 +1,148 @@ +/* + * Copyright 2019 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto + +import android.os.MemoryFile +import android.util.Base64 +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments +import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo +import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileKey +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertNotNull +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import java.io.ByteArrayInputStream +import java.io.InputStream + +/** + * Unit tests AttachmentEncryptionTest. + */ +@Suppress("SpellCheckingInspection") +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class AttachmentEncryptionTest { + + private fun checkDecryption(input: String, encryptedFileInfo: EncryptedFileInfo): String { + val `in` = Base64.decode(input, Base64.DEFAULT) + + val inputStream: InputStream + + inputStream = if (`in`.isEmpty()) { + ByteArrayInputStream(`in`) + } else { + val memoryFile = MemoryFile("file" + System.currentTimeMillis(), `in`.size) + memoryFile.outputStream.write(`in`) + memoryFile.inputStream + } + + val decryptedStream = MXEncryptedAttachments.decryptAttachment(inputStream, encryptedFileInfo) + + assertNotNull(decryptedStream) + + val buffer = ByteArray(100) + + val len = decryptedStream!!.read(buffer) + + decryptedStream.close() + + return Base64.encodeToString(buffer, 0, len, Base64.DEFAULT).replace("\n".toRegex(), "").replace("=".toRegex(), "") + } + + @Test + fun checkDecrypt1() { + val encryptedFileInfo = EncryptedFileInfo( + v = "v2", + hashes = mapOf("sha256" to "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU"), + key = EncryptedFileKey( + alg = "A256CTR", + k = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + key_ops = listOf("encrypt", "decrypt"), + kty = "oct", + ext = true + ), + iv = "AAAAAAAAAAAAAAAAAAAAAA", + url = "dummyUrl" + ) + + assertEquals("", checkDecryption("", encryptedFileInfo)) + } + + @Test + fun checkDecrypt2() { + val encryptedFileInfo = EncryptedFileInfo( + v = "v2", + hashes = mapOf("sha256" to "YzF08lARDdOCzJpzuSwsjTNlQc4pHxpdHcXiD/wpK6k"), + key = EncryptedFileKey( + alg = "A256CTR", + k = "__________________________________________8", + key_ops = listOf("encrypt", "decrypt"), + kty = "oct", + ext = true + ), + iv = "//////////8AAAAAAAAAAA", + url = "dummyUrl" + ) + + assertEquals("SGVsbG8sIFdvcmxk", checkDecryption("5xJZTt5cQicm+9f4", encryptedFileInfo)) + } + + @Test + fun checkDecrypt3() { + val encryptedFileInfo = EncryptedFileInfo( + v = "v2", + hashes = mapOf("sha256" to "IOq7/dHHB+mfHfxlRY5XMeCWEwTPmlf4cJcgrkf6fVU"), + key = EncryptedFileKey( + alg = "A256CTR", + k = "__________________________________________8", + key_ops = listOf("encrypt", "decrypt"), + kty = "oct", + ext = true + ), + iv = "//////////8AAAAAAAAAAA", + url = "dummyUrl" + ) + + assertEquals("YWxwaGFudW1lcmljYWxseWFscGhhbnVtZXJpY2FsbHlhbHBoYW51bWVyaWNhbGx5YWxwaGFudW1lcmljYWxseQ", + checkDecryption("zhtFStAeFx0s+9L/sSQO+WQMtldqYEHqTxMduJrCIpnkyer09kxJJuA4K+adQE4w+7jZe/vR9kIcqj9rOhDR8Q", + encryptedFileInfo)) + } + + @Test + fun checkDecrypt4() { + val encryptedFileInfo = EncryptedFileInfo( + v = "v2", + hashes = mapOf("sha256" to "LYG/orOViuFwovJpv2YMLSsmVKwLt7pY3f8SYM7KU5E"), + key = EncryptedFileKey( + alg = "A256CTR", + k = "__________________________________________8", + key_ops = listOf("encrypt", "decrypt"), + kty = "oct", + ext = true + ), + iv = "/////////////////////w", + url = "dummyUrl" + ) + + assertNotEquals("YWxwaGFudW1lcmljYWxseWFscGhhbnVtZXJpY2FsbHlhbHBoYW51bWVyaWNhbGx5YWxwaGFudW1lcmljYWxseQ", + checkDecryption("tJVNBVJ/vl36UQt4Y5e5m84bRUrQHhcdLPvS/7EkDvlkDLZXamBB6k8THbiawiKZ5Mnq9PZMSSbgOCvmnUBOMA", + encryptedFileInfo)) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreHelper.kt new file mode 100644 index 0000000000..261c0903f0 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreHelper.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2018 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto + +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStore +import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreModule +import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper +import org.matrix.android.sdk.internal.di.MoshiProvider +import io.realm.RealmConfiguration +import kotlin.random.Random + +internal class CryptoStoreHelper { + + fun createStore(): IMXCryptoStore { + return RealmCryptoStore( + realmConfiguration = RealmConfiguration.Builder() + .name("test.realm") + .modules(RealmCryptoStoreModule()) + .build(), + crossSigningKeysMapper = CrossSigningKeysMapper(MoshiProvider.providesMoshi()), + credentials = createCredential()) + } + + fun createCredential() = Credentials( + userId = "userId_" + Random.nextInt(), + homeServer = "http://matrix.org", + accessToken = "access_token", + refreshToken = null, + deviceId = "deviceId_sample" + ) +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreTest.kt new file mode 100644 index 0000000000..79477e3a4d --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreTest.kt @@ -0,0 +1,130 @@ +/* + * Copyright 2018 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import io.realm.Realm +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.matrix.olm.OlmAccount +import org.matrix.olm.OlmManager +import org.matrix.olm.OlmSession + +private const val DUMMY_DEVICE_KEY = "DeviceKey" + +@RunWith(AndroidJUnit4::class) +class CryptoStoreTest : InstrumentedTest { + + private val cryptoStoreHelper = CryptoStoreHelper() + + @Before + fun setup() { + Realm.init(context()) + } + +// @Test +// fun test_metadata_realm_ok() { +// val cryptoStore: IMXCryptoStore = cryptoStoreHelper.createStore() +// +// assertFalse(cryptoStore.hasData()) +// +// cryptoStore.open() +// +// assertEquals("deviceId_sample", cryptoStore.getDeviceId()) +// +// assertTrue(cryptoStore.hasData()) +// +// // Cleanup +// cryptoStore.close() +// cryptoStore.deleteStore() +// } + + @Test + fun test_lastSessionUsed() { + // Ensure Olm is initialized + OlmManager() + + val cryptoStore: IMXCryptoStore = cryptoStoreHelper.createStore() + + assertNull(cryptoStore.getLastUsedSessionId(DUMMY_DEVICE_KEY)) + + val olmAccount1 = OlmAccount().apply { + generateOneTimeKeys(1) + } + + val olmSession1 = OlmSession().apply { + initOutboundSession(olmAccount1, + olmAccount1.identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY], + olmAccount1.oneTimeKeys()[OlmAccount.JSON_KEY_ONE_TIME_KEY]?.values?.first()) + } + + val sessionId1 = olmSession1.sessionIdentifier() + val olmSessionWrapper1 = OlmSessionWrapper(olmSession1) + + cryptoStore.storeSession(olmSessionWrapper1, DUMMY_DEVICE_KEY) + + assertEquals(sessionId1, cryptoStore.getLastUsedSessionId(DUMMY_DEVICE_KEY)) + + val olmAccount2 = OlmAccount().apply { + generateOneTimeKeys(1) + } + + val olmSession2 = OlmSession().apply { + initOutboundSession(olmAccount2, + olmAccount2.identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY], + olmAccount2.oneTimeKeys()[OlmAccount.JSON_KEY_ONE_TIME_KEY]?.values?.first()) + } + + val sessionId2 = olmSession2.sessionIdentifier() + val olmSessionWrapper2 = OlmSessionWrapper(olmSession2) + + cryptoStore.storeSession(olmSessionWrapper2, DUMMY_DEVICE_KEY) + + // Ensure sessionIds are distinct + assertNotEquals(sessionId1, sessionId2) + + // Note: we cannot be sure what will be the result of getLastUsedSessionId() here + + olmSessionWrapper2.onMessageReceived() + cryptoStore.storeSession(olmSessionWrapper2, DUMMY_DEVICE_KEY) + + // sessionId2 is returned now + assertEquals(sessionId2, cryptoStore.getLastUsedSessionId(DUMMY_DEVICE_KEY)) + + Thread.sleep(2) + + olmSessionWrapper1.onMessageReceived() + cryptoStore.storeSession(olmSessionWrapper1, DUMMY_DEVICE_KEY) + + // sessionId1 is returned now + assertEquals(sessionId1, cryptoStore.getLastUsedSessionId(DUMMY_DEVICE_KEY)) + + // Cleanup + olmSession1.releaseSession() + olmSession2.releaseSession() + + olmAccount1.releaseAccount() + olmAccount2.releaseAccount() + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ExportEncryptionTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ExportEncryptionTest.kt new file mode 100644 index 0000000000..0ee79c2e1e --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ExportEncryptionTest.kt @@ -0,0 +1,208 @@ +/* + * Copyright 2018 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters + +/** + * Unit tests ExportEncryptionTest. + */ +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class ExportEncryptionTest { + + @Test + fun checkExportError1() { + val password = "password" + val input = "-----" + var failed = false + + try { + MXMegolmExportEncryption.decryptMegolmKeyFile(input.toByteArray(charset("UTF-8")), password) + } catch (e: Exception) { + failed = true + } + + assertTrue(failed) + } + + @Test + fun checkExportError2() { + val password = "password" + val input = "-----BEGIN MEGOLM SESSION DATA-----\n" + "-----" + var failed = false + + try { + MXMegolmExportEncryption.decryptMegolmKeyFile(input.toByteArray(charset("UTF-8")), password) + } catch (e: Exception) { + failed = true + } + + assertTrue(failed) + } + + @Test + fun checkExportError3() { + val password = "password" + val input = "-----BEGIN MEGOLM SESSION DATA-----\n" + + " AXNhbHRzYWx0c2FsdHNhbHSIiIiIiIiIiIiIiIiIiIiIAAAACmIRUW2OjZ3L2l6j9h0lHlV3M2dx\n" + + " cissyYBxjsfsAn\n" + + " -----END MEGOLM SESSION DATA-----" + var failed = false + + try { + MXMegolmExportEncryption.decryptMegolmKeyFile(input.toByteArray(charset("UTF-8")), password) + } catch (e: Exception) { + failed = true + } + + assertTrue(failed) + } + + @Test + fun checkExportDecrypt1() { + val password = "password" + val input = "-----BEGIN MEGOLM SESSION DATA-----\nAXNhbHRzYWx0c2FsdHNhbHSIiIiIiIiIiIiIiIiIiIiIAAAACmIRUW2OjZ3L2l6j9h0lHlV3M2dx\n" + "cissyYBxjsfsAndErh065A8=\n-----END MEGOLM SESSION DATA-----" + val expectedString = "plain" + + var decodedString: String? = null + try { + decodedString = MXMegolmExportEncryption.decryptMegolmKeyFile(input.toByteArray(charset("UTF-8")), password) + } catch (e: Exception) { + fail("## checkExportDecrypt1() failed : " + e.message) + } + + assertEquals("## checkExportDecrypt1() : expectedString $expectedString -- decodedString $decodedString", + expectedString, + decodedString) + } + + @Test + fun checkExportDecrypt2() { + val password = "betterpassword" + val input = "-----BEGIN MEGOLM SESSION DATA-----\nAW1vcmVzYWx0bW9yZXNhbHT//////////wAAAAAAAAAAAAAD6KyBpe1Niv5M5NPm4ZATsJo5nghk\n" + "KYu63a0YQ5DRhUWEKk7CcMkrKnAUiZny\n-----END MEGOLM SESSION DATA-----" + val expectedString = "Hello, World" + + var decodedString: String? = null + try { + decodedString = MXMegolmExportEncryption.decryptMegolmKeyFile(input.toByteArray(charset("UTF-8")), password) + } catch (e: Exception) { + fail("## checkExportDecrypt2() failed : " + e.message) + } + + assertEquals("## checkExportDecrypt2() : expectedString $expectedString -- decodedString $decodedString", + expectedString, + decodedString) + } + + @Test + fun checkExportDecrypt3() { + val password = "SWORDFISH" + val input = "-----BEGIN MEGOLM SESSION DATA-----\nAXllc3NhbHR5Z29vZG5lc3P//////////wAAAAAAAAAAAAAD6OIW+Je7gwvjd4kYrb+49gKCfExw\n" + "MgJBMD4mrhLkmgAngwR1pHjbWXaoGybtiAYr0moQ93GrBQsCzPbvl82rZhaXO3iH5uHo/RCEpOqp\nPgg29363BGR+/Ripq/VCLKGNbw==\n-----END MEGOLM SESSION DATA-----" + val expectedString = "alphanumericallyalphanumericallyalphanumericallyalphanumerically" + + var decodedString: String? = null + try { + decodedString = MXMegolmExportEncryption.decryptMegolmKeyFile(input.toByteArray(charset("UTF-8")), password) + } catch (e: Exception) { + fail("## checkExportDecrypt3() failed : " + e.message) + } + + assertEquals("## checkExportDecrypt3() : expectedString $expectedString -- decodedString $decodedString", + expectedString, + decodedString) + } + + @Test + fun checkExportEncrypt1() { + val password = "password" + val expectedString = "plain" + var decodedString: String? = null + + try { + decodedString = MXMegolmExportEncryption + .decryptMegolmKeyFile(MXMegolmExportEncryption.encryptMegolmKeyFile(expectedString, password, 1000), password) + } catch (e: Exception) { + fail("## checkExportEncrypt1() failed : " + e.message) + } + + assertEquals("## checkExportEncrypt1() : expectedString $expectedString -- decodedString $decodedString", + expectedString, + decodedString) + } + + @Test + fun checkExportEncrypt2() { + val password = "betterpassword" + val expectedString = "Hello, World" + var decodedString: String? = null + + try { + decodedString = MXMegolmExportEncryption + .decryptMegolmKeyFile(MXMegolmExportEncryption.encryptMegolmKeyFile(expectedString, password, 1000), password) + } catch (e: Exception) { + fail("## checkExportEncrypt2() failed : " + e.message) + } + + assertEquals("## checkExportEncrypt2() : expectedString $expectedString -- decodedString $decodedString", + expectedString, + decodedString) + } + + @Test + fun checkExportEncrypt3() { + val password = "SWORDFISH" + val expectedString = "alphanumericallyalphanumericallyalphanumericallyalphanumerically" + var decodedString: String? = null + + try { + decodedString = MXMegolmExportEncryption + .decryptMegolmKeyFile(MXMegolmExportEncryption.encryptMegolmKeyFile(expectedString, password, 1000), password) + } catch (e: Exception) { + fail("## checkExportEncrypt3() failed : " + e.message) + } + + assertEquals("## checkExportEncrypt3() : expectedString $expectedString -- decodedString $decodedString", + expectedString, + decodedString) + } + + @Test + fun checkExportEncrypt4() { + val password = "passwordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpassword" + "passwordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpassword" + val expectedString = "alphanumericallyalphanumericallyalphanumericallyalphanumerically" + var decodedString: String? = null + + try { + decodedString = MXMegolmExportEncryption + .decryptMegolmKeyFile(MXMegolmExportEncryption.encryptMegolmKeyFile(expectedString, password, 1000), password) + } catch (e: Exception) { + fail("## checkExportEncrypt4() failed : " + e.message) + } + + assertEquals("## checkExportEncrypt4() : expectedString $expectedString -- decodedString $decodedString", + expectedString, + decodedString) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt new file mode 100644 index 0000000000..e5e71f3944 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt @@ -0,0 +1,247 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.timeline.Timeline +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.TestConstants +import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper +import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent +import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth +import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm +import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm +import org.amshove.kluent.shouldBe +import org.junit.Assert +import org.junit.Before +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.matrix.olm.OlmSession +import timber.log.Timber +import java.util.concurrent.CountDownLatch + +/** + * Ref: + * - https://github.com/matrix-org/matrix-doc/pull/1719 + * - https://matrix.org/docs/spec/client_server/latest#recovering-from-undecryptable-messages + * - https://github.com/matrix-org/matrix-js-sdk/pull/780 + * - https://github.com/matrix-org/matrix-ios-sdk/pull/778 + * - https://github.com/matrix-org/matrix-ios-sdk/pull/784 + */ +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class UnwedgingTest : InstrumentedTest { + + private lateinit var messagesReceivedByBob: List + private val mTestHelper = CommonTestHelper(context()) + private val mCryptoTestHelper = CryptoTestHelper(mTestHelper) + + @Before + fun init() { + messagesReceivedByBob = emptyList() + } + + /** + * - Alice & Bob in a e2e room + * - Alice sends a 1st message with a 1st megolm session + * - Store the olm session between A&B devices + * - Alice sends a 2nd message with a 2nd megolm session + * - Simulate Alice using a backup of her OS and make her crypto state like after the first message + * - Alice sends a 3rd message with a 3rd megolm session but a wedged olm session + * + * What Bob must see: + * -> No issue with the 2 first messages + * -> The third event must fail to decrypt at first because Bob the olm session is wedged + * -> This is automatically fixed after SDKs restarted the olm session + */ + @Test + fun testUnwedging() { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val aliceSession = cryptoTestData.firstSession + val aliceRoomId = cryptoTestData.roomId + val bobSession = cryptoTestData.secondSession!! + + val aliceCryptoStore = (aliceSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting + + // bobSession.cryptoService().setWarnOnUnknownDevices(false) + // aliceSession.cryptoService().setWarnOnUnknownDevices(false) + + val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!! + val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!! + + val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(20)) + bobTimeline.start() + + val bobFinalLatch = CountDownLatch(1) + val bobHasThreeDecryptedEventsListener = object : Timeline.Listener { + override fun onTimelineFailure(throwable: Throwable) { + // noop + } + + override fun onNewTimelineEvents(eventIds: List) { + // noop + } + + override fun onTimelineUpdated(snapshot: List) { + val decryptedEventReceivedByBob = snapshot.filter { it.root.type == EventType.ENCRYPTED } + Timber.d("Bob can now decrypt ${decryptedEventReceivedByBob.size} messages") + if (decryptedEventReceivedByBob.size == 3) { + if (decryptedEventReceivedByBob[0].root.mCryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) { + bobFinalLatch.countDown() + } + } + } + } + bobTimeline.addListener(bobHasThreeDecryptedEventsListener) + + var latch = CountDownLatch(1) + var bobEventsListener = createEventListener(latch, 1) + bobTimeline.addListener(bobEventsListener) + messagesReceivedByBob = emptyList() + + // - Alice sends a 1st message with a 1st megolm session + roomFromAlicePOV.sendTextMessage("First message") + + // Wait for the message to be received by Bob + mTestHelper.await(latch) + bobTimeline.removeListener(bobEventsListener) + + messagesReceivedByBob.size shouldBe 1 + val firstMessageSession = messagesReceivedByBob[0].root.content.toModel()!!.sessionId!! + + // - Store the olm session between A&B devices + // Let us pickle our session with bob here so we can later unpickle it + // and wedge our session. + val sessionIdsForBob = aliceCryptoStore.getDeviceSessionIds(bobSession.cryptoService().getMyDevice().identityKey()!!) + sessionIdsForBob!!.size shouldBe 1 + val olmSession = aliceCryptoStore.getDeviceSession(sessionIdsForBob.first(), bobSession.cryptoService().getMyDevice().identityKey()!!)!! + + val oldSession = serializeForRealm(olmSession.olmSession) + + aliceSession.cryptoService().discardOutboundSession(roomFromAlicePOV.roomId) + Thread.sleep(6_000) + + latch = CountDownLatch(1) + bobEventsListener = createEventListener(latch, 2) + bobTimeline.addListener(bobEventsListener) + messagesReceivedByBob = emptyList() + + Timber.i("## CRYPTO | testUnwedging: Alice sends a 2nd message with a 2nd megolm session") + // - Alice sends a 2nd message with a 2nd megolm session + roomFromAlicePOV.sendTextMessage("Second message") + + // Wait for the message to be received by Bob + mTestHelper.await(latch) + bobTimeline.removeListener(bobEventsListener) + + messagesReceivedByBob.size shouldBe 2 + // Session should have changed + val secondMessageSession = messagesReceivedByBob[0].root.content.toModel()!!.sessionId!! + Assert.assertNotEquals(firstMessageSession, secondMessageSession) + + // Let us wedge the session now. Set crypto state like after the first message + Timber.i("## CRYPTO | testUnwedging: wedge the session now. Set crypto state like after the first message") + + aliceCryptoStore.storeSession(OlmSessionWrapper(deserializeFromRealm(oldSession)!!), bobSession.cryptoService().getMyDevice().identityKey()!!) + Thread.sleep(6_000) + + // Force new session, and key share + aliceSession.cryptoService().discardOutboundSession(roomFromAlicePOV.roomId) + + // Wait for the message to be received by Bob + mTestHelper.waitWithLatch { + bobEventsListener = createEventListener(it, 3) + bobTimeline.addListener(bobEventsListener) + messagesReceivedByBob = emptyList() + + Timber.i("## CRYPTO | testUnwedging: Alice sends a 3rd message with a 3rd megolm session but a wedged olm session") + // - Alice sends a 3rd message with a 3rd megolm session but a wedged olm session + roomFromAlicePOV.sendTextMessage("Third message") + // Bob should not be able to decrypt, because the session key could not be sent + } + bobTimeline.removeListener(bobEventsListener) + + messagesReceivedByBob.size shouldBe 3 + + val thirdMessageSession = messagesReceivedByBob[0].root.content.toModel()!!.sessionId!! + Timber.i("## CRYPTO | testUnwedging: third message session ID $thirdMessageSession") + Assert.assertNotEquals(secondMessageSession, thirdMessageSession) + + Assert.assertEquals(EventType.ENCRYPTED, messagesReceivedByBob[0].root.getClearType()) + Assert.assertEquals(EventType.MESSAGE, messagesReceivedByBob[1].root.getClearType()) + Assert.assertEquals(EventType.MESSAGE, messagesReceivedByBob[2].root.getClearType()) + // Bob Should not be able to decrypt last message, because session could not be sent as the olm channel was wedged + mTestHelper.await(bobFinalLatch) + bobTimeline.removeListener(bobHasThreeDecryptedEventsListener) + + // It's a trick to force key request on fail to decrypt + mTestHelper.doSync { + bobSession.cryptoService().crossSigningService() + .initializeCrossSigning(UserPasswordAuth( + user = bobSession.myUserId, + password = TestConstants.PASSWORD + ), it) + } + + // Wait until we received back the key + mTestHelper.waitWithLatch { + mTestHelper.retryPeriodicallyWithLatch(it) { + // we should get back the key and be able to decrypt + val result = tryThis { + bobSession.cryptoService().decryptEvent(messagesReceivedByBob[0].root, "") + } + Timber.i("## CRYPTO | testUnwedging: decrypt result ${result?.clearEvent}") + result != null + } + } + + bobTimeline.dispose() + + cryptoTestData.cleanUp(mTestHelper) + } + + private fun createEventListener(latch: CountDownLatch, expectedNumberOfMessages: Int): Timeline.Listener { + return object : Timeline.Listener { + override fun onTimelineFailure(throwable: Throwable) { + // noop + } + + override fun onNewTimelineEvents(eventIds: List) { + // noop + } + + override fun onTimelineUpdated(snapshot: List) { + messagesReceivedByBob = snapshot.filter { it.root.type == EventType.ENCRYPTED } + + if (messagesReceivedByBob.size == expectedNumberOfMessages) { + latch.countDown() + } + } + } + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/ExtensionsKtTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/ExtensionsKtTest.kt new file mode 100644 index 0000000000..9467e861db --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/ExtensionsKtTest.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.crosssigning + +import org.amshove.kluent.shouldBeNull +import org.amshove.kluent.shouldBeTrue +import org.junit.Test + +@Suppress("SpellCheckingInspection") +class ExtensionsKtTest { + + @Test + fun testComparingBase64StringWithOrWithoutPadding() { + // Without padding + "NMJyumnhMic".fromBase64().contentEquals("NMJyumnhMic".fromBase64()).shouldBeTrue() + // With padding + "NMJyumnhMic".fromBase64().contentEquals("NMJyumnhMic=".fromBase64()).shouldBeTrue() + } + + @Test + fun testBadBase64() { + "===".fromBase64Safe().shouldBeNull() + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt new file mode 100644 index 0000000000..09f14032d0 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt @@ -0,0 +1,161 @@ +package org.matrix.android.sdk.internal.crypto.crosssigning + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.SessionTestParams +import org.matrix.android.sdk.common.TestConstants +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters + +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class XSigningTest : InstrumentedTest { + + private val mTestHelper = CommonTestHelper(context()) + private val mCryptoTestHelper = CryptoTestHelper(mTestHelper) + + @Test + fun test_InitializeAndStoreKeys() { + val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) + + mTestHelper.doSync { + aliceSession.cryptoService().crossSigningService() + .initializeCrossSigning(UserPasswordAuth( + user = aliceSession.myUserId, + password = TestConstants.PASSWORD + ), it) + } + + val myCrossSigningKeys = aliceSession.cryptoService().crossSigningService().getMyCrossSigningKeys() + val masterPubKey = myCrossSigningKeys?.masterKey() + assertNotNull("Master key should be stored", masterPubKey?.unpaddedBase64PublicKey) + val selfSigningKey = myCrossSigningKeys?.selfSigningKey() + assertNotNull("SelfSigned key should be stored", selfSigningKey?.unpaddedBase64PublicKey) + val userKey = myCrossSigningKeys?.userKey() + assertNotNull("User key should be stored", userKey?.unpaddedBase64PublicKey) + + assertTrue("Signing Keys should be trusted", myCrossSigningKeys?.isTrusted() == true) + + assertTrue("Signing Keys should be trusted", aliceSession.cryptoService().crossSigningService().checkUserTrust(aliceSession.myUserId).isVerified()) + + mTestHelper.signOutAndClose(aliceSession) + } + + @Test + fun test_CrossSigningCheckBobSeesTheKeys() { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession + + val aliceAuthParams = UserPasswordAuth( + user = aliceSession.myUserId, + password = TestConstants.PASSWORD + ) + val bobAuthParams = UserPasswordAuth( + user = bobSession!!.myUserId, + password = TestConstants.PASSWORD + ) + + mTestHelper.doSync { aliceSession.cryptoService().crossSigningService().initializeCrossSigning(aliceAuthParams, it) } + mTestHelper.doSync { bobSession.cryptoService().crossSigningService().initializeCrossSigning(bobAuthParams, it) } + + // Check that alice can see bob keys + mTestHelper.doSync> { aliceSession.cryptoService().downloadKeys(listOf(bobSession.myUserId), true, it) } + + val bobKeysFromAlicePOV = aliceSession.cryptoService().crossSigningService().getUserCrossSigningKeys(bobSession.myUserId) + assertNotNull("Alice can see bob Master key", bobKeysFromAlicePOV!!.masterKey()) + assertNull("Alice should not see bob User key", bobKeysFromAlicePOV.userKey()) + assertNotNull("Alice can see bob SelfSigned key", bobKeysFromAlicePOV.selfSigningKey()) + + assertEquals("Bob keys from alice pov should match", bobKeysFromAlicePOV.masterKey()?.unpaddedBase64PublicKey, bobSession.cryptoService().crossSigningService().getMyCrossSigningKeys()?.masterKey()?.unpaddedBase64PublicKey) + assertEquals("Bob keys from alice pov should match", bobKeysFromAlicePOV.selfSigningKey()?.unpaddedBase64PublicKey, bobSession.cryptoService().crossSigningService().getMyCrossSigningKeys()?.selfSigningKey()?.unpaddedBase64PublicKey) + + assertFalse("Bob keys from alice pov should not be trusted", bobKeysFromAlicePOV.isTrusted()) + + mTestHelper.signOutAndClose(aliceSession) + mTestHelper.signOutAndClose(bobSession) + } + + @Test + fun test_CrossSigningTestAliceTrustBobNewDevice() { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession + + val aliceAuthParams = UserPasswordAuth( + user = aliceSession.myUserId, + password = TestConstants.PASSWORD + ) + val bobAuthParams = UserPasswordAuth( + user = bobSession!!.myUserId, + password = TestConstants.PASSWORD + ) + + mTestHelper.doSync { aliceSession.cryptoService().crossSigningService().initializeCrossSigning(aliceAuthParams, it) } + mTestHelper.doSync { bobSession.cryptoService().crossSigningService().initializeCrossSigning(bobAuthParams, it) } + + // Check that alice can see bob keys + val bobUserId = bobSession.myUserId + mTestHelper.doSync> { aliceSession.cryptoService().downloadKeys(listOf(bobUserId), true, it) } + + val bobKeysFromAlicePOV = aliceSession.cryptoService().crossSigningService().getUserCrossSigningKeys(bobUserId) + assertTrue("Bob keys from alice pov should not be trusted", bobKeysFromAlicePOV?.isTrusted() == false) + + mTestHelper.doSync { aliceSession.cryptoService().crossSigningService().trustUser(bobUserId, it) } + + // Now bobs logs in on a new device and verifies it + // We will want to test that in alice POV, this new device would be trusted by cross signing + + val bobSession2 = mTestHelper.logIntoAccount(bobUserId, SessionTestParams(true)) + val bobSecondDeviceId = bobSession2.sessionParams.deviceId!! + + // Check that bob first session sees the new login + val data = mTestHelper.doSync> { + bobSession.cryptoService().downloadKeys(listOf(bobUserId), true, it) + } + + if (data.getUserDeviceIds(bobUserId)?.contains(bobSecondDeviceId) == false) { + fail("Bob should see the new device") + } + + val bobSecondDevicePOVFirstDevice = bobSession.cryptoService().getDeviceInfo(bobUserId, bobSecondDeviceId) + assertNotNull("Bob Second device should be known and persisted from first", bobSecondDevicePOVFirstDevice) + + // Manually mark it as trusted from first session + mTestHelper.doSync { + bobSession.cryptoService().crossSigningService().trustDevice(bobSecondDeviceId, it) + } + + // Now alice should cross trust bob's second device + val data2 = mTestHelper.doSync> { + aliceSession.cryptoService().downloadKeys(listOf(bobUserId), true, it) + } + + // check that the device is seen + if (data2.getUserDeviceIds(bobUserId)?.contains(bobSecondDeviceId) == false) { + fail("Alice should see the new device") + } + + val result = aliceSession.cryptoService().crossSigningService().checkDeviceTrust(bobUserId, bobSecondDeviceId, null) + assertTrue("Bob second device should be trusted from alice POV", result.isCrossSignedVerified()) + + mTestHelper.signOutAndClose(aliceSession) + mTestHelper.signOutAndClose(bobSession) + mTestHelper.signOutAndClose(bobSession2) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt new file mode 100644 index 0000000000..7c1a88dc75 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt @@ -0,0 +1,299 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.gossiping + +import android.util.Log +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction +import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction +import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod +import org.matrix.android.sdk.api.session.crypto.verification.VerificationService +import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction +import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility +import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.SessionTestParams +import org.matrix.android.sdk.common.TestConstants +import org.matrix.android.sdk.internal.crypto.GossipingRequestState +import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestState +import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel +import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent +import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull +import junit.framework.TestCase.assertTrue +import junit.framework.TestCase.fail +import org.junit.Assert +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import java.util.concurrent.CountDownLatch + +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class KeyShareTests : InstrumentedTest { + + private val mTestHelper = CommonTestHelper(context()) + private val mCryptoTestHelper = CryptoTestHelper(mTestHelper) + + @Test + fun test_DoNotSelfShareIfNotTrusted() { + val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) + + // Create an encrypted room and add a message + val roomId = mTestHelper.doSync { + aliceSession.createRoom( + CreateRoomParams().apply { + visibility = RoomDirectoryVisibility.PRIVATE + enableEncryption() + }, + it + ) + } + val room = aliceSession.getRoom(roomId) + assertNotNull(room) + Thread.sleep(4_000) + assertTrue(room?.isEncrypted() == true) + val sentEventId = mTestHelper.sendTextMessage(room!!, "My Message", 1).first().eventId + + // Open a new sessionx + + val aliceSession2 = mTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true)) + + val roomSecondSessionPOV = aliceSession2.getRoom(roomId) + + val receivedEvent = roomSecondSessionPOV?.getTimeLineEvent(sentEventId) + assertNotNull(receivedEvent) + assert(receivedEvent!!.isEncrypted()) + + try { + aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") + fail("should fail") + } catch (failure: Throwable) { + } + + val outgoingRequestsBefore = aliceSession2.cryptoService().getOutgoingRoomKeyRequests() + // Try to request + aliceSession2.cryptoService().requestRoomKeyForEvent(receivedEvent.root) + + val waitLatch = CountDownLatch(1) + val eventMegolmSessionId = receivedEvent.root.content.toModel()?.sessionId + + var outGoingRequestId: String? = null + + mTestHelper.retryPeriodicallyWithLatch(waitLatch) { + aliceSession2.cryptoService().getOutgoingRoomKeyRequests() + .filter { req -> + // filter out request that was known before + !outgoingRequestsBefore.any { req.requestId == it.requestId } + } + .let { + val outgoing = it.firstOrNull { it.sessionId == eventMegolmSessionId } + outGoingRequestId = outgoing?.requestId + outgoing != null + } + } + mTestHelper.await(waitLatch) + + Log.v("TEST", "=======> Outgoing requet Id is $outGoingRequestId") + + val outgoingRequestAfter = aliceSession2.cryptoService().getOutgoingRoomKeyRequests() + + // We should have a new request + Assert.assertTrue(outgoingRequestAfter.size > outgoingRequestsBefore.size) + Assert.assertNotNull(outgoingRequestAfter.first { it.sessionId == eventMegolmSessionId }) + + // The first session should see an incoming request + // the request should be refused, because the device is not trusted + mTestHelper.waitWithLatch { latch -> + mTestHelper.retryPeriodicallyWithLatch(latch) { + // DEBUG LOGS + aliceSession.cryptoService().getIncomingRoomKeyRequests().let { + Log.v("TEST", "Incoming request Session 1 (looking for $outGoingRequestId)") + Log.v("TEST", "=========================") + it.forEach { keyRequest -> + Log.v("TEST", "[ts${keyRequest.localCreationTimestamp}] requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId} is ${keyRequest.state}") + } + Log.v("TEST", "=========================") + } + + val incoming = aliceSession.cryptoService().getIncomingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId } + incoming?.state == GossipingRequestState.REJECTED + } + } + + try { + aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") + fail("should fail") + } catch (failure: Throwable) { + } + + // Mark the device as trusted + aliceSession.cryptoService().setDeviceVerification(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), aliceSession.myUserId, + aliceSession2.sessionParams.deviceId ?: "") + + // Re request + aliceSession2.cryptoService().reRequestRoomKeyForEvent(receivedEvent.root) + + mTestHelper.waitWithLatch { latch -> + mTestHelper.retryPeriodicallyWithLatch(latch) { + aliceSession.cryptoService().getIncomingRoomKeyRequests().let { + Log.v("TEST", "Incoming request Session 1") + Log.v("TEST", "=========================") + it.forEach { + Log.v("TEST", "requestId ${it.requestId}, for sessionId ${it.requestBody?.sessionId} is ${it.state}") + } + Log.v("TEST", "=========================") + + it.any { it.requestBody?.sessionId == eventMegolmSessionId && it.state == GossipingRequestState.ACCEPTED } + } + } + } + + Thread.sleep(6_000) + mTestHelper.waitWithLatch { latch -> + mTestHelper.retryPeriodicallyWithLatch(latch) { + aliceSession2.cryptoService().getOutgoingRoomKeyRequests().let { + it.any { it.requestBody?.sessionId == eventMegolmSessionId && it.state == OutgoingGossipingRequestState.CANCELLED } + } + } + } + + try { + aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") + } catch (failure: Throwable) { + fail("should have been able to decrypt") + } + + mTestHelper.signOutAndClose(aliceSession) + mTestHelper.signOutAndClose(aliceSession2) + } + + @Test + fun test_ShareSSSSSecret() { + val aliceSession1 = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) + + mTestHelper.doSync { + aliceSession1.cryptoService().crossSigningService() + .initializeCrossSigning(UserPasswordAuth( + user = aliceSession1.myUserId, + password = TestConstants.PASSWORD + ), it) + } + + // Also bootstrap keybackup on first session + val creationInfo = mTestHelper.doSync { + aliceSession1.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it) + } + val version = mTestHelper.doSync { + aliceSession1.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it) + } + // Save it for gossiping + aliceSession1.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version) + + val aliceSession2 = mTestHelper.logIntoAccount(aliceSession1.myUserId, SessionTestParams(true)) + + val aliceVerificationService1 = aliceSession1.cryptoService().verificationService() + val aliceVerificationService2 = aliceSession2.cryptoService().verificationService() + + // force keys download + mTestHelper.doSync> { + aliceSession1.cryptoService().downloadKeys(listOf(aliceSession1.myUserId), true, it) + } + mTestHelper.doSync> { + aliceSession2.cryptoService().downloadKeys(listOf(aliceSession2.myUserId), true, it) + } + + var session1ShortCode: String? = null + var session2ShortCode: String? = null + + aliceVerificationService1.addListener(object : VerificationService.Listener { + override fun transactionUpdated(tx: VerificationTransaction) { + Log.d("#TEST", "AA: tx incoming?:${tx.isIncoming} state ${tx.state}") + if (tx is SasVerificationTransaction) { + if (tx.state == VerificationTxState.OnStarted) { + (tx as IncomingSasVerificationTransaction).performAccept() + } + if (tx.state == VerificationTxState.ShortCodeReady) { + session1ShortCode = tx.getDecimalCodeRepresentation() + Thread.sleep(500) + tx.userHasVerifiedShortCode() + } + } + } + }) + + aliceVerificationService2.addListener(object : VerificationService.Listener { + override fun transactionUpdated(tx: VerificationTransaction) { + Log.d("#TEST", "BB: tx incoming?:${tx.isIncoming} state ${tx.state}") + if (tx is SasVerificationTransaction) { + if (tx.state == VerificationTxState.ShortCodeReady) { + session2ShortCode = tx.getDecimalCodeRepresentation() + Thread.sleep(500) + tx.userHasVerifiedShortCode() + } + } + } + }) + + val txId = "m.testVerif12" + aliceVerificationService2.beginKeyVerification(VerificationMethod.SAS, aliceSession1.myUserId, aliceSession1.sessionParams.deviceId + ?: "", txId) + + mTestHelper.waitWithLatch { latch -> + mTestHelper.retryPeriodicallyWithLatch(latch) { + aliceSession1.cryptoService().getDeviceInfo(aliceSession1.myUserId, aliceSession2.sessionParams.deviceId ?: "")?.isVerified == true + } + } + + assertNotNull(session1ShortCode) + Log.d("#TEST", "session1ShortCode: $session1ShortCode") + assertNotNull(session2ShortCode) + Log.d("#TEST", "session2ShortCode: $session2ShortCode") + assertEquals(session1ShortCode, session2ShortCode) + + // SSK and USK private keys should have been shared + + mTestHelper.waitWithLatch(60_000) { latch -> + mTestHelper.retryPeriodicallyWithLatch(latch) { + Log.d("#TEST", "CAN XS :${aliceSession2.cryptoService().crossSigningService().getMyCrossSigningKeys()}") + aliceSession2.cryptoService().crossSigningService().canCrossSign() + } + } + + // Test that key backup key has been shared to + mTestHelper.waitWithLatch(60_000) { latch -> + val keysBackupService = aliceSession2.cryptoService().keysBackupService() + mTestHelper.retryPeriodicallyWithLatch(latch) { + Log.d("#TEST", "Recovery :${keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey}") + keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey == creationInfo.recoveryKey + } + } + + mTestHelper.signOutAndClose(aliceSession1) + mTestHelper.signOutAndClose(aliceSession2) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt new file mode 100644 index 0000000000..7154487219 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt @@ -0,0 +1,245 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.gossiping + +import android.util.Log +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.NoOpMatrixCallback +import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.MockOkHttpInterceptor +import org.matrix.android.sdk.common.SessionTestParams +import org.matrix.android.sdk.common.TestConstants +import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent +import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode +import org.junit.Assert +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters + +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class WithHeldTests : InstrumentedTest { + + private val mTestHelper = CommonTestHelper(context()) + private val mCryptoTestHelper = CryptoTestHelper(mTestHelper) + + @Test + fun test_WithHeldUnverifiedReason() { + // ============================= + // ARRANGE + // ============================= + + val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) + val bobSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) + + // Initialize cross signing on both + mCryptoTestHelper.initializeCrossSigning(aliceSession) + mCryptoTestHelper.initializeCrossSigning(bobSession) + + val roomId = mCryptoTestHelper.createDM(aliceSession, bobSession) + mCryptoTestHelper.verifySASCrossSign(aliceSession, bobSession, roomId) + + val roomAlicePOV = aliceSession.getRoom(roomId)!! + + val bobUnverifiedSession = mTestHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true)) + + // ============================= + // ACT + // ============================= + + // Alice decide to not send to unverified sessions + aliceSession.cryptoService().setGlobalBlacklistUnverifiedDevices(true) + + val timelineEvent = mTestHelper.sendTextMessage(roomAlicePOV, "Hello Bob", 1).first() + + // await for bob unverified session to get the message + mTestHelper.waitWithLatch { latch -> + mTestHelper.retryPeriodicallyWithLatch(latch) { + bobUnverifiedSession.getRoom(roomId)?.getTimeLineEvent(timelineEvent.eventId) != null + } + } + + val eventBobPOV = bobUnverifiedSession.getRoom(roomId)?.getTimeLineEvent(timelineEvent.eventId)!! + + // ============================= + // ASSERT + // ============================= + + // Bob should not be able to decrypt because the keys is withheld + try { + // .. might need to wait a bit for stability? + bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "") + Assert.fail("This session should not be able to decrypt") + } catch (failure: Throwable) { + val type = (failure as MXCryptoError.Base).errorType + val technicalMessage = failure.technicalMessage + Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) + Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage) + } + + // enable back sending to unverified + aliceSession.cryptoService().setGlobalBlacklistUnverifiedDevices(false) + + val secondEvent = mTestHelper.sendTextMessage(roomAlicePOV, "Verify your device!!", 1).first() + + mTestHelper.waitWithLatch { latch -> + mTestHelper.retryPeriodicallyWithLatch(latch) { + val ev = bobUnverifiedSession.getRoom(roomId)?.getTimeLineEvent(secondEvent.eventId) + // wait until it's decrypted + ev?.root?.getClearType() == EventType.MESSAGE + } + } + + // Previous message should still be undecryptable (partially withheld session) + try { + // .. might need to wait a bit for stability? + bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "") + Assert.fail("This session should not be able to decrypt") + } catch (failure: Throwable) { + val type = (failure as MXCryptoError.Base).errorType + val technicalMessage = failure.technicalMessage + Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) + Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage) + } + + mTestHelper.signOutAndClose(aliceSession) + mTestHelper.signOutAndClose(bobSession) + mTestHelper.signOutAndClose(bobUnverifiedSession) + } + + @Test + fun test_WithHeldNoOlm() { + val testData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + val aliceSession = testData.firstSession + val bobSession = testData.secondSession!! + val aliceInterceptor = mTestHelper.getTestInterceptor(aliceSession) + + // Simulate no OTK + aliceInterceptor!!.addRule(MockOkHttpInterceptor.SimpleRule( + "/keys/claim", + 200, + """ + { "one_time_keys" : {} } + """ + )) + Log.d("#TEST", "Recovery :${aliceSession.sessionParams.credentials.accessToken}") + + val roomAlicePov = aliceSession.getRoom(testData.roomId)!! + + val eventId = mTestHelper.sendTextMessage(roomAlicePov, "first message", 1).first().eventId + + // await for bob session to get the message + mTestHelper.waitWithLatch { latch -> + mTestHelper.retryPeriodicallyWithLatch(latch) { + bobSession.getRoom(testData.roomId)?.getTimeLineEvent(eventId) != null + } + } + + // Previous message should still be undecryptable (partially withheld session) + val eventBobPOV = bobSession.getRoom(testData.roomId)?.getTimeLineEvent(eventId) + try { + // .. might need to wait a bit for stability? + bobSession.cryptoService().decryptEvent(eventBobPOV!!.root, "") + Assert.fail("This session should not be able to decrypt") + } catch (failure: Throwable) { + val type = (failure as MXCryptoError.Base).errorType + val technicalMessage = failure.technicalMessage + Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) + Assert.assertEquals("Cause should be unverified", WithHeldCode.NO_OLM.value, technicalMessage) + } + + // Ensure that alice has marked the session to be shared with bob + val sessionId = eventBobPOV!!.root.content.toModel()!!.sessionId!! + val chainIndex = aliceSession.cryptoService().getSharedWithInfo(testData.roomId, sessionId).getObject(bobSession.myUserId, bobSession.sessionParams.credentials.deviceId) + + Assert.assertEquals("Alice should have marked bob's device for this session", 0, chainIndex) + // Add a new device for bob + + aliceInterceptor.clearRules() + val bobSecondSession = mTestHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(withInitialSync = true)) + // send a second message + val secondMessageId = mTestHelper.sendTextMessage(roomAlicePov, "second message", 1).first().eventId + + // Check that the + // await for bob SecondSession session to get the message + mTestHelper.waitWithLatch { latch -> + mTestHelper.retryPeriodicallyWithLatch(latch) { + bobSecondSession.getRoom(testData.roomId)?.getTimeLineEvent(secondMessageId) != null + } + } + + val chainIndex2 = aliceSession.cryptoService().getSharedWithInfo(testData.roomId, sessionId).getObject(bobSecondSession.myUserId, bobSecondSession.sessionParams.credentials.deviceId) + + Assert.assertEquals("Alice should have marked bob's device for this session", 1, chainIndex2) + + aliceInterceptor.clearRules() + testData.cleanUp(mTestHelper) + mTestHelper.signOutAndClose(bobSecondSession) + } + + @Test + fun test_WithHeldKeyRequest() { + val testData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + val aliceSession = testData.firstSession + val bobSession = testData.secondSession!! + + val roomAlicePov = aliceSession.getRoom(testData.roomId)!! + + val eventId = mTestHelper.sendTextMessage(roomAlicePov, "first message", 1).first().eventId + + mTestHelper.signOutAndClose(bobSession) + + // Create a new session for bob + + val bobSecondSession = mTestHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true)) + // initialize to force request keys if missing + mCryptoTestHelper.initializeCrossSigning(bobSecondSession) + + // Trust bob second device from Alice POV + aliceSession.cryptoService().crossSigningService().trustDevice(bobSecondSession.sessionParams.deviceId!!, NoOpMatrixCallback()) + bobSecondSession.cryptoService().crossSigningService().trustDevice(aliceSession.sessionParams.deviceId!!, NoOpMatrixCallback()) + + var sessionId: String? = null + // Check that the + // await for bob SecondSession session to get the message + mTestHelper.waitWithLatch { latch -> + mTestHelper.retryPeriodicallyWithLatch(latch) { + val timeLineEvent = bobSecondSession.getRoom(testData.roomId)?.getTimeLineEvent(eventId)?.also { + // try to decrypt and force key request + tryThis { bobSecondSession.cryptoService().decryptEvent(it.root, "") } + } + sessionId = timeLineEvent?.root?.content?.toModel()?.sessionId + timeLineEvent != null + } + } + + // Check that bob second session requested the key + mTestHelper.waitWithLatch { latch -> + mTestHelper.retryPeriodicallyWithLatch(latch) { + val wc = bobSecondSession.cryptoService().getWithHeldMegolmSession(roomAlicePov.roomId, sessionId!!) + wc?.code == WithHeldCode.UNAUTHORISED + } + } + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupPasswordTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupPasswordTest.kt new file mode 100644 index 0000000000..f38b55beba --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupPasswordTest.kt @@ -0,0 +1,180 @@ +/* + * Copyright 2019 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.listeners.ProgressListener +import org.matrix.android.sdk.common.assertByteArrayNotEqual +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.matrix.olm.OlmManager +import org.matrix.olm.OlmPkDecryption + +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class KeysBackupPasswordTest : InstrumentedTest { + + @Before + fun ensureLibLoaded() { + OlmManager() + } + + /** + * Check KeysBackupPassword utilities + */ + @Test + fun passwordConverter_ok() { + val generatePrivateKeyResult = generatePrivateKeyWithPassword(PASSWORD, null) + + assertEquals(32, generatePrivateKeyResult.salt.length) + assertEquals(500_000, generatePrivateKeyResult.iterations) + assertEquals(OlmPkDecryption.privateKeyLength(), generatePrivateKeyResult.privateKey.size) + + // Reverse operation + val retrievedPrivateKey = retrievePrivateKeyWithPassword(PASSWORD, + generatePrivateKeyResult.salt, + generatePrivateKeyResult.iterations) + + assertEquals(OlmPkDecryption.privateKeyLength(), retrievedPrivateKey.size) + assertArrayEquals(generatePrivateKeyResult.privateKey, retrievedPrivateKey) + } + + /** + * Check generatePrivateKeyWithPassword progress listener behavior + */ + @Test + fun passwordConverter_progress_ok() { + val progressValues = ArrayList(101) + var lastTotal = 0 + + generatePrivateKeyWithPassword(PASSWORD, object : ProgressListener { + override fun onProgress(progress: Int, total: Int) { + if (!progressValues.contains(progress)) { + progressValues.add(progress) + } + + lastTotal = total + } + }) + + assertEquals(100, lastTotal) + + // Ensure all values are here + assertEquals(101, progressValues.size) + + for (i in 0..100) { + assertTrue(progressValues[i] == i) + } + } + + /** + * Check KeysBackupPassword utilities, with bad password + */ + @Test + fun passwordConverter_badPassword_ok() { + val generatePrivateKeyResult = generatePrivateKeyWithPassword(PASSWORD, null) + + assertEquals(32, generatePrivateKeyResult.salt.length) + assertEquals(500_000, generatePrivateKeyResult.iterations) + assertEquals(OlmPkDecryption.privateKeyLength(), generatePrivateKeyResult.privateKey.size) + + // Reverse operation, with bad password + val retrievedPrivateKey = retrievePrivateKeyWithPassword(BAD_PASSWORD, + generatePrivateKeyResult.salt, + generatePrivateKeyResult.iterations) + + assertEquals(OlmPkDecryption.privateKeyLength(), retrievedPrivateKey.size) + assertByteArrayNotEqual(generatePrivateKeyResult.privateKey, retrievedPrivateKey) + } + + /** + * Check KeysBackupPassword utilities, with bad password + */ + @Test + fun passwordConverter_badIteration_ok() { + val generatePrivateKeyResult = generatePrivateKeyWithPassword(PASSWORD, null) + + assertEquals(32, generatePrivateKeyResult.salt.length) + assertEquals(500_000, generatePrivateKeyResult.iterations) + assertEquals(OlmPkDecryption.privateKeyLength(), generatePrivateKeyResult.privateKey.size) + + // Reverse operation, with bad iteration + val retrievedPrivateKey = retrievePrivateKeyWithPassword(PASSWORD, + generatePrivateKeyResult.salt, + 500_001) + + assertEquals(OlmPkDecryption.privateKeyLength(), retrievedPrivateKey.size) + assertByteArrayNotEqual(generatePrivateKeyResult.privateKey, retrievedPrivateKey) + } + + /** + * Check KeysBackupPassword utilities, with bad salt + */ + @Test + fun passwordConverter_badSalt_ok() { + val generatePrivateKeyResult = generatePrivateKeyWithPassword(PASSWORD, null) + + assertEquals(32, generatePrivateKeyResult.salt.length) + assertEquals(500_000, generatePrivateKeyResult.iterations) + assertEquals(OlmPkDecryption.privateKeyLength(), generatePrivateKeyResult.privateKey.size) + + // Reverse operation, with bad iteration + val retrievedPrivateKey = retrievePrivateKeyWithPassword(PASSWORD, + BAD_SALT, + generatePrivateKeyResult.iterations) + + assertEquals(OlmPkDecryption.privateKeyLength(), retrievedPrivateKey.size) + assertByteArrayNotEqual(generatePrivateKeyResult.privateKey, retrievedPrivateKey) + } + + /** + * Check [retrievePrivateKeyWithPassword] with data coming from another platform (RiotWeb). + */ + @Test + fun passwordConverter_crossPlatform_ok() { + val password = "This is a passphrase!" + val salt = "TO0lxhQ9aYgGfMsclVWPIAublg8h9Nlu" + val iteration = 500_000 + + val retrievedPrivateKey = retrievePrivateKeyWithPassword(password, salt, iteration) + + assertEquals(OlmPkDecryption.privateKeyLength(), retrievedPrivateKey.size) + + // Data from RiotWeb + val privateKeyBytes = byteArrayOf( + 116.toByte(), 224.toByte(), 229.toByte(), 224.toByte(), 9.toByte(), 3.toByte(), 178.toByte(), 162.toByte(), + 120.toByte(), 23.toByte(), 108.toByte(), 218.toByte(), 22.toByte(), 61.toByte(), 241.toByte(), 200.toByte(), + 235.toByte(), 173.toByte(), 236.toByte(), 100.toByte(), 115.toByte(), 247.toByte(), 33.toByte(), 132.toByte(), + 195.toByte(), 154.toByte(), 64.toByte(), 158.toByte(), 184.toByte(), 148.toByte(), 20.toByte(), 85.toByte()) + + assertArrayEquals(privateKeyBytes, retrievedPrivateKey) + } + + companion object { + private const val PASSWORD = "password" + private const val BAD_PASSWORD = "passw0rd" + + private const val BAD_SALT = "AA0lxhQ9aYgGfMsclVWPIAublg8h9Nlu" + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupScenarioData.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupScenarioData.kt new file mode 100644 index 0000000000..29a0b5ffd6 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupScenarioData.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup + +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CryptoTestData +import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 + +/** + * Data class to store result of [KeysBackupTestHelper.createKeysBackupScenarioWithPassword] + */ +data class KeysBackupScenarioData(val cryptoTestData: CryptoTestData, + val aliceKeys: List, + val prepareKeysBackupDataResult: PrepareKeysBackupDataResult, + val aliceSession2: Session) { + fun cleanUp(testHelper: CommonTestHelper) { + cryptoTestData.cleanUp(testHelper) + testHelper.signOutAndClose(aliceSession2) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt new file mode 100644 index 0000000000..aef97d5687 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt @@ -0,0 +1,1099 @@ +/* + * Copyright 2018 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.listeners.ProgressListener +import org.matrix.android.sdk.api.listeners.StepProgressListener +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.TestConstants +import org.matrix.android.sdk.common.TestMatrixCallback +import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP +import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel +import org.matrix.android.sdk.internal.crypto.keysbackup.model.KeysBackupVersionTrust +import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult +import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import java.util.ArrayList +import java.util.Collections +import java.util.concurrent.CountDownLatch + +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class KeysBackupTest : InstrumentedTest { + + private val mTestHelper = CommonTestHelper(context()) + private val mCryptoTestHelper = CryptoTestHelper(mTestHelper) + private val mKeysBackupTestHelper = KeysBackupTestHelper(mTestHelper, mCryptoTestHelper) + + /** + * - From doE2ETestWithAliceAndBobInARoomWithEncryptedMessages, we should have no backed up keys + * - Check backup keys after having marked one as backed up + * - Reset keys backup markers + */ + @Test + fun roomKeysTest_testBackupStore_ok() { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() + + // From doE2ETestWithAliceAndBobInARoomWithEncryptedMessages, we should have no backed up keys + val cryptoStore = (cryptoTestData.firstSession.cryptoService().keysBackupService() as DefaultKeysBackupService).store + val sessions = cryptoStore.inboundGroupSessionsToBackup(100) + val sessionsCount = sessions.size + + assertFalse(sessions.isEmpty()) + assertEquals(sessionsCount, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(false)) + assertEquals(0, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(true)) + + // - Check backup keys after having marked one as backed up + val session = sessions[0] + + cryptoStore.markBackupDoneForInboundGroupSessions(Collections.singletonList(session)) + + assertEquals(sessionsCount, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(false)) + assertEquals(1, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(true)) + + val sessions2 = cryptoStore.inboundGroupSessionsToBackup(100) + assertEquals(sessionsCount - 1, sessions2.size) + + // - Reset keys backup markers + cryptoStore.resetBackupMarkers() + + val sessions3 = cryptoStore.inboundGroupSessionsToBackup(100) + assertEquals(sessionsCount, sessions3.size) + assertEquals(sessionsCount, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(false)) + assertEquals(0, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(true)) + + cryptoTestData.cleanUp(mTestHelper) + } + + /** + * Check that prepareKeysBackupVersionWithPassword returns valid data + */ + @Test + fun prepareKeysBackupVersionTest() { + val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, KeysBackupTestConstants.defaultSessionParams) + + assertNotNull(bobSession.cryptoService().keysBackupService()) + + val keysBackup = bobSession.cryptoService().keysBackupService() + + val stateObserver = StateObserver(keysBackup) + + assertFalse(keysBackup.isEnabled) + + val megolmBackupCreationInfo = mTestHelper.doSync { + keysBackup.prepareKeysBackupVersion(null, null, it) + } + + assertEquals(MXCRYPTO_ALGORITHM_MEGOLM_BACKUP, megolmBackupCreationInfo.algorithm) + assertNotNull(megolmBackupCreationInfo.authData) + assertNotNull(megolmBackupCreationInfo.authData!!.publicKey) + assertNotNull(megolmBackupCreationInfo.authData!!.signatures) + assertNotNull(megolmBackupCreationInfo.recoveryKey) + + stateObserver.stopAndCheckStates(null) + mTestHelper.signOutAndClose(bobSession) + } + + /** + * Test creating a keys backup version and check that createKeysBackupVersion() returns valid data + */ + @Test + fun createKeysBackupVersionTest() { + val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, KeysBackupTestConstants.defaultSessionParams) + + val keysBackup = bobSession.cryptoService().keysBackupService() + + val stateObserver = StateObserver(keysBackup) + + assertFalse(keysBackup.isEnabled) + + val megolmBackupCreationInfo = mTestHelper.doSync { + keysBackup.prepareKeysBackupVersion(null, null, it) + } + + assertFalse(keysBackup.isEnabled) + + // Create the version + mTestHelper.doSync { + keysBackup.createKeysBackupVersion(megolmBackupCreationInfo, it) + } + + // Backup must be enable now + assertTrue(keysBackup.isEnabled) + + stateObserver.stopAndCheckStates(null) + mTestHelper.signOutAndClose(bobSession) + } + + /** + * - Check that createKeysBackupVersion() launches the backup + * - Check the backup completes + */ + @Test + fun backupAfterCreateKeysBackupVersionTest() { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() + + val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService() + + val latch = CountDownLatch(1) + + assertEquals(2, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(false)) + assertEquals(0, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(true)) + + val stateObserver = StateObserver(keysBackup, latch, 5) + + mKeysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup) + + mTestHelper.await(latch) + + val nbOfKeys = cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(false) + val backedUpKeys = cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(true) + + assertEquals(2, nbOfKeys) + assertEquals("All keys must have been marked as backed up", nbOfKeys, backedUpKeys) + + // Check the several backup state changes + stateObserver.stopAndCheckStates( + listOf( + KeysBackupState.Enabling, + KeysBackupState.ReadyToBackUp, + KeysBackupState.WillBackUp, + KeysBackupState.BackingUp, + KeysBackupState.ReadyToBackUp + ) + ) + cryptoTestData.cleanUp(mTestHelper) + } + + /** + * Check that backupAllGroupSessions() returns valid data + */ + @Test + fun backupAllGroupSessionsTest() { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() + + val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService() + + val stateObserver = StateObserver(keysBackup) + + mKeysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup) + + // Check that backupAllGroupSessions returns valid data + val nbOfKeys = cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(false) + + assertEquals(2, nbOfKeys) + + var lastBackedUpKeysProgress = 0 + + mTestHelper.doSync { + keysBackup.backupAllGroupSessions(object : ProgressListener { + override fun onProgress(progress: Int, total: Int) { + assertEquals(nbOfKeys, total) + lastBackedUpKeysProgress = progress + } + }, it) + } + + assertEquals(nbOfKeys, lastBackedUpKeysProgress) + + val backedUpKeys = cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(true) + + assertEquals("All keys must have been marked as backed up", nbOfKeys, backedUpKeys) + + stateObserver.stopAndCheckStates(null) + cryptoTestData.cleanUp(mTestHelper) + } + + /** + * Check encryption and decryption of megolm keys in the backup. + * - Pick a megolm key + * - Check [MXKeyBackup encryptGroupSession] returns stg + * - Check [MXKeyBackup pkDecryptionFromRecoveryKey] is able to create a OLMPkDecryption + * - Check [MXKeyBackup decryptKeyBackupData] returns stg + * - Compare the decrypted megolm key with the original one + */ + @Test + fun testEncryptAndDecryptKeysBackupData() { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() + + val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService() as DefaultKeysBackupService + + val stateObserver = StateObserver(keysBackup) + + // - Pick a megolm key + val session = keysBackup.store.inboundGroupSessionsToBackup(1)[0] + + val keyBackupCreationInfo = mKeysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup).megolmBackupCreationInfo + + // - Check encryptGroupSession() returns stg + val keyBackupData = keysBackup.encryptGroupSession(session) + assertNotNull(keyBackupData) + assertNotNull(keyBackupData.sessionData) + + // - Check pkDecryptionFromRecoveryKey() is able to create a OlmPkDecryption + val decryption = keysBackup.pkDecryptionFromRecoveryKey(keyBackupCreationInfo.recoveryKey) + assertNotNull(decryption) + // - Check decryptKeyBackupData() returns stg + val sessionData = keysBackup + .decryptKeyBackupData(keyBackupData, + session.olmInboundGroupSession!!.sessionIdentifier(), + cryptoTestData.roomId, + decryption!!) + assertNotNull(sessionData) + // - Compare the decrypted megolm key with the original one + mKeysBackupTestHelper.assertKeysEquals(session.exportKeys(), sessionData) + + stateObserver.stopAndCheckStates(null) + cryptoTestData.cleanUp(mTestHelper) + } + + /** + * - Do an e2e backup to the homeserver with a recovery key + * - Log Alice on a new device + * - Restore the e2e backup from the homeserver with the recovery key + * - Restore must be successful + */ + @Test + fun restoreKeysBackupTest() { + val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(null) + + // - Restore the e2e backup from the homeserver + val importRoomKeysResult = mTestHelper.doSync { + testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, + null, + null, + null, + it + ) + } + + mKeysBackupTestHelper.checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys) + + testData.cleanUp(mTestHelper) + } + + /** + * + * This is the same as `testRestoreKeyBackup` but this test checks that pending key + * share requests are cancelled. + * + * - Do an e2e backup to the homeserver with a recovery key + * - Log Alice on a new device + * - *** Check the SDK sent key share requests + * - Restore the e2e backup from the homeserver with the recovery key + * - Restore must be successful + * - *** There must be no more pending key share requests + */ +// @Test +// fun restoreKeysBackupAndKeyShareRequestTest() { +// fail("Check with Valere for this test. I think we do not send key share request") +// +// val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(null) +// +// // - Check the SDK sent key share requests +// val cryptoStore2 = (testData.aliceSession2.cryptoService().keysBackupService() as DefaultKeysBackupService).store +// val unsentRequest = cryptoStore2 +// .getOutgoingRoomKeyRequestByState(setOf(ShareRequestState.UNSENT)) +// val sentRequest = cryptoStore2 +// .getOutgoingRoomKeyRequestByState(setOf(ShareRequestState.SENT)) +// +// // Request is either sent or unsent +// assertTrue(unsentRequest != null || sentRequest != null) +// +// // - Restore the e2e backup from the homeserver +// val importRoomKeysResult = mTestHelper.doSync { +// testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, +// testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, +// null, +// null, +// null, +// it +// ) +// } +// +// mKeysBackupTestHelper.checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys) +// +// // - There must be no more pending key share requests +// val unsentRequestAfterRestoration = cryptoStore2 +// .getOutgoingRoomKeyRequestByState(setOf(ShareRequestState.UNSENT)) +// val sentRequestAfterRestoration = cryptoStore2 +// .getOutgoingRoomKeyRequestByState(setOf(ShareRequestState.SENT)) +// +// // Request is either sent or unsent +// assertTrue(unsentRequestAfterRestoration == null && sentRequestAfterRestoration == null) +// +// testData.cleanUp(mTestHelper) +// } + + /** + * - Do an e2e backup to the homeserver with a recovery key + * - And log Alice on a new device + * - The new device must see the previous backup as not trusted + * - Trust the backup from the new device + * - Backup must be enabled on the new device + * - Retrieve the last version from the server + * - It must be the same + * - It must be trusted and must have with 2 signatures now + */ + @Test + fun trustKeyBackupVersionTest() { + // - Do an e2e backup to the homeserver with a recovery key + // - And log Alice on a new device + val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(null) + + val stateObserver = StateObserver(testData.aliceSession2.cryptoService().keysBackupService()) + + // - The new device must see the previous backup as not trusted + assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion) + assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled) + assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().state) + + // - Trust the backup from the new device + mTestHelper.doSync { + testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersion( + testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + true, + it + ) + } + + // Wait for backup state to be ReadyToBackUp + mKeysBackupTestHelper.waitForKeysBackupToBeInState(testData.aliceSession2, KeysBackupState.ReadyToBackUp) + + // - Backup must be enabled on the new device, on the same version + assertEquals(testData.prepareKeysBackupDataResult.version, testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion?.version) + assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled) + + // - Retrieve the last version from the server + val keysVersionResult = mTestHelper.doSync { + testData.aliceSession2.cryptoService().keysBackupService().getCurrentVersion(it) + } + + // - It must be the same + assertEquals(testData.prepareKeysBackupDataResult.version, keysVersionResult!!.version) + + val keysBackupVersionTrust = mTestHelper.doSync { + testData.aliceSession2.cryptoService().keysBackupService().getKeysBackupTrust(keysVersionResult, it) + } + + // - It must be trusted and must have 2 signatures now + assertTrue(keysBackupVersionTrust.usable) + assertEquals(2, keysBackupVersionTrust.signatures.size) + + stateObserver.stopAndCheckStates(null) + testData.cleanUp(mTestHelper) + } + + /** + * - Do an e2e backup to the homeserver with a recovery key + * - And log Alice on a new device + * - The new device must see the previous backup as not trusted + * - Trust the backup from the new device with the recovery key + * - Backup must be enabled on the new device + * - Retrieve the last version from the server + * - It must be the same + * - It must be trusted and must have with 2 signatures now + */ + @Test + fun trustKeyBackupVersionWithRecoveryKeyTest() { + // - Do an e2e backup to the homeserver with a recovery key + // - And log Alice on a new device + val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(null) + + val stateObserver = StateObserver(testData.aliceSession2.cryptoService().keysBackupService()) + + // - The new device must see the previous backup as not trusted + assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion) + assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled) + assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().state) + + // - Trust the backup from the new device with the recovery key + mTestHelper.doSync { + testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithRecoveryKey( + testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, + it + ) + } + + // Wait for backup state to be ReadyToBackUp + mKeysBackupTestHelper.waitForKeysBackupToBeInState(testData.aliceSession2, KeysBackupState.ReadyToBackUp) + + // - Backup must be enabled on the new device, on the same version + assertEquals(testData.prepareKeysBackupDataResult.version, testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion?.version) + assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled) + + // - Retrieve the last version from the server + val keysVersionResult = mTestHelper.doSync { + testData.aliceSession2.cryptoService().keysBackupService().getCurrentVersion(it) + } + + // - It must be the same + assertEquals(testData.prepareKeysBackupDataResult.version, keysVersionResult!!.version) + + val keysBackupVersionTrust = mTestHelper.doSync { + testData.aliceSession2.cryptoService().keysBackupService().getKeysBackupTrust(keysVersionResult, it) + } + + // - It must be trusted and must have 2 signatures now + assertTrue(keysBackupVersionTrust.usable) + assertEquals(2, keysBackupVersionTrust.signatures.size) + + stateObserver.stopAndCheckStates(null) + testData.cleanUp(mTestHelper) + } + + /** + * - Do an e2e backup to the homeserver with a recovery key + * - And log Alice on a new device + * - The new device must see the previous backup as not trusted + * - Try to trust the backup from the new device with a wrong recovery key + * - It must fail + * - The backup must still be untrusted and disabled + */ + @Test + fun trustKeyBackupVersionWithWrongRecoveryKeyTest() { + // - Do an e2e backup to the homeserver with a recovery key + // - And log Alice on a new device + val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(null) + + val stateObserver = StateObserver(testData.aliceSession2.cryptoService().keysBackupService()) + + // - The new device must see the previous backup as not trusted + assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion) + assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled) + assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().state) + + // - Try to trust the backup from the new device with a wrong recovery key + val latch = CountDownLatch(1) + testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithRecoveryKey( + testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + "Bad recovery key", + TestMatrixCallback(latch, false) + ) + mTestHelper.await(latch) + + // - The new device must still see the previous backup as not trusted + assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion) + assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled) + assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().state) + + stateObserver.stopAndCheckStates(null) + testData.cleanUp(mTestHelper) + } + + /** + * - Do an e2e backup to the homeserver with a password + * - And log Alice on a new device + * - The new device must see the previous backup as not trusted + * - Trust the backup from the new device with the password + * - Backup must be enabled on the new device + * - Retrieve the last version from the server + * - It must be the same + * - It must be trusted and must have with 2 signatures now + */ + @Test + fun trustKeyBackupVersionWithPasswordTest() { + val password = "Password" + + // - Do an e2e backup to the homeserver with a password + // - And log Alice on a new device + val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(password) + + val stateObserver = StateObserver(testData.aliceSession2.cryptoService().keysBackupService()) + + // - The new device must see the previous backup as not trusted + assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion) + assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled) + assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().state) + + // - Trust the backup from the new device with the password + mTestHelper.doSync { + testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithPassphrase( + testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + password, + it + ) + } + + // Wait for backup state to be ReadyToBackUp + mKeysBackupTestHelper.waitForKeysBackupToBeInState(testData.aliceSession2, KeysBackupState.ReadyToBackUp) + + // - Backup must be enabled on the new device, on the same version + assertEquals(testData.prepareKeysBackupDataResult.version, testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion?.version) + assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled) + + // - Retrieve the last version from the server + val keysVersionResult = mTestHelper.doSync { + testData.aliceSession2.cryptoService().keysBackupService().getCurrentVersion(it) + } + + // - It must be the same + assertEquals(testData.prepareKeysBackupDataResult.version, keysVersionResult!!.version) + + val keysBackupVersionTrust = mTestHelper.doSync { + testData.aliceSession2.cryptoService().keysBackupService().getKeysBackupTrust(keysVersionResult, it) + } + + // - It must be trusted and must have 2 signatures now + assertTrue(keysBackupVersionTrust.usable) + assertEquals(2, keysBackupVersionTrust.signatures.size) + + stateObserver.stopAndCheckStates(null) + testData.cleanUp(mTestHelper) + } + + /** + * - Do an e2e backup to the homeserver with a password + * - And log Alice on a new device + * - The new device must see the previous backup as not trusted + * - Try to trust the backup from the new device with a wrong password + * - It must fail + * - The backup must still be untrusted and disabled + */ + @Test + fun trustKeyBackupVersionWithWrongPasswordTest() { + val password = "Password" + val badPassword = "Bad Password" + + // - Do an e2e backup to the homeserver with a password + // - And log Alice on a new device + val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(password) + + val stateObserver = StateObserver(testData.aliceSession2.cryptoService().keysBackupService()) + + // - The new device must see the previous backup as not trusted + assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion) + assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled) + assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().state) + + // - Try to trust the backup from the new device with a wrong password + val latch = CountDownLatch(1) + testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithPassphrase( + testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + badPassword, + TestMatrixCallback(latch, false) + ) + mTestHelper.await(latch) + + // - The new device must still see the previous backup as not trusted + assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion) + assertFalse(testData.aliceSession2.cryptoService().keysBackupService().isEnabled) + assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().state) + + stateObserver.stopAndCheckStates(null) + testData.cleanUp(mTestHelper) + } + + /** + * - Do an e2e backup to the homeserver with a recovery key + * - Log Alice on a new device + * - Try to restore the e2e backup with a wrong recovery key + * - It must fail + */ + @Test + fun restoreKeysBackupWithAWrongRecoveryKeyTest() { + val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(null) + + // - Try to restore the e2e backup with a wrong recovery key + val latch2 = CountDownLatch(1) + var importRoomKeysResult: ImportRoomKeysResult? = null + testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", + null, + null, + null, + object : TestMatrixCallback(latch2, false) { + override fun onSuccess(data: ImportRoomKeysResult) { + importRoomKeysResult = data + super.onSuccess(data) + } + } + ) + mTestHelper.await(latch2) + + // onSuccess may not have been called + assertNull(importRoomKeysResult) + + testData.cleanUp(mTestHelper) + } + + /** + * - Do an e2e backup to the homeserver with a password + * - Log Alice on a new device + * - Restore the e2e backup with the password + * - Restore must be successful + */ + @Test + fun testBackupWithPassword() { + val password = "password" + + val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(password) + + // - Restore the e2e backup with the password + val steps = ArrayList() + + val importRoomKeysResult = mTestHelper.doSync { + testData.aliceSession2.cryptoService().keysBackupService().restoreKeyBackupWithPassword(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + password, + null, + null, + object : StepProgressListener { + override fun onStepProgress(step: StepProgressListener.Step) { + steps.add(step) + } + }, + it + ) + } + + // Check steps + assertEquals(105, steps.size) + + for (i in 0..100) { + assertTrue(steps[i] is StepProgressListener.Step.ComputingKey) + assertEquals(i, (steps[i] as StepProgressListener.Step.ComputingKey).progress) + assertEquals(100, (steps[i] as StepProgressListener.Step.ComputingKey).total) + } + + assertTrue(steps[101] is StepProgressListener.Step.DownloadingKey) + + // 2 Keys to import, value will be 0%, 50%, 100% + for (i in 102..104) { + assertTrue(steps[i] is StepProgressListener.Step.ImportingKey) + assertEquals(100, (steps[i] as StepProgressListener.Step.ImportingKey).total) + } + + assertEquals(0, (steps[102] as StepProgressListener.Step.ImportingKey).progress) + assertEquals(50, (steps[103] as StepProgressListener.Step.ImportingKey).progress) + assertEquals(100, (steps[104] as StepProgressListener.Step.ImportingKey).progress) + + mKeysBackupTestHelper.checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys) + + testData.cleanUp(mTestHelper) + } + + /** + * - Do an e2e backup to the homeserver with a password + * - Log Alice on a new device + * - Try to restore the e2e backup with a wrong password + * - It must fail + */ + @Test + fun restoreKeysBackupWithAWrongPasswordTest() { + val password = "password" + val wrongPassword = "passw0rd" + + val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(password) + + // - Try to restore the e2e backup with a wrong password + val latch2 = CountDownLatch(1) + var importRoomKeysResult: ImportRoomKeysResult? = null + testData.aliceSession2.cryptoService().keysBackupService().restoreKeyBackupWithPassword(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + wrongPassword, + null, + null, + null, + object : TestMatrixCallback(latch2, false) { + override fun onSuccess(data: ImportRoomKeysResult) { + importRoomKeysResult = data + super.onSuccess(data) + } + } + ) + mTestHelper.await(latch2) + + // onSuccess may not have been called + assertNull(importRoomKeysResult) + + testData.cleanUp(mTestHelper) + } + + /** + * - Do an e2e backup to the homeserver with a password + * - Log Alice on a new device + * - Restore the e2e backup with the recovery key. + * - Restore must be successful + */ + @Test + fun testUseRecoveryKeyToRestoreAPasswordBasedKeysBackup() { + val password = "password" + + val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(password) + + // - Restore the e2e backup with the recovery key. + val importRoomKeysResult = mTestHelper.doSync { + testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, + null, + null, + null, + it + ) + } + + mKeysBackupTestHelper.checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys) + + testData.cleanUp(mTestHelper) + } + + /** + * - Do an e2e backup to the homeserver with a recovery key + * - And log Alice on a new device + * - Try to restore the e2e backup with a password + * - It must fail + */ + @Test + fun testUsePasswordToRestoreARecoveryKeyBasedKeysBackup() { + val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(null) + + // - Try to restore the e2e backup with a password + val latch2 = CountDownLatch(1) + var importRoomKeysResult: ImportRoomKeysResult? = null + testData.aliceSession2.cryptoService().keysBackupService().restoreKeyBackupWithPassword(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + "password", + null, + null, + null, + object : TestMatrixCallback(latch2, false) { + override fun onSuccess(data: ImportRoomKeysResult) { + importRoomKeysResult = data + super.onSuccess(data) + } + } + ) + mTestHelper.await(latch2) + + // onSuccess may not have been called + assertNull(importRoomKeysResult) + + testData.cleanUp(mTestHelper) + } + + /** + * - Create a backup version + * - Check the returned KeysVersionResult is trusted + */ + @Test + fun testIsKeysBackupTrusted() { + // - Create a backup version + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() + + val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService() + + val stateObserver = StateObserver(keysBackup) + + // - Do an e2e backup to the homeserver + mKeysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup) + + // Get key backup version from the home server + val keysVersionResult = mTestHelper.doSync { + keysBackup.getCurrentVersion(it) + } + + // - Check the returned KeyBackupVersion is trusted + val keysBackupVersionTrust = mTestHelper.doSync { + keysBackup.getKeysBackupTrust(keysVersionResult!!, it) + } + + assertNotNull(keysBackupVersionTrust) + assertTrue(keysBackupVersionTrust.usable) + assertEquals(1, keysBackupVersionTrust.signatures.size) + + val signature = keysBackupVersionTrust.signatures[0] + assertTrue(signature.valid) + assertNotNull(signature.device) + assertEquals(cryptoTestData.firstSession.cryptoService().getMyDevice().deviceId, signature.deviceId) + assertEquals(signature.device!!.deviceId, cryptoTestData.firstSession.sessionParams.deviceId) + + stateObserver.stopAndCheckStates(null) + cryptoTestData.cleanUp(mTestHelper) + } + + /** + * Check backup starts automatically if there is an existing and compatible backup + * version on the homeserver. + * - Create a backup version + * - Restart alice session + * -> The new alice session must back up to the same version + */ + @Test + fun testCheckAndStartKeysBackupWhenRestartingAMatrixSession() { + fail("This test still fail. To investigate") + // - Create a backup version + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() + + val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService() + + val stateObserver = StateObserver(keysBackup) + + assertFalse(keysBackup.isEnabled) + + val keyBackupCreationInfo = mKeysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup) + + assertTrue(keysBackup.isEnabled) + + // - Restart alice session + // - Log Alice on a new device + val aliceSession2 = mTestHelper.logIntoAccount(cryptoTestData.firstSession.myUserId, KeysBackupTestConstants.defaultSessionParamsWithInitialSync) + + cryptoTestData.cleanUp(mTestHelper) + + val keysBackup2 = aliceSession2.cryptoService().keysBackupService() + + val stateObserver2 = StateObserver(keysBackup2) + + // -> The new alice session must back up to the same version + val latch = CountDownLatch(1) + var count = 0 + keysBackup2.addListener(object : KeysBackupStateListener { + override fun onStateChange(newState: KeysBackupState) { + // Check the backup completes + if (newState == KeysBackupState.ReadyToBackUp) { + count++ + + if (count == 2) { + // Remove itself from the list of listeners + keysBackup2.removeListener(this) + + latch.countDown() + } + } + } + }) + mTestHelper.await(latch) + + assertEquals(keyBackupCreationInfo.version, keysBackup2.currentBackupVersion) + + stateObserver.stopAndCheckStates(null) + stateObserver2.stopAndCheckStates(null) + mTestHelper.signOutAndClose(aliceSession2) + } + + /** + * Check WrongBackUpVersion state + * + * - Make alice back up her keys to her homeserver + * - Create a new backup with fake data on the homeserver + * - Make alice back up all her keys again + * -> That must fail and her backup state must be WrongBackUpVersion + */ + @Test + fun testBackupWhenAnotherBackupWasCreated() { + // - Create a backup version + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() + + val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService() + + val stateObserver = StateObserver(keysBackup) + + assertFalse(keysBackup.isEnabled) + + // Wait for keys backup to be finished + val latch0 = CountDownLatch(1) + var count = 0 + keysBackup.addListener(object : KeysBackupStateListener { + override fun onStateChange(newState: KeysBackupState) { + // Check the backup completes + if (newState == KeysBackupState.ReadyToBackUp) { + count++ + + if (count == 2) { + // Remove itself from the list of listeners + keysBackup.removeListener(this) + + latch0.countDown() + } + } + } + }) + + // - Make alice back up her keys to her homeserver + mKeysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup) + + assertTrue(keysBackup.isEnabled) + + mTestHelper.await(latch0) + + // - Create a new backup with fake data on the homeserver, directly using the rest client + val megolmBackupCreationInfo = mCryptoTestHelper.createFakeMegolmBackupCreationInfo() + mTestHelper.doSync { + (keysBackup as DefaultKeysBackupService).createFakeKeysBackupVersion(megolmBackupCreationInfo, it) + } + + // Reset the store backup status for keys + (cryptoTestData.firstSession.cryptoService().keysBackupService() as DefaultKeysBackupService).store.resetBackupMarkers() + + // - Make alice back up all her keys again + val latch2 = CountDownLatch(1) + keysBackup.backupAllGroupSessions(null, TestMatrixCallback(latch2, false)) + mTestHelper.await(latch2) + + // -> That must fail and her backup state must be WrongBackUpVersion + assertEquals(KeysBackupState.WrongBackUpVersion, keysBackup.state) + assertFalse(keysBackup.isEnabled) + + stateObserver.stopAndCheckStates(null) + cryptoTestData.cleanUp(mTestHelper) + } + + /** + * - Do an e2e backup to the homeserver + * - Log Alice on a new device + * - Post a message to have a new megolm session + * - Try to backup all + * -> It must fail. Backup state must be NotTrusted + * - Validate the old device from the new one + * -> Backup should automatically enable on the new device + * -> It must use the same backup version + * - Try to backup all again + * -> It must success + */ + @Test + fun testBackupAfterVerifyingADevice() { + // - Create a backup version + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() + + val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService() + + val stateObserver = StateObserver(keysBackup) + + // - Make alice back up her keys to her homeserver + mKeysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup) + + // Wait for keys backup to finish by asking again to backup keys. + mTestHelper.doSync { + keysBackup.backupAllGroupSessions(null, it) + } + + val oldDeviceId = cryptoTestData.firstSession.sessionParams.deviceId!! + val oldKeyBackupVersion = keysBackup.currentBackupVersion + val aliceUserId = cryptoTestData.firstSession.myUserId + + // - Log Alice on a new device + val aliceSession2 = mTestHelper.logIntoAccount(aliceUserId, KeysBackupTestConstants.defaultSessionParamsWithInitialSync) + + // - Post a message to have a new megolm session + aliceSession2.cryptoService().setWarnOnUnknownDevices(false) + + val room2 = aliceSession2.getRoom(cryptoTestData.roomId)!! + + mTestHelper.sendTextMessage(room2, "New key", 1) + + // - Try to backup all in aliceSession2, it must fail + val keysBackup2 = aliceSession2.cryptoService().keysBackupService() + + val stateObserver2 = StateObserver(keysBackup2) + + var isSuccessful = false + val latch2 = CountDownLatch(1) + keysBackup2.backupAllGroupSessions( + null, + object : TestMatrixCallback(latch2, false) { + override fun onSuccess(data: Unit) { + isSuccessful = true + super.onSuccess(data) + } + }) + mTestHelper.await(latch2) + + assertFalse(isSuccessful) + + // Backup state must be NotTrusted + assertEquals(KeysBackupState.NotTrusted, keysBackup2.state) + assertFalse(keysBackup2.isEnabled) + + // - Validate the old device from the new one + aliceSession2.cryptoService().setDeviceVerification(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), aliceSession2.myUserId, oldDeviceId) + + // -> Backup should automatically enable on the new device + val latch4 = CountDownLatch(1) + keysBackup2.addListener(object : KeysBackupStateListener { + override fun onStateChange(newState: KeysBackupState) { + // Check the backup completes + if (keysBackup2.state == KeysBackupState.ReadyToBackUp) { + // Remove itself from the list of listeners + keysBackup2.removeListener(this) + + latch4.countDown() + } + } + }) + mTestHelper.await(latch4) + + // -> It must use the same backup version + assertEquals(oldKeyBackupVersion, aliceSession2.cryptoService().keysBackupService().currentBackupVersion) + + mTestHelper.doSync { + aliceSession2.cryptoService().keysBackupService().backupAllGroupSessions(null, it) + } + + // -> It must success + assertTrue(aliceSession2.cryptoService().keysBackupService().isEnabled) + + stateObserver.stopAndCheckStates(null) + stateObserver2.stopAndCheckStates(null) + mTestHelper.signOutAndClose(aliceSession2) + cryptoTestData.cleanUp(mTestHelper) + } + + /** + * - Do an e2e backup to the homeserver with a recovery key + * - Delete the backup + */ + @Test + fun deleteKeysBackupTest() { + // - Create a backup version + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() + + val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService() + + val stateObserver = StateObserver(keysBackup) + + assertFalse(keysBackup.isEnabled) + + val keyBackupCreationInfo = mKeysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup) + + assertTrue(keysBackup.isEnabled) + + // Delete the backup + mTestHelper.doSync { keysBackup.deleteBackup(keyBackupCreationInfo.version, it) } + + // Backup is now disabled + assertFalse(keysBackup.isEnabled) + + stateObserver.stopAndCheckStates(null) + cryptoTestData.cleanUp(mTestHelper) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestConstants.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestConstants.kt new file mode 100644 index 0000000000..f31e67b0e8 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestConstants.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup + +import org.matrix.android.sdk.common.SessionTestParams + +object KeysBackupTestConstants { + val defaultSessionParams = SessionTestParams(withInitialSync = false) + val defaultSessionParamsWithInitialSync = SessionTestParams(withInitialSync = true) +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt new file mode 100644 index 0000000000..f84a90708c --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup + +import org.matrix.android.sdk.api.listeners.ProgressListener +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.assertDictEquals +import org.matrix.android.sdk.common.assertListEquals +import org.matrix.android.sdk.internal.crypto.MegolmSessionData +import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion +import org.junit.Assert +import java.util.concurrent.CountDownLatch + +class KeysBackupTestHelper( + private val mTestHelper: CommonTestHelper, + private val mCryptoTestHelper: CryptoTestHelper) { + + /** + * Common initial condition + * - Do an e2e backup to the homeserver + * - Log Alice on a new device, and wait for its keysBackup object to be ready (in state NotTrusted) + * + * @param password optional password + */ + fun createKeysBackupScenarioWithPassword(password: String?): KeysBackupScenarioData { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() + + val cryptoStore = (cryptoTestData.firstSession.cryptoService().keysBackupService() as DefaultKeysBackupService).store + val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService() + + val stateObserver = StateObserver(keysBackup) + + val aliceKeys = cryptoStore.inboundGroupSessionsToBackup(100) + + // - Do an e2e backup to the homeserver + val prepareKeysBackupDataResult = prepareAndCreateKeysBackupData(keysBackup, password) + + var lastProgress = 0 + var lastTotal = 0 + mTestHelper.doSync { + keysBackup.backupAllGroupSessions(object : ProgressListener { + override fun onProgress(progress: Int, total: Int) { + lastProgress = progress + lastTotal = total + } + }, it) + } + + Assert.assertEquals(2, lastProgress) + Assert.assertEquals(2, lastTotal) + + val aliceUserId = cryptoTestData.firstSession.myUserId + + // - Log Alice on a new device + val aliceSession2 = mTestHelper.logIntoAccount(aliceUserId, KeysBackupTestConstants.defaultSessionParamsWithInitialSync) + + // Test check: aliceSession2 has no keys at login + Assert.assertEquals(0, aliceSession2.cryptoService().inboundGroupSessionsCount(false)) + + // Wait for backup state to be NotTrusted + waitForKeysBackupToBeInState(aliceSession2, KeysBackupState.NotTrusted) + + stateObserver.stopAndCheckStates(null) + + return KeysBackupScenarioData(cryptoTestData, + aliceKeys, + prepareKeysBackupDataResult, + aliceSession2) + } + + fun prepareAndCreateKeysBackupData(keysBackup: KeysBackupService, + password: String? = null): PrepareKeysBackupDataResult { + val stateObserver = StateObserver(keysBackup) + + val megolmBackupCreationInfo = mTestHelper.doSync { + keysBackup.prepareKeysBackupVersion(password, null, it) + } + + Assert.assertNotNull(megolmBackupCreationInfo) + + Assert.assertFalse(keysBackup.isEnabled) + + // Create the version + val keysVersion = mTestHelper.doSync { + keysBackup.createKeysBackupVersion(megolmBackupCreationInfo, it) + } + + Assert.assertNotNull(keysVersion.version) + + // Backup must be enable now + Assert.assertTrue(keysBackup.isEnabled) + + stateObserver.stopAndCheckStates(null) + return PrepareKeysBackupDataResult(megolmBackupCreationInfo, keysVersion.version!!) + } + + /** + * As KeysBackup is doing asynchronous call to update its internal state, this method help to wait for the + * KeysBackup object to be in the specified state + */ + fun waitForKeysBackupToBeInState(session: Session, state: KeysBackupState) { + // If already in the wanted state, return + if (session.cryptoService().keysBackupService().state == state) { + return + } + + // Else observe state changes + val latch = CountDownLatch(1) + + session.cryptoService().keysBackupService().addListener(object : KeysBackupStateListener { + override fun onStateChange(newState: KeysBackupState) { + if (newState == state) { + session.cryptoService().keysBackupService().removeListener(this) + latch.countDown() + } + } + }) + + mTestHelper.await(latch) + } + + fun assertKeysEquals(keys1: MegolmSessionData?, keys2: MegolmSessionData?) { + Assert.assertNotNull(keys1) + Assert.assertNotNull(keys2) + + Assert.assertEquals(keys1?.algorithm, keys2?.algorithm) + Assert.assertEquals(keys1?.roomId, keys2?.roomId) + // No need to compare the shortcut + // assertEquals(keys1?.sender_claimed_ed25519_key, keys2?.sender_claimed_ed25519_key) + Assert.assertEquals(keys1?.senderKey, keys2?.senderKey) + Assert.assertEquals(keys1?.sessionId, keys2?.sessionId) + Assert.assertEquals(keys1?.sessionKey, keys2?.sessionKey) + + assertListEquals(keys1?.forwardingCurve25519KeyChain, keys2?.forwardingCurve25519KeyChain) + assertDictEquals(keys1?.senderClaimedKeys, keys2?.senderClaimedKeys) + } + + /** + * Common restore success check after [KeysBackupTestHelper.createKeysBackupScenarioWithPassword]: + * - Imported keys number must be correct + * - The new device must have the same count of megolm keys + * - Alice must have the same keys on both devices + */ + fun checkRestoreSuccess(testData: KeysBackupScenarioData, + total: Int, + imported: Int) { + // - Imported keys number must be correct + Assert.assertEquals(testData.aliceKeys.size, total) + Assert.assertEquals(total, imported) + + // - The new device must have the same count of megolm keys + Assert.assertEquals(testData.aliceKeys.size, testData.aliceSession2.cryptoService().inboundGroupSessionsCount(false)) + + // - Alice must have the same keys on both devices + for (aliceKey1 in testData.aliceKeys) { + val aliceKey2 = (testData.aliceSession2.cryptoService().keysBackupService() as DefaultKeysBackupService).store + .getInboundGroupSession(aliceKey1.olmInboundGroupSession!!.sessionIdentifier(), aliceKey1.senderKey!!) + Assert.assertNotNull(aliceKey2) + assertKeysEquals(aliceKey1.exportKeys(), aliceKey2!!.exportKeys()) + } + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/PrepareKeysBackupDataResult.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/PrepareKeysBackupDataResult.kt new file mode 100644 index 0000000000..c28b7990e0 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/PrepareKeysBackupDataResult.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup + +import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo + +data class PrepareKeysBackupDataResult(val megolmBackupCreationInfo: MegolmBackupCreationInfo, + val version: String) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/StateObserver.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/StateObserver.kt new file mode 100644 index 0000000000..90d2fd7812 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/StateObserver.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2019 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup + +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import java.util.concurrent.CountDownLatch + +/** + * This class observe the state change of a KeysBackup object and provide a method to check the several state change + * It checks all state transitions and detected forbidden transition + */ +internal class StateObserver(private val keysBackup: KeysBackupService, + private val latch: CountDownLatch? = null, + private val expectedStateChange: Int = -1) : KeysBackupStateListener { + + private val allowedStateTransitions = listOf( + KeysBackupState.BackingUp to KeysBackupState.ReadyToBackUp, + KeysBackupState.BackingUp to KeysBackupState.WrongBackUpVersion, + + KeysBackupState.CheckingBackUpOnHomeserver to KeysBackupState.Disabled, + KeysBackupState.CheckingBackUpOnHomeserver to KeysBackupState.NotTrusted, + KeysBackupState.CheckingBackUpOnHomeserver to KeysBackupState.ReadyToBackUp, + KeysBackupState.CheckingBackUpOnHomeserver to KeysBackupState.Unknown, + KeysBackupState.CheckingBackUpOnHomeserver to KeysBackupState.WrongBackUpVersion, + + KeysBackupState.Disabled to KeysBackupState.Enabling, + + KeysBackupState.Enabling to KeysBackupState.Disabled, + KeysBackupState.Enabling to KeysBackupState.ReadyToBackUp, + + KeysBackupState.NotTrusted to KeysBackupState.CheckingBackUpOnHomeserver, + // This transition happens when we trust the device + KeysBackupState.NotTrusted to KeysBackupState.ReadyToBackUp, + + KeysBackupState.ReadyToBackUp to KeysBackupState.WillBackUp, + + KeysBackupState.Unknown to KeysBackupState.CheckingBackUpOnHomeserver, + + KeysBackupState.WillBackUp to KeysBackupState.BackingUp, + + KeysBackupState.WrongBackUpVersion to KeysBackupState.CheckingBackUpOnHomeserver, + + // FIXME These transitions are observed during test, and I'm not sure they should occur. Don't have time to investigate now + KeysBackupState.ReadyToBackUp to KeysBackupState.BackingUp, + KeysBackupState.ReadyToBackUp to KeysBackupState.ReadyToBackUp, + KeysBackupState.WillBackUp to KeysBackupState.ReadyToBackUp, + KeysBackupState.WillBackUp to KeysBackupState.Unknown + ) + + private val stateList = ArrayList() + private var lastTransitionError: String? = null + + init { + keysBackup.addListener(this) + } + + // TODO Make expectedStates mandatory to enforce test + fun stopAndCheckStates(expectedStates: List?) { + keysBackup.removeListener(this) + + expectedStates?.let { + assertEquals(it.size, stateList.size) + + for (i in it.indices) { + assertEquals("The state $i is not correct. states: " + stateList.joinToString(separator = " "), it[i], stateList[i]) + } + } + + assertNull("states: " + stateList.joinToString(separator = " "), lastTransitionError) + } + + override fun onStateChange(newState: KeysBackupState) { + stateList.add(newState) + + // Check that state transition is valid + if (stateList.size >= 2 + && !allowedStateTransitions.contains(stateList[stateList.size - 2] to newState)) { + // Forbidden transition detected + lastTransitionError = "Forbidden transition detected from " + stateList[stateList.size - 2] + " to " + newState + } + + if (expectedStateChange == stateList.size) { + latch?.countDown() + } + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt new file mode 100644 index 0000000000..42cee74334 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt @@ -0,0 +1,363 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.ssss + +import androidx.lifecycle.Observer +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.securestorage.EncryptedSecretContent +import org.matrix.android.sdk.api.session.securestorage.KeySigner +import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec +import org.matrix.android.sdk.api.session.securestorage.SecretStorageKeyContent +import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService +import org.matrix.android.sdk.api.session.securestorage.SsssKeyCreationInfo +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.SessionTestParams +import org.matrix.android.sdk.common.TestConstants +import org.matrix.android.sdk.common.TestMatrixCallback +import org.matrix.android.sdk.internal.crypto.SSSS_ALGORITHM_AES_HMAC_SHA2 +import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding +import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorageService +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.amshove.kluent.shouldBe +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import java.util.concurrent.CountDownLatch + +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class QuadSTests : InstrumentedTest { + + private val mTestHelper = CommonTestHelper(context()) + + private val emptyKeySigner = object : KeySigner { + override fun sign(canonicalJson: String): Map>? { + return null + } + } + + @Test + fun test_Generate4SKey() { + val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) + + val quadS = aliceSession.sharedSecretStorageService + + val TEST_KEY_ID = "my.test.Key" + + mTestHelper.doSync { + quadS.generateKey(TEST_KEY_ID, null, "Test Key", emptyKeySigner, it) + } + + // Assert Account data is updated + val accountDataLock = CountDownLatch(1) + var accountData: UserAccountDataEvent? = null + + val liveAccountData = runBlocking(Dispatchers.Main) { + aliceSession.getLiveAccountDataEvent("${DefaultSharedSecretStorageService.KEY_ID_BASE}.$TEST_KEY_ID") + } + val accountDataObserver = Observer?> { t -> + if (t?.getOrNull()?.type == "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$TEST_KEY_ID") { + accountData = t.getOrNull() + accountDataLock.countDown() + } + } + GlobalScope.launch(Dispatchers.Main) { liveAccountData.observeForever(accountDataObserver) } + + mTestHelper.await(accountDataLock) + + assertNotNull("Key should be stored in account data", accountData) + val parsed = SecretStorageKeyContent.fromJson(accountData!!.content) + assertNotNull("Key Content cannot be parsed", parsed) + assertEquals("Unexpected Algorithm", SSSS_ALGORITHM_AES_HMAC_SHA2, parsed!!.algorithm) + assertEquals("Unexpected key name", "Test Key", parsed.name) + assertNull("Key was not generated from passphrase", parsed.passphrase) + + // Set as default key + quadS.setDefaultKey(TEST_KEY_ID, object : MatrixCallback {}) + + var defaultKeyAccountData: UserAccountDataEvent? = null + val defaultDataLock = CountDownLatch(1) + + val liveDefAccountData = runBlocking(Dispatchers.Main) { + aliceSession.getLiveAccountDataEvent(DefaultSharedSecretStorageService.DEFAULT_KEY_ID) + } + val accountDefDataObserver = Observer?> { t -> + if (t?.getOrNull()?.type == DefaultSharedSecretStorageService.DEFAULT_KEY_ID) { + defaultKeyAccountData = t.getOrNull()!! + defaultDataLock.countDown() + } + } + GlobalScope.launch(Dispatchers.Main) { liveDefAccountData.observeForever(accountDefDataObserver) } + + mTestHelper.await(defaultDataLock) + + assertNotNull(defaultKeyAccountData?.content) + assertEquals("Unexpected default key ${defaultKeyAccountData?.content}", TEST_KEY_ID, defaultKeyAccountData?.content?.get("key")) + + mTestHelper.signOutAndClose(aliceSession) + } + + @Test + fun test_StoreSecret() { + val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) + val keyId = "My.Key" + val info = generatedSecret(aliceSession, keyId, true) + + val keySpec = RawBytesKeySpec.fromRecoveryKey(info.recoveryKey) + + // Store a secret + val clearSecret = "42".toByteArray().toBase64NoPadding() + mTestHelper.doSync { + aliceSession.sharedSecretStorageService.storeSecret( + "secret.of.life", + clearSecret, + listOf(SharedSecretStorageService.KeyRef(null, keySpec)), // default key + it + ) + } + + val secretAccountData = assertAccountData(aliceSession, "secret.of.life") + + val encryptedContent = secretAccountData.content["encrypted"] as? Map<*, *> + assertNotNull("Element should be encrypted", encryptedContent) + assertNotNull("Secret should be encrypted with default key", encryptedContent?.get(keyId)) + + val secret = EncryptedSecretContent.fromJson(encryptedContent?.get(keyId)) + assertNotNull(secret?.ciphertext) + assertNotNull(secret?.mac) + assertNotNull(secret?.initializationVector) + + // Try to decrypt?? + + val decryptedSecret = mTestHelper.doSync { + aliceSession.sharedSecretStorageService.getSecret( + "secret.of.life", + null, // default key + keySpec!!, + it + ) + } + + assertEquals("Secret mismatch", clearSecret, decryptedSecret) + mTestHelper.signOutAndClose(aliceSession) + } + + @Test + fun test_SetDefaultLocalEcho() { + val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) + + val quadS = aliceSession.sharedSecretStorageService + + val TEST_KEY_ID = "my.test.Key" + + mTestHelper.doSync { + quadS.generateKey(TEST_KEY_ID, null, "Test Key", emptyKeySigner, it) + } + + // Test that we don't need to wait for an account data sync to access directly the keyid from DB + mTestHelper.doSync { + quadS.setDefaultKey(TEST_KEY_ID, it) + } + + mTestHelper.signOutAndClose(aliceSession) + } + + @Test + fun test_StoreSecretWithMultipleKey() { + val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) + val keyId1 = "Key.1" + val key1Info = generatedSecret(aliceSession, keyId1, true) + val keyId2 = "Key2" + val key2Info = generatedSecret(aliceSession, keyId2, true) + + val mySecretText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit" + + mTestHelper.doSync { + aliceSession.sharedSecretStorageService.storeSecret( + "my.secret", + mySecretText.toByteArray().toBase64NoPadding(), + listOf( + SharedSecretStorageService.KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)), + SharedSecretStorageService.KeyRef(keyId2, RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey)) + ), + it + ) + } + + val accountDataEvent = aliceSession.getAccountDataEvent("my.secret") + val encryptedContent = accountDataEvent?.content?.get("encrypted") as? Map<*, *> + + assertEquals("Content should contains two encryptions", 2, encryptedContent?.keys?.size ?: 0) + + assertNotNull(encryptedContent?.get(keyId1)) + assertNotNull(encryptedContent?.get(keyId2)) + + // Assert that can decrypt with both keys + mTestHelper.doSync { + aliceSession.sharedSecretStorageService.getSecret("my.secret", + keyId1, + RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)!!, + it + ) + } + + mTestHelper.doSync { + aliceSession.sharedSecretStorageService.getSecret("my.secret", + keyId2, + RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey)!!, + it + ) + } + + mTestHelper.signOutAndClose(aliceSession) + } + + @Test + fun test_GetSecretWithBadPassphrase() { + val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) + val keyId1 = "Key.1" + val passphrase = "The good pass phrase" + val key1Info = generatedSecretFromPassphrase(aliceSession, passphrase, keyId1, true) + + val mySecretText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit" + + mTestHelper.doSync { + aliceSession.sharedSecretStorageService.storeSecret( + "my.secret", + mySecretText.toByteArray().toBase64NoPadding(), + listOf(SharedSecretStorageService.KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey))), + it + ) + } + + val decryptCountDownLatch = CountDownLatch(1) + var error = false + aliceSession.sharedSecretStorageService.getSecret("my.secret", + keyId1, + RawBytesKeySpec.fromPassphrase( + "A bad passphrase", + key1Info.content?.passphrase?.salt ?: "", + key1Info.content?.passphrase?.iterations ?: 0, + null), + object : MatrixCallback { + override fun onSuccess(data: String) { + decryptCountDownLatch.countDown() + } + + override fun onFailure(failure: Throwable) { + error = true + decryptCountDownLatch.countDown() + } + } + ) + + mTestHelper.await(decryptCountDownLatch) + + error shouldBe true + + // Now try with correct key + mTestHelper.doSync { + aliceSession.sharedSecretStorageService.getSecret("my.secret", + keyId1, + RawBytesKeySpec.fromPassphrase( + passphrase, + key1Info.content?.passphrase?.salt ?: "", + key1Info.content?.passphrase?.iterations ?: 0, + null), + it + ) + } + + mTestHelper.signOutAndClose(aliceSession) + } + + private fun assertAccountData(session: Session, type: String): UserAccountDataEvent { + val accountDataLock = CountDownLatch(1) + var accountData: UserAccountDataEvent? = null + + val liveAccountData = runBlocking(Dispatchers.Main) { + session.getLiveAccountDataEvent(type) + } + val accountDataObserver = Observer?> { t -> + if (t?.getOrNull()?.type == type) { + accountData = t.getOrNull() + accountDataLock.countDown() + } + } + GlobalScope.launch(Dispatchers.Main) { liveAccountData.observeForever(accountDataObserver) } + mTestHelper.await(accountDataLock) + + assertNotNull("Account Data type:$type should be found", accountData) + + return accountData!! + } + + private fun generatedSecret(session: Session, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo { + val quadS = session.sharedSecretStorageService + + val creationInfo = mTestHelper.doSync { + quadS.generateKey(keyId, null, keyId, emptyKeySigner, it) + } + + assertAccountData(session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId") + + if (asDefault) { + mTestHelper.doSync { + quadS.setDefaultKey(keyId, it) + } + assertAccountData(session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID) + } + + return creationInfo + } + + private fun generatedSecretFromPassphrase(session: Session, passphrase: String, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo { + val quadS = session.sharedSecretStorageService + + val creationInfo = mTestHelper.doSync { + quadS.generateKeyWithPassphrase( + keyId, + keyId, + passphrase, + emptyKeySigner, + null, + it) + } + + assertAccountData(session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId") + if (asDefault) { + val setDefaultLatch = CountDownLatch(1) + quadS.setDefaultKey(keyId, TestMatrixCallback(setDefaultLatch)) + mTestHelper.await(setDefaultLatch) + assertAccountData(session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID) + } + + return creationInfo + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt new file mode 100644 index 0000000000..a6beeb123c --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt @@ -0,0 +1,629 @@ +/* + * Copyright 2019 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.verification + +import android.util.Log +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.crypto.verification.CancelCode +import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction +import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction +import org.matrix.android.sdk.api.session.crypto.verification.SasMode +import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction +import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod +import org.matrix.android.sdk.api.session.crypto.verification.VerificationService +import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction +import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationCancel +import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationStart +import org.matrix.android.sdk.internal.crypto.model.rest.toValue +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import java.util.concurrent.CountDownLatch + +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class SASTest : InstrumentedTest { + private val mTestHelper = CommonTestHelper(context()) + private val mCryptoTestHelper = CryptoTestHelper(mTestHelper) + + @Test + fun test_aliceStartThenAliceCancel() { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession + + val aliceVerificationService = aliceSession.cryptoService().verificationService() + val bobVerificationService = bobSession!!.cryptoService().verificationService() + + val bobTxCreatedLatch = CountDownLatch(1) + val bobListener = object : VerificationService.Listener { + override fun transactionUpdated(tx: VerificationTransaction) { + bobTxCreatedLatch.countDown() + } + } + bobVerificationService.addListener(bobListener) + + val txID = aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, + bobSession.myUserId, + bobSession.cryptoService().getMyDevice().deviceId, + null) + assertNotNull("Alice should have a started transaction", txID) + + val aliceKeyTx = aliceVerificationService.getExistingTransaction(bobSession.myUserId, txID!!) + assertNotNull("Alice should have a started transaction", aliceKeyTx) + + mTestHelper.await(bobTxCreatedLatch) + bobVerificationService.removeListener(bobListener) + + val bobKeyTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, txID) + + assertNotNull("Bob should have started verif transaction", bobKeyTx) + assertTrue(bobKeyTx is SASDefaultVerificationTransaction) + assertNotNull("Bob should have starting a SAS transaction", bobKeyTx) + assertTrue(aliceKeyTx is SASDefaultVerificationTransaction) + assertEquals("Alice and Bob have same transaction id", aliceKeyTx!!.transactionId, bobKeyTx!!.transactionId) + + val aliceSasTx = aliceKeyTx as SASDefaultVerificationTransaction? + val bobSasTx = bobKeyTx as SASDefaultVerificationTransaction? + + assertEquals("Alice state should be started", VerificationTxState.Started, aliceSasTx!!.state) + assertEquals("Bob state should be started by alice", VerificationTxState.OnStarted, bobSasTx!!.state) + + // Let's cancel from alice side + val cancelLatch = CountDownLatch(1) + + val bobListener2 = object : VerificationService.Listener { + override fun transactionUpdated(tx: VerificationTransaction) { + if (tx.transactionId == txID) { + val immutableState = (tx as SASDefaultVerificationTransaction).state + if (immutableState is VerificationTxState.Cancelled && !immutableState.byMe) { + cancelLatch.countDown() + } + } + } + } + bobVerificationService.addListener(bobListener2) + + aliceSasTx.cancel(CancelCode.User) + mTestHelper.await(cancelLatch) + + assertTrue("Should be cancelled on alice side", aliceSasTx.state is VerificationTxState.Cancelled) + assertTrue("Should be cancelled on bob side", bobSasTx.state is VerificationTxState.Cancelled) + + val aliceCancelState = aliceSasTx.state as VerificationTxState.Cancelled + val bobCancelState = bobSasTx.state as VerificationTxState.Cancelled + + assertTrue("Should be cancelled by me on alice side", aliceCancelState.byMe) + assertFalse("Should be cancelled by other on bob side", bobCancelState.byMe) + + assertEquals("Should be User cancelled on alice side", CancelCode.User, aliceCancelState.cancelCode) + assertEquals("Should be User cancelled on bob side", CancelCode.User, bobCancelState.cancelCode) + + assertNull(bobVerificationService.getExistingTransaction(aliceSession.myUserId, txID)) + assertNull(aliceVerificationService.getExistingTransaction(bobSession.myUserId, txID)) + + cryptoTestData.cleanUp(mTestHelper) + } + + @Test + fun test_key_agreement_protocols_must_include_curve25519() { + fail("Not passing for the moment") + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val bobSession = cryptoTestData.secondSession!! + + val protocols = listOf("meh_dont_know") + val tid = "00000000" + + // Bob should receive a cancel + var cancelReason: CancelCode? = null + val cancelLatch = CountDownLatch(1) + + val bobListener = object : VerificationService.Listener { + override fun transactionUpdated(tx: VerificationTransaction) { + if (tx.transactionId == tid && tx.state is VerificationTxState.Cancelled) { + cancelReason = (tx.state as VerificationTxState.Cancelled).cancelCode + cancelLatch.countDown() + } + } + } + bobSession.cryptoService().verificationService().addListener(bobListener) + + // TODO bobSession!!.dataHandler.addListener(object : MXEventListener() { + // TODO override fun onToDeviceEvent(event: Event?) { + // TODO if (event!!.getType() == CryptoEvent.EVENT_TYPE_KEY_VERIFICATION_CANCEL) { + // TODO if (event.contentAsJsonObject?.get("transaction_id")?.asString == tid) { + // TODO canceledToDeviceEvent = event + // TODO cancelLatch.countDown() + // TODO } + // TODO } + // TODO } + // TODO }) + + val aliceSession = cryptoTestData.firstSession + val aliceUserID = aliceSession.myUserId + val aliceDevice = aliceSession.cryptoService().getMyDevice().deviceId + + val aliceListener = object : VerificationService.Listener { + override fun transactionUpdated(tx: VerificationTransaction) { + if ((tx as IncomingSasVerificationTransaction).uxState === IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) { + (tx as IncomingSasVerificationTransaction).performAccept() + } + } + } + aliceSession.cryptoService().verificationService().addListener(aliceListener) + + fakeBobStart(bobSession, aliceUserID, aliceDevice, tid, protocols = protocols) + + mTestHelper.await(cancelLatch) + + assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod, cancelReason) + + cryptoTestData.cleanUp(mTestHelper) + } + + @Test + fun test_key_agreement_macs_Must_include_hmac_sha256() { + fail("Not passing for the moment") + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val bobSession = cryptoTestData.secondSession!! + + val mac = listOf("shaBit") + val tid = "00000000" + + // Bob should receive a cancel + var canceledToDeviceEvent: Event? = null + val cancelLatch = CountDownLatch(1) + // TODO bobSession!!.dataHandler.addListener(object : MXEventListener() { + // TODO override fun onToDeviceEvent(event: Event?) { + // TODO if (event!!.getType() == CryptoEvent.EVENT_TYPE_KEY_VERIFICATION_CANCEL) { + // TODO if (event.contentAsJsonObject?.get("transaction_id")?.asString == tid) { + // TODO canceledToDeviceEvent = event + // TODO cancelLatch.countDown() + // TODO } + // TODO } + // TODO } + // TODO }) + + val aliceSession = cryptoTestData.firstSession + val aliceUserID = aliceSession.myUserId + val aliceDevice = aliceSession.cryptoService().getMyDevice().deviceId + + fakeBobStart(bobSession, aliceUserID, aliceDevice, tid, mac = mac) + + mTestHelper.await(cancelLatch) + + val cancelReq = canceledToDeviceEvent!!.content.toModel()!! + assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod.value, cancelReq.code) + + cryptoTestData.cleanUp(mTestHelper) + } + + @Test + fun test_key_agreement_short_code_include_decimal() { + fail("Not passing for the moment") + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val bobSession = cryptoTestData.secondSession!! + + val codes = listOf("bin", "foo", "bar") + val tid = "00000000" + + // Bob should receive a cancel + var canceledToDeviceEvent: Event? = null + val cancelLatch = CountDownLatch(1) + // TODO bobSession!!.dataHandler.addListener(object : MXEventListener() { + // TODO override fun onToDeviceEvent(event: Event?) { + // TODO if (event!!.getType() == CryptoEvent.EVENT_TYPE_KEY_VERIFICATION_CANCEL) { + // TODO if (event.contentAsJsonObject?.get("transaction_id")?.asString == tid) { + // TODO canceledToDeviceEvent = event + // TODO cancelLatch.countDown() + // TODO } + // TODO } + // TODO } + // TODO }) + + val aliceSession = cryptoTestData.firstSession + val aliceUserID = aliceSession.myUserId + val aliceDevice = aliceSession.cryptoService().getMyDevice().deviceId + + fakeBobStart(bobSession, aliceUserID, aliceDevice, tid, codes = codes) + + mTestHelper.await(cancelLatch) + + val cancelReq = canceledToDeviceEvent!!.content.toModel()!! + assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod.value, cancelReq.code) + + cryptoTestData.cleanUp(mTestHelper) + } + + private fun fakeBobStart(bobSession: Session, + aliceUserID: String?, + aliceDevice: String?, + tid: String, + protocols: List = SASDefaultVerificationTransaction.KNOWN_AGREEMENT_PROTOCOLS, + hashes: List = SASDefaultVerificationTransaction.KNOWN_HASHES, + mac: List = SASDefaultVerificationTransaction.KNOWN_MACS, + codes: List = SASDefaultVerificationTransaction.KNOWN_SHORT_CODES) { + val startMessage = KeyVerificationStart( + fromDevice = bobSession.cryptoService().getMyDevice().deviceId, + method = VerificationMethod.SAS.toValue(), + transactionId = tid, + keyAgreementProtocols = protocols, + hashes = hashes, + messageAuthenticationCodes = mac, + shortAuthenticationStrings = codes + ) + + val contentMap = MXUsersDevicesMap() + contentMap.setObject(aliceUserID, aliceDevice, startMessage) + + // TODO val sendLatch = CountDownLatch(1) + // TODO bobSession.cryptoRestClient.sendToDevice( + // TODO EventType.KEY_VERIFICATION_START, + // TODO contentMap, + // TODO tid, + // TODO TestMatrixCallback(sendLatch) + // TODO ) + } + + // any two devices may only have at most one key verification in flight at a time. + // If a device has two verifications in progress with the same device, then it should cancel both verifications. + @Test + fun test_aliceStartTwoRequests() { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession + + val aliceVerificationService = aliceSession.cryptoService().verificationService() + + val aliceCreatedLatch = CountDownLatch(2) + val aliceCancelledLatch = CountDownLatch(2) + val createdTx = mutableListOf() + val aliceListener = object : VerificationService.Listener { + override fun transactionCreated(tx: VerificationTransaction) { + createdTx.add(tx as SASDefaultVerificationTransaction) + aliceCreatedLatch.countDown() + } + + override fun transactionUpdated(tx: VerificationTransaction) { + if ((tx as SASDefaultVerificationTransaction).state is VerificationTxState.Cancelled && !(tx.state as VerificationTxState.Cancelled).byMe) { + aliceCancelledLatch.countDown() + } + } + } + aliceVerificationService.addListener(aliceListener) + + val bobUserId = bobSession!!.myUserId + val bobDeviceId = bobSession.cryptoService().getMyDevice().deviceId + aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId, null) + aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId, null) + + mTestHelper.await(aliceCreatedLatch) + mTestHelper.await(aliceCancelledLatch) + + cryptoTestData.cleanUp(mTestHelper) + } + + /** + * Test that when alice starts a 'correct' request, bob agrees. + */ + @Test + fun test_aliceAndBobAgreement() { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession + + val aliceVerificationService = aliceSession.cryptoService().verificationService() + val bobVerificationService = bobSession!!.cryptoService().verificationService() + + var accepted: ValidVerificationInfoAccept? = null + var startReq: ValidVerificationInfoStart.SasVerificationInfoStart? = null + + val aliceAcceptedLatch = CountDownLatch(1) + val aliceListener = object : VerificationService.Listener { + override fun transactionUpdated(tx: VerificationTransaction) { + Log.v("TEST", "== aliceTx state ${tx.state} => ${(tx as? OutgoingSasVerificationTransaction)?.uxState}") + if ((tx as SASDefaultVerificationTransaction).state === VerificationTxState.OnAccepted) { + val at = tx as SASDefaultVerificationTransaction + accepted = at.accepted + startReq = at.startReq + aliceAcceptedLatch.countDown() + } + } + } + aliceVerificationService.addListener(aliceListener) + + val bobListener = object : VerificationService.Listener { + override fun transactionUpdated(tx: VerificationTransaction) { + Log.v("TEST", "== bobTx state ${tx.state} => ${(tx as? IncomingSasVerificationTransaction)?.uxState}") + if ((tx as IncomingSasVerificationTransaction).uxState === IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) { + bobVerificationService.removeListener(this) + val at = tx as IncomingSasVerificationTransaction + at.performAccept() + } + } + } + bobVerificationService.addListener(bobListener) + + val bobUserId = bobSession.myUserId + val bobDeviceId = bobSession.cryptoService().getMyDevice().deviceId + aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId, null) + mTestHelper.await(aliceAcceptedLatch) + + assertTrue("Should have receive a commitment", accepted!!.commitment?.trim()?.isEmpty() == false) + + // check that agreement is valid + assertTrue("Agreed Protocol should be Valid", accepted != null) + assertTrue("Agreed Protocol should be known by alice", startReq!!.keyAgreementProtocols.contains(accepted!!.keyAgreementProtocol)) + assertTrue("Hash should be known by alice", startReq!!.hashes.contains(accepted!!.hash)) + assertTrue("Hash should be known by alice", startReq!!.messageAuthenticationCodes.contains(accepted!!.messageAuthenticationCode)) + + accepted!!.shortAuthenticationStrings.forEach { + assertTrue("all agreed Short Code should be known by alice", startReq!!.shortAuthenticationStrings.contains(it)) + } + + cryptoTestData.cleanUp(mTestHelper) + } + + @Test + fun test_aliceAndBobSASCode() { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession + + val aliceVerificationService = aliceSession.cryptoService().verificationService() + val bobVerificationService = bobSession!!.cryptoService().verificationService() + + val aliceSASLatch = CountDownLatch(1) + val aliceListener = object : VerificationService.Listener { + override fun transactionUpdated(tx: VerificationTransaction) { + val uxState = (tx as OutgoingSasVerificationTransaction).uxState + when (uxState) { + OutgoingSasVerificationTransaction.UxState.SHOW_SAS -> { + aliceSASLatch.countDown() + } + else -> Unit + } + } + } + aliceVerificationService.addListener(aliceListener) + + val bobSASLatch = CountDownLatch(1) + val bobListener = object : VerificationService.Listener { + override fun transactionUpdated(tx: VerificationTransaction) { + val uxState = (tx as IncomingSasVerificationTransaction).uxState + when (uxState) { + IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> { + tx.performAccept() + } + else -> Unit + } + if (uxState === IncomingSasVerificationTransaction.UxState.SHOW_SAS) { + bobSASLatch.countDown() + } + } + } + bobVerificationService.addListener(bobListener) + + val bobUserId = bobSession.myUserId + val bobDeviceId = bobSession.cryptoService().getMyDevice().deviceId + val verificationSAS = aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId, null) + mTestHelper.await(aliceSASLatch) + mTestHelper.await(bobSASLatch) + + val aliceTx = aliceVerificationService.getExistingTransaction(bobUserId, verificationSAS!!) as SASDefaultVerificationTransaction + val bobTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, verificationSAS) as SASDefaultVerificationTransaction + + assertEquals("Should have same SAS", aliceTx.getShortCodeRepresentation(SasMode.DECIMAL), + bobTx.getShortCodeRepresentation(SasMode.DECIMAL)) + + cryptoTestData.cleanUp(mTestHelper) + } + + @Test + fun test_happyPath() { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession + + val aliceVerificationService = aliceSession.cryptoService().verificationService() + val bobVerificationService = bobSession!!.cryptoService().verificationService() + + val aliceSASLatch = CountDownLatch(1) + val aliceListener = object : VerificationService.Listener { + var matchOnce = true + override fun transactionUpdated(tx: VerificationTransaction) { + val uxState = (tx as OutgoingSasVerificationTransaction).uxState + Log.v("TEST", "== aliceState ${uxState.name}") + when (uxState) { + OutgoingSasVerificationTransaction.UxState.SHOW_SAS -> { + tx.userHasVerifiedShortCode() + } + OutgoingSasVerificationTransaction.UxState.VERIFIED -> { + if (matchOnce) { + matchOnce = false + aliceSASLatch.countDown() + } + } + else -> Unit + } + } + } + aliceVerificationService.addListener(aliceListener) + + val bobSASLatch = CountDownLatch(1) + val bobListener = object : VerificationService.Listener { + var acceptOnce = true + var matchOnce = true + override fun transactionUpdated(tx: VerificationTransaction) { + val uxState = (tx as IncomingSasVerificationTransaction).uxState + Log.v("TEST", "== bobState ${uxState.name}") + when (uxState) { + IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> { + if (acceptOnce) { + acceptOnce = false + tx.performAccept() + } + } + IncomingSasVerificationTransaction.UxState.SHOW_SAS -> { + if (matchOnce) { + matchOnce = false + tx.userHasVerifiedShortCode() + } + } + IncomingSasVerificationTransaction.UxState.VERIFIED -> { + bobSASLatch.countDown() + } + else -> Unit + } + } + } + bobVerificationService.addListener(bobListener) + + val bobUserId = bobSession.myUserId + val bobDeviceId = bobSession.cryptoService().getMyDevice().deviceId + aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId, null) + mTestHelper.await(aliceSASLatch) + mTestHelper.await(bobSASLatch) + + // Assert that devices are verified + val bobDeviceInfoFromAlicePOV: CryptoDeviceInfo? = aliceSession.cryptoService().getDeviceInfo(bobUserId, bobDeviceId) + val aliceDeviceInfoFromBobPOV: CryptoDeviceInfo? = bobSession.cryptoService().getDeviceInfo(aliceSession.myUserId, aliceSession.cryptoService().getMyDevice().deviceId) + + // latch wait a bit again + Thread.sleep(1000) + + assertTrue("alice device should be verified from bob point of view", aliceDeviceInfoFromBobPOV!!.isVerified) + assertTrue("bob device should be verified from alice point of view", bobDeviceInfoFromAlicePOV!!.isVerified) + cryptoTestData.cleanUp(mTestHelper) + } + + @Test + fun test_ConcurrentStart() { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession + + val aliceVerificationService = aliceSession.cryptoService().verificationService() + val bobVerificationService = bobSession!!.cryptoService().verificationService() + + val req = aliceVerificationService.requestKeyVerificationInDMs( + listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW), + bobSession.myUserId, + cryptoTestData.roomId + ) + + var requestID : String? = null + + mTestHelper.waitWithLatch { + mTestHelper.retryPeriodicallyWithLatch(it) { + val prAlicePOV = aliceVerificationService.getExistingVerificationRequest(bobSession.myUserId)?.firstOrNull() + requestID = prAlicePOV?.transactionId + Log.v("TEST", "== alicePOV is $prAlicePOV") + prAlicePOV?.transactionId != null && prAlicePOV.localId == req.localId + } + } + + Log.v("TEST", "== requestID is $requestID") + + mTestHelper.waitWithLatch { + mTestHelper.retryPeriodicallyWithLatch(it) { + val prBobPOV = bobVerificationService.getExistingVerificationRequest(aliceSession.myUserId)?.firstOrNull() + Log.v("TEST", "== prBobPOV is $prBobPOV") + prBobPOV?.transactionId == requestID + } + } + + bobVerificationService.readyPendingVerification( + listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW), + aliceSession.myUserId, + requestID!! + ) + + // wait for alice to get the ready + mTestHelper.waitWithLatch { + mTestHelper.retryPeriodicallyWithLatch(it) { + val prAlicePOV = aliceVerificationService.getExistingVerificationRequest(bobSession.myUserId)?.firstOrNull() + Log.v("TEST", "== prAlicePOV is $prAlicePOV") + prAlicePOV?.transactionId == requestID && prAlicePOV?.isReady != null + } + } + + // Start concurrent! + aliceVerificationService.beginKeyVerificationInDMs( + VerificationMethod.SAS, + requestID!!, + cryptoTestData.roomId, + bobSession.myUserId, + bobSession.sessionParams.deviceId!!, + null) + + bobVerificationService.beginKeyVerificationInDMs( + VerificationMethod.SAS, + requestID!!, + cryptoTestData.roomId, + aliceSession.myUserId, + aliceSession.sessionParams.deviceId!!, + null) + + // we should reach SHOW SAS on both + var alicePovTx: SasVerificationTransaction? + var bobPovTx: SasVerificationTransaction? + + mTestHelper.waitWithLatch { + mTestHelper.retryPeriodicallyWithLatch(it) { + alicePovTx = aliceVerificationService.getExistingTransaction(bobSession.myUserId, requestID!!) as? SasVerificationTransaction + Log.v("TEST", "== alicePovTx is $alicePovTx") + alicePovTx?.state == VerificationTxState.ShortCodeReady + } + } + // wait for alice to get the ready + mTestHelper.waitWithLatch { + mTestHelper.retryPeriodicallyWithLatch(it) { + bobPovTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, requestID!!) as? SasVerificationTransaction + Log.v("TEST", "== bobPovTx is $bobPovTx") + bobPovTx?.state == VerificationTxState.ShortCodeReady + } + } + + cryptoTestData.cleanUp(mTestHelper) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/HexParser.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/HexParser.kt new file mode 100644 index 0000000000..cd5aa32d59 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/HexParser.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.verification.qrcode + +fun hexToByteArray(hex: String): ByteArray { + // Remove all spaces + return hex.replace(" ", "") + .let { + if (it.length % 2 != 0) "0$it" else it + } + .let { + ByteArray(it.length / 2) + .apply { + for (i in this.indices) { + val index = i * 2 + val v = it.substring(index, index + 2).toInt(16) + this[i] = v.toByte() + } + } + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeTest.kt new file mode 100644 index 0000000000..54a0f7e771 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeTest.kt @@ -0,0 +1,249 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.verification.qrcode + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.matrix.android.sdk.InstrumentedTest +import org.amshove.kluent.shouldBeNull +import org.amshove.kluent.shouldEqual +import org.amshove.kluent.shouldEqualTo +import org.amshove.kluent.shouldNotBeNull +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters + +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class QrCodeTest : InstrumentedTest { + + private val qrCode1 = QrCodeData.VerifyingAnotherUser( + transactionId = "MaTransaction", + userMasterCrossSigningPublicKey = "ktEwcUP6su1xh+GuE+CYkQ3H6W/DIl+ybHFdaEOrolU", + otherUserMasterCrossSigningPublicKey = "TXluZKTZLvSRWOTPlOqLq534bA+/K4zLFKSu9cGLQaU", + sharedSecret = "MTIzNDU2Nzg" + ) + + private val value1 = "MATRIX\u0002\u0000\u0000\u000DMaTransaction\u0092Ñ0qCú²íq\u0087á®\u0013à\u0098\u0091\u000DÇéoÃ\"_²lq]hC«¢UMynd¤Ù.ô\u0091XäÏ\u0094ê\u008B«\u009Døl\u000F¿+\u008CË\u0014¤®õÁ\u008BA¥12345678" + + private val qrCode2 = QrCodeData.SelfVerifyingMasterKeyTrusted( + transactionId = "MaTransaction", + userMasterCrossSigningPublicKey = "ktEwcUP6su1xh+GuE+CYkQ3H6W/DIl+ybHFdaEOrolU", + otherDeviceKey = "TXluZKTZLvSRWOTPlOqLq534bA+/K4zLFKSu9cGLQaU", + sharedSecret = "MTIzNDU2Nzg" + ) + + private val value2 = "MATRIX\u0002\u0001\u0000\u000DMaTransaction\u0092Ñ0qCú²íq\u0087á®\u0013à\u0098\u0091\u000DÇéoÃ\"_²lq]hC«¢UMynd¤Ù.ô\u0091XäÏ\u0094ê\u008B«\u009Døl\u000F¿+\u008CË\u0014¤®õÁ\u008BA¥12345678" + + private val qrCode3 = QrCodeData.SelfVerifyingMasterKeyNotTrusted( + transactionId = "MaTransaction", + deviceKey = "TXluZKTZLvSRWOTPlOqLq534bA+/K4zLFKSu9cGLQaU", + userMasterCrossSigningPublicKey = "ktEwcUP6su1xh+GuE+CYkQ3H6W/DIl+ybHFdaEOrolU", + sharedSecret = "MTIzNDU2Nzg" + ) + + private val value3 = "MATRIX\u0002\u0002\u0000\u000DMaTransactionMynd¤Ù.ô\u0091XäÏ\u0094ê\u008B«\u009Døl\u000F¿+\u008CË\u0014¤®õÁ\u008BA¥\u0092Ñ0qCú²íq\u0087á®\u0013à\u0098\u0091\u000DÇéoÃ\"_²lq]hC«¢U12345678" + + private val sharedSecretByteArray = "12345678".toByteArray(Charsets.ISO_8859_1) + + private val tlx_byteArray = hexToByteArray("4d 79 6e 64 a4 d9 2e f4 91 58 e4 cf 94 ea 8b ab 9d f8 6c 0f bf 2b 8c cb 14 a4 ae f5 c1 8b 41 a5") + + private val kte_byteArray = hexToByteArray("92 d1 30 71 43 fa b2 ed 71 87 e1 ae 13 e0 98 91 0d c7 e9 6f c3 22 5f b2 6c 71 5d 68 43 ab a2 55") + + @Test + fun testEncoding1() { + qrCode1.toEncodedString() shouldEqual value1 + } + + @Test + fun testEncoding2() { + qrCode2.toEncodedString() shouldEqual value2 + } + + @Test + fun testEncoding3() { + qrCode3.toEncodedString() shouldEqual value3 + } + + @Test + fun testSymmetry1() { + qrCode1.toEncodedString().toQrCodeData() shouldEqual qrCode1 + } + + @Test + fun testSymmetry2() { + qrCode2.toEncodedString().toQrCodeData() shouldEqual qrCode2 + } + + @Test + fun testSymmetry3() { + qrCode3.toEncodedString().toQrCodeData() shouldEqual qrCode3 + } + + @Test + fun testCase1() { + val url = qrCode1.toEncodedString() + + val byteArray = url.toByteArray(Charsets.ISO_8859_1) + checkHeader(byteArray) + + // Mode + byteArray[7] shouldEqualTo 0 + + checkSizeAndTransaction(byteArray) + + compareArray(byteArray.copyOfRange(23, 23 + 32), kte_byteArray) + compareArray(byteArray.copyOfRange(23 + 32, 23 + 64), tlx_byteArray) + + compareArray(byteArray.copyOfRange(23 + 64, byteArray.size), sharedSecretByteArray) + } + + @Test + fun testCase2() { + val url = qrCode2.toEncodedString() + + val byteArray = url.toByteArray(Charsets.ISO_8859_1) + checkHeader(byteArray) + + // Mode + byteArray[7] shouldEqualTo 1 + + checkSizeAndTransaction(byteArray) + compareArray(byteArray.copyOfRange(23, 23 + 32), kte_byteArray) + compareArray(byteArray.copyOfRange(23 + 32, 23 + 64), tlx_byteArray) + + compareArray(byteArray.copyOfRange(23 + 64, byteArray.size), sharedSecretByteArray) + } + + @Test + fun testCase3() { + val url = qrCode3.toEncodedString() + + val byteArray = url.toByteArray(Charsets.ISO_8859_1) + checkHeader(byteArray) + + // Mode + byteArray[7] shouldEqualTo 2 + + checkSizeAndTransaction(byteArray) + compareArray(byteArray.copyOfRange(23, 23 + 32), tlx_byteArray) + compareArray(byteArray.copyOfRange(23 + 32, 23 + 64), kte_byteArray) + + compareArray(byteArray.copyOfRange(23 + 64, byteArray.size), sharedSecretByteArray) + } + + @Test + fun testLongTransactionId() { + // Size on two bytes (2_000 = 0x07D0) + val longTransactionId = "PatternId_".repeat(200) + + val qrCode = qrCode1.copy(transactionId = longTransactionId) + + val result = qrCode.toEncodedString() + val expected = value1.replace("\u0000\u000DMaTransaction", "\u0007\u00D0$longTransactionId") + + result shouldEqual expected + + // Reverse operation + expected.toQrCodeData() shouldEqual qrCode + } + + @Test + fun testAnyTransactionId() { + for (qty in 0 until 0x1FFF step 200) { + val longTransactionId = "a".repeat(qty) + + val qrCode = qrCode1.copy(transactionId = longTransactionId) + + // Symmetric operation + qrCode.toEncodedString().toQrCodeData() shouldEqual qrCode + } + } + + // Error cases + @Test + fun testErrorHeader() { + value1.replace("MATRIX", "MOTRIX").toQrCodeData().shouldBeNull() + value1.replace("MATRIX", "MATRI").toQrCodeData().shouldBeNull() + value1.replace("MATRIX", "").toQrCodeData().shouldBeNull() + } + + @Test + fun testErrorVersion() { + value1.replace("MATRIX\u0002", "MATRIX\u0000").toQrCodeData().shouldBeNull() + value1.replace("MATRIX\u0002", "MATRIX\u0001").toQrCodeData().shouldBeNull() + value1.replace("MATRIX\u0002", "MATRIX\u0003").toQrCodeData().shouldBeNull() + value1.replace("MATRIX\u0002", "MATRIX").toQrCodeData().shouldBeNull() + } + + @Test + fun testErrorSecretTooShort() { + value1.replace("12345678", "1234567").toQrCodeData().shouldBeNull() + } + + @Test + fun testErrorNoTransactionNoKeyNoSecret() { + // But keep transaction length + "MATRIX\u0002\u0000\u0000\u000D".toQrCodeData().shouldBeNull() + } + + @Test + fun testErrorNoKeyNoSecret() { + "MATRIX\u0002\u0000\u0000\u000DMaTransaction".toQrCodeData().shouldBeNull() + } + + @Test + fun testErrorTransactionLengthTooShort() { + // In this case, the secret will be longer, so this is not an error, but it will lead to keys mismatch + value1.replace("\u000DMaTransaction", "\u000CMaTransaction").toQrCodeData().shouldNotBeNull() + } + + @Test + fun testErrorTransactionLengthTooBig() { + value1.replace("\u000DMaTransaction", "\u000EMaTransaction").toQrCodeData().shouldBeNull() + } + + private fun compareArray(actual: ByteArray, expected: ByteArray) { + actual.size shouldEqual expected.size + + for (i in actual.indices) { + actual[i] shouldEqualTo expected[i] + } + } + + private fun checkHeader(byteArray: ByteArray) { + // MATRIX + byteArray[0] shouldEqualTo 'M'.toByte() + byteArray[1] shouldEqualTo 'A'.toByte() + byteArray[2] shouldEqualTo 'T'.toByte() + byteArray[3] shouldEqualTo 'R'.toByte() + byteArray[4] shouldEqualTo 'I'.toByte() + byteArray[5] shouldEqualTo 'X'.toByte() + + // Version + byteArray[6] shouldEqualTo 2 + } + + private fun checkSizeAndTransaction(byteArray: ByteArray) { + // Size + byteArray[8] shouldEqualTo 0 + byteArray[9] shouldEqualTo 13 + + // Transaction + byteArray.copyOfRange(10, 10 + "MaTransaction".length).toString(Charsets.ISO_8859_1) shouldEqual "MaTransaction" + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/SharedSecretTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/SharedSecretTest.kt new file mode 100644 index 0000000000..4032890723 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/SharedSecretTest.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.verification.qrcode + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.matrix.android.sdk.InstrumentedTest +import org.amshove.kluent.shouldBe +import org.amshove.kluent.shouldNotBeEqualTo +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters + +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class SharedSecretTest : InstrumentedTest { + + @Test + fun testSharedSecretLengthCase() { + repeat(100) { + generateSharedSecretV2().length shouldBe 11 + } + } + + @Test + fun testSharedDiffCase() { + val sharedSecret1 = generateSharedSecretV2() + val sharedSecret2 = generateSharedSecretV2() + + sharedSecret1 shouldNotBeEqualTo sharedSecret2 + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt new file mode 100644 index 0000000000..0c003215ee --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt @@ -0,0 +1,232 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.verification.qrcode + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod +import org.matrix.android.sdk.api.session.crypto.verification.VerificationService +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.TestConstants +import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth +import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest +import org.amshove.kluent.shouldBe +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import java.util.concurrent.CountDownLatch + +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class VerificationTest : InstrumentedTest { + private val mTestHelper = CommonTestHelper(context()) + private val mCryptoTestHelper = CryptoTestHelper(mTestHelper) + + data class ExpectedResult( + val sasIsSupported: Boolean = false, + val otherCanScanQrCode: Boolean = false, + val otherCanShowQrCode: Boolean = false + ) + + private val sas = listOf( + VerificationMethod.SAS + ) + + private val sasShow = listOf( + VerificationMethod.SAS, + VerificationMethod.QR_CODE_SHOW + ) + + private val sasScan = listOf( + VerificationMethod.SAS, + VerificationMethod.QR_CODE_SCAN + ) + + private val sasShowScan = listOf( + VerificationMethod.SAS, + VerificationMethod.QR_CODE_SHOW, + VerificationMethod.QR_CODE_SCAN + ) + + @Test + fun test_aliceAndBob_sas_sas() = doTest( + sas, + sas, + ExpectedResult(sasIsSupported = true), + ExpectedResult(sasIsSupported = true) + ) + + @Test + fun test_aliceAndBob_sas_show() = doTest( + sas, + sasShow, + ExpectedResult(sasIsSupported = true), + ExpectedResult(sasIsSupported = true) + ) + + @Test + fun test_aliceAndBob_show_sas() = doTest( + sasShow, + sas, + ExpectedResult(sasIsSupported = true), + ExpectedResult(sasIsSupported = true) + ) + + @Test + fun test_aliceAndBob_sas_scan() = doTest( + sas, + sasScan, + ExpectedResult(sasIsSupported = true), + ExpectedResult(sasIsSupported = true) + ) + + @Test + fun test_aliceAndBob_scan_sas() = doTest( + sasScan, + sas, + ExpectedResult(sasIsSupported = true), + ExpectedResult(sasIsSupported = true) + ) + + @Test + fun test_aliceAndBob_scan_scan() = doTest( + sasScan, + sasScan, + ExpectedResult(sasIsSupported = true), + ExpectedResult(sasIsSupported = true) + ) + + @Test + fun test_aliceAndBob_show_show() = doTest( + sasShow, + sasShow, + ExpectedResult(sasIsSupported = true), + ExpectedResult(sasIsSupported = true) + ) + + @Test + fun test_aliceAndBob_show_scan() = doTest( + sasShow, + sasScan, + ExpectedResult(sasIsSupported = true, otherCanScanQrCode = true), + ExpectedResult(sasIsSupported = true, otherCanShowQrCode = true) + ) + + @Test + fun test_aliceAndBob_scan_show() = doTest( + sasScan, + sasShow, + ExpectedResult(sasIsSupported = true, otherCanShowQrCode = true), + ExpectedResult(sasIsSupported = true, otherCanScanQrCode = true) + ) + + @Test + fun test_aliceAndBob_all_all() = doTest( + sasShowScan, + sasShowScan, + ExpectedResult(sasIsSupported = true, otherCanShowQrCode = true, otherCanScanQrCode = true), + ExpectedResult(sasIsSupported = true, otherCanShowQrCode = true, otherCanScanQrCode = true) + ) + + // TODO Add tests without SAS + + private fun doTest(aliceSupportedMethods: List, + bobSupportedMethods: List, + expectedResultForAlice: ExpectedResult, + expectedResultForBob: ExpectedResult) { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession!! + + mTestHelper.doSync { callback -> + aliceSession.cryptoService().crossSigningService() + .initializeCrossSigning(UserPasswordAuth( + user = aliceSession.myUserId, + password = TestConstants.PASSWORD + ), callback) + } + + mTestHelper.doSync { callback -> + bobSession.cryptoService().crossSigningService() + .initializeCrossSigning(UserPasswordAuth( + user = bobSession.myUserId, + password = TestConstants.PASSWORD + ), callback) + } + + val aliceVerificationService = aliceSession.cryptoService().verificationService() + val bobVerificationService = bobSession.cryptoService().verificationService() + + var aliceReadyPendingVerificationRequest: PendingVerificationRequest? = null + var bobReadyPendingVerificationRequest: PendingVerificationRequest? = null + + val latch = CountDownLatch(2) + val aliceListener = object : VerificationService.Listener { + override fun verificationRequestUpdated(pr: PendingVerificationRequest) { + // Step 4: Alice receive the ready request + if (pr.isReady) { + aliceReadyPendingVerificationRequest = pr + latch.countDown() + } + } + } + aliceVerificationService.addListener(aliceListener) + + val bobListener = object : VerificationService.Listener { + override fun verificationRequestCreated(pr: PendingVerificationRequest) { + // Step 2: Bob accepts the verification request + bobVerificationService.readyPendingVerificationInDMs( + bobSupportedMethods, + aliceSession.myUserId, + cryptoTestData.roomId, + pr.transactionId!! + ) + } + + override fun verificationRequestUpdated(pr: PendingVerificationRequest) { + // Step 3: Bob is ready + if (pr.isReady) { + bobReadyPendingVerificationRequest = pr + latch.countDown() + } + } + } + bobVerificationService.addListener(bobListener) + + val bobUserId = bobSession.myUserId + // Step 1: Alice starts a verification request + aliceVerificationService.requestKeyVerificationInDMs(aliceSupportedMethods, bobUserId, cryptoTestData.roomId) + mTestHelper.await(latch) + + aliceReadyPendingVerificationRequest!!.let { pr -> + pr.isSasSupported() shouldBe expectedResultForAlice.sasIsSupported + pr.otherCanShowQrCode() shouldBe expectedResultForAlice.otherCanShowQrCode + pr.otherCanScanQrCode() shouldBe expectedResultForAlice.otherCanScanQrCode + } + + bobReadyPendingVerificationRequest!!.let { pr -> + pr.isSasSupported() shouldBe expectedResultForBob.sasIsSupported + pr.otherCanShowQrCode() shouldBe expectedResultForBob.otherCanShowQrCode + pr.otherCanScanQrCode() shouldBe expectedResultForBob.otherCanScanQrCode + } + + cryptoTestData.cleanUp(mTestHelper) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt new file mode 100644 index 0000000000..9b85310d50 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt @@ -0,0 +1,278 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.send + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.matrix.android.sdk.InstrumentedTest +import org.commonmark.parser.Parser +import org.commonmark.renderer.html.HtmlRenderer +import org.commonmark.renderer.text.TextContentRenderer +import org.junit.Assert.assertEquals +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters + +/** + * It will not be possible to test all combinations. For the moment I add a few tests, then, depending on the problem discovered in the wild, + * we can add more tests to cover the edge cases. + * Some tests are suffixed with `_not_passing`, maybe one day we will fix them... + * Riot-Web should be used as a reference for expected results, but not always. Especially Riot-Web add lots of `\n` in the + * formatted body, which is quite useless. + * Also Riot-Web does not provide plain text body when formatted text is provided. The body contains what the user has entered. + * See https://matrix.org/docs/spec/client_server/latest#m-room-message-msgtypes + */ +@Suppress("SpellCheckingInspection") +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class MarkdownParserTest : InstrumentedTest { + + /** + * Create the same parser than in the RoomModule + */ + private val markdownParser = MarkdownParser( + Parser.builder().build(), + HtmlRenderer.builder().build(), + TextContentRenderer.builder().build() + ) + + @Test + fun parseNoMarkdown() { + testIdentity("") + testIdentity("a") + testIdentity("1") + testIdentity("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et " + + "dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea com" + + "modo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pari" + + "atur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.") + } + + @Test + fun parseSpaces() { + testIdentity(" ") + testIdentity(" ") + testIdentity("\n") + } + + @Test + fun parseNewLines() { + testIdentity("line1\nline2") + testIdentity("line1\nline2\nline3") + } + + @Test + fun parseBold() { + testType( + name = "bold", + markdownPattern = "**", + htmlExpectedTag = "strong" + ) + } + + @Test + fun parseItalic() { + testType( + name = "italic", + markdownPattern = "*", + htmlExpectedTag = "em" + ) + } + + @Test + fun parseItalic2() { + // Riot-Web format + "_italic_".let { markdownParser.parse(it) }.expect("italic", "italic") + } + + /** + * Note: the test is not passing, it does not work on Riot-Web neither + */ + @Test + fun parseStrike_not_passing() { + testType( + name = "strike", + markdownPattern = "~~", + htmlExpectedTag = "del" + ) + } + + @Test + fun parseCode() { + testType( + name = "code", + markdownPattern = "`", + htmlExpectedTag = "code", + plainTextPrefix = "\"", + plainTextSuffix = "\"" + ) + } + + @Test + fun parseCode2() { + testType( + name = "code", + markdownPattern = "``", + htmlExpectedTag = "code", + plainTextPrefix = "\"", + plainTextSuffix = "\"" + ) + } + + @Test + fun parseCode3() { + testType( + name = "code", + markdownPattern = "```", + htmlExpectedTag = "code", + plainTextPrefix = "\"", + plainTextSuffix = "\"" + ) + } + + @Test + fun parseUnorderedList() { + "- item1".let { markdownParser.parse(it).expect(it, "

  • item1
") } + "- item1\n- item2".let { markdownParser.parse(it).expect(it, "
  • item1
  • item2
") } + } + + @Test + fun parseOrderedList() { + "1. item1".let { markdownParser.parse(it).expect(it, "
  1. item1
") } + "1. item1\n2. item2".let { markdownParser.parse(it).expect(it, "
  1. item1
  2. item2
") } + } + + @Test + fun parseHorizontalLine() { + "---".let { markdownParser.parse(it) }.expect("***", "
") + } + + @Test + fun parseH2AndContent() { + "a\n---\nb".let { markdownParser.parse(it) }.expect("a\nb", "

a

b

") + } + + @Test + fun parseQuote() { + "> quoted".let { markdownParser.parse(it) }.expect("«quoted»", "

quoted

") + } + + @Test + fun parseQuote_not_passing() { + "> quoted\nline2".let { markdownParser.parse(it) }.expect("«quoted\nline2»", "

quoted
line2

") + } + + @Test + fun parseBoldItalic() { + "*italic* **bold**".let { markdownParser.parse(it) }.expect("italic bold", "italic bold") + "**bold** *italic*".let { markdownParser.parse(it) }.expect("bold italic", "bold italic") + } + + @Test + fun parseHead() { + "# head1".let { markdownParser.parse(it) }.expect("head1", "

head1

") + "## head2".let { markdownParser.parse(it) }.expect("head2", "

head2

") + "### head3".let { markdownParser.parse(it) }.expect("head3", "

head3

") + "#### head4".let { markdownParser.parse(it) }.expect("head4", "

head4

") + "##### head5".let { markdownParser.parse(it) }.expect("head5", "
head5
") + "###### head6".let { markdownParser.parse(it) }.expect("head6", "
head6
") + } + + @Test + fun parseHeads() { + "# head1\n# head2".let { markdownParser.parse(it) }.expect("head1\nhead2", "

head1

head2

") + } + + @Test + fun parseBoldNewLines_not_passing() { + "**bold**\nline2".let { markdownParser.parse(it) }.expect("bold\nline2", "bold
line2") + } + + @Test + fun parseLinks() { + "[link](target)".let { markdownParser.parse(it) }.expect(""""link" (target)""", """link""") + } + + @Test + fun parseParagraph() { + "# head\ncontent".let { markdownParser.parse(it) }.expect("head\ncontent", "

head

content

") + } + + private fun testIdentity(text: String) { + markdownParser.parse(text).expect(text, null) + } + + private fun testType(name: String, + markdownPattern: String, + htmlExpectedTag: String, + plainTextPrefix: String = "", + plainTextSuffix: String = "") { + // Test simple case + "$markdownPattern$name$markdownPattern" + .let { markdownParser.parse(it) } + .expect(expectedText = "$plainTextPrefix$name$plainTextSuffix", + expectedFormattedText = "<$htmlExpectedTag>$name") + + // Test twice the same tag + "$markdownPattern$name$markdownPattern and $markdownPattern$name bis$markdownPattern" + .let { markdownParser.parse(it) } + .expect(expectedText = "$plainTextPrefix$name$plainTextSuffix and $plainTextPrefix$name bis$plainTextSuffix", + expectedFormattedText = "<$htmlExpectedTag>$name and <$htmlExpectedTag>$name bis") + + val textBefore = "a" + val textAfter = "b" + + // With sticked text before + "$textBefore$markdownPattern$name$markdownPattern" + .let { markdownParser.parse(it) } + .expect(expectedText = "$textBefore$plainTextPrefix$name$plainTextSuffix", + expectedFormattedText = "$textBefore<$htmlExpectedTag>$name") + + // With text before and space + "$textBefore $markdownPattern$name$markdownPattern" + .let { markdownParser.parse(it) } + .expect(expectedText = "$textBefore $plainTextPrefix$name$plainTextSuffix", + expectedFormattedText = "$textBefore <$htmlExpectedTag>$name") + + // With sticked text after + "$markdownPattern$name$markdownPattern$textAfter" + .let { markdownParser.parse(it) } + .expect(expectedText = "$plainTextPrefix$name$plainTextSuffix$textAfter", + expectedFormattedText = "<$htmlExpectedTag>$name$textAfter") + + // With space and text after + "$markdownPattern$name$markdownPattern $textAfter" + .let { markdownParser.parse(it) } + .expect(expectedText = "$plainTextPrefix$name$plainTextSuffix $textAfter", + expectedFormattedText = "<$htmlExpectedTag>$name $textAfter") + + // With sticked text before and text after + "$textBefore$markdownPattern$name$markdownPattern$textAfter" + .let { markdownParser.parse(it) } + .expect(expectedText = "$textBefore$plainTextPrefix$name$plainTextSuffix$textAfter", + expectedFormattedText = "a<$htmlExpectedTag>$name$textAfter") + + // With text before and after, with spaces + "$textBefore $markdownPattern$name$markdownPattern $textAfter" + .let { markdownParser.parse(it) } + .expect(expectedText = "$textBefore $plainTextPrefix$name$plainTextSuffix $textAfter", + expectedFormattedText = "$textBefore <$htmlExpectedTag>$name $textAfter") + } + + private fun TextContent.expect(expectedText: String, expectedFormattedText: String?) { + assertEquals("TextContent are not identical", TextContent(expectedText, expectedFormattedText), this) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/util/JsonCanonicalizerTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/util/JsonCanonicalizerTest.kt new file mode 100644 index 0000000000..854d420a82 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/util/JsonCanonicalizerTest.kt @@ -0,0 +1,153 @@ +/* + * Copyright 2019 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.internal.util + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.matrix.android.sdk.InstrumentedTest +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +internal class JsonCanonicalizerTest : InstrumentedTest { + + @Test + fun identityTest() { + listOf( + "{}", + """{"a":true}""", + """{"a":false}""", + """{"a":1}""", + """{"a":1.2}""", + """{"a":null}""", + """{"a":[]}""", + """{"a":["b":"c"]}""", + """{"a":["c":"b","d":"e"]}""", + """{"a":["d":"b","c":"e"]}""" + ).forEach { + assertEquals(it, + JsonCanonicalizer.canonicalize(it)) + } + } + + @Test + fun reorderTest() { + assertEquals("""{"a":true,"b":false}""", + JsonCanonicalizer.canonicalize("""{"b":false,"a":true}""")) + } + + @Test + fun realSampleTest() { + assertEquals("""{"algorithms":["m.megolm.v1.aes-sha2","m.olm.v1.curve25519-aes-sha2"],"device_id":"VSCUNFSOUI","keys":{"curve25519:VSCUNFSOUI":"utyOjnhiQ73qNhi9HlN0OgWIowe5gthTS8r0r9TcJ3o","ed25519:VSCUNFSOUI":"qNhEt+Yggaajet0hX\/FjTRLfySgs65ldYyomm7PIx6U"},"user_id":"@benoitx:matrix.org"}""", + JsonCanonicalizer.canonicalize("""{"algorithms":["m.megolm.v1.aes-sha2","m.olm.v1.curve25519-aes-sha2"],"device_id":"VSCUNFSOUI","user_id":"@benoitx:matrix.org","keys":{"curve25519:VSCUNFSOUI":"utyOjnhiQ73qNhi9HlN0OgWIowe5gthTS8r0r9TcJ3o","ed25519:VSCUNFSOUI":"qNhEt+Yggaajet0hX/FjTRLfySgs65ldYyomm7PIx6U"}}""")) + } + + @Test + fun doubleQuoteTest() { + assertEquals("{\"a\":\"\\\"\"}", + JsonCanonicalizer.canonicalize("{\"a\":\"\\\"\"}")) + } + + /* ========================================================================================== + * Test from https://matrix.org/docs/spec/appendices.html#examples + * ========================================================================================== */ + + @Test + fun matrixOrg001Test() { + assertEquals("""{}""", + JsonCanonicalizer.canonicalize("""{}""")) + } + + @Test + fun matrixOrg002Test() { + assertEquals("""{"one":1,"two":"Two"}""", + JsonCanonicalizer.canonicalize("""{ + "one": 1, + "two": "Two" +}""")) + } + + @Test + fun matrixOrg003Test() { + assertEquals("""{"a":"1","b":"2"}""", + JsonCanonicalizer.canonicalize("""{ + "b": "2", + "a": "1" +}""")) + } + + @Test + fun matrixOrg004Test() { + assertEquals("""{"a":"1","b":"2"}""", + JsonCanonicalizer.canonicalize("""{"b":"2","a":"1"}""")) + } + + @Test + fun matrixOrg005Test() { + assertEquals("""{"auth":{"mxid":"@john.doe:example.com","profile":{"display_name":"John Doe","three_pids":[{"address":"john.doe@example.org","medium":"email"},{"address":"123456789","medium":"msisdn"}]},"success":true}}""", + JsonCanonicalizer.canonicalize("""{ + "auth": { + "success": true, + "mxid": "@john.doe:example.com", + "profile": { + "display_name": "John Doe", + "three_pids": [ + { + "medium": "email", + "address": "john.doe@example.org" + }, + { + "medium": "msisdn", + "address": "123456789" + } + ] + } + } +}""")) + } + + @Test + fun matrixOrg006Test() { + assertEquals("""{"a":"日本語"}""", + JsonCanonicalizer.canonicalize("""{ + "a": "日本語" +}""")) + } + + @Test + fun matrixOrg007Test() { + assertEquals("""{"日":1,"本":2}""", + JsonCanonicalizer.canonicalize("""{ + "本": 2, + "日": 1 +}""")) + } + + @Test + fun matrixOrg008Test() { + assertEquals("""{"a":"日"}""", + JsonCanonicalizer.canonicalize("{\"a\": \"\u65E5\"}")) + } + + @Test + fun matrixOrg009Test() { + assertEquals("""{"a":null}""", + JsonCanonicalizer.canonicalize("""{ + "a": null +}""")) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/ChunkEntityTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/ChunkEntityTest.kt new file mode 100644 index 0000000000..a2a1586864 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/ChunkEntityTest.kt @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.session.room.timeline + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.database.helper.addTimelineEvent +import org.matrix.android.sdk.internal.database.helper.merge +import org.matrix.android.sdk.internal.database.mapper.toEntity +import org.matrix.android.sdk.internal.database.model.ChunkEntity +import org.matrix.android.sdk.internal.database.model.SessionRealmModule +import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection +import org.matrix.android.sdk.session.room.timeline.RoomDataHelper.createFakeListOfEvents +import org.matrix.android.sdk.session.room.timeline.RoomDataHelper.createFakeMessageEvent +import io.realm.Realm +import io.realm.RealmConfiguration +import io.realm.kotlin.createObject +import org.amshove.kluent.shouldBeTrue +import org.amshove.kluent.shouldEqual +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +internal class ChunkEntityTest : InstrumentedTest { + + private lateinit var monarchy: Monarchy + + @Before + fun setup() { + Realm.init(context()) + val testConfig = RealmConfiguration.Builder() + .inMemory() + .name("test-realm") + .modules(SessionRealmModule()) + .build() + monarchy = Monarchy.Builder().setRealmConfiguration(testConfig).build() + } + + @Test + fun add_shouldAdd_whenNotAlreadyIncluded() { + monarchy.runTransactionSync { realm -> + val chunk: ChunkEntity = realm.createObject() + + val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED, System.currentTimeMillis()).let { + realm.copyToRealmOrUpdate(it) + } + chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap()) + chunk.timelineEvents.size shouldEqual 1 + } + } + + @Test + fun add_shouldNotAdd_whenAlreadyIncluded() { + monarchy.runTransactionSync { realm -> + val chunk: ChunkEntity = realm.createObject() + val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED, System.currentTimeMillis()).let { + realm.copyToRealmOrUpdate(it) + } + chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap()) + chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap()) + chunk.timelineEvents.size shouldEqual 1 + } + } + + @Test + fun merge_shouldAddEvents_whenMergingBackward() { + monarchy.runTransactionSync { realm -> + val chunk1: ChunkEntity = realm.createObject() + val chunk2: ChunkEntity = realm.createObject() + chunk1.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS) + chunk2.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS) + chunk1.merge(ROOM_ID, chunk2, PaginationDirection.BACKWARDS) + chunk1.timelineEvents.size shouldEqual 60 + } + } + + @Test + fun merge_shouldAddOnlyDifferentEvents_whenMergingBackward() { + monarchy.runTransactionSync { realm -> + val chunk1: ChunkEntity = realm.createObject() + val chunk2: ChunkEntity = realm.createObject() + val eventsForChunk1 = createFakeListOfEvents(30) + val eventsForChunk2 = eventsForChunk1 + createFakeListOfEvents(10) + chunk1.isLastForward = true + chunk2.isLastForward = false + chunk1.addAll(ROOM_ID, eventsForChunk1, PaginationDirection.FORWARDS) + chunk2.addAll(ROOM_ID, eventsForChunk2, PaginationDirection.BACKWARDS) + chunk1.merge(ROOM_ID, chunk2, PaginationDirection.BACKWARDS) + chunk1.timelineEvents.size shouldEqual 40 + chunk1.isLastForward.shouldBeTrue() + } + } + + @Test + fun merge_shouldPrevTokenMerged_whenMergingForwards() { + monarchy.runTransactionSync { realm -> + val chunk1: ChunkEntity = realm.createObject() + val chunk2: ChunkEntity = realm.createObject() + val prevToken = "prev_token" + chunk1.prevToken = prevToken + chunk1.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS) + chunk2.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS) + chunk1.merge(ROOM_ID, chunk2, PaginationDirection.FORWARDS) + chunk1.prevToken shouldEqual prevToken + } + } + + @Test + fun merge_shouldNextTokenMerged_whenMergingBackwards() { + monarchy.runTransactionSync { realm -> + val chunk1: ChunkEntity = realm.createObject() + val chunk2: ChunkEntity = realm.createObject() + val nextToken = "next_token" + chunk1.nextToken = nextToken + chunk1.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS) + chunk2.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS) + chunk1.merge(ROOM_ID, chunk2, PaginationDirection.BACKWARDS) + chunk1.nextToken shouldEqual nextToken + } + } + + private fun ChunkEntity.addAll(roomId: String, + events: List, + direction: PaginationDirection) { + events.forEach { event -> + val fakeEvent = event.toEntity(roomId, SendState.SYNCED, System.currentTimeMillis()).let { + realm.copyToRealmOrUpdate(it) + } + addTimelineEvent(roomId, fakeEvent, direction, emptyMap()) + } + } + + companion object { + private const val ROOM_ID = "roomId" + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/FakeGetContextOfEventTask.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/FakeGetContextOfEventTask.kt new file mode 100644 index 0000000000..9a133032b6 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/FakeGetContextOfEventTask.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.session.room.timeline + +import org.matrix.android.sdk.internal.session.room.timeline.GetContextOfEventTask +import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection +import org.matrix.android.sdk.internal.session.room.timeline.TokenChunkEventPersistor +import kotlin.random.Random + +internal class FakeGetContextOfEventTask constructor(private val tokenChunkEventPersistor: TokenChunkEventPersistor) : GetContextOfEventTask { + + override suspend fun execute(params: GetContextOfEventTask.Params): TokenChunkEventPersistor.Result { + val fakeEvents = RoomDataHelper.createFakeListOfEvents(30) + val tokenChunkEvent = FakeTokenChunkEvent( + Random.nextLong(System.currentTimeMillis()).toString(), + Random.nextLong(System.currentTimeMillis()).toString(), + fakeEvents + ) + return tokenChunkEventPersistor.insertInDb(tokenChunkEvent, params.roomId, PaginationDirection.BACKWARDS) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/FakePaginationTask.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/FakePaginationTask.kt new file mode 100644 index 0000000000..06828ef3d1 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/FakePaginationTask.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.session.room.timeline + +import org.matrix.android.sdk.internal.session.room.timeline.PaginationTask +import org.matrix.android.sdk.internal.session.room.timeline.TokenChunkEventPersistor +import javax.inject.Inject +import kotlin.random.Random + +internal class FakePaginationTask @Inject constructor(private val tokenChunkEventPersistor: TokenChunkEventPersistor) : PaginationTask { + + override suspend fun execute(params: PaginationTask.Params): TokenChunkEventPersistor.Result { + val fakeEvents = RoomDataHelper.createFakeListOfEvents(30) + val tokenChunkEvent = FakeTokenChunkEvent(params.from, Random.nextLong(System.currentTimeMillis()).toString(), fakeEvents) + return tokenChunkEventPersistor.insertInDb(tokenChunkEvent, params.roomId, params.direction) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/FakeTokenChunkEvent.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/FakeTokenChunkEvent.kt new file mode 100644 index 0000000000..0301157d09 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/FakeTokenChunkEvent.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.session.room.timeline + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.internal.session.room.timeline.TokenChunkEvent + +internal data class FakeTokenChunkEvent(override val start: String?, + override val end: String?, + override val events: List = emptyList(), + override val stateEvents: List = emptyList() +) : TokenChunkEvent diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/RoomDataHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/RoomDataHelper.kt new file mode 100644 index 0000000000..a6fe675218 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/RoomDataHelper.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.session.room.timeline + +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary +import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent +import org.matrix.android.sdk.api.session.room.model.message.MessageType +import kotlin.random.Random + +object RoomDataHelper { + + private const val FAKE_TEST_SENDER = "@sender:test.org" + private val EVENT_FACTORIES = hashMapOf( + 0 to { createFakeMessageEvent() }, + 1 to { createFakeRoomMemberEvent() } + ) + + fun createFakeListOfEvents(size: Int = 10): List { + return (0 until size).mapNotNull { + val nextInt = Random.nextInt(EVENT_FACTORIES.size) + EVENT_FACTORIES[nextInt]?.invoke() + } + } + + fun createFakeEvent(type: String, + content: Content? = null, + prevContent: Content? = null, + sender: String = FAKE_TEST_SENDER, + stateKey: String = FAKE_TEST_SENDER + ): Event { + return Event( + type = type, + eventId = Random.nextLong().toString(), + content = content, + prevContent = prevContent, + senderId = sender, + stateKey = stateKey + ) + } + + fun createFakeMessageEvent(): Event { + val message = MessageTextContent(MessageType.MSGTYPE_TEXT, "Fake message #${Random.nextLong()}").toContent() + return createFakeEvent(EventType.MESSAGE, message) + } + + fun createFakeRoomMemberEvent(): Event { + val roomMember = RoomMemberSummary(Membership.JOIN, "Fake name #${Random.nextLong()}").toContent() + return createFakeEvent(EventType.STATE_ROOM_MEMBER, roomMember) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineBackToPreviousLastForwardTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineBackToPreviousLastForwardTest.kt new file mode 100644 index 0000000000..8c5e7f17f2 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineBackToPreviousLastForwardTest.kt @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.session.room.timeline + +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.timeline.Timeline +import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.checkSendOrder +import org.amshove.kluent.shouldBeFalse +import org.amshove.kluent.shouldBeTrue +import org.junit.Assert.assertTrue +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters +import timber.log.Timber +import java.util.concurrent.CountDownLatch + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class TimelineBackToPreviousLastForwardTest : InstrumentedTest { + + private val commonTestHelper = CommonTestHelper(context()) + private val cryptoTestHelper = CryptoTestHelper(commonTestHelper) + + /** + * This test ensure that if we have a chunk in the timeline which is due to a sync, and we click to permalink of an + * even contained in a previous lastForward chunk, we will be able to go back to the live + */ + @Test + fun backToPreviousLastForwardTest() { + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false) + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession!! + val aliceRoomId = cryptoTestData.roomId + + aliceSession.cryptoService().setWarnOnUnknownDevices(false) + bobSession.cryptoService().setWarnOnUnknownDevices(false) + + val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!! + val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!! + + val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(30)) + bobTimeline.start() + + var roomCreationEventId: String? = null + + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Bob timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root}") + } + + roomCreationEventId = snapshot.lastOrNull()?.root?.eventId + // Ok, we have the 8 first messages of the initial sync (room creation and bob join event) + snapshot.size == 8 + } + + bobTimeline.addListener(eventsListener) + commonTestHelper.await(lock) + bobTimeline.removeAllListeners() + + bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() + bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() + } + + // Bob stop to sync + bobSession.stopSync() + + val messageRoot = "First messages from Alice" + + // Alice sends 30 messages + commonTestHelper.sendTextMessage( + roomFromAlicePOV, + messageRoot, + 30) + + // Bob start to sync + bobSession.startSync(true) + + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Bob timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root}") + } + + // Ok, we have the 10 last messages from Alice. + snapshot.size == 10 + && snapshot.all { it.root.content.toModel()?.body?.startsWith(messageRoot).orFalse() } + } + + bobTimeline.addListener(eventsListener) + commonTestHelper.await(lock) + bobTimeline.removeAllListeners() + + bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue() + bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() + } + + // Bob navigate to the first event (room creation event), so inside the previous last forward chunk + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Bob timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root}") + } + + // The event is in db, so it is fetch and auto pagination occurs, half of the number of events we have for this chunk (?) + snapshot.size == 4 + } + + bobTimeline.addListener(eventsListener) + + // Restart the timeline to the first sent event, which is already in the database, so pagination should start automatically + assertTrue(roomFromBobPOV.getTimeLineEvent(roomCreationEventId!!) != null) + + bobTimeline.restartWithEventId(roomCreationEventId) + + commonTestHelper.await(lock) + bobTimeline.removeAllListeners() + + bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue() + bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() + } + + // Bob scroll to the future + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Bob timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root}") + } + + // Bob can see the first event of the room (so Back pagination has worked) + snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE + // 8 for room creation item, and 30 for the forward pagination + && snapshot.size == 38 + && snapshot.checkSendOrder(messageRoot, 30, 0) + } + + bobTimeline.addListener(eventsListener) + + bobTimeline.paginate(Timeline.Direction.FORWARDS, 50) + + commonTestHelper.await(lock) + bobTimeline.removeAllListeners() + + bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() + bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() + } + bobTimeline.dispose() + + cryptoTestData.cleanUp(commonTestHelper) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt new file mode 100644 index 0000000000..facb905b35 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.session.room.timeline + +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.timeline.Timeline +import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.checkSendOrder +import org.amshove.kluent.shouldBeFalse +import org.amshove.kluent.shouldBeTrue +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters +import timber.log.Timber +import java.util.concurrent.CountDownLatch + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class TimelineForwardPaginationTest : InstrumentedTest { + + private val commonTestHelper = CommonTestHelper(context()) + private val cryptoTestHelper = CryptoTestHelper(commonTestHelper) + + /** + * This test ensure that if we click to permalink, we will be able to go back to the live + */ + @Test + fun forwardPaginationTest() { + val numberOfMessagesToSend = 90 + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false) + + val aliceSession = cryptoTestData.firstSession + val aliceRoomId = cryptoTestData.roomId + + aliceSession.cryptoService().setWarnOnUnknownDevices(false) + + val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!! + + // Alice sends X messages + val message = "Message from Alice" + val sentMessages = commonTestHelper.sendTextMessage( + roomFromAlicePOV, + message, + numberOfMessagesToSend) + + // Alice clear the cache + commonTestHelper.doSync { + aliceSession.clearCache(it) + } + + // And restarts the sync + aliceSession.startSync(true) + + val aliceTimeline = roomFromAlicePOV.createTimeline(null, TimelineSettings(30)) + aliceTimeline.start() + + // Alice sees the 10 last message of the room, and can only navigate BACKWARD + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Alice timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root.content}") + } + + // Ok, we have the 10 last messages of the initial sync + snapshot.size == 10 + && snapshot.all { it.root.content.toModel()?.body?.startsWith(message).orFalse() } + } + + // Open the timeline at last sent message + aliceTimeline.addListener(eventsListener) + commonTestHelper.await(lock) + aliceTimeline.removeAllListeners() + + aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue() + aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() + } + + // Alice navigates to the first message of the room, which is not in its database. A GET /context is performed + // Then she can paginate BACKWARD and FORWARD + run { + val lock = CountDownLatch(1) + val aliceEventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Alice timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root.content}") + } + + // The event is not in db, so it is fetch alone + snapshot.size == 1 + && snapshot.all { it.root.content.toModel()?.body?.startsWith("Message from Alice").orFalse() } + } + + aliceTimeline.addListener(aliceEventsListener) + + // Restart the timeline to the first sent event + aliceTimeline.restartWithEventId(sentMessages.last().eventId) + + commonTestHelper.await(lock) + aliceTimeline.removeAllListeners() + + aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue() + aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue() + } + + // Alice paginates BACKWARD and FORWARD of 50 events each + // Then she can only navigate FORWARD + run { + val lock = CountDownLatch(1) + val aliceEventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Alice timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root.content}") + } + + // Alice can see the first event of the room (so Back pagination has worked) + snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE + // 6 for room creation item (backward pagination), 1 for the context, and 50 for the forward pagination + && snapshot.size == 6 + 1 + 50 + } + + aliceTimeline.addListener(aliceEventsListener) + + // Restart the timeline to the first sent event + // We ask to load event backward and forward + aliceTimeline.paginate(Timeline.Direction.BACKWARDS, 50) + aliceTimeline.paginate(Timeline.Direction.FORWARDS, 50) + + commonTestHelper.await(lock) + aliceTimeline.removeAllListeners() + + aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue() + aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() + } + + // Alice paginates once again FORWARD for 50 events + // All the timeline is retrieved, she cannot paginate anymore in both direction + run { + val lock = CountDownLatch(1) + val aliceEventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Alice timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root.content}") + } + // 6 for room creation item (backward pagination),and numberOfMessagesToSend (all the message of the room) + snapshot.size == 6 + numberOfMessagesToSend + && snapshot.checkSendOrder(message, numberOfMessagesToSend, 0) + } + + aliceTimeline.addListener(aliceEventsListener) + + // Ask for a forward pagination + aliceTimeline.paginate(Timeline.Direction.FORWARDS, 50) + + commonTestHelper.await(lock) + aliceTimeline.removeAllListeners() + + // The timeline is fully loaded + aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() + aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() + } + + aliceTimeline.dispose() + + cryptoTestData.cleanUp(commonTestHelper) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelinePreviousLastForwardTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelinePreviousLastForwardTest.kt new file mode 100644 index 0000000000..28ce75c221 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelinePreviousLastForwardTest.kt @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.session.room.timeline + +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.timeline.Timeline +import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.checkSendOrder +import org.amshove.kluent.shouldBeFalse +import org.amshove.kluent.shouldBeTrue +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters +import timber.log.Timber +import java.util.concurrent.CountDownLatch + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class TimelinePreviousLastForwardTest : InstrumentedTest { + + private val commonTestHelper = CommonTestHelper(context()) + private val cryptoTestHelper = CryptoTestHelper(commonTestHelper) + + /** + * This test ensure that if we have a chunk in the timeline which is due to a sync, and we click to permalink, we will be able to go back to the live + */ + @Test + fun previousLastForwardTest() { + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false) + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession!! + val aliceRoomId = cryptoTestData.roomId + + aliceSession.cryptoService().setWarnOnUnknownDevices(false) + bobSession.cryptoService().setWarnOnUnknownDevices(false) + + val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!! + val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!! + + val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(30)) + bobTimeline.start() + + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Bob timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root}") + } + + // Ok, we have the 8 first messages of the initial sync (room creation and bob invite and join events) + snapshot.size == 8 + } + + bobTimeline.addListener(eventsListener) + commonTestHelper.await(lock) + bobTimeline.removeAllListeners() + + bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() + bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() + } + + // Bob stop to sync + bobSession.stopSync() + + val firstMessage = "First messages from Alice" + // Alice sends 30 messages + val firstMessageFromAliceId = commonTestHelper.sendTextMessage( + roomFromAlicePOV, + firstMessage, + 30) + .last() + .eventId + + // Bob start to sync + bobSession.startSync(true) + + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Bob timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root}") + } + + // Ok, we have the 10 last messages from Alice. This will be our future previous lastForward chunk + snapshot.size == 10 + && snapshot.all { it.root.content.toModel()?.body?.startsWith(firstMessage).orFalse() } + } + + bobTimeline.addListener(eventsListener) + commonTestHelper.await(lock) + bobTimeline.removeAllListeners() + + bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue() + bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() + } + + // Bob stop to sync + bobSession.stopSync() + + val secondMessage = "Second messages from Alice" + // Alice sends again 30 messages + commonTestHelper.sendTextMessage( + roomFromAlicePOV, + secondMessage, + 30) + + // Bob start to sync + bobSession.startSync(true) + + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Bob timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root}") + } + + // Ok, we have the 10 last messages from Alice. This will be our future previous lastForward chunk + snapshot.size == 10 + && snapshot.all { it.root.content.toModel()?.body?.startsWith(secondMessage).orFalse() } + } + + bobTimeline.addListener(eventsListener) + commonTestHelper.await(lock) + bobTimeline.removeAllListeners() + + bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue() + bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() + } + + // Bob navigate to the first message sent from Alice + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Bob timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root}") + } + + // The event is not in db, so it is fetch + snapshot.size == 1 + } + + bobTimeline.addListener(eventsListener) + + // Restart the timeline to the first sent event, and paginate in both direction + bobTimeline.restartWithEventId(firstMessageFromAliceId) + bobTimeline.paginate(Timeline.Direction.BACKWARDS, 50) + bobTimeline.paginate(Timeline.Direction.FORWARDS, 50) + + commonTestHelper.await(lock) + bobTimeline.removeAllListeners() + + bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue() + bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue() + } + + // Paginate in both direction + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Bob timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root}") + } + + snapshot.size == 8 + 1 + 35 + } + + bobTimeline.addListener(eventsListener) + + // Paginate in both direction + bobTimeline.paginate(Timeline.Direction.BACKWARDS, 50) + // Ensure the chunk in the middle is included in the next pagination + bobTimeline.paginate(Timeline.Direction.FORWARDS, 35) + + commonTestHelper.await(lock) + bobTimeline.removeAllListeners() + + bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue() + bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() + } + + // Bob scroll to the future, till the live + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Bob timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root}") + } + + // Bob can see the first event of the room (so Back pagination has worked) + snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE + // 8 for room creation item 60 message from Alice + && snapshot.size == 8 + 60 + && snapshot.checkSendOrder(secondMessage, 30, 0) + && snapshot.checkSendOrder(firstMessage, 30, 30) + } + + bobTimeline.addListener(eventsListener) + + bobTimeline.paginate(Timeline.Direction.FORWARDS, 50) + + commonTestHelper.await(lock) + bobTimeline.removeAllListeners() + + bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() + bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() + } + + bobTimeline.dispose() + + cryptoTestData.cleanUp(commonTestHelper) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineTest.kt new file mode 100644 index 0000000000..b0da49cdbb --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineTest.kt @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.session.room.timeline + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.InstrumentedTest + +internal class TimelineTest : InstrumentedTest { + + companion object { + private const val ROOM_ID = "roomId" + } + + private lateinit var monarchy: Monarchy + +// @Before +// fun setup() { +// Timber.plant(Timber.DebugTree()) +// Realm.init(context()) +// val testConfiguration = RealmConfiguration.Builder().name("test-realm") +// .modules(SessionRealmModule()).build() +// +// Realm.deleteRealm(testConfiguration) +// monarchy = Monarchy.Builder().setRealmConfiguration(testConfiguration).build() +// RoomDataHelper.fakeInitialSync(monarchy, ROOM_ID) +// } +// +// private fun createTimeline(initialEventId: String? = null): Timeline { +// val taskExecutor = TaskExecutor(testCoroutineDispatchers) +// val tokenChunkEventPersistor = TokenChunkEventPersistor(monarchy) +// val paginationTask = FakePaginationTask @Inject constructor(tokenChunkEventPersistor) +// val getContextOfEventTask = FakeGetContextOfEventTask @Inject constructor(tokenChunkEventPersistor) +// val roomMemberExtractor = SenderRoomMemberExtractor(ROOM_ID) +// val timelineEventFactory = TimelineEventFactory(roomMemberExtractor, EventRelationExtractor()) +// return DefaultTimeline( +// ROOM_ID, +// initialEventId, +// monarchy.realmConfiguration, +// taskExecutor, +// getContextOfEventTask, +// timelineEventFactory, +// paginationTask, +// null) +// } +// +// @Test +// fun backPaginate_shouldLoadMoreEvents_whenPaginateIsCalled() { +// val timeline = createTimeline() +// timeline.start() +// val paginationCount = 30 +// var initialLoad = 0 +// val latch = CountDownLatch(2) +// var timelineEvents: List = emptyList() +// timeline.listener = object : Timeline.Listener { +// override fun onTimelineUpdated(snapshot: List) { +// if (snapshot.isNotEmpty()) { +// if (initialLoad == 0) { +// initialLoad = snapshot.size +// } +// timelineEvents = snapshot +// latch.countDown() +// timeline.paginate(Timeline.Direction.BACKWARDS, paginationCount) +// } +// } +// } +// latch.await() +// timelineEvents.size shouldEqual initialLoad + paginationCount +// timeline.dispose() +// } +} diff --git a/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/database/RealmDebugTools.kt b/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/database/RealmDebugTools.kt new file mode 100644 index 0000000000..324a3c1062 --- /dev/null +++ b/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/database/RealmDebugTools.kt @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.internal.database + +import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMetadataEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.KeyInfoEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity +import io.realm.Realm +import io.realm.RealmConfiguration +import io.realm.kotlin.where +import timber.log.Timber + +object RealmDebugTools { + /** + * Log info about the crypto DB + */ + fun dumpCryptoDb(realmConfiguration: RealmConfiguration) { + Realm.getInstance(realmConfiguration).use { + Timber.d("Realm located at : ${realmConfiguration.realmDirectory}/${realmConfiguration.realmFileName}") + + val key = realmConfiguration.encryptionKey.joinToString("") { byte -> "%02x".format(byte) } + Timber.d("Realm encryption key : $key") + + // Check if we have data + Timber.e("Realm is empty: ${it.isEmpty}") + + Timber.d("Realm has CryptoMetadataEntity: ${it.where().count()}") + Timber.d("Realm has CryptoRoomEntity: ${it.where().count()}") + Timber.d("Realm has DeviceInfoEntity: ${it.where().count()}") + Timber.d("Realm has KeysBackupDataEntity: ${it.where().count()}") + Timber.d("Realm has OlmInboundGroupSessionEntity: ${it.where().count()}") + Timber.d("Realm has OlmSessionEntity: ${it.where().count()}") + Timber.d("Realm has UserEntity: ${it.where().count()}") + Timber.d("Realm has KeyInfoEntity: ${it.where().count()}") + Timber.d("Realm has CrossSigningInfoEntity: ${it.where().count()}") + Timber.d("Realm has TrustLevelEntity: ${it.where().count()}") + Timber.d("Realm has GossipingEventEntity: ${it.where().count()}") + Timber.d("Realm has IncomingGossipingRequestEntity: ${it.where().count()}") + Timber.d("Realm has OutgoingGossipingRequestEntity: ${it.where().count()}") + Timber.d("Realm has MyDeviceLastSeenInfoEntity: ${it.where().count()}") + } + } +} diff --git a/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/CurlLoggingInterceptor.kt b/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/CurlLoggingInterceptor.kt new file mode 100644 index 0000000000..ee2c6076cc --- /dev/null +++ b/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/CurlLoggingInterceptor.kt @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2016 Jeff Gilfelt. + * Copyright 2019 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.internal.network.interceptors + +import org.matrix.android.sdk.internal.di.MatrixScope +import okhttp3.Interceptor +import okhttp3.Response +import okio.Buffer +import timber.log.Timber +import java.io.IOException +import java.nio.charset.Charset +import javax.inject.Inject + +/** + * An OkHttp interceptor that logs requests as curl shell commands. They can then + * be copied, pasted and executed inside a terminal environment. This might be + * useful for troubleshooting client/server API interaction during development, + * making it easy to isolate and share requests made by the app.

Warning: The + * logs generated by this interceptor have the potential to leak sensitive + * information. It should only be used in a controlled manner or in a + * non-production environment. + */ +@MatrixScope +internal class CurlLoggingInterceptor @Inject constructor() + : Interceptor { + + /** + * Set any additional curl command options (see 'curl --help'). + */ + var curlOptions: String? = null + + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + + var compressed = false + + var curlCmd = "curl" + curlOptions?.let { + curlCmd += " $it" + } + curlCmd += " -X " + request.method + + val requestBody = request.body + if (requestBody != null) { + if (requestBody.contentLength() > 100_000) { + Timber.w("Unable to log curl command data, size is too big (${requestBody.contentLength()})") + // Ensure the curl command will failed + curlCmd += "DATA IS TOO BIG" + } else { + val buffer = Buffer() + requestBody.writeTo(buffer) + var charset: Charset? = UTF8 + val contentType = requestBody.contentType() + if (contentType != null) { + charset = contentType.charset(UTF8) + } + // try to keep to a single line and use a subshell to preserve any line breaks + curlCmd += " --data $'" + buffer.readString(charset!!).replace("\n", "\\n") + "'" + } + } + + val headers = request.headers + var i = 0 + val count = headers.size + while (i < count) { + val name = headers.name(i) + val value = headers.value(i) + if ("Accept-Encoding".equals(name, ignoreCase = true) && "gzip".equals(value, ignoreCase = true)) { + compressed = true + } + curlCmd += " -H \"$name: $value\"" + i++ + } + + curlCmd += ((if (compressed) " --compressed " else " ") + "'" + request.url.toString() + // Replace localhost for emulator by localhost for shell + .replace("://10.0.2.2:8080/".toRegex(), "://127.0.0.1:8080/") + + "'") + + // Add Json formatting + curlCmd += " | python -m json.tool" + + Timber.d("--- cURL (${request.url})") + Timber.d(curlCmd) + + return chain.proceed(request) + } + + companion object { + private val UTF8 = Charset.forName("UTF-8") + } +} diff --git a/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt b/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt new file mode 100644 index 0000000000..349110aff8 --- /dev/null +++ b/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2019 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.internal.network.interceptors + +import androidx.annotation.NonNull +import org.matrix.android.sdk.BuildConfig +import okhttp3.logging.HttpLoggingInterceptor +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import timber.log.Timber + +class FormattedJsonHttpLogger : HttpLoggingInterceptor.Logger { + + companion object { + private const val INDENT_SPACE = 2 + } + + /** + * Log the message and try to log it again as a JSON formatted string + * Note: it can consume a lot of memory but it is only in DEBUG mode + * + * @param message + */ + @Synchronized + override fun log(@NonNull message: String) { + // In RELEASE there is no log, but for sure, test again BuildConfig.DEBUG + if (BuildConfig.DEBUG) { + Timber.v(message) + + if (message.startsWith("{")) { + // JSON Detected + try { + val o = JSONObject(message) + logJson(o.toString(INDENT_SPACE)) + } catch (e: JSONException) { + // Finally this is not a JSON string... + Timber.e(e) + } + } else if (message.startsWith("[")) { + // JSON Array detected + try { + val o = JSONArray(message) + logJson(o.toString(INDENT_SPACE)) + } catch (e: JSONException) { + // Finally not JSON... + Timber.e(e) + } + } + // Else not a json string to log + } + } + + private fun logJson(formattedJson: String) { + val arr = formattedJson.split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + for (s in arr) { + Timber.v(s) + } + } +} diff --git a/matrix-sdk-android/src/main/AndroidManifest.xml b/matrix-sdk-android/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..52238f824c --- /dev/null +++ b/matrix-sdk-android/src/main/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + diff --git a/matrix-sdk-android/src/main/assets/postMessageAPI.js b/matrix-sdk-android/src/main/assets/postMessageAPI.js new file mode 100755 index 0000000000..4936a78538 --- /dev/null +++ b/matrix-sdk-android/src/main/assets/postMessageAPI.js @@ -0,0 +1,54 @@ +var android_widget_events = {}; + +var sendObjectMessageToRiotAndroid = function(parameters) { + Android.onWidgetEvent(JSON.stringify(parameters)); +}; + +var onWidgetMessageToRiotAndroid = function(event) { + /* Use an internal "_id" field for matching onMessage events and requests + _id was originally used by the Modular API. Keep it */ + if (!event.data._id) { + /* The Matrix Widget API v2 spec says: + "The requestId field should be unique and included in all requests" */ + event.data._id = event.data.requestId; + } + /* Make sure to have one id */ + if (!event.data._id) { + event.data._id = Date.now() + "-" + Math.random().toString(36); + } + + console.log("onWidgetMessageToRiotAndroid " + event.data._id); + + if (android_widget_events[event.data._id]) { + console.log("onWidgetMessageToRiotAndroid : already managed"); + return; + } + + if (!event.origin) { + event.origin = event.originalEvent.origin; + } + + android_widget_events[event.data._id] = event; + + console.log("onWidgetMessageToRiotAndroid : manage " + event.data); + sendObjectMessageToRiotAndroid({'event.data': event.data}); +}; + +var sendResponseFromRiotAndroid = function(eventId, res) { + var event = android_widget_events[eventId]; + + console.log("sendResponseFromRiotAndroid to " + event.data.action + " for "+ eventId + ": " + JSON.stringify(res)); + + var data = JSON.parse(JSON.stringify(event.data)); + + data.response = res; + + console.log("sendResponseFromRiotAndroid ---> " + data); + + event.source.postMessage(data, event.origin); + android_widget_events[eventId] = true; + + console.log("sendResponseFromRiotAndroid to done"); +}; + +window.addEventListener('message', onWidgetMessageToRiotAndroid, false); diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt new file mode 100644 index 0000000000..6cd003ddae --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api + +import android.content.Context +import androidx.lifecycle.ProcessLifecycleOwner +import androidx.work.Configuration +import androidx.work.WorkManager +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.BuildConfig +import org.matrix.android.sdk.api.auth.AuthenticationService +import org.matrix.android.sdk.api.legacy.LegacySessionImporter +import org.matrix.android.sdk.internal.SessionManager +import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt +import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments +import org.matrix.android.sdk.internal.di.DaggerMatrixComponent +import org.matrix.android.sdk.internal.network.UserAgentHolder +import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver +import org.matrix.olm.OlmManager +import java.io.InputStream +import java.util.concurrent.Executors +import java.util.concurrent.atomic.AtomicBoolean +import javax.inject.Inject + +/** + * This is the main entry point to the matrix sdk. + * To get the singleton instance, use getInstance static method. + */ +class Matrix private constructor(context: Context, matrixConfiguration: MatrixConfiguration) { + + @Inject internal lateinit var legacySessionImporter: LegacySessionImporter + @Inject internal lateinit var authenticationService: AuthenticationService + @Inject internal lateinit var userAgentHolder: UserAgentHolder + @Inject internal lateinit var backgroundDetectionObserver: BackgroundDetectionObserver + @Inject internal lateinit var olmManager: OlmManager + @Inject internal lateinit var sessionManager: SessionManager + + init { + Monarchy.init(context) + DaggerMatrixComponent.factory().create(context, matrixConfiguration).inject(this) + if (context.applicationContext !is Configuration.Provider) { + WorkManager.initialize(context, Configuration.Builder().setExecutor(Executors.newCachedThreadPool()).build()) + } + ProcessLifecycleOwner.get().lifecycle.addObserver(backgroundDetectionObserver) + } + + fun getUserAgent() = userAgentHolder.userAgent + + fun authenticationService(): AuthenticationService { + return authenticationService + } + + fun legacySessionImporter(): LegacySessionImporter { + return legacySessionImporter + } + + companion object { + + private lateinit var instance: Matrix + private val isInit = AtomicBoolean(false) + + fun initialize(context: Context, matrixConfiguration: MatrixConfiguration) { + if (isInit.compareAndSet(false, true)) { + instance = Matrix(context.applicationContext, matrixConfiguration) + } + } + + fun getInstance(context: Context): Matrix { + if (isInit.compareAndSet(false, true)) { + val appContext = context.applicationContext + if (appContext is MatrixConfiguration.Provider) { + val matrixConfiguration = (appContext as MatrixConfiguration.Provider).providesMatrixConfiguration() + instance = Matrix(appContext, matrixConfiguration) + } else { + throw IllegalStateException("Matrix is not initialized properly." + + " You should call Matrix.initialize or let your application implements MatrixConfiguration.Provider.") + } + } + return instance + } + + fun getSdkVersion(): String { + return BuildConfig.VERSION_NAME + " (" + BuildConfig.GIT_SDK_REVISION + ")" + } + + fun decryptStream(inputStream: InputStream?, elementToDecrypt: ElementToDecrypt): InputStream? { + return MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixCallback.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixCallback.kt new file mode 100644 index 0000000000..e20d9074a8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixCallback.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api + +/** + * Generic callback interface for asynchronously. + * @param the type of data to return on success + */ +interface MatrixCallback { + + /** + * On success method, default to no-op + * @param data the data successfully returned from the async function + */ + fun onSuccess(data: T) { + // no-op + } + + /** + * On failure method, default to no-op + * @param failure the failure data returned from the async function + */ + fun onFailure(failure: Throwable) { + // no-op + } +} + +/** + * Basic no op implementation + */ +class NoOpMatrixCallback: MatrixCallback diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt new file mode 100644 index 0000000000..bfcc9105eb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api + +import org.matrix.android.sdk.api.crypto.MXCryptoConfig +import java.net.Proxy + +data class MatrixConfiguration( + val applicationFlavor: String = "Default-application-flavor", + val cryptoConfig: MXCryptoConfig = MXCryptoConfig(), + val integrationUIUrl: String = "https://scalar.vector.im/", + val integrationRestUrl: String = "https://scalar.vector.im/api", + val integrationWidgetUrls: List = listOf( + "https://scalar.vector.im/_matrix/integrations/v1", + "https://scalar.vector.im/api", + "https://scalar-staging.vector.im/_matrix/integrations/v1", + "https://scalar-staging.vector.im/api", + "https://scalar-staging.riot.im/scalar/api" + ), + /** + * Optional proxy to connect to the matrix servers + * You can create one using for instance Proxy(proxyType, InetSocketAddress.createUnresolved(hostname, port) + */ + val proxy: Proxy? = null +) { + + /** + * Can be implemented by your Application class + */ + interface Provider { + fun providesMatrixConfiguration(): MatrixConfiguration + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt new file mode 100644 index 0000000000..f6e9a33aee --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt @@ -0,0 +1,150 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api + +/** + * This class contains pattern to match the different Matrix ids + */ +object MatrixPatterns { + + // Note: TLD is not mandatory (localhost, IP address...) + private const val DOMAIN_REGEX = ":[A-Z0-9.-]+(:[0-9]{2,5})?" + + // regex pattern to find matrix user ids in a string. + // See https://matrix.org/speculator/spec/HEAD/appendices.html#historical-user-ids + private const val MATRIX_USER_IDENTIFIER_REGEX = "@[A-Z0-9\\x21-\\x39\\x3B-\\x7F]+$DOMAIN_REGEX" + val PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER = MATRIX_USER_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE) + + // regex pattern to find room ids in a string. + private const val MATRIX_ROOM_IDENTIFIER_REGEX = "![A-Z0-9]+$DOMAIN_REGEX" + private val PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER = MATRIX_ROOM_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE) + + // regex pattern to find room aliases in a string. + private const val MATRIX_ROOM_ALIAS_REGEX = "#[A-Z0-9._%#@=+-]+$DOMAIN_REGEX" + private val PATTERN_CONTAIN_MATRIX_ALIAS = MATRIX_ROOM_ALIAS_REGEX.toRegex(RegexOption.IGNORE_CASE) + + // regex pattern to find message ids in a string. + private const val MATRIX_EVENT_IDENTIFIER_REGEX = "\\$[A-Z0-9]+$DOMAIN_REGEX" + private val PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER = MATRIX_EVENT_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE) + + // regex pattern to find message ids in a string. + private const val MATRIX_EVENT_IDENTIFIER_V3_REGEX = "\\$[A-Z0-9/+]+" + private val PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V3 = MATRIX_EVENT_IDENTIFIER_V3_REGEX.toRegex(RegexOption.IGNORE_CASE) + + // Ref: https://matrix.org/docs/spec/rooms/v4#event-ids + private const val MATRIX_EVENT_IDENTIFIER_V4_REGEX = "\\$[A-Z0-9\\-_]+" + private val PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4 = MATRIX_EVENT_IDENTIFIER_V4_REGEX.toRegex(RegexOption.IGNORE_CASE) + + // regex pattern to find group ids in a string. + private const val MATRIX_GROUP_IDENTIFIER_REGEX = "\\+[A-Z0-9=_\\-./]+$DOMAIN_REGEX" + private val PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER = MATRIX_GROUP_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE) + + // regex pattern to find permalink with message id. + // Android does not support in URL so extract it. + private const val PERMALINK_BASE_REGEX = "https://matrix\\.to/#/" + private const val APP_BASE_REGEX = "https://[A-Z0-9.-]+\\.[A-Z]{2,}/[A-Z]{3,}/#/room/" + const val SEP_REGEX = "/" + + private const val LINK_TO_ROOM_ID_REGEXP = PERMALINK_BASE_REGEX + MATRIX_ROOM_IDENTIFIER_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX + private val PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ID = LINK_TO_ROOM_ID_REGEXP.toRegex(RegexOption.IGNORE_CASE) + + private const val LINK_TO_ROOM_ALIAS_REGEXP = PERMALINK_BASE_REGEX + MATRIX_ROOM_ALIAS_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX + private val PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ALIAS = LINK_TO_ROOM_ALIAS_REGEXP.toRegex(RegexOption.IGNORE_CASE) + + private const val LINK_TO_APP_ROOM_ID_REGEXP = APP_BASE_REGEX + MATRIX_ROOM_IDENTIFIER_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX + private val PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ID = LINK_TO_APP_ROOM_ID_REGEXP.toRegex(RegexOption.IGNORE_CASE) + + private const val LINK_TO_APP_ROOM_ALIAS_REGEXP = APP_BASE_REGEX + MATRIX_ROOM_ALIAS_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX + private val PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ALIAS = LINK_TO_APP_ROOM_ALIAS_REGEXP.toRegex(RegexOption.IGNORE_CASE) + + // list of patterns to find some matrix item. + val MATRIX_PATTERNS = listOf( + PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ID, + PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ALIAS, + PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ID, + PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ALIAS, + PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER, + PATTERN_CONTAIN_MATRIX_ALIAS, + PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER, + PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER, + PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER + ) + + /** + * Tells if a string is a valid user Id. + * + * @param str the string to test + * @return true if the string is a valid user id + */ + fun isUserId(str: String?): Boolean { + return str != null && str matches PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER + } + + /** + * Tells if a string is a valid room id. + * + * @param str the string to test + * @return true if the string is a valid room Id + */ + fun isRoomId(str: String?): Boolean { + return str != null && str matches PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER + } + + /** + * Tells if a string is a valid room alias. + * + * @param str the string to test + * @return true if the string is a valid room alias. + */ + fun isRoomAlias(str: String?): Boolean { + return str != null && str matches PATTERN_CONTAIN_MATRIX_ALIAS + } + + /** + * Tells if a string is a valid event id. + * + * @param str the string to test + * @return true if the string is a valid event id. + */ + fun isEventId(str: String?): Boolean { + return str != null + && (str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER + || str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V3 + || str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4) + } + + /** + * Tells if a string is a valid group id. + * + * @param str the string to test + * @return true if the string is a valid group id. + */ + fun isGroupId(str: String?): Boolean { + return str != null && str matches PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER + } + + /** + * Extract server name from a matrix id + * + * @param matrixId + * @return null if not found or if matrixId is null + */ + fun extractServerNameFromId(matrixId: String?): String? { + return matrixId?.substringAfter(":", missingDelimiterValue = "")?.takeIf { it.isNotEmpty() } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt new file mode 100644 index 0000000000..91e2845cd2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.auth + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import org.matrix.android.sdk.api.auth.data.LoginFlowResult +import org.matrix.android.sdk.api.auth.login.LoginWizard +import org.matrix.android.sdk.api.auth.registration.RegistrationWizard +import org.matrix.android.sdk.api.auth.wellknown.WellknownResult +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.util.Cancelable + +/** + * This interface defines methods to authenticate or to create an account to a matrix server. + */ +interface AuthenticationService { + /** + * Request the supported login flows for this homeserver. + * This is the first method to call to be able to get a wizard to login or the create an account + */ + fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback): Cancelable + + /** + * Request the supported login flows for the corresponding sessionId. + */ + fun getLoginFlowOfSession(sessionId: String, callback: MatrixCallback): Cancelable + + /** + * Return a LoginWizard, to login to the homeserver. The login flow has to be retrieved first. + */ + fun getLoginWizard(): LoginWizard + + /** + * Return a RegistrationWizard, to create an matrix account on the homeserver. The login flow has to be retrieved first. + */ + fun getRegistrationWizard(): RegistrationWizard + + /** + * True when login and password has been sent with success to the homeserver + */ + val isRegistrationStarted: Boolean + + /** + * Cancel pending login or pending registration + */ + fun cancelPendingLoginOrRegistration() + + /** + * Reset all pending settings, including current HomeServerConnectionConfig + */ + fun reset() + + /** + * Check if there is an authenticated [Session]. + * @return true if there is at least one active session. + */ + fun hasAuthenticatedSessions(): Boolean + + /** + * Get the last authenticated [Session], if there is an active session. + * @return the last active session if any, or null + */ + fun getLastAuthenticatedSession(): Session? + + /** + * Create a session after a SSO successful login + */ + fun createSessionFromSso(homeServerConnectionConfig: HomeServerConnectionConfig, + credentials: Credentials, + callback: MatrixCallback): Cancelable + + /** + * Perform a wellknown request, using the domain from the matrixId + */ + fun getWellKnownData(matrixId: String, + homeServerConnectionConfig: HomeServerConnectionConfig?, + callback: MatrixCallback): Cancelable + + /** + * Authenticate with a matrixId and a password + * Usually call this after a successful call to getWellKnownData() + */ + fun directAuthentication(homeServerConnectionConfig: HomeServerConnectionConfig, + matrixId: String, + password: String, + initialDeviceName: String, + callback: MatrixCallback): Cancelable +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/Constants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/Constants.kt new file mode 100644 index 0000000000..590b84f35b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/Constants.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.auth + +/** + * Path to use when the client does not supported any or all login flows + * Ref: https://matrix.org/docs/spec/client_server/latest#login-fallback + * */ +const val LOGIN_FALLBACK_PATH = "/_matrix/static/client/login/" + +/** + * Path to use when the client does not supported any or all registration flows + * Not documented + */ +const val REGISTER_FALLBACK_PATH = "/_matrix/static/client/register/" + +/** + * Path to use when the client want to connect using SSO + * Ref: https://matrix.org/docs/spec/client_server/latest#sso-client-login + */ +const val SSO_REDIRECT_PATH = "/_matrix/client/r0/login/sso/redirect" + +const val SSO_REDIRECT_URL_PARAM = "redirectUrl" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/Credentials.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/Credentials.kt new file mode 100644 index 0000000000..6dfa56f16a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/Credentials.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.auth.data + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.util.md5 + +/** + * This data class hold credentials user data. + * You shouldn't have to instantiate it. + * The access token should be use to authenticate user in all server requests. + * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-login + */ +@JsonClass(generateAdapter = true) +data class Credentials( + /** + * The fully-qualified Matrix ID that has been registered. + */ + @Json(name = "user_id") val userId: String, + /** + * An access token for the account. This access token can then be used to authorize other requests. + */ + @Json(name = "access_token") val accessToken: String, + /** + * Not documented + */ + @Json(name = "refresh_token") val refreshToken: String?, + /** + * The server_name of the homeserver on which the account has been registered. + * @Deprecated. Clients should extract the server_name from user_id (by splitting at the first colon) + * if they require it. Note also that homeserver is not spelt this way. + */ + @Json(name = "home_server") val homeServer: String?, + /** + * ID of the logged-in device. Will be the same as the corresponding parameter in the request, if one was specified. + */ + @Json(name = "device_id") val deviceId: String?, + /** + * Optional client configuration provided by the server. If present, clients SHOULD use the provided object to + * reconfigure themselves, optionally validating the URLs within. + * This object takes the same form as the one returned from .well-known autodiscovery. + */ + @Json(name = "well_known") val discoveryInformation: DiscoveryInformation? = null +) + +internal fun Credentials.sessionId(): String { + return (if (deviceId.isNullOrBlank()) userId else "$userId|$deviceId").md5() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/DiscoveryInformation.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/DiscoveryInformation.kt new file mode 100644 index 0000000000..d5d732ccc2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/DiscoveryInformation.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.auth.data + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This is a light version of Wellknown model, used for login response + * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-login + */ +@JsonClass(generateAdapter = true) +data class DiscoveryInformation( + /** + * Required. Used by clients to discover homeserver information. + */ + @Json(name = "m.homeserver") + val homeServer: WellKnownBaseConfig? = null, + + /** + * Used by clients to discover identity server information. + * Note: matrix.org does not send this field + */ + @Json(name = "m.identity_server") + val identityServer: WellKnownBaseConfig? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/HomeServerConnectionConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/HomeServerConnectionConfig.kt new file mode 100644 index 0000000000..02fab04067 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/HomeServerConnectionConfig.kt @@ -0,0 +1,251 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.auth.data + +import android.net.Uri +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig.Builder +import org.matrix.android.sdk.internal.network.ssl.Fingerprint +import org.matrix.android.sdk.internal.util.ensureTrailingSlash +import okhttp3.CipherSuite +import okhttp3.TlsVersion + +/** + * This data class holds how to connect to a specific Homeserver. + * It's used with [org.matrix.android.sdk.api.auth.AuthenticationService] class. + * You should use the [Builder] to create one. + */ +@JsonClass(generateAdapter = true) +data class HomeServerConnectionConfig( + val homeServerUri: Uri, + val identityServerUri: Uri? = null, + val antiVirusServerUri: Uri? = null, + val allowedFingerprints: List = emptyList(), + val shouldPin: Boolean = false, + val tlsVersions: List? = null, + val tlsCipherSuites: List? = null, + val shouldAcceptTlsExtensions: Boolean = true, + val allowHttpExtension: Boolean = false, + val forceUsageTlsVersions: Boolean = false +) { + + /** + * This builder should be use to create a [HomeServerConnectionConfig] instance. + */ + class Builder { + + private lateinit var homeServerUri: Uri + private var identityServerUri: Uri? = null + private var antiVirusServerUri: Uri? = null + private val allowedFingerprints: MutableList = ArrayList() + private var shouldPin: Boolean = false + private val tlsVersions: MutableList = ArrayList() + private val tlsCipherSuites: MutableList = ArrayList() + private var shouldAcceptTlsExtensions: Boolean = true + private var allowHttpExtension: Boolean = false + private var forceUsageTlsVersions: Boolean = false + + fun withHomeServerUri(hsUriString: String): Builder { + return withHomeServerUri(Uri.parse(hsUriString)) + } + + /** + * @param hsUri The URI to use to connect to the homeserver. + * @return this builder + */ + fun withHomeServerUri(hsUri: Uri): Builder { + if (hsUri.scheme != "http" && hsUri.scheme != "https") { + throw RuntimeException("Invalid home server URI: $hsUri") + } + // ensure trailing / + val hsString = hsUri.toString().ensureTrailingSlash() + homeServerUri = try { + Uri.parse(hsString) + } catch (e: Exception) { + throw RuntimeException("Invalid home server URI: $hsUri") + } + return this + } + + fun withIdentityServerUri(identityServerUriString: String): Builder { + return withIdentityServerUri(Uri.parse(identityServerUriString)) + } + + /** + * @param identityServerUri The URI to use to manage identity. + * @return this builder + */ + fun withIdentityServerUri(identityServerUri: Uri): Builder { + if (identityServerUri.scheme != "http" && identityServerUri.scheme != "https") { + throw RuntimeException("Invalid identity server URI: $identityServerUri") + } + // ensure trailing / + val isString = identityServerUri.toString().ensureTrailingSlash() + this.identityServerUri = try { + Uri.parse(isString) + } catch (e: Exception) { + throw RuntimeException("Invalid identity server URI: $identityServerUri") + } + return this + } + + /** + * @param allowedFingerprints If using SSL, allow server certs that match these fingerprints. + * @return this builder + */ + fun withAllowedFingerPrints(allowedFingerprints: List?): Builder { + if (allowedFingerprints != null) { + this.allowedFingerprints.addAll(allowedFingerprints) + } + return this + } + + /** + * @param pin If true only allow certs matching given fingerprints, otherwise fallback to + * standard X509 checks. + * @return this builder + */ + fun withPin(pin: Boolean): Builder { + this.shouldPin = pin + return this + } + + /** + * @param shouldAcceptTlsExtension + * @return this builder + */ + fun withShouldAcceptTlsExtensions(shouldAcceptTlsExtension: Boolean): Builder { + this.shouldAcceptTlsExtensions = shouldAcceptTlsExtension + return this + } + + /** + * Add an accepted TLS version for TLS connections with the home server. + * + * @param tlsVersion the tls version to add to the set of TLS versions accepted. + * @return this builder + */ + fun addAcceptedTlsVersion(tlsVersion: TlsVersion): Builder { + this.tlsVersions.add(tlsVersion) + return this + } + + /** + * Force the usage of TlsVersion. This can be usefull for device on Android version < 20 + * + * @param forceUsageOfTlsVersions set to true to force the usage of specified TlsVersions (with [.addAcceptedTlsVersion] + * @return this builder + */ + fun forceUsageOfTlsVersions(forceUsageOfTlsVersions: Boolean): Builder { + this.forceUsageTlsVersions = forceUsageOfTlsVersions + return this + } + + /** + * Add a TLS cipher suite to the list of accepted TLS connections with the home server. + * + * @param tlsCipherSuite the tls cipher suite to add. + * @return this builder + */ + fun addAcceptedTlsCipherSuite(tlsCipherSuite: CipherSuite): Builder { + this.tlsCipherSuites.add(tlsCipherSuite) + return this + } + + fun withAntiVirusServerUri(antivirusServerUriString: String?): Builder { + return withAntiVirusServerUri(antivirusServerUriString?.let { Uri.parse(it) }) + } + + /** + * Update the anti-virus server URI. + * + * @param antivirusServerUri the new anti-virus uri. Can be null + * @return this builder + */ + fun withAntiVirusServerUri(antivirusServerUri: Uri?): Builder { + if (null != antivirusServerUri && "http" != antivirusServerUri.scheme && "https" != antivirusServerUri.scheme) { + throw RuntimeException("Invalid antivirus server URI: $antivirusServerUri") + } + this.antiVirusServerUri = antivirusServerUri + return this + } + + /** + * Convenient method to limit the TLS versions and cipher suites for this Builder + * Ref: + * - https://www.ssi.gouv.fr/uploads/2017/02/security-recommendations-for-tls_v1.1.pdf + * - https://developer.android.com/reference/javax/net/ssl/SSLEngine + * + * @param tlsLimitations true to use Tls limitations + * @param enableCompatibilityMode set to true for Android < 20 + * @return this builder + */ + fun withTlsLimitations(tlsLimitations: Boolean, enableCompatibilityMode: Boolean): Builder { + if (tlsLimitations) { + withShouldAcceptTlsExtensions(false) + + // Tls versions + addAcceptedTlsVersion(TlsVersion.TLS_1_2) + addAcceptedTlsVersion(TlsVersion.TLS_1_3) + + forceUsageOfTlsVersions(enableCompatibilityMode) + + // Cipher suites + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256) + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256) + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256) + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256) + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384) + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256) + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256) + + if (enableCompatibilityMode) { + // Adopt some preceding cipher suites for Android < 20 to be able to negotiate + // a TLS session. + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA) + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA) + } + } + return this + } + + fun withAllowHttpConnection(allowHttpExtension: Boolean): Builder { + this.allowHttpExtension = allowHttpExtension + return this + } + + /** + * @return the [HomeServerConnectionConfig] + */ + fun build(): HomeServerConnectionConfig { + return HomeServerConnectionConfig( + homeServerUri, + identityServerUri, + antiVirusServerUri, + allowedFingerprints, + shouldPin, + tlsVersions, + tlsCipherSuites, + shouldAcceptTlsExtensions, + allowHttpExtension, + forceUsageTlsVersions + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt new file mode 100644 index 0000000000..c3686da7dd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.auth.data + +// Either a list of supported login types, or an error if the homeserver is outdated +sealed class LoginFlowResult { + data class Success( + val supportedLoginTypes: List, + val isLoginAndRegistrationSupported: Boolean, + val homeServerUrl: String + ) : LoginFlowResult() + + object OutdatedHomeserver : LoginFlowResult() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowTypes.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowTypes.kt new file mode 100644 index 0000000000..64a1fd88d1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowTypes.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.auth.data + +object LoginFlowTypes { + const val PASSWORD = "m.login.password" + const val OAUTH2 = "m.login.oauth2" + const val EMAIL_CODE = "m.login.email.code" + const val EMAIL_URL = "m.login.email.url" + const val EMAIL_IDENTITY = "m.login.email.identity" + const val MSISDN = "m.login.msisdn" + const val RECAPTCHA = "m.login.recaptcha" + const val DUMMY = "m.login.dummy" + const val TERMS = "m.login.terms" + const val TOKEN = "m.login.token" + const val SSO = "m.login.sso" +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SessionParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SessionParams.kt new file mode 100644 index 0000000000..cbeece7e03 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SessionParams.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.auth.data + +/** + * This data class holds necessary data to open a session. + * You don't have to manually instantiate it. + */ +data class SessionParams( + /** + * Please consider using shortcuts instead + */ + val credentials: Credentials, + + /** + * Please consider using shortcuts instead + */ + val homeServerConnectionConfig: HomeServerConnectionConfig, + + /** + * Set to false if the current token is not valid anymore. Application should not have to use this info. + */ + val isTokenValid: Boolean +) { + /* + * Shortcuts. Usually the application should only need to use these shortcuts + */ + + /** + * The userId of the session (Ex: "@user:domain.org") + */ + val userId = credentials.userId + + /** + * The deviceId of the session (Ex: "ABCDEFGH") + */ + val deviceId = credentials.deviceId + + /** + * The current homeserver Url. It can be different that the homeserver url entered + * during login phase, because a redirection may have occurred + */ + val homeServerUrl = homeServerConnectionConfig.homeServerUri.toString() + + /** + * The current homeserver host + */ + val homeServerHost = homeServerConnectionConfig.homeServerUri.host + + /** + * The default identity server url if any, returned by the homeserver during login phase + */ + val defaultIdentityServerUrl = homeServerConnectionConfig.identityServerUri?.toString() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/WellKnown.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/WellKnown.kt new file mode 100644 index 0000000000..a4bd8badd7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/WellKnown.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.auth.data + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.util.JsonDict + +/** + * https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery + *

+ * {
+ *     "m.homeserver": {
+ *         "base_url": "https://matrix.org"
+ *     },
+ *     "m.identity_server": {
+ *         "base_url": "https://vector.im"
+ *     }
+ *     "m.integrations": {
+ *          "managers": [
+ *              {
+ *                  "api_url": "https://integrations.example.org",
+ *                  "ui_url": "https://integrations.example.org/ui"
+ *              },
+ *              {
+ *                  "api_url": "https://bots.example.org"
+ *              }
+ *          ]
+ *    }
+ * }
+ * 
+ */ +@JsonClass(generateAdapter = true) +data class WellKnown( + @Json(name = "m.homeserver") + val homeServer: WellKnownBaseConfig? = null, + + @Json(name = "m.identity_server") + val identityServer: WellKnownBaseConfig? = null, + + @Json(name = "m.integrations") + val integrations: JsonDict? = null, + + @Json(name = "im.vector.riot.e2ee") + val e2eAdminSetting: E2EWellKnownConfig? = null + +) + +@JsonClass(generateAdapter = true) +data class E2EWellKnownConfig( + @Json(name = "default") + val e2eDefault: Boolean = true +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/WellKnownBaseConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/WellKnownBaseConfig.kt new file mode 100644 index 0000000000..f0b252f973 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/WellKnownBaseConfig.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.auth.data + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery + *
+ * {
+ *     "base_url": "https://vector.im"
+ * }
+ * 
+ */ +@JsonClass(generateAdapter = true) +data class WellKnownBaseConfig( + @Json(name = "base_url") + val baseURL: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt new file mode 100644 index 0000000000..25cf8209fe --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.auth.login + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.util.Cancelable + +interface LoginWizard { + + /** + * @param login the login field + * @param password the password field + * @param deviceName the initial device name + * @param callback the matrix callback on which you'll receive the result of authentication. + * @return return a [Cancelable] + */ + fun login(login: String, + password: String, + deviceName: String, + callback: MatrixCallback): Cancelable + + /** + * Exchange a login token to an access token + */ + fun loginWithToken(loginToken: String, + callback: MatrixCallback): Cancelable + + /** + * Reset user password + */ + fun resetPassword(email: String, + newPassword: String, + callback: MatrixCallback): Cancelable + + /** + * Confirm the new password, once the user has checked his email + */ + fun resetPasswordMailConfirmed(callback: MatrixCallback): Cancelable +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegisterThreePid.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegisterThreePid.kt new file mode 100644 index 0000000000..3dd2b460b2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegisterThreePid.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.auth.registration + +sealed class RegisterThreePid { + data class Email(val email: String) : RegisterThreePid() + data class Msisdn(val msisdn: String, val countryCode: String) : RegisterThreePid() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationResult.kt new file mode 100644 index 0000000000..544cbf63cc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationResult.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.auth.registration + +import org.matrix.android.sdk.api.session.Session + +// Either a session or an object containing data about registration stages +sealed class RegistrationResult { + data class Success(val session: Session) : RegistrationResult() + data class FlowResponse(val flowResult: FlowResult) : RegistrationResult() +} + +data class FlowResult( + val missingStages: List, + val completedStages: List +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationWizard.kt new file mode 100644 index 0000000000..0629915a42 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationWizard.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.auth.registration + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.util.Cancelable + +interface RegistrationWizard { + + fun getRegistrationFlow(callback: MatrixCallback): Cancelable + + fun createAccount(userName: String, password: String, initialDeviceDisplayName: String?, callback: MatrixCallback): Cancelable + + fun performReCaptcha(response: String, callback: MatrixCallback): Cancelable + + fun acceptTerms(callback: MatrixCallback): Cancelable + + fun dummy(callback: MatrixCallback): Cancelable + + fun addThreePid(threePid: RegisterThreePid, callback: MatrixCallback): Cancelable + + fun sendAgainThreePid(callback: MatrixCallback): Cancelable + + fun handleValidateThreePid(code: String, callback: MatrixCallback): Cancelable + + fun checkIfEmailHasBeenValidated(delayMillis: Long, callback: MatrixCallback): Cancelable + + val currentThreePid: String? + + // True when login and password has been sent with success to the homeserver + val isRegistrationStarted: Boolean +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/Stage.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/Stage.kt new file mode 100644 index 0000000000..2635adc733 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/Stage.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.auth.registration + +sealed class Stage(open val mandatory: Boolean) { + + // m.login.recaptcha + data class ReCaptcha(override val mandatory: Boolean, val publicKey: String) : Stage(mandatory) + + // m.login.email.identity + data class Email(override val mandatory: Boolean) : Stage(mandatory) + + // m.login.msisdn + data class Msisdn(override val mandatory: Boolean) : Stage(mandatory) + + // m.login.dummy, can be mandatory if there is no other stages. In this case the account cannot be created by just sending a username + // and a password, the dummy stage has to be done + data class Dummy(override val mandatory: Boolean) : Stage(mandatory) + + // Undocumented yet: m.login.terms + data class Terms(override val mandatory: Boolean, val policies: TermPolicies) : Stage(mandatory) + + // For unknown stages + data class Other(override val mandatory: Boolean, val type: String, val params: Map<*, *>?) : Stage(mandatory) +} + +typealias TermPolicies = Map<*, *> diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/wellknown/WellknownResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/wellknown/WellknownResult.kt new file mode 100644 index 0000000000..a736a4f1be --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/wellknown/WellknownResult.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.auth.wellknown + +import org.matrix.android.sdk.api.auth.data.WellKnown + +/** + * Ref: https://matrix.org/docs/spec/client_server/latest#well-known-uri + */ +sealed class WellknownResult { + /** + * The provided matrixId is no valid. Unable to extract a domain name. + */ + object InvalidMatrixId : WellknownResult() + + /** + * Retrieve the specific piece of information from the user in a way which fits within the existing client user experience, + * if the client is inclined to do so. Failure can take place instead if no good user experience for this is possible at this point. + */ + data class Prompt(val homeServerUrl: String, + val identityServerUrl: String?, + val wellKnown: WellKnown) : WellknownResult() + + /** + * Stop the current auto-discovery mechanism. If no more auto-discovery mechanisms are available, + * then the client may use other methods of determining the required parameters, such as prompting the user, or using default values. + */ + object Ignore : WellknownResult() + + /** + * Inform the user that auto-discovery failed due to invalid/empty data and PROMPT for the parameter. + */ + object FailPrompt : WellknownResult() + + /** + * Inform the user that auto-discovery did not return any usable URLs. Do not continue further with the current login process. + * At this point, valid data was obtained, but no homeserver is available to serve the client. + * No further guess should be attempted and the user should make a conscientious decision what to do next. + */ + object FailError : WellknownResult() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/comparators/DatedObjectComparators.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/comparators/DatedObjectComparators.kt new file mode 100644 index 0000000000..409fec4437 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/comparators/DatedObjectComparators.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.comparators + +import org.matrix.android.sdk.api.interfaces.DatedObject + +object DatedObjectComparators { + + /** + * Comparator to sort DatedObjects from the oldest to the latest. + */ + val ascComparator by lazy { + Comparator { datedObject1, datedObject2 -> + (datedObject1.date - datedObject2.date).toInt() + } + } + + /** + * Comparator to sort DatedObjects from the latest to the oldest. + */ + val descComparator by lazy { + Comparator { datedObject1, datedObject2 -> + (datedObject2.date - datedObject1.date).toInt() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/Emojis.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/Emojis.kt new file mode 100644 index 0000000000..97454684a3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/Emojis.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.crypto + +import org.matrix.android.sdk.api.session.crypto.verification.EmojiRepresentation +import org.matrix.android.sdk.internal.crypto.verification.getEmojiForCode + +/** + * Provide all the emojis used for SAS verification (for debug purpose) + */ +fun getAllVerificationEmojis(): List { + return (0..63).map { getEmojiForCode(it) } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/MXCryptoConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/MXCryptoConfig.kt new file mode 100644 index 0000000000..9eae1265f0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/MXCryptoConfig.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.crypto + +/** + * Class to define the parameters used to customize or configure the end-to-end crypto. + */ +data class MXCryptoConfig constructor( + // Tell whether the encryption of the event content is enabled for the invited members. + // SDK clients can disable this by settings it to false. + // Note that the encryption for the invited members will be blocked if the history visibility is "joined". + val enableEncryptionForInvitedMembers: Boolean = true, + + /** + * If set to true, the SDK will automatically ignore room key request (gossiping) + * coming from your other untrusted sessions (or blocked). + * If set to false, the request will be forwarded to the application layer; in this + * case the application can decide to prompt the user. + */ + val discardRoomKeyRequestsFromUntrustedDevices: Boolean = true +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/RoomEncryptionTrustLevel.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/RoomEncryptionTrustLevel.kt new file mode 100644 index 0000000000..23f21f8829 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/RoomEncryptionTrustLevel.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.crypto + +/** + * RoomEncryptionTrustLevel represents the trust level in an encrypted room. + */ +enum class RoomEncryptionTrustLevel { + // No one in the room has been verified -> Black shield + Default, + + // There are one or more device un-verified -> the app should display a red shield + Warning, + + // All devices in the room are verified -> the app should display a green shield + Trusted +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/Booleans.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/Booleans.kt new file mode 100644 index 0000000000..606f321196 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/Booleans.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.extensions + +fun Boolean?.orTrue() = this ?: true + +fun Boolean?.orFalse() = this ?: false diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/MatrixSdkExtensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/MatrixSdkExtensions.kt new file mode 100644 index 0000000000..2f3c8c13c5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/MatrixSdkExtensions.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.extensions + +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo + +/* ========================================================================================== + * MXDeviceInfo + * ========================================================================================== */ + +fun CryptoDeviceInfo.getFingerprintHumanReadable() = fingerprint() + ?.chunked(4) + ?.joinToString(separator = " ") + +/* ========================================================================================== + * DeviceInfo + * ========================================================================================== */ + +fun List.sortByLastSeen(): List { + return this.sortedByDescending { it.lastSeenTs ?: 0 } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/Strings.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/Strings.kt new file mode 100644 index 0000000000..f25898077a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/Strings.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.extensions + +fun CharSequence.ensurePrefix(prefix: CharSequence): CharSequence { + return when { + startsWith(prefix) -> this + else -> "$prefix$this" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/Try.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/Try.kt new file mode 100644 index 0000000000..baae9b70f5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/Try.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.extensions + +import timber.log.Timber + +inline fun tryThis(message: String? = null, operation: () -> A): A? { + return try { + operation() + } catch (any: Throwable) { + if (message != null) { + Timber.e(any, message) + } + null + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt new file mode 100644 index 0000000000..8caed519b2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.failure + +import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse +import org.matrix.android.sdk.internal.di.MoshiProvider +import java.io.IOException +import javax.net.ssl.HttpsURLConnection + +fun Throwable.is401() = + this is Failure.ServerError + && httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED /* 401 */ + && error.code == MatrixError.M_UNAUTHORIZED + +fun Throwable.isTokenError() = + this is Failure.ServerError + && (error.code == MatrixError.M_UNKNOWN_TOKEN || error.code == MatrixError.M_MISSING_TOKEN) + +fun Throwable.shouldBeRetried(): Boolean { + return this is Failure.NetworkConnection + || this is IOException + || (this is Failure.ServerError && error.code == MatrixError.M_LIMIT_EXCEEDED) +} + +fun Throwable.isInvalidPassword(): Boolean { + return this is Failure.ServerError + && error.code == MatrixError.M_FORBIDDEN + && error.message == "Invalid password" +} + +/** + * Try to convert to a RegistrationFlowResponse. Return null in the cases it's not possible + */ +fun Throwable.toRegistrationFlowResponse(): RegistrationFlowResponse? { + return if (this is Failure.OtherServerError && this.httpCode == 401) { + tryThis { + MoshiProvider.providesMoshi() + .adapter(RegistrationFlowResponse::class.java) + .fromJson(this.errorBody) + } + } else { + null + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Failure.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Failure.kt new file mode 100644 index 0000000000..a930d7d633 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Failure.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.failure + +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse +import org.matrix.android.sdk.internal.network.ssl.Fingerprint +import java.io.IOException + +/** + * This class allows to expose different kinds of error to be then handled by the application. + * As it is a sealed class, you typically use it like that : + * when(failure) { + * is NetworkConnection -> Unit + * is ServerError -> Unit + * is Unknown -> Unit + * } + */ +sealed class Failure(cause: Throwable? = null) : Throwable(cause = cause) { + data class Unknown(val throwable: Throwable? = null) : Failure(throwable) + data class Cancelled(val throwable: Throwable? = null) : Failure(throwable) + data class UnrecognizedCertificateFailure(val url: String, val fingerprint: Fingerprint) : Failure() + data class NetworkConnection(val ioException: IOException? = null) : Failure(ioException) + data class ServerError(val error: MatrixError, val httpCode: Int) : Failure(RuntimeException(error.toString())) + object SuccessError : Failure(RuntimeException(RuntimeException("SuccessResult is false"))) + + // When server send an error, but it cannot be interpreted as a MatrixError + data class OtherServerError(val errorBody: String, val httpCode: Int) : Failure(RuntimeException("HTTP $httpCode: $errorBody")) + + data class RegistrationFlowError(val registrationFlowResponse: RegistrationFlowResponse) : Failure(RuntimeException(registrationFlowResponse.toString())) + + data class CryptoError(val error: MXCryptoError) : Failure(error) + + abstract class FeatureFailure : Failure() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/GlobalError.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/GlobalError.kt new file mode 100644 index 0000000000..053ad670b9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/GlobalError.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.failure + +import org.matrix.android.sdk.internal.network.ssl.Fingerprint + +// This class will be sent to the bus +sealed class GlobalError { + data class InvalidToken(val softLogout: Boolean) : GlobalError() + data class ConsentNotGivenError(val consentUri: String) : GlobalError() + data class CertificateError(val fingerprint: Fingerprint) : GlobalError() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/MatrixError.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/MatrixError.kt new file mode 100644 index 0000000000..ff68107ffc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/MatrixError.kt @@ -0,0 +1,144 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.failure + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This data class holds the error defined by the matrix specifications. + * You shouldn't have to instantiate it. + * Ref: https://matrix.org/docs/spec/client_server/latest#api-standards + */ +@JsonClass(generateAdapter = true) +data class MatrixError( + /** unique string which can be used to handle an error message */ + @Json(name = "errcode") val code: String, + /** human-readable error message */ + @Json(name = "error") val message: String, + + // For M_CONSENT_NOT_GIVEN + @Json(name = "consent_uri") val consentUri: String? = null, + // For M_RESOURCE_LIMIT_EXCEEDED + @Json(name = "limit_type") val limitType: String? = null, + @Json(name = "admin_contact") val adminUri: String? = null, + // For M_LIMIT_EXCEEDED + @Json(name = "retry_after_ms") val retryAfterMillis: Long? = null, + // For M_UNKNOWN_TOKEN + @Json(name = "soft_logout") val isSoftLogout: Boolean = false, + // For M_INVALID_PEPPER + // {"error": "pepper does not match 'erZvr'", "lookup_pepper": "pQgMS", "algorithm": "sha256", "errcode": "M_INVALID_PEPPER"} + @Json(name = "lookup_pepper") val newLookupPepper: String? = null +) { + + companion object { + /** Forbidden access, e.g. joining a room without permission, failed login. */ + const val M_FORBIDDEN = "M_FORBIDDEN" + /** An unknown error has occurred. */ + const val M_UNKNOWN = "M_UNKNOWN" + /** The access token specified was not recognised. */ + const val M_UNKNOWN_TOKEN = "M_UNKNOWN_TOKEN" + /** No access token was specified for the request. */ + const val M_MISSING_TOKEN = "M_MISSING_TOKEN" + /** Request contained valid JSON, but it was malformed in some way, e.g. missing required keys, invalid values for keys. */ + const val M_BAD_JSON = "M_BAD_JSON" + /** Request did not contain valid JSON. */ + const val M_NOT_JSON = "M_NOT_JSON" + /** No resource was found for this request. */ + const val M_NOT_FOUND = "M_NOT_FOUND" + /** Too many requests have been sent in a short period of time. Wait a while then try again. */ + const val M_LIMIT_EXCEEDED = "M_LIMIT_EXCEEDED" + + /* ========================================================================================== + * Other error codes the client might encounter are + * ========================================================================================== */ + + /** Encountered when trying to register a user ID which has been taken. */ + const val M_USER_IN_USE = "M_USER_IN_USE" + /** Sent when the room alias given to the createRoom API is already in use. */ + const val M_ROOM_IN_USE = "M_ROOM_IN_USE" + /** (Not documented yet) */ + const val M_BAD_PAGINATION = "M_BAD_PAGINATION" + /** The request was not correctly authorized. Usually due to login failures. */ + const val M_UNAUTHORIZED = "M_UNAUTHORIZED" + /** (Not documented yet) */ + const val M_OLD_VERSION = "M_OLD_VERSION" + /** The server did not understand the request. */ + const val M_UNRECOGNIZED = "M_UNRECOGNIZED" + /** (Not documented yet) */ + const val M_LOGIN_EMAIL_URL_NOT_YET = "M_LOGIN_EMAIL_URL_NOT_YET" + /** Authentication could not be performed on the third party identifier. */ + const val M_THREEPID_AUTH_FAILED = "M_THREEPID_AUTH_FAILED" + /** Sent when a threepid given to an API cannot be used because no record matching the threepid was found. */ + const val M_THREEPID_NOT_FOUND = "M_THREEPID_NOT_FOUND" + /** Sent when a threepid given to an API cannot be used because the same threepid is already in use. */ + const val M_THREEPID_IN_USE = "M_THREEPID_IN_USE" + /** The client's request used a third party server, eg. identity server, that this server does not trust. */ + const val M_SERVER_NOT_TRUSTED = "M_SERVER_NOT_TRUSTED" + /** The request or entity was too large. */ + const val M_TOO_LARGE = "M_TOO_LARGE" + /** (Not documented yet) */ + const val M_CONSENT_NOT_GIVEN = "M_CONSENT_NOT_GIVEN" + /** The request cannot be completed because the homeserver has reached a resource limit imposed on it. For example, + * a homeserver held in a shared hosting environment may reach a resource limit if it starts using too much memory + * or disk space. The error MUST have an admin_contact field to provide the user receiving the error a place to reach + * out to. Typically, this error will appear on routes which attempt to modify state (eg: sending messages, account + * data, etc) and not routes which only read state (eg: /sync, get account data, etc). */ + const val M_RESOURCE_LIMIT_EXCEEDED = "M_RESOURCE_LIMIT_EXCEEDED" + /** The user ID associated with the request has been deactivated. Typically for endpoints that prove authentication, such as /login. */ + const val M_USER_DEACTIVATED = "M_USER_DEACTIVATED" + /** Encountered when trying to register a user ID which is not valid. */ + const val M_INVALID_USERNAME = "M_INVALID_USERNAME" + /** Sent when the initial state given to the createRoom API is invalid. */ + const val M_INVALID_ROOM_STATE = "M_INVALID_ROOM_STATE" + /** The server does not permit this third party identifier. This may happen if the server only permits, + * for example, email addresses from a particular domain. */ + const val M_THREEPID_DENIED = "M_THREEPID_DENIED" + /** The client's request to create a room used a room version that the server does not support. */ + const val M_UNSUPPORTED_ROOM_VERSION = "M_UNSUPPORTED_ROOM_VERSION" + /** The client attempted to join a room that has a version the server does not support. + * Inspect the room_version property of the error response for the room's version. */ + const val M_INCOMPATIBLE_ROOM_VERSION = "M_INCOMPATIBLE_ROOM_VERSION" + /** The state change requested cannot be performed, such as attempting to unban a user who is not banned. */ + const val M_BAD_STATE = "M_BAD_STATE" + /** The room or resource does not permit guests to access it. */ + const val M_GUEST_ACCESS_FORBIDDEN = "M_GUEST_ACCESS_FORBIDDEN" + /** A Captcha is required to complete the request. */ + const val M_CAPTCHA_NEEDED = "M_CAPTCHA_NEEDED" + /** The Captcha provided did not match what was expected. */ + const val M_CAPTCHA_INVALID = "M_CAPTCHA_INVALID" + /** A required parameter was missing from the request. */ + const val M_MISSING_PARAM = "M_MISSING_PARAM" + /** A parameter that was specified has the wrong value. For example, the server expected an integer and instead received a string. */ + const val M_INVALID_PARAM = "M_INVALID_PARAM" + /** The resource being requested is reserved by an application service, or the application service making the request has not created the resource. */ + const val M_EXCLUSIVE = "M_EXCLUSIVE" + /** The user is unable to reject an invite to join the server notices room. See the Server Notices module for more information. */ + const val M_CANNOT_LEAVE_SERVER_NOTICE_ROOM = "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM" + /** (Not documented yet) */ + const val M_WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION" + + const val M_TERMS_NOT_SIGNED = "M_TERMS_NOT_SIGNED" + + // For identity service + const val M_INVALID_PEPPER = "M_INVALID_PEPPER" + + // Possible value for "limit_type" + const val LIMIT_TYPE_MAU = "monthly_active_user" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/interfaces/DatedObject.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/interfaces/DatedObject.kt new file mode 100644 index 0000000000..b1296c8aa3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/interfaces/DatedObject.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.interfaces + +/** + * Can be implemented by any object containing a timestamp. + * This interface can be use to sort such object + */ +interface DatedObject { + val date: Long +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/legacy/LegacySessionImporter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/legacy/LegacySessionImporter.kt new file mode 100644 index 0000000000..05128005cc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/legacy/LegacySessionImporter.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.legacy + +interface LegacySessionImporter { + + /** + * Will eventually import a session created by the legacy app. + * @return true if a session has been imported + */ + fun process(): Boolean +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/listeners/ProgressListener.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/listeners/ProgressListener.kt new file mode 100644 index 0000000000..9d6c9387f1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/listeners/ProgressListener.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.listeners + +/** + * Interface to send a progress info + */ +interface ProgressListener { + /** + * Will be invoked on the background thread, not in UI thread. + * @param progress from 0 to total by contract + * @param total + */ + fun onProgress(progress: Int, total: Int) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/listeners/StepProgressListener.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/listeners/StepProgressListener.kt new file mode 100644 index 0000000000..afe6ac51bd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/listeners/StepProgressListener.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.listeners + +/** + * Interface to send a progress info + */ +interface StepProgressListener { + + sealed class Step { + data class ComputingKey(val progress: Int, val total: Int) : Step() + object DownloadingKey : Step() + data class ImportingKey(val progress: Int, val total: Int) : Step() + } + + /** + * @param step The current step, containing progress data if available. Else you should consider progress as indeterminate + */ + fun onStepProgress(step: Step) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/permalinks/MatrixLinkify.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/permalinks/MatrixLinkify.kt new file mode 100644 index 0000000000..b7e719d6db --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/permalinks/MatrixLinkify.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.permalinks + +import android.text.Spannable + +/** + * MatrixLinkify take a piece of text and turns all of the + * matrix patterns matches in the text into clickable links. + */ +object MatrixLinkify { + + /** + * Find the matrix spans i.e matrix id , user id ... to display them as URL. + * + * @param spannable the text in which the matrix items has to be clickable. + */ + @Suppress("UNUSED_PARAMETER") + fun addLinks(spannable: Spannable, callback: MatrixPermalinkSpan.Callback?): Boolean { + /** + * I disable it because it mess up with pills, and even with pills, it does not work correctly: + * The url is not correct. Ex: for @user:matrix.org, the url will be @user:matrix.org, instead of a matrix.to + */ + /* + // sanity checks + if (spannable.isEmpty()) { + return false + } + val text = spannable.toString() + var hasMatch = false + for (pattern in MatrixPatterns.MATRIX_PATTERNS) { + for (match in pattern.findAll(spannable)) { + hasMatch = true + val startPos = match.range.first + if (startPos == 0 || text[startPos - 1] != '/') { + val endPos = match.range.last + 1 + val url = text.substring(match.range) + val span = MatrixPermalinkSpan(url, callback) + spannable.setSpan(span, startPos, endPos, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } + } + } + return hasMatch + */ + return false + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/permalinks/MatrixPermalinkSpan.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/permalinks/MatrixPermalinkSpan.kt new file mode 100644 index 0000000000..15957d359a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/permalinks/MatrixPermalinkSpan.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.permalinks + +import android.text.style.ClickableSpan +import android.view.View +import org.matrix.android.sdk.api.permalinks.MatrixPermalinkSpan.Callback + +/** + * This MatrixPermalinkSpan is a clickable span which use a [Callback] to communicate back. + * @param url the permalink url tied to the span + * @param callback the callback to use. + */ +class MatrixPermalinkSpan(private val url: String, + private val callback: Callback? = null) : ClickableSpan() { + + interface Callback { + fun onUrlClicked(url: String) + } + + override fun onClick(widget: View) { + callback?.onUrlClicked(url) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/permalinks/PermalinkData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/permalinks/PermalinkData.kt new file mode 100644 index 0000000000..3955c850c5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/permalinks/PermalinkData.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.permalinks + +import android.net.Uri + +/** + * This sealed class represents all the permalink cases. + * You don't have to instantiate yourself but should use [PermalinkParser] instead. + */ +sealed class PermalinkData { + + data class RoomLink(val roomIdOrAlias: String, val isRoomAlias: Boolean, val eventId: String?) : PermalinkData() + + data class UserLink(val userId: String) : PermalinkData() + + data class GroupLink(val groupId: String) : PermalinkData() + + data class FallbackLink(val uri: Uri) : PermalinkData() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/permalinks/PermalinkFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/permalinks/PermalinkFactory.kt new file mode 100644 index 0000000000..87b42f5ae8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/permalinks/PermalinkFactory.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.permalinks + +import org.matrix.android.sdk.api.session.events.model.Event + +/** + * Useful methods to create Matrix permalink (matrix.to links). + */ +object PermalinkFactory { + + const val MATRIX_TO_URL_BASE = "https://matrix.to/#/" + + /** + * Creates a permalink for an event. + * Ex: "https://matrix.to/#/!nbzmcXAqpxBXjAdgoX:matrix.org/$1531497316352799BevdV:matrix.org" + * + * @param event the event + * @return the permalink, or null in case of error + */ + fun createPermalink(event: Event): String? { + if (event.roomId.isNullOrEmpty() || event.eventId.isNullOrEmpty()) { + return null + } + return createPermalink(event.roomId, event.eventId) + } + + /** + * Creates a permalink for an id (can be a user Id, Room Id, etc.). + * Ex: "https://matrix.to/#/@benoit:matrix.org" + * + * @param id the id + * @return the permalink, or null in case of error + */ + fun createPermalink(id: String): String? { + return if (id.isEmpty()) { + null + } else MATRIX_TO_URL_BASE + escape(id) + } + + /** + * Creates a permalink for an event. If you have an event you can use [.createPermalink] + * Ex: "https://matrix.to/#/!nbzmcXAqpxBXjAdgoX:matrix.org/$1531497316352799BevdV:matrix.org" + * + * @param roomId the id of the room + * @param eventId the id of the event + * @return the permalink + */ + fun createPermalink(roomId: String, eventId: String): String { + return MATRIX_TO_URL_BASE + escape(roomId) + "/" + escape(eventId) + } + + /** + * Extract the linked id from the universal link + * + * @param url the universal link, Ex: "https://matrix.to/#/@benoit:matrix.org" + * @return the id from the url, ex: "@benoit:matrix.org", or null if the url is not a permalink + */ + fun getLinkedId(url: String): String? { + val isSupported = url.startsWith(MATRIX_TO_URL_BASE) + + return if (isSupported) { + url.substring(MATRIX_TO_URL_BASE.length) + } else null + } + + /** + * Escape '/' in id, because it is used as a separator + * + * @param id the id to escape + * @return the escaped id + */ + internal fun escape(id: String): String { + return id.replace("/", "%2F") + } + + /** + * Unescape '/' in id + * + * @param id the id to escape + * @return the escaped id + */ + internal fun unescape(id: String): String { + return id.replace("%2F", "/") + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/permalinks/PermalinkParser.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/permalinks/PermalinkParser.kt new file mode 100644 index 0000000000..4cf9331f0e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/permalinks/PermalinkParser.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.permalinks + +import android.net.Uri +import org.matrix.android.sdk.api.MatrixPatterns + +/** + * This class turns an uri to a [PermalinkData] + */ +object PermalinkParser { + + /** + * Turns an uri string to a [PermalinkData] + */ + fun parse(uriString: String): PermalinkData { + val uri = Uri.parse(uriString) + return parse(uri) + } + + /** + * Turns an uri to a [PermalinkData] + */ + fun parse(uri: Uri): PermalinkData { + if (!uri.toString().startsWith(PermalinkFactory.MATRIX_TO_URL_BASE)) { + return PermalinkData.FallbackLink(uri) + } + + val fragment = uri.fragment + if (fragment.isNullOrEmpty()) { + return PermalinkData.FallbackLink(uri) + } + + val indexOfQuery = fragment.indexOf("?") + val safeFragment = if (indexOfQuery != -1) fragment.substring(0, indexOfQuery) else fragment + + // we are limiting to 2 params + val params = safeFragment + .split(MatrixPatterns.SEP_REGEX.toRegex()) + .filter { it.isNotEmpty() } + .take(2) + + val identifier = params.getOrNull(0) + val extraParameter = params.getOrNull(1) + return when { + identifier.isNullOrEmpty() -> PermalinkData.FallbackLink(uri) + MatrixPatterns.isUserId(identifier) -> PermalinkData.UserLink(userId = identifier) + MatrixPatterns.isGroupId(identifier) -> PermalinkData.GroupLink(groupId = identifier) + MatrixPatterns.isRoomId(identifier) -> { + PermalinkData.RoomLink( + roomIdOrAlias = identifier, + isRoomAlias = false, + eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) } + ) + } + MatrixPatterns.isRoomAlias(identifier) -> { + PermalinkData.RoomLink( + roomIdOrAlias = identifier, + isRoomAlias = true, + eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) } + ) + } + else -> PermalinkData.FallbackLink(uri) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/Action.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/Action.kt new file mode 100644 index 0000000000..7a21920e58 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/Action.kt @@ -0,0 +1,129 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.pushrules + +import org.matrix.android.sdk.api.pushrules.rest.PushRule +import timber.log.Timber + +sealed class Action { + object Notify : Action() + object DoNotNotify : Action() + data class Sound(val sound: String = ACTION_OBJECT_VALUE_VALUE_DEFAULT) : Action() + data class Highlight(val highlight: Boolean) : Action() +} + +private const val ACTION_NOTIFY = "notify" +private const val ACTION_DONT_NOTIFY = "dont_notify" +private const val ACTION_COALESCE = "coalesce" + +// Ref: https://matrix.org/docs/spec/client_server/latest#tweaks +private const val ACTION_OBJECT_SET_TWEAK_KEY = "set_tweak" + +private const val ACTION_OBJECT_SET_TWEAK_VALUE_SOUND = "sound" +private const val ACTION_OBJECT_SET_TWEAK_VALUE_HIGHLIGHT = "highlight" + +private const val ACTION_OBJECT_VALUE_KEY = "value" +private const val ACTION_OBJECT_VALUE_VALUE_DEFAULT = "default" + +/** + * Ref: https://matrix.org/docs/spec/client_server/latest#actions + * + * Convert + *
+ * "actions": [
+ *     "notify",
+ *     {
+ *         "set_tweak": "sound",
+ *         "value": "default"
+ *     },
+ *     {
+ *         "set_tweak": "highlight"
+ *     }
+ *   ]
+ *
+ * To
+ * [
+ *     Action.Notify,
+ *     Action.Sound("default"),
+ *     Action.Highlight(true)
+ * ]
+ *
+ * 
+ */ + +@Suppress("IMPLICIT_CAST_TO_ANY") +fun List.toJson(): List { + return map { action -> + when (action) { + is Action.Notify -> ACTION_NOTIFY + is Action.DoNotNotify -> ACTION_DONT_NOTIFY + is Action.Sound -> { + mapOf( + ACTION_OBJECT_SET_TWEAK_KEY to ACTION_OBJECT_SET_TWEAK_VALUE_SOUND, + ACTION_OBJECT_VALUE_KEY to action.sound + ) + } + is Action.Highlight -> { + mapOf( + ACTION_OBJECT_SET_TWEAK_KEY to ACTION_OBJECT_SET_TWEAK_VALUE_HIGHLIGHT, + ACTION_OBJECT_VALUE_KEY to action.highlight + ) + } + } + } +} + +fun PushRule.getActions(): List { + val result = ArrayList() + + actions.forEach { actionStrOrObj -> + when (actionStrOrObj) { + ACTION_NOTIFY -> Action.Notify + ACTION_DONT_NOTIFY -> Action.DoNotNotify + is Map<*, *> -> { + when (actionStrOrObj[ACTION_OBJECT_SET_TWEAK_KEY]) { + ACTION_OBJECT_SET_TWEAK_VALUE_SOUND -> { + (actionStrOrObj[ACTION_OBJECT_VALUE_KEY] as? String)?.let { stringValue -> + Action.Sound(stringValue) + } + // When the value is not there, default sound (not specified by the spec) + ?: Action.Sound(ACTION_OBJECT_VALUE_VALUE_DEFAULT) + } + ACTION_OBJECT_SET_TWEAK_VALUE_HIGHLIGHT -> { + (actionStrOrObj[ACTION_OBJECT_VALUE_KEY] as? Boolean)?.let { boolValue -> + Action.Highlight(boolValue) + } + // When the value is not there, default is true, says the spec + ?: Action.Highlight(true) + } + else -> { + Timber.w("Unsupported set_tweak value ${actionStrOrObj[ACTION_OBJECT_SET_TWEAK_KEY]}") + null + } + } + } + else -> { + Timber.w("Unsupported action type $actionStrOrObj") + null + } + }?.let { + result.add(it) + } + } + + return result +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/Condition.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/Condition.kt new file mode 100644 index 0000000000..50c2f8505b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/Condition.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.pushrules + +import org.matrix.android.sdk.api.session.events.model.Event + +abstract class Condition(val kind: Kind) { + + enum class Kind(val value: String) { + EventMatch("event_match"), + ContainsDisplayName("contains_display_name"), + RoomMemberCount("room_member_count"), + SenderNotificationPermission("sender_notification_permission"), + Unrecognised(""); + + companion object { + + fun fromString(value: String): Kind { + return when (value) { + "event_match" -> EventMatch + "contains_display_name" -> ContainsDisplayName + "room_member_count" -> RoomMemberCount + "sender_notification_permission" -> SenderNotificationPermission + else -> Unrecognised + } + } + } + } + + abstract fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean + + open fun technicalDescription(): String { + return "Kind: $kind" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/ConditionResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/ConditionResolver.kt new file mode 100644 index 0000000000..dc92ce8d29 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/ConditionResolver.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.pushrules + +import org.matrix.android.sdk.api.session.events.model.Event + +/** + * Acts like a visitor on Conditions. + * This class as all required context needed to evaluate rules + */ +interface ConditionResolver { + fun resolveEventMatchCondition(event: Event, + condition: EventMatchCondition): Boolean + + fun resolveRoomMemberCountCondition(event: Event, + condition: RoomMemberCountCondition): Boolean + + fun resolveSenderNotificationPermissionCondition(event: Event, + condition: SenderNotificationPermissionCondition): Boolean + + fun resolveContainsDisplayNameCondition(event: Event, + condition: ContainsDisplayNameCondition): Boolean +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/ContainsDisplayNameCondition.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/ContainsDisplayNameCondition.kt new file mode 100644 index 0000000000..a836c24c4e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/ContainsDisplayNameCondition.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.pushrules + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import timber.log.Timber + +class ContainsDisplayNameCondition : Condition(Kind.ContainsDisplayName) { + + override fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean { + return conditionResolver.resolveContainsDisplayNameCondition(event, this) + } + + override fun technicalDescription(): String { + return "User is mentioned" + } + + fun isSatisfied(event: Event, displayName: String): Boolean { + val message = when (event.type) { + EventType.MESSAGE -> { + event.content.toModel() + } + // TODO the spec says: + // Matches any message whose content is unencrypted and contains the user's current display name + // EventType.ENCRYPTED -> { + // event.root.getClearContent()?.toModel() + // } + else -> null + } ?: return false + + return caseInsensitiveFind(displayName, message.body) + } + + companion object { + /** + * Returns whether a string contains an occurrence of another, as a standalone word, regardless of case. + * + * @param subString the string to search for + * @param longString the string to search in + * @return whether a match was found + */ + fun caseInsensitiveFind(subString: String, longString: String): Boolean { + // add sanity checks + if (subString.isEmpty() || longString.isEmpty()) { + return false + } + + try { + val regex = Regex("(\\W|^)" + Regex.escape(subString) + "(\\W|$)", RegexOption.IGNORE_CASE) + return regex.containsMatchIn(longString) + } catch (e: Exception) { + Timber.e(e, "## caseInsensitiveFind() : failed") + } + + return false + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/EventMatchCondition.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/EventMatchCondition.kt new file mode 100644 index 0000000000..5eed785899 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/EventMatchCondition.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.pushrules + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.internal.di.MoshiProvider +import timber.log.Timber + +class EventMatchCondition( + /** + * The dot-separated field of the event to match, e.g. content.body + */ + val key: String, + /** + * The glob-style pattern to match against. Patterns with no special glob characters should + * be treated as having asterisks prepended and appended when testing the condition. + */ + val pattern: String +) : Condition(Kind.EventMatch) { + + override fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean { + return conditionResolver.resolveEventMatchCondition(event, this) + } + + override fun technicalDescription(): String { + return "'$key' Matches '$pattern'" + } + + fun isSatisfied(event: Event): Boolean { + // TODO encrypted events? + val rawJson = MoshiProvider.providesMoshi().adapter(Event::class.java).toJsonValue(event) as? Map<*, *> + ?: return false + val value = extractField(rawJson, key) ?: return false + + // Patterns with no special glob characters should be treated as having asterisks prepended + // and appended when testing the condition. + try { + val modPattern = if (hasSpecialGlobChar(pattern)) simpleGlobToRegExp(pattern) else simpleGlobToRegExp("*$pattern*") + val regex = Regex(modPattern, RegexOption.DOT_MATCHES_ALL) + return regex.containsMatchIn(value) + } catch (e: Throwable) { + // e.g PatternSyntaxException + Timber.e(e, "Failed to evaluate push condition") + return false + } + } + + private fun extractField(jsonObject: Map<*, *>, fieldPath: String): String? { + val fieldParts = fieldPath.split(".") + if (fieldParts.isEmpty()) return null + + var jsonElement: Map<*, *> = jsonObject + fieldParts.forEachIndexed { index, pathSegment -> + if (index == fieldParts.lastIndex) { + return jsonElement[pathSegment]?.toString() + } else { + val sub = jsonElement[pathSegment] ?: return null + if (sub is Map<*, *>) { + jsonElement = sub + } else { + return null + } + } + } + return null + } + + companion object { + + private fun hasSpecialGlobChar(glob: String): Boolean { + return glob.contains("*") || glob.contains("?") + } + + // Very simple glob to regexp converter + private fun simpleGlobToRegExp(glob: String): String { + var out = "" // "^" + for (element in glob) { + when (element) { + '*' -> out += ".*" + '?' -> out += '.'.toString() + '.' -> out += "\\." + '\\' -> out += "\\\\" + else -> out += element + } + } + out += "" // '$'.toString() + return out + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt new file mode 100644 index 0000000000..64ccdcdece --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.pushrules + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.pushrules.rest.PushRule +import org.matrix.android.sdk.api.pushrules.rest.RuleSet +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.util.Cancelable + +interface PushRuleService { + /** + * Fetch the push rules from the server + */ + fun fetchPushRules(scope: String = RuleScope.GLOBAL) + + fun getPushRules(scope: String = RuleScope.GLOBAL): RuleSet + + fun updatePushRuleEnableStatus(kind: RuleKind, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback): Cancelable + + fun addPushRule(kind: RuleKind, pushRule: PushRule, callback: MatrixCallback): Cancelable + + fun updatePushRuleActions(kind: RuleKind, oldPushRule: PushRule, newPushRule: PushRule, callback: MatrixCallback): Cancelable + + fun removePushRule(kind: RuleKind, pushRule: PushRule, callback: MatrixCallback): Cancelable + + fun addPushRuleListener(listener: PushRuleListener) + + fun removePushRuleListener(listener: PushRuleListener) + +// fun fulfilledBingRule(event: Event, rules: List): PushRule? + + interface PushRuleListener { + fun onMatchRule(event: Event, actions: List) + fun onRoomJoined(roomId: String) + fun onRoomLeft(roomId: String) + fun onEventRedacted(redactedEventId: String) + fun batchFinish() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/RoomMemberCountCondition.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/RoomMemberCountCondition.kt new file mode 100644 index 0000000000..f97636a7bd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/RoomMemberCountCondition.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.pushrules + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.internal.session.room.RoomGetter +import timber.log.Timber + +private val regex = Regex("^(==|<=|>=|<|>)?(\\d*)$") + +class RoomMemberCountCondition( + /** + * A decimal integer optionally prefixed by one of ==, <, >, >= or <=. + * A prefix of < matches rooms where the member count is strictly less than the given number and so forth. + * If no prefix is present, this parameter defaults to ==. + */ + val iz: String +) : Condition(Kind.RoomMemberCount) { + + override fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean { + return conditionResolver.resolveRoomMemberCountCondition(event, this) + } + + override fun technicalDescription(): String { + return "Room member count is $iz" + } + + internal fun isSatisfied(event: Event, roomGetter: RoomGetter): Boolean { + // sanity checks + val roomId = event.roomId ?: return false + val room = roomGetter.getRoom(roomId) ?: return false + + // Parse the is field into prefix and number the first time + val (prefix, count) = parseIsField() ?: return false + + val numMembers = room.getNumberOfJoinedMembers() + + return when (prefix) { + "<" -> numMembers < count + ">" -> numMembers > count + "<=" -> numMembers <= count + ">=" -> numMembers >= count + else -> numMembers == count + } + } + + /** + * Parse the is field to extract meaningful information. + */ + private fun parseIsField(): Pair? { + try { + val match = regex.find(iz) ?: return null + val (prefix, count) = match.destructured + return prefix to count.toInt() + } catch (t: Throwable) { + Timber.e(t, "Unable to parse 'is' field") + } + return null + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/RuleIds.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/RuleIds.kt new file mode 100644 index 0000000000..eeb2577d4c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/RuleIds.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.pushrules + +/** + * Known rule ids + * + * Ref: https://matrix.org/docs/spec/client_server/latest#predefined-rules + */ +object RuleIds { + // Default Override Rules + const val RULE_ID_DISABLE_ALL = ".m.rule.master" + const val RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS = ".m.rule.suppress_notices" + const val RULE_ID_INVITE_ME = ".m.rule.invite_for_me" + const val RULE_ID_PEOPLE_JOIN_LEAVE = ".m.rule.member_event" + const val RULE_ID_CONTAIN_DISPLAY_NAME = ".m.rule.contains_display_name" + + const val RULE_ID_TOMBSTONE = ".m.rule.tombstone" + const val RULE_ID_ROOM_NOTIF = ".m.rule.roomnotif" + + // Default Content Rules + const val RULE_ID_CONTAIN_USER_NAME = ".m.rule.contains_user_name" + + // Default Underride Rules + const val RULE_ID_CALL = ".m.rule.call" + const val RULE_ID_ONE_TO_ONE_ENCRYPTED_ROOM = ".m.rule.encrypted_room_one_to_one" + const val RULE_ID_ONE_TO_ONE_ROOM = ".m.rule.room_one_to_one" + const val RULE_ID_ALL_OTHER_MESSAGES_ROOMS = ".m.rule.message" + const val RULE_ID_ENCRYPTED = ".m.rule.encrypted" + + // Not documented + const val RULE_ID_FALLBACK = ".m.rule.fallback" +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/RuleScope.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/RuleScope.kt new file mode 100644 index 0000000000..d94026f438 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/RuleScope.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.pushrules + +object RuleScope { + const val GLOBAL = "global" +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/RuleSetKey.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/RuleSetKey.kt new file mode 100644 index 0000000000..f716b33f23 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/RuleSetKey.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.pushrules + +/** + * Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-pushrules + */ +enum class RuleSetKey(val value: String) { + CONTENT("content"), + OVERRIDE("override"), + ROOM("room"), + SENDER("sender"), + UNDERRIDE("underride") +} + +/** + * Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-pushrules-scope-kind-ruleid + */ +typealias RuleKind = RuleSetKey diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/SenderNotificationPermissionCondition.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/SenderNotificationPermissionCondition.kt new file mode 100644 index 0000000000..a8d08e5458 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/SenderNotificationPermissionCondition.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.pushrules + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent +import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper + +class SenderNotificationPermissionCondition( + /** + * A string that determines the power level the sender must have to trigger notifications of a given type, + * such as room. Refer to the m.room.power_levels event schema for information about what the defaults are + * and how to interpret the event. The key is used to look up the power level required to send a notification + * type from the notifications object in the power level event content. + */ + val key: String +) : Condition(Kind.SenderNotificationPermission) { + + override fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean { + return conditionResolver.resolveSenderNotificationPermissionCondition(event, this) + } + + override fun technicalDescription(): String { + return "User power level <$key>" + } + + fun isSatisfied(event: Event, powerLevels: PowerLevelsContent): Boolean { + val powerLevelsHelper = PowerLevelsHelper(powerLevels) + return event.senderId != null && powerLevelsHelper.getUserPowerLevelValue(event.senderId) >= powerLevelsHelper.notificationLevel(key) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/GetPushRulesResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/GetPushRulesResponse.kt new file mode 100644 index 0000000000..f83d893c0a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/GetPushRulesResponse.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.pushrules.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * All push rulesets for a user. + * Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-pushrules + */ +@JsonClass(generateAdapter = true) +internal data class GetPushRulesResponse( + /** + * Global rules, account level applying to all devices + */ + @Json(name = "global") + val global: RuleSet, + + /** + * Device specific rules, apply only to current device + */ + @Json(name = "device") + val device: RuleSet? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/PushCondition.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/PushCondition.kt new file mode 100644 index 0000000000..9469da3ea5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/PushCondition.kt @@ -0,0 +1,96 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.pushrules.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.pushrules.Condition +import org.matrix.android.sdk.api.pushrules.ContainsDisplayNameCondition +import org.matrix.android.sdk.api.pushrules.EventMatchCondition +import org.matrix.android.sdk.api.pushrules.RoomMemberCountCondition +import org.matrix.android.sdk.api.pushrules.SenderNotificationPermissionCondition +import timber.log.Timber + +/** + * Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-pushrules + */ +@JsonClass(generateAdapter = true) +data class PushCondition( + /** + * Required. The kind of condition to apply. + */ + @Json(name = "kind") + val kind: String, + + /** + * Required for event_match conditions. The dot- separated field of the event to match. + */ + @Json(name = "key") + val key: String? = null, + + /** + * Required for event_match conditions. + */ + @Json(name = "pattern") + val pattern: String? = null, + + /** + * Required for room_member_count conditions. + * A decimal integer optionally prefixed by one of, ==, <, >, >= or <=. + * A prefix of < matches rooms where the member count is strictly less than the given number and so forth. + * If no prefix is present, this parameter defaults to ==. + */ + @Json(name = "is") + val iz: String? = null +) { + + fun asExecutableCondition(): Condition? { + return when (Condition.Kind.fromString(kind)) { + Condition.Kind.EventMatch -> { + if (key != null && pattern != null) { + EventMatchCondition(key, pattern) + } else { + Timber.e("Malformed Event match condition") + null + } + } + Condition.Kind.ContainsDisplayName -> { + ContainsDisplayNameCondition() + } + Condition.Kind.RoomMemberCount -> { + if (iz.isNullOrEmpty()) { + Timber.e("Malformed ROOM_MEMBER_COUNT condition") + null + } else { + RoomMemberCountCondition(iz) + } + } + Condition.Kind.SenderNotificationPermission -> { + if (key == null) { + Timber.e("Malformed Sender Notification Permission condition") + null + } else { + SenderNotificationPermissionCondition(key) + } + } + Condition.Kind.Unrecognised -> { + Timber.e("Unknown kind $kind") + null + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/PushRule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/PushRule.kt new file mode 100644 index 0000000000..46d73a8aa2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/PushRule.kt @@ -0,0 +1,176 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.pushrules.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.pushrules.Action +import org.matrix.android.sdk.api.pushrules.getActions +import org.matrix.android.sdk.api.pushrules.toJson + +/** + * Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-pushrules + */ +@JsonClass(generateAdapter = true) +data class PushRule( + /** + * Required. The actions to perform when this rule is matched. + */ + @Json(name = "actions") + val actions: List, + /** + * Required. Whether this is a default rule, or has been set explicitly. + */ + @Json(name = "default") + val default: Boolean? = false, + /** + * Required. Whether the push rule is enabled or not. + */ + @Json(name = "enabled") + val enabled: Boolean, + /** + * Required. The ID of this rule. + */ + @Json(name = "rule_id") + val ruleId: String, + /** + * The conditions that must hold true for an event in order for a rule to be applied to an event + */ + @Json(name = "conditions") + val conditions: List? = null, + /** + * The glob-style pattern to match against. Only applicable to content rules. + */ + @Json(name = "pattern") + val pattern: String? = null +) { + /** + * Add the default notification sound. + */ + fun setNotificationSound(): PushRule { + return setNotificationSound(ACTION_VALUE_DEFAULT) + } + + fun getNotificationSound(): String? { + return (getActions().firstOrNull { it is Action.Sound } as? Action.Sound)?.sound + } + + /** + * Set the notification sound + * + * @param sound notification sound + */ + fun setNotificationSound(sound: String): PushRule { + return copy( + actions = (getActions().filter { it !is Action.Sound } + Action.Sound(sound)).toJson() + ) + } + + /** + * Remove the notification sound + */ + fun removeNotificationSound(): PushRule { + return copy( + actions = getActions().filter { it !is Action.Sound }.toJson() + ) + } + + /** + * Set the highlight status. + * + * @param highlight the highlight status + */ + fun setHighlight(highlight: Boolean): PushRule { + return copy( + actions = (getActions().filter { it !is Action.Highlight } + Action.Highlight(highlight)).toJson() + ) + } + + /** + * Set the notification status. + * + * @param notify true to notify + */ + fun setNotify(notify: Boolean): PushRule { + val mutableActions = actions.toMutableList() + + mutableActions.remove(ACTION_DONT_NOTIFY) + mutableActions.remove(ACTION_NOTIFY) + + if (notify) { + mutableActions.add(ACTION_NOTIFY) + } else { + mutableActions.add(ACTION_DONT_NOTIFY) + } + + return copy(actions = mutableActions) + } + + /** + * Return true if the rule should highlight the event. + * + * @return true if the rule should play sound + */ + fun shouldNotify() = actions.contains(ACTION_NOTIFY) + + /** + * Return true if the rule should not highlight the event. + * + * @return true if the rule should not play sound + */ + fun shouldNotNotify() = actions.contains(ACTION_DONT_NOTIFY) + + companion object { + /* ========================================================================================== + * Rule id + * ========================================================================================== */ + + const val RULE_ID_DISABLE_ALL = ".m.rule.master" + const val RULE_ID_CONTAIN_USER_NAME = ".m.rule.contains_user_name" + const val RULE_ID_CONTAIN_DISPLAY_NAME = ".m.rule.contains_display_name" + const val RULE_ID_ONE_TO_ONE_ROOM = ".m.rule.room_one_to_one" + const val RULE_ID_INVITE_ME = ".m.rule.invite_for_me" + const val RULE_ID_PEOPLE_JOIN_LEAVE = ".m.rule.member_event" + const val RULE_ID_CALL = ".m.rule.call" + const val RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS = ".m.rule.suppress_notices" + const val RULE_ID_ALL_OTHER_MESSAGES_ROOMS = ".m.rule.message" + const val RULE_ID_AT_ROOMS = ".m.rule.roomnotif" + const val RULE_ID_TOMBSTONE = ".m.rule.tombstone" + const val RULE_ID_E2E_ONE_TO_ONE_ROOM = ".m.rule.encrypted_room_one_to_one" + const val RULE_ID_E2E_GROUP = ".m.rule.encrypted" + const val RULE_ID_REACTION = ".m.rule.reaction" + const val RULE_ID_FALLBACK = ".m.rule.fallback" + + /* ========================================================================================== + * Actions + * ========================================================================================== */ + + const val ACTION_NOTIFY = "notify" + const val ACTION_DONT_NOTIFY = "dont_notify" + const val ACTION_COALESCE = "coalesce" + + const val ACTION_SET_TWEAK_SOUND_VALUE = "sound" + const val ACTION_SET_TWEAK_HIGHLIGHT_VALUE = "highlight" + + const val ACTION_PARAMETER_SET_TWEAK = "set_tweak" + const val ACTION_PARAMETER_VALUE = "value" + + const val ACTION_VALUE_DEFAULT = "default" + const val ACTION_VALUE_RING = "ring" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/RuleSet.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/RuleSet.kt new file mode 100644 index 0000000000..eb813dba45 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/RuleSet.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.pushrules.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.pushrules.RuleSetKey + +/** + * Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-pushrules + */ +@JsonClass(generateAdapter = true) +data class RuleSet( + @Json(name = "content") + val content: List? = null, + @Json(name = "override") + val override: List? = null, + @Json(name = "room") + val room: List? = null, + @Json(name = "sender") + val sender: List? = null, + @Json(name = "underride") + val underride: List? = null +) { + fun getAllRules(): List { + // Ref. for the order: https://matrix.org/docs/spec/client_server/latest#push-rules + return override.orEmpty() + content.orEmpty() + room.orEmpty() + sender.orEmpty() + underride.orEmpty() + } + + /** + * Find a rule from its ruleID. + * + * @param ruleId a RULE_ID_XX value + * @return the matched bing rule or null it doesn't exist. + */ + fun findDefaultRule(ruleId: String?): PushRuleAndKind? { + var result: PushRuleAndKind? = null + // sanity check + if (null != ruleId) { + if (PushRule.RULE_ID_CONTAIN_USER_NAME == ruleId) { + result = findRule(content, ruleId)?.let { PushRuleAndKind(it, RuleSetKey.CONTENT) } + } else { + // assume that the ruleId is unique. + result = findRule(override, ruleId)?.let { PushRuleAndKind(it, RuleSetKey.OVERRIDE) } + if (null == result) { + result = findRule(underride, ruleId)?.let { PushRuleAndKind(it, RuleSetKey.UNDERRIDE) } + } + } + } + return result + } + + /** + * Find a rule from its rule Id. + * + * @param rules the rules list. + * @param ruleId the rule Id. + * @return the bing rule if it exists, else null. + */ + private fun findRule(rules: List?, ruleId: String): PushRule? { + return rules?.firstOrNull { it.ruleId == ruleId } + } +} + +data class PushRuleAndKind( + val pushRule: PushRule, + val kind: RuleSetKey +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/QueryStringValue.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/QueryStringValue.kt new file mode 100644 index 0000000000..21ff3aebca --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/QueryStringValue.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.query + +/** + * Basic query language. All these cases are mutually exclusive. + */ +sealed class QueryStringValue { + object NoCondition : QueryStringValue() + object IsNull : QueryStringValue() + object IsNotNull : QueryStringValue() + object IsEmpty : QueryStringValue() + object IsNotEmpty : QueryStringValue() + data class Equals(val string: String, val case: Case = Case.SENSITIVE) : QueryStringValue() + data class Contains(val string: String, val case: Case = Case.SENSITIVE) : QueryStringValue() + + enum class Case { + SENSITIVE, + INSENSITIVE + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/InitialSyncProgressService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/InitialSyncProgressService.kt new file mode 100644 index 0000000000..42bb29efca --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/InitialSyncProgressService.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.session + +import androidx.annotation.StringRes +import androidx.lifecycle.LiveData + +interface InitialSyncProgressService { + + fun getInitialSyncProgressStatus(): LiveData + + sealed class Status { + object Idle : Status() + data class Progressing( + @StringRes val statusText: Int, + val percentProgress: Int = 0 + ) : Status() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt new file mode 100644 index 0000000000..95370c0188 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt @@ -0,0 +1,230 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session + +import androidx.annotation.MainThread +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.auth.data.SessionParams +import org.matrix.android.sdk.api.failure.GlobalError +import org.matrix.android.sdk.api.pushrules.PushRuleService +import org.matrix.android.sdk.api.session.account.AccountService +import org.matrix.android.sdk.api.session.accountdata.AccountDataService +import org.matrix.android.sdk.api.session.cache.CacheService +import org.matrix.android.sdk.api.session.call.CallSignalingService +import org.matrix.android.sdk.api.session.content.ContentUploadStateTracker +import org.matrix.android.sdk.api.session.content.ContentUrlResolver +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.file.ContentDownloadStateTracker +import org.matrix.android.sdk.api.session.file.FileService +import org.matrix.android.sdk.api.session.group.GroupService +import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService +import org.matrix.android.sdk.api.session.identity.IdentityService +import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService +import org.matrix.android.sdk.api.session.profile.ProfileService +import org.matrix.android.sdk.api.session.pushers.PushersService +import org.matrix.android.sdk.api.session.room.RoomDirectoryService +import org.matrix.android.sdk.api.session.room.RoomService +import org.matrix.android.sdk.api.session.securestorage.SecureStorageService +import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService +import org.matrix.android.sdk.api.session.signout.SignOutService +import org.matrix.android.sdk.api.session.sync.FilterService +import org.matrix.android.sdk.api.session.sync.SyncState +import org.matrix.android.sdk.api.session.terms.TermsService +import org.matrix.android.sdk.api.session.typing.TypingUsersTracker +import org.matrix.android.sdk.api.session.user.UserService +import org.matrix.android.sdk.api.session.widgets.WidgetService +import okhttp3.OkHttpClient + +/** + * This interface defines interactions with a session. + * An instance of a session will be provided by the SDK. + */ +interface Session : + RoomService, + RoomDirectoryService, + GroupService, + UserService, + CacheService, + SignOutService, + FilterService, + TermsService, + ProfileService, + PushRuleService, + PushersService, + InitialSyncProgressService, + HomeServerCapabilitiesService, + SecureStorageService, + AccountDataService, + AccountService { + + /** + * The params associated to the session + */ + val sessionParams: SessionParams + + /** + * The session is valid, i.e. it has a valid token so far + */ + val isOpenable: Boolean + + /** + * Useful shortcut to get access to the userId + */ + val myUserId: String + get() = sessionParams.userId + + /** + * The sessionId + */ + val sessionId: String + + /** + * This method allow to open a session. It does start some service on the background. + */ + @MainThread + fun open() + + /** + * Requires a one time background sync + */ + fun requireBackgroundSync() + + /** + * Launches infinite periodic background syncs + * This does not work in doze mode :/ + * If battery optimization is on it can work in app standby but that's all :/ + */ + fun startAutomaticBackgroundSync(repeatDelay: Long = 30_000L) + + fun stopAnyBackgroundSync() + + /** + * This method start the sync thread. + */ + fun startSync(fromForeground: Boolean) + + /** + * This method stop the sync thread. + */ + fun stopSync() + + /** + * This method allows to listen the sync state. + * @return a [LiveData] of [SyncState]. + */ + fun getSyncStateLive(): LiveData + + /** + * This method returns the current sync state. + * @return the current [SyncState]. + */ + fun getSyncState(): SyncState + + /** + * This methods return true if an initial sync has been processed + */ + fun hasAlreadySynced(): Boolean + + /** + * This method allow to close a session. It does stop some services. + */ + fun close() + + /** + * Returns the ContentUrlResolver associated to the session. + */ + fun contentUrlResolver(): ContentUrlResolver + + /** + * Returns the ContentUploadProgressTracker associated with the session + */ + fun contentUploadProgressTracker(): ContentUploadStateTracker + + /** + * Returns the TypingUsersTracker associated with the session + */ + fun typingUsersTracker(): TypingUsersTracker + + /** + * Returns the ContentDownloadStateTracker associated with the session + */ + fun contentDownloadProgressTracker(): ContentDownloadStateTracker + + /** + * Returns the cryptoService associated with the session + */ + fun cryptoService(): CryptoService + + /** + * Returns the identity service associated with the session + */ + fun identityService(): IdentityService + + /** + * Returns the widget service associated with the session + */ + fun widgetService(): WidgetService + + /** + * Returns the integration manager service associated with the session + */ + fun integrationManagerService(): IntegrationManagerService + + /** + * Returns the call signaling service associated with the session + */ + fun callSignalingService(): CallSignalingService + + /** + * Returns the file download service associated with the session + */ + fun fileService(): FileService + + /** + * Add a listener to the session. + * @param listener the listener to add. + */ + fun addListener(listener: Listener) + + /** + * Remove a listener from the session. + * @param listener the listener to remove. + */ + fun removeListener(listener: Listener) + + /** + * Will return a OkHttpClient which will manage pinned certificates and Proxy if configured. + * It will not add any access-token to the request. + * So it is exposed to let the app be able to download image with Glide or any other libraries which accept an OkHttp client. + */ + fun getOkHttpClient(): OkHttpClient + + /** + * A global session listener to get notified for some events. + */ + interface Listener { + /** + * Possible cases: + * - The access token is not valid anymore, + * - a M_CONSENT_NOT_GIVEN error has been received from the homeserver + */ + fun onGlobalError(globalError: GlobalError) + } + + val sharedSecretStorageService: SharedSecretStorageService +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/account/AccountService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/account/AccountService.kt new file mode 100644 index 0000000000..40c373820c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/account/AccountService.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.account + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.util.Cancelable + +/** + * This interface defines methods to manage the account. It's implemented at the session level. + */ +interface AccountService { + /** + * Ask the homeserver to change the password. + * @param password Current password. + * @param newPassword New password + */ + fun changePassword(password: String, newPassword: String, callback: MatrixCallback): Cancelable + + /** + * Deactivate the account. + * + * This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register + * the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account + * details from your identity server. This action is irreversible.\n\nDeactivating your account does not by default + * cause us to forget messages you have sent. If you would like us to forget your messages, please tick the box below. + * + * Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not + * be shared with any new or unregistered users, but registered users who already have access to these messages will still + * have access to their copy. + * + * @param password the account password + * @param eraseAllData set to true to forget all messages that have been sent. Warning: this will cause future users to see + * an incomplete view of conversations + */ + fun deactivateAccount(password: String, eraseAllData: Boolean, callback: MatrixCallback): Cancelable +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/AccountDataService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/AccountDataService.kt new file mode 100644 index 0000000000..a90a34de4b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/AccountDataService.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.accountdata + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.Optional + +interface AccountDataService { + /** + * Retrieve the account data with the provided type or null if not found + */ + fun getAccountDataEvent(type: String): UserAccountDataEvent? + + /** + * Observe the account data with the provided type + */ + fun getLiveAccountDataEvent(type: String): LiveData> + + /** + * Retrieve the account data with the provided types. The return list can have a different size that + * the size of the types set, because some AccountData may not exist. + * If an empty set is provided, all the AccountData are retrieved + */ + fun getAccountDataEvents(types: Set): List + + /** + * Observe the account data with the provided types. If an empty set is provided, all the AccountData are observed + */ + fun getLiveAccountDataEvents(types: Set): LiveData> + + /** + * Update the account data with the provided type and the provided account data content + */ + fun updateAccountData(type: String, content: Content, callback: MatrixCallback? = null): Cancelable +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/UserAccountDataEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/UserAccountDataEvent.kt new file mode 100644 index 0000000000..57eda657ac --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/UserAccountDataEvent.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.accountdata + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Content + +/** + * This is a simplified Event with just a type and a content. + * Currently used types are defined in [UserAccountDataTypes]. + */ +@JsonClass(generateAdapter = true) +data class UserAccountDataEvent( + @Json(name = "type") val type: String, + @Json(name = "content") val content: Content +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/UserAccountDataTypes.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/UserAccountDataTypes.kt new file mode 100644 index 0000000000..2414e4a1fb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/UserAccountDataTypes.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.accountdata + +object UserAccountDataTypes { + const val TYPE_IGNORED_USER_LIST = "m.ignored_user_list" + const val TYPE_DIRECT_MESSAGES = "m.direct" + const val TYPE_BREADCRUMBS = "im.vector.setting.breadcrumbs" + const val TYPE_PREVIEW_URLS = "org.matrix.preview_urls" + const val TYPE_WIDGETS = "m.widgets" + const val TYPE_PUSH_RULES = "m.push_rules" + const val TYPE_INTEGRATION_PROVISIONING = "im.vector.setting.integration_provisioning" + const val TYPE_ALLOWED_WIDGETS = "im.vector.setting.allowed_widgets" + const val TYPE_IDENTITY_SERVER = "m.identity_server" + const val TYPE_ACCEPTED_TERMS = "m.accepted_terms" +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/cache/CacheService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/cache/CacheService.kt new file mode 100644 index 0000000000..a36856a7e6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/cache/CacheService.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.cache + +import org.matrix.android.sdk.api.MatrixCallback + +/** + * This interface defines a method to clear the cache. It's implemented at the session level. + */ +interface CacheService { + + /** + * Clear the whole cached data, except credentials. Once done, the sync has to be restarted by the sdk user. + */ + fun clearCache(callback: MatrixCallback) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallSignalingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallSignalingService.kt new file mode 100644 index 0000000000..2962f9fac3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallSignalingService.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.call + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.util.Cancelable + +interface CallSignalingService { + + fun getTurnServer(callback: MatrixCallback): Cancelable + + /** + * Create an outgoing call + */ + fun createOutgoingCall(roomId: String, otherUserId: String, isVideoCall: Boolean): MxCall + + fun addCallListener(listener: CallsListener) + + fun removeCallListener(listener: CallsListener) + + fun getCallWithId(callId: String) : MxCall? +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallState.kt new file mode 100644 index 0000000000..60268abf70 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallState.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.call + +import org.webrtc.PeerConnection + +sealed class CallState { + + /** Idle, setting up objects */ + object Idle : CallState() + + /** Dialing. Outgoing call is signaling the remote peer */ + object Dialing : CallState() + + /** Local ringing. Incoming call offer received */ + object LocalRinging : CallState() + + /** Answering. Incoming call is responding to remote peer */ + object Answering : CallState() + + /** + * Connected. Incoming/Outgoing call, ice layer connecting or connected + * Notice that the PeerState failed is not always final, if you switch network, new ice candidtates + * could be exchanged, and the connection could go back to connected + * */ + data class Connected(val iceConnectionState: PeerConnection.PeerConnectionState) : CallState() + + /** Terminated. Incoming/Outgoing call, the call is terminated */ + object Terminated : CallState() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallsListener.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallsListener.kt new file mode 100644 index 0000000000..81430c71ea --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallsListener.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.call + +import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent +import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent +import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent +import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent + +interface CallsListener { + /** + * Called when there is an incoming call within the room. + */ + fun onCallInviteReceived(mxCall: MxCall, callInviteContent: CallInviteContent) + + fun onCallIceCandidateReceived(mxCall: MxCall, iceCandidatesContent: CallCandidatesContent) + + /** + * An outgoing call is started. + */ + fun onCallAnswerReceived(callAnswerContent: CallAnswerContent) + + /** + * Called when a called has been hung up + */ + fun onCallHangupReceived(callHangupContent: CallHangupContent) + + fun onCallManagedByOtherSession(callId: String) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/EglUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/EglUtils.kt new file mode 100644 index 0000000000..6b70d8500f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/EglUtils.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.session.call + +import org.webrtc.EglBase +import timber.log.Timber + +/** + * The root [EglBase] instance shared by the entire application for + * the sake of reducing the utilization of system resources (such as EGL + * contexts) + * by performing a runtime check. + */ +object EglUtils { + + // TODO how do we release that? + + /** + * Lazily creates and returns the one and only [EglBase] which will + * serve as the root for all contexts that are needed. + */ + @get:Synchronized var rootEglBase: EglBase? = null + get() { + if (field == null) { + val configAttributes = EglBase.CONFIG_PLAIN + try { + field = EglBase.createEgl14(configAttributes) + ?: EglBase.createEgl10(configAttributes) // Fall back to EglBase10. + } catch (ex: Throwable) { + Timber.e(ex, "Failed to create EglBase") + } + } + return field + } + private set + + val rootEglBaseContext: EglBase.Context? + get() { + val eglBase = rootEglBase + return eglBase?.eglBaseContext + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt new file mode 100644 index 0000000000..04af588b93 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.call + +import org.webrtc.IceCandidate +import org.webrtc.SessionDescription + +interface MxCallDetail { + val callId: String + val isOutgoing: Boolean + val roomId: String + val otherUserId: String + val isVideoCall: Boolean +} + +/** + * Define both an incoming call and on outgoing call + */ +interface MxCall : MxCallDetail { + + var state: CallState + /** + * Pick Up the incoming call + * It has no effect on outgoing call + */ + fun accept(sdp: SessionDescription) + + /** + * Reject an incoming call + * It's an alias to hangUp + */ + fun reject() = hangUp() + + /** + * End the call + */ + fun hangUp() + + /** + * Start a call + * Send offer SDP to the other participant. + */ + fun offerSdp(sdp: SessionDescription) + + /** + * Send Ice candidate to the other participant. + */ + fun sendLocalIceCandidates(candidates: List) + + /** + * Send removed ICE candidates to the other participant. + */ + fun sendLocalIceCandidateRemovals(candidates: List) + + fun addListener(listener: StateListener) + fun removeListener(listener: StateListener) + + interface StateListener { + fun onStateUpdate(call: MxCall) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/TurnServerResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/TurnServerResponse.kt new file mode 100644 index 0000000000..f63a1a0d28 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/TurnServerResponse.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.call + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +// TODO Should not be exposed +/** + * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-voip-turnserver + */ +@JsonClass(generateAdapter = true) +data class TurnServerResponse( + /** + * Required. The username to use. + */ + @Json(name = "username") val username: String?, + + /** + * Required. The password to use. + */ + @Json(name = "password") val password: String?, + + /** + * Required. A list of TURN URIs + */ + @Json(name = "uris") val uris: List?, + + /** + * Required. The time-to-live in seconds + */ + @Json(name = "ttl") val ttl: Int? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentAttachmentData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentAttachmentData.kt new file mode 100644 index 0000000000..045a9bc1a0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentAttachmentData.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.content + +import android.net.Uri +import android.os.Parcelable +import androidx.exifinterface.media.ExifInterface +import kotlinx.android.parcel.Parcelize + +@Parcelize +data class ContentAttachmentData( + val size: Long = 0, + val duration: Long? = 0, + val date: Long = 0, + val height: Long? = 0, + val width: Long? = 0, + val exifOrientation: Int = ExifInterface.ORIENTATION_UNDEFINED, + val name: String? = null, + val queryUri: Uri, + private val mimeType: String?, + val type: Type +) : Parcelable { + + enum class Type { + FILE, + IMAGE, + AUDIO, + VIDEO + } + + fun getSafeMimeType() = if (mimeType == "image/jpg") "image/jpeg" else mimeType +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUploadStateTracker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUploadStateTracker.kt new file mode 100644 index 0000000000..a29e7110e2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUploadStateTracker.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.content + +interface ContentUploadStateTracker { + + fun track(key: String, updateListener: UpdateListener) + + fun untrack(key: String, updateListener: UpdateListener) + + fun clear() + + interface UpdateListener { + fun onUpdate(state: State) + } + + sealed class State { + object Idle : State() + object EncryptingThumbnail : State() + data class UploadingThumbnail(val current: Long, val total: Long) : State() + object Encrypting : State() + data class Uploading(val current: Long, val total: Long) : State() + object Success : State() + data class Failure(val throwable: Throwable) : State() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUrlResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUrlResolver.kt new file mode 100644 index 0000000000..890e72edd9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUrlResolver.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.content + +/** + * This interface defines methods for accessing content from the current session. + */ +interface ContentUrlResolver { + + enum class ThumbnailMethod(val value: String) { + CROP("crop"), + SCALE("scale") + } + + /** + * URL to use to upload content + */ + val uploadUrl: String + + /** + * Get the actual URL for accessing the full-size image of a Matrix media content URI. + * + * @param contentUrl the Matrix media content URI (in the form of "mxc://..."). + * @return the URL to access the described resource, or null if the url is invalid. + */ + fun resolveFullSize(contentUrl: String?): String? + + /** + * Get the actual URL for accessing the thumbnail image of a given Matrix media content URI. + * + * @param contentUrl the Matrix media content URI (in the form of "mxc://..."). + * @param width the desired width + * @param height the desired height + * @param method the desired method (METHOD_CROP or METHOD_SCALE) + * @return the URL to access the described resource, or null if the url is invalid. + */ + fun resolveThumbnail(contentUrl: String?, width: Int, height: Int, method: ThumbnailMethod): String? +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt new file mode 100644 index 0000000000..726f9b624a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt @@ -0,0 +1,154 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.crypto + +import android.content.Context +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.listeners.ProgressListener +import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService +import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener +import org.matrix.android.sdk.api.session.crypto.verification.VerificationService +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.internal.crypto.IncomingRoomKeyRequest +import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult +import org.matrix.android.sdk.internal.crypto.NewSessionListener +import org.matrix.android.sdk.internal.crypto.OutgoingRoomKeyRequest +import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult +import org.matrix.android.sdk.internal.crypto.model.MXDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.MXEncryptEventContentResult +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent +import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo +import org.matrix.android.sdk.internal.crypto.model.rest.DevicesListResponse +import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody + +interface CryptoService { + + fun verificationService(): VerificationService + + fun crossSigningService(): CrossSigningService + + fun keysBackupService(): KeysBackupService + + fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback) + + fun deleteDevice(deviceId: String, callback: MatrixCallback) + + fun deleteDeviceWithUserPassword(deviceId: String, authSession: String?, password: String, callback: MatrixCallback) + + fun getCryptoVersion(context: Context, longFormat: Boolean): String + + fun isCryptoEnabled(): Boolean + + fun isRoomBlacklistUnverifiedDevices(roomId: String?): Boolean + + fun setWarnOnUnknownDevices(warn: Boolean) + + fun setDeviceVerification(trustLevel: DeviceTrustLevel, userId: String, deviceId: String) + + fun getUserDevices(userId: String): MutableList + + fun setDevicesKnown(devices: List, callback: MatrixCallback?) + + fun deviceWithIdentityKey(senderKey: String, algorithm: String): CryptoDeviceInfo? + + fun getMyDevice(): CryptoDeviceInfo + + fun getGlobalBlacklistUnverifiedDevices(): Boolean + + fun setGlobalBlacklistUnverifiedDevices(block: Boolean) + + fun setRoomUnBlacklistUnverifiedDevices(roomId: String) + + fun getDeviceTrackingStatus(userId: String): Int + + fun importRoomKeys(roomKeysAsArray: ByteArray, password: String, progressListener: ProgressListener?, callback: MatrixCallback) + + fun exportRoomKeys(password: String, callback: MatrixCallback) + + fun setRoomBlacklistUnverifiedDevices(roomId: String) + + fun getDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo? + + fun requestRoomKeyForEvent(event: Event) + + fun reRequestRoomKeyForEvent(event: Event) + + fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) + + fun addRoomKeysRequestListener(listener: GossipingRequestListener) + + fun removeRoomKeysRequestListener(listener: GossipingRequestListener) + + fun fetchDevicesList(callback: MatrixCallback) + + fun getMyDevicesInfo() : List + + fun getLiveMyDevicesInfo() : LiveData> + + fun getDeviceInfo(deviceId: String, callback: MatrixCallback) + + fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int + + fun isRoomEncrypted(roomId: String): Boolean + + fun encryptEventContent(eventContent: Content, + eventType: String, + roomId: String, + callback: MatrixCallback) + + fun discardOutboundSession(roomId: String) + + @Throws(MXCryptoError::class) + fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult + + fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback) + + fun getEncryptionAlgorithm(roomId: String): String? + + fun shouldEncryptForInvitedMembers(roomId: String): Boolean + + fun downloadKeys(userIds: List, forceDownload: Boolean, callback: MatrixCallback>) + + fun getCryptoDeviceInfo(userId: String): List + + fun getLiveCryptoDeviceInfo(): LiveData> + + fun getLiveCryptoDeviceInfo(userId: String): LiveData> + + fun getLiveCryptoDeviceInfo(userIds: List): LiveData> + + fun addNewSessionListener(newSessionListener: NewSessionListener) + + fun removeSessionListener(listener: NewSessionListener) + + fun getOutgoingRoomKeyRequests(): List + + fun getIncomingRoomKeyRequests(): List + + fun getGossipingEventsTrail(): List + + // For testing shared session + fun getSharedWithInfo(roomId: String?, sessionId: String) : MXUsersDevicesMap + fun getWithHeldMegolmSession(roomId: String, sessionId: String) : RoomKeyWithHeldContent? +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/MXCryptoError.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/MXCryptoError.kt new file mode 100644 index 0000000000..53bee09f11 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/MXCryptoError.kt @@ -0,0 +1,95 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.crypto + +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.olm.OlmException + +/** + * Represents a crypto error response. + */ +sealed class MXCryptoError : Throwable() { + + data class Base(val errorType: ErrorType, + val technicalMessage: String, + /** + * Describe the error with more details + */ + val detailedErrorDescription: String? = null) : MXCryptoError() + + data class OlmError(val olmException: OlmException) : MXCryptoError() + + data class UnknownDevice(val deviceList: MXUsersDevicesMap) : MXCryptoError() + + enum class ErrorType { + ENCRYPTING_NOT_ENABLED, + UNABLE_TO_ENCRYPT, + UNABLE_TO_DECRYPT, + UNKNOWN_INBOUND_SESSION_ID, + INBOUND_SESSION_MISMATCH_ROOM_ID, + MISSING_FIELDS, + BAD_EVENT_FORMAT, + MISSING_SENDER_KEY, + MISSING_CIPHER_TEXT, + BAD_DECRYPTED_FORMAT, + NOT_INCLUDE_IN_RECIPIENTS, + BAD_RECIPIENT, + BAD_RECIPIENT_KEY, + FORWARDED_MESSAGE, + BAD_ROOM, + BAD_ENCRYPTED_MESSAGE, + DUPLICATED_MESSAGE_INDEX, + MISSING_PROPERTY, + OLM, + UNKNOWN_DEVICES, + UNKNOWN_MESSAGE_INDEX, + KEYS_WITHHELD + } + + companion object { + /** + * Resource for technicalMessage + */ + const val UNABLE_TO_ENCRYPT_REASON = "Unable to encrypt %s" + const val UNABLE_TO_DECRYPT_REASON = "Unable to decrypt %1\$s. Algorithm: %2\$s" + const val OLM_REASON = "OLM error: %1\$s" + const val DETAILED_OLM_REASON = "Unable to decrypt %1\$s. OLM error: %2\$s" + const val UNKNOWN_INBOUND_SESSION_ID_REASON = "Unknown inbound session id" + const val INBOUND_SESSION_MISMATCH_ROOM_ID_REASON = "Mismatched room_id for inbound group session (expected %1\$s, was %2\$s)" + const val MISSING_FIELDS_REASON = "Missing fields in input" + const val BAD_EVENT_FORMAT_TEXT_REASON = "Bad event format" + const val MISSING_SENDER_KEY_TEXT_REASON = "Missing senderKey" + const val MISSING_CIPHER_TEXT_REASON = "Missing ciphertext" + const val BAD_DECRYPTED_FORMAT_TEXT_REASON = "Bad decrypted event format" + const val NOT_INCLUDED_IN_RECIPIENT_REASON = "Not included in recipients" + const val BAD_RECIPIENT_REASON = "Message was intended for %1\$s" + const val BAD_RECIPIENT_KEY_REASON = "Message not intended for this device" + const val FORWARDED_MESSAGE_REASON = "Message forwarded from %1\$s" + const val BAD_ROOM_REASON = "Message intended for room %1\$s" + const val BAD_ENCRYPTED_MESSAGE_REASON = "Bad Encrypted Message" + const val DUPLICATE_MESSAGE_INDEX_REASON = "Duplicate message index, possible replay attack %1\$s" + const val ERROR_MISSING_PROPERTY_REASON = "No '%1\$s' property. Cannot prevent unknown-key attack" + const val UNKNOWN_DEVICES_REASON = "This room contains unknown devices which have not been verified.\n" + + "We strongly recommend you verify them before continuing." + const val NO_MORE_ALGORITHM_REASON = "Room was previously configured to use encryption, but is no longer." + + " Perhaps the homeserver is hiding the configuration event." + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CrossSigningService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CrossSigningService.kt new file mode 100644 index 0000000000..490b1d19c1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CrossSigningService.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.crypto.crosssigning + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustResult +import org.matrix.android.sdk.internal.crypto.crosssigning.UserTrustResult +import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth +import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo + +interface CrossSigningService { + + fun isCrossSigningVerified(): Boolean + + fun isUserTrusted(otherUserId: String): Boolean + + /** + * Will not force a download of the key, but will verify signatures trust chain. + * Checks that my trusted user key has signed the other user UserKey + */ + fun checkUserTrust(otherUserId: String): UserTrustResult + + /** + * Initialize cross signing for this user. + * Users needs to enter credentials + */ + fun initializeCrossSigning(authParams: UserPasswordAuth?, + callback: MatrixCallback) + + fun isCrossSigningInitialized(): Boolean = getMyCrossSigningKeys() != null + + fun checkTrustFromPrivateKeys(masterKeyPrivateKey: String?, + uskKeyPrivateKey: String?, + sskPrivateKey: String?): UserTrustResult + + fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo? + + fun getLiveCrossSigningKeys(userId: String): LiveData> + + fun getMyCrossSigningKeys(): MXCrossSigningInfo? + + fun getCrossSigningPrivateKeys(): PrivateKeysInfo? + + fun getLiveCrossSigningPrivateKeys(): LiveData> + + fun canCrossSign(): Boolean + + fun allPrivateKeysKnown(): Boolean + + fun trustUser(otherUserId: String, + callback: MatrixCallback) + + fun markMyMasterKeyAsTrusted() + + /** + * Sign one of your devices and upload the signature + */ + fun trustDevice(deviceId: String, + callback: MatrixCallback) + + fun checkDeviceTrust(otherUserId: String, + otherDeviceId: String, + locallyTrusted: Boolean?): DeviceTrustResult + + // FIXME Those method do not have to be in the service + fun onSecretMSKGossip(mskPrivateKey: String) + fun onSecretSSKGossip(sskPrivateKey: String) + fun onSecretUSKGossip(uskPrivateKey: String) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CrossSigningSsssSecretConstants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CrossSigningSsssSecretConstants.kt new file mode 100644 index 0000000000..bc85254f69 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CrossSigningSsssSecretConstants.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.crypto.crosssigning + +const val MASTER_KEY_SSSS_NAME = "m.cross_signing.master" + +const val USER_SIGNING_KEY_SSSS_NAME = "m.cross_signing.user_signing" + +const val SELF_SIGNING_KEY_SSSS_NAME = "m.cross_signing.self_signing" + +const val KEYBACKUP_SECRET_SSSS_NAME = "m.megolm_backup.v1" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/MXCrossSigningInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/MXCrossSigningInfo.kt new file mode 100644 index 0000000000..0212dee36c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/MXCrossSigningInfo.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.crypto.crosssigning + +import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey +import org.matrix.android.sdk.internal.crypto.model.KeyUsage + +data class MXCrossSigningInfo( + val userId: String, + val crossSigningKeys: List +) { + + fun isTrusted(): Boolean = masterKey()?.trustLevel?.isVerified() == true + && selfSigningKey()?.trustLevel?.isVerified() == true + + fun masterKey(): CryptoCrossSigningKey? = crossSigningKeys + .firstOrNull { it.usages?.contains(KeyUsage.MASTER.value) == true } + + fun userKey(): CryptoCrossSigningKey? = crossSigningKeys + .firstOrNull { it.usages?.contains(KeyUsage.USER_SIGNING.value) == true } + + fun selfSigningKey(): CryptoCrossSigningKey? = crossSigningKeys + .firstOrNull { it.usages?.contains(KeyUsage.SELF_SIGNING.value) == true } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupService.kt new file mode 100644 index 0000000000..ceeb87c128 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupService.kt @@ -0,0 +1,223 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.crypto.keysbackup + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.listeners.ProgressListener +import org.matrix.android.sdk.api.listeners.StepProgressListener +import org.matrix.android.sdk.internal.crypto.keysbackup.model.KeysBackupVersionTrust +import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult +import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult +import org.matrix.android.sdk.internal.crypto.store.SavedKeyBackupKeyInfo + +interface KeysBackupService { + /** + * Retrieve the current version of the backup from the home server + * + * It can be different than keysBackupVersion. + * @param callback onSuccess(null) will be called if there is no backup on the server + */ + fun getCurrentVersion(callback: MatrixCallback) + + /** + * Create a new keys backup version and enable it, using the information return from [prepareKeysBackupVersion]. + * + * @param keysBackupCreationInfo the info object from [prepareKeysBackupVersion]. + * @param callback Asynchronous callback + */ + fun createKeysBackupVersion(keysBackupCreationInfo: MegolmBackupCreationInfo, + callback: MatrixCallback) + + /** + * Facility method to get the total number of locally stored keys + */ + fun getTotalNumbersOfKeys(): Int + + /** + * Facility method to get the number of backed up keys + */ + fun getTotalNumbersOfBackedUpKeys(): Int + + /** + * Start to back up keys immediately. + * + * @param progressListener the callback to follow the progress + * @param callback the main callback + */ + fun backupAllGroupSessions(progressListener: ProgressListener?, + callback: MatrixCallback?) + + /** + * Check trust on a key backup version. + * + * @param keysBackupVersion the backup version to check. + * @param callback block called when the operations completes. + */ + fun getKeysBackupTrust(keysBackupVersion: KeysVersionResult, + callback: MatrixCallback) + + /** + * Return the current progress of the backup + */ + fun getBackupProgress(progressListener: ProgressListener) + + /** + * Get information about a backup version defined on the homeserver. + * + * It can be different than keysBackupVersion. + * @param version the backup version + * @param callback + */ + fun getVersion(version: String, + callback: MatrixCallback) + + /** + * This method fetches the last backup version on the server, then compare to the currently backup version use. + * If versions are not the same, the current backup is deleted (on server or locally), then the backup may be started again, using the last version. + * + * @param callback true if backup is already using the last version, and false if it is not the case + */ + fun forceUsingLastVersion(callback: MatrixCallback) + + /** + * Check the server for an active key backup. + * + * If one is present and has a valid signature from one of the user's verified + * devices, start backing up to it. + */ + fun checkAndStartKeysBackup() + + fun addListener(listener: KeysBackupStateListener) + + fun removeListener(listener: KeysBackupStateListener) + + /** + * Set up the data required to create a new backup version. + * The backup version will not be created and enabled until [createKeysBackupVersion] + * is called. + * The returned [MegolmBackupCreationInfo] object has a `recoveryKey` member with + * the user-facing recovery key string. + * + * @param password an optional passphrase string that can be entered by the user + * when restoring the backup as an alternative to entering the recovery key. + * @param progressListener a progress listener, as generating private key from password may take a while + * @param callback Asynchronous callback + */ + fun prepareKeysBackupVersion(password: String?, + progressListener: ProgressListener?, + callback: MatrixCallback) + + /** + * Delete a keys backup version. It will delete all backed up keys on the server, and the backup itself. + * If we are backing up to this version. Backup will be stopped. + * + * @param version the backup version to delete. + * @param callback Asynchronous callback + */ + fun deleteBackup(version: String, + callback: MatrixCallback?) + + /** + * Ask if the backup on the server contains keys that we may do not have locally. + * This should be called when entering in the state READY_TO_BACKUP + */ + fun canRestoreKeys(): Boolean + + /** + * Set trust on a keys backup version. + * It adds (or removes) the signature of the current device to the authentication part of the keys backup version. + * + * @param keysBackupVersion the backup version to check. + * @param trust the trust to set to the keys backup. + * @param callback block called when the operations completes. + */ + fun trustKeysBackupVersion(keysBackupVersion: KeysVersionResult, + trust: Boolean, + callback: MatrixCallback) + + /** + * Set trust on a keys backup version. + * + * @param keysBackupVersion the backup version to check. + * @param recoveryKey the recovery key to challenge with the key backup public key. + * @param callback block called when the operations completes. + */ + fun trustKeysBackupVersionWithRecoveryKey(keysBackupVersion: KeysVersionResult, + recoveryKey: String, + callback: MatrixCallback) + + /** + * Set trust on a keys backup version. + * + * @param keysBackupVersion the backup version to check. + * @param password the pass phrase to challenge with the keyBackupVersion public key. + * @param callback block called when the operations completes. + */ + fun trustKeysBackupVersionWithPassphrase(keysBackupVersion: KeysVersionResult, + password: String, + callback: MatrixCallback) + + fun onSecretKeyGossip(secret: String) + + /** + * Restore a backup with a recovery key from a given backup version stored on the homeserver. + * + * @param keysVersionResult the backup version to restore from. + * @param recoveryKey the recovery key to decrypt the retrieved backup. + * @param roomId the id of the room to get backup data from. + * @param sessionId the id of the session to restore. + * @param stepProgressListener the step progress listener + * @param callback Callback. It provides the number of found keys and the number of successfully imported keys. + */ + fun restoreKeysWithRecoveryKey(keysVersionResult: KeysVersionResult, + recoveryKey: String, roomId: String?, + sessionId: String?, + stepProgressListener: StepProgressListener?, + callback: MatrixCallback) + + /** + * Restore a backup with a password from a given backup version stored on the homeserver. + * + * @param keysBackupVersion the backup version to restore from. + * @param password the password to decrypt the retrieved backup. + * @param roomId the id of the room to get backup data from. + * @param sessionId the id of the session to restore. + * @param stepProgressListener the step progress listener + * @param callback Callback. It provides the number of found keys and the number of successfully imported keys. + */ + fun restoreKeyBackupWithPassword(keysBackupVersion: KeysVersionResult, + password: String, + roomId: String?, + sessionId: String?, + stepProgressListener: StepProgressListener?, + callback: MatrixCallback) + + val keysBackupVersion: KeysVersionResult? + val currentBackupVersion: String? + val isEnabled: Boolean + val isStucked: Boolean + val state: KeysBackupState + + // For gossiping + fun saveBackupRecoveryKey(recoveryKey: String?, version: String?) + fun getKeyBackupRecoveryKeyInfo() : SavedKeyBackupKeyInfo? + + fun isValidRecoveryKeyForCurrentVersion(recoveryKey: String, callback: MatrixCallback) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupState.kt new file mode 100644 index 0000000000..9ab190ac98 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupState.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.crypto.keysbackup + +/** + * E2e keys backup states. + * + *
+ *                               |
+ *                               V        deleteKeyBackupVersion (on current backup)
+ *  +---------------------->  UNKNOWN  <-------------
+ *  |                            |
+ *  |                            | checkAndStartKeysBackup (at startup or on new verified device or a new detected backup)
+ *  |                            V
+ *  |                     CHECKING BACKUP
+ *  |                            |
+ *  |    Network error           |
+ *  +<----------+----------------+-------> DISABLED <----------------------+
+ *  |           |                |            |                            |
+ *  |           |                |            | createKeysBackupVersion    |
+ *  |           V                |            V                            |
+ *  +<---  WRONG VERSION         |         ENABLING                        |
+ *      |       ^                |            |                            |
+ *      |       |                V       ok   |     error                  |
+ *      |       |     +------> READY <--------+----------------------------+
+ *      V       |     |          |
+ * NOT TRUSTED  |     |          | on new key
+ *              |     |          V
+ *              |     |     WILL BACK UP (waiting a random duration)
+ *              |     |          |
+ *              |     |          |
+ *              |     | ok       V
+ *              |     +----- BACKING UP
+ *              |                |
+ *              |      Error     |
+ *              +<---------------+
+ * 
+ */ +enum class KeysBackupState { + // Need to check the current backup version on the homeserver + Unknown, + // Checking if backup is enabled on home server + CheckingBackUpOnHomeserver, + // Backup has been stopped because a new backup version has been detected on the homeserver + WrongBackUpVersion, + // Backup from this device is not enabled + Disabled, + // There is a backup available on the homeserver but it is not trusted. + // It is not trusted because the signature is invalid or the device that created it is not verified + // Use [KeysBackup.getKeysBackupTrust()] to get trust details. + // Consequently, the backup from this device is not enabled. + NotTrusted, + // Backup is being enabled: the backup version is being created on the homeserver + Enabling, + // Backup is enabled and ready to send backup to the homeserver + ReadyToBackUp, + // e2e keys are going to be sent to the homeserver + WillBackUp, + // e2e keys are being sent to the homeserver + BackingUp +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupStateListener.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupStateListener.kt new file mode 100644 index 0000000000..10cfe6ce85 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupStateListener.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.crypto.keysbackup + +interface KeysBackupStateListener { + + /** + * The keys backup state has changed + * @param newState the new state + */ + fun onStateChange(newState: KeysBackupState) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keyshare/GossipingRequestListener.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keyshare/GossipingRequestListener.kt new file mode 100644 index 0000000000..3daee31bcf --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keyshare/GossipingRequestListener.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.crypto.keyshare + +import org.matrix.android.sdk.internal.crypto.IncomingRoomKeyRequest +import org.matrix.android.sdk.internal.crypto.IncomingRequestCancellation +import org.matrix.android.sdk.internal.crypto.IncomingSecretShareRequest + +/** + * Room keys events listener + */ +interface GossipingRequestListener { + /** + * An room key request has been received. + * + * @param request the request + */ + fun onRoomKeyRequest(request: IncomingRoomKeyRequest) + + /** + * Returns the secret value to be shared + * @return true if is handled + */ + fun onSecretShareRequest(request: IncomingSecretShareRequest) : Boolean + + /** + * A room key request cancellation has been received. + * + * @param request the cancellation request + */ + fun onRoomKeyRequestCancellation(request: IncomingRequestCancellation) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/CancelCode.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/CancelCode.kt new file mode 100644 index 0000000000..acd0866401 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/CancelCode.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.crypto.verification + +enum class CancelCode(val value: String, val humanReadable: String) { + User("m.user", "the user cancelled the verification"), + Timeout("m.timeout", "the verification process timed out"), + UnknownTransaction("m.unknown_transaction", "the device does not know about that transaction"), + UnknownMethod("m.unknown_method", "the device can’t agree on a key agreement, hash, MAC, or SAS method"), + MismatchedCommitment("m.mismatched_commitment", "the hash commitment did not match"), + MismatchedSas("m.mismatched_sas", "the SAS did not match"), + UnexpectedMessage("m.unexpected_message", "the device received an unexpected message"), + InvalidMessage("m.invalid_message", "an invalid message was received"), + MismatchedKeys("m.key_mismatch", "Key mismatch"), + UserError("m.user_error", "User error"), + MismatchedUser("m.user_mismatch", "User mismatch"), + QrCodeInvalid("m.qr_code.invalid", "Invalid QR code") +} + +fun safeValueOf(code: String?): CancelCode { + return CancelCode.values().firstOrNull { code == it.value } ?: CancelCode.User +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/EmojiRepresentation.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/EmojiRepresentation.kt new file mode 100644 index 0000000000..6b568ee143 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/EmojiRepresentation.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.crypto.verification + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes + +data class EmojiRepresentation(val emoji: String, + @StringRes val nameResId: Int, + @DrawableRes val drawableRes: Int? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/IncomingSasVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/IncomingSasVerificationTransaction.kt new file mode 100644 index 0000000000..45d04e66f0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/IncomingSasVerificationTransaction.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.crypto.verification + +interface IncomingSasVerificationTransaction : SasVerificationTransaction { + val uxState: UxState + + fun performAccept() + + enum class UxState { + UNKNOWN, + SHOW_ACCEPT, + WAIT_FOR_KEY_AGREEMENT, + SHOW_SAS, + WAIT_FOR_VERIFICATION, + VERIFIED, + CANCELLED_BY_ME, + CANCELLED_BY_OTHER + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/OutgoingSasVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/OutgoingSasVerificationTransaction.kt new file mode 100644 index 0000000000..c6be940cde --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/OutgoingSasVerificationTransaction.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.crypto.verification + +interface OutgoingSasVerificationTransaction : SasVerificationTransaction { + val uxState: UxState + + enum class UxState { + UNKNOWN, + WAIT_FOR_START, + WAIT_FOR_KEY_AGREEMENT, + SHOW_SAS, + WAIT_FOR_VERIFICATION, + VERIFIED, + CANCELLED_BY_ME, + CANCELLED_BY_OTHER + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/PendingVerificationRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/PendingVerificationRequest.kt new file mode 100644 index 0000000000..8da60976ac --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/PendingVerificationRequest.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.session.crypto.verification + +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SCAN +import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SHOW +import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_SAS +import java.util.UUID + +/** + * Stores current pending verification requests + */ +data class PendingVerificationRequest( + val ageLocalTs: Long, + val isIncoming: Boolean = false, + val localId: String = UUID.randomUUID().toString(), + val otherUserId: String, + val roomId: String?, + val transactionId: String? = null, + val requestInfo: ValidVerificationInfoRequest? = null, + val readyInfo: ValidVerificationInfoReady? = null, + val cancelConclusion: CancelCode? = null, + val isSuccessful: Boolean = false, + val handledByOtherSession: Boolean = false, + // In case of to device it is sent to a list of devices + val targetDevices: List? = null +) { + val isReady: Boolean = readyInfo != null + val isSent: Boolean = transactionId != null + + val isFinished: Boolean = isSuccessful || cancelConclusion != null + + /** + * SAS is supported if I support it and the other party support it + */ + fun isSasSupported(): Boolean { + return requestInfo?.methods?.contains(VERIFICATION_METHOD_SAS).orFalse() + && readyInfo?.methods?.contains(VERIFICATION_METHOD_SAS).orFalse() + } + + /** + * Other can show QR code if I can scan QR code and other can show QR code + */ + fun otherCanShowQrCode(): Boolean { + return if (isIncoming) { + requestInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW).orFalse() + && readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN).orFalse() + } else { + requestInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN).orFalse() + && readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW).orFalse() + } + } + + /** + * Other can scan QR code if I can show QR code and other can scan QR code + */ + fun otherCanScanQrCode(): Boolean { + return if (isIncoming) { + requestInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN).orFalse() + && readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW).orFalse() + } else { + requestInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW).orFalse() + && readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN).orFalse() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/QrCodeVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/QrCodeVerificationTransaction.kt new file mode 100644 index 0000000000..e4956aaabb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/QrCodeVerificationTransaction.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.crypto.verification + +interface QrCodeVerificationTransaction : VerificationTransaction { + + /** + * To use to display a qr code, for the other user to scan it + */ + val qrCodeText: String? + + /** + * Call when you have scan the other user QR code + */ + fun userHasScannedOtherQrCode(otherQrCodeText: String) + + /** + * Call when you confirm that other user has scanned your QR code + */ + fun otherUserScannedMyQrCode() + + /** + * Call when you do not confirm that other user has scanned your QR code + */ + fun otherUserDidNotScannedMyQrCode() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/SasMode.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/SasMode.kt new file mode 100644 index 0000000000..2dc5c308ee --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/SasMode.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.crypto.verification + +object SasMode { + const val DECIMAL = "decimal" + const val EMOJI = "emoji" +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/SasVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/SasVerificationTransaction.kt new file mode 100644 index 0000000000..00da238bd2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/SasVerificationTransaction.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.crypto.verification + +interface SasVerificationTransaction : VerificationTransaction { + + fun supportsEmoji(): Boolean + + fun supportsDecimal(): Boolean + + fun getEmojiCodeRepresentation(): List + + fun getDecimalCodeRepresentation(): String + + /** + * To be called by the client when the user has verified that + * both short codes do match + */ + fun userHasVerifiedShortCode() + + fun shortCodeDoesNotMatch() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/ValidVerificationInfoReady.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/ValidVerificationInfoReady.kt new file mode 100644 index 0000000000..68c1bb7bb0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/ValidVerificationInfoReady.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.crypto.verification + +data class ValidVerificationInfoReady( + val transactionId: String, + val fromDevice: String, + val methods: List +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/ValidVerificationInfoRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/ValidVerificationInfoRequest.kt new file mode 100644 index 0000000000..431c9728ee --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/ValidVerificationInfoRequest.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.crypto.verification + +data class ValidVerificationInfoRequest( + val transactionId: String, + val fromDevice: String, + val methods: List, + val timestamp: Long? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationMethod.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationMethod.kt new file mode 100644 index 0000000000..15a728ccf2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationMethod.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.crypto.verification + +/** + * Verification methods + */ +enum class VerificationMethod { + // Use it when your application supports the SAS verification method + SAS, + // Use it if your application is able to display QR codes + QR_CODE_SHOW, + // Use it if your application is able to scan QR codes + QR_CODE_SCAN +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationService.kt new file mode 100644 index 0000000000..623f9e5c0e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationService.kt @@ -0,0 +1,145 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.crypto.verification + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.LocalEcho + +/** + * https://matrix.org/docs/spec/client_server/r0.5.0#key-verification-framework + * + * Verifying keys manually by reading out the Ed25519 key is not very user friendly, and can lead to errors. + * Verification is a user-friendly key verification process. + * Verification is intended to be a highly interactive process for users, + * and as such exposes verification methods which are easier for users to use. + */ +interface VerificationService { + + fun addListener(listener: Listener) + + fun removeListener(listener: Listener) + + /** + * Mark this device as verified manually + */ + fun markedLocallyAsManuallyVerified(userId: String, deviceID: String) + + fun getExistingTransaction(otherUserId: String, tid: String): VerificationTransaction? + + fun getExistingVerificationRequest(otherUserId: String): List? + + fun getExistingVerificationRequest(otherUserId: String, tid: String?): PendingVerificationRequest? + + fun getExistingVerificationRequestInRoom(roomId: String, tid: String?): PendingVerificationRequest? + + fun beginKeyVerification(method: VerificationMethod, + otherUserId: String, + otherDeviceId: String, + transactionId: String?): String? + + /** + * Request a key verification from another user using toDevice events. + */ + fun requestKeyVerificationInDMs(methods: List, + otherUserId: String, + roomId: String, + localId: String? = LocalEcho.createLocalEchoId()): PendingVerificationRequest + + fun cancelVerificationRequest(request: PendingVerificationRequest) + + /** + * Request a key verification from another user using toDevice events. + */ + fun requestKeyVerification(methods: List, + otherUserId: String, + otherDevices: List?): PendingVerificationRequest + + fun declineVerificationRequestInDMs(otherUserId: String, + transactionId: String, + roomId: String) + + // Only SAS method is supported for the moment + // TODO Parameter otherDeviceId should be removed in this case + fun beginKeyVerificationInDMs(method: VerificationMethod, + transactionId: String, + roomId: String, + otherUserId: String, + otherDeviceId: String, + callback: MatrixCallback?): String? + + /** + * Returns false if the request is unknown + */ + fun readyPendingVerificationInDMs(methods: List, + otherUserId: String, + roomId: String, + transactionId: String): Boolean + + /** + * Returns false if the request is unknown + */ + fun readyPendingVerification(methods: List, + otherUserId: String, + transactionId: String): Boolean + + interface Listener { + /** + * Called when a verification request is created either by the user, or by the other user. + */ + fun verificationRequestCreated(pr: PendingVerificationRequest) {} + + /** + * Called when a verification request is updated. + */ + fun verificationRequestUpdated(pr: PendingVerificationRequest) {} + + /** + * Called when a transaction is created, either by the user or initiated by the other user. + */ + fun transactionCreated(tx: VerificationTransaction) {} + + /** + * Called when a transaction is updated. You may be interested to track the state of the VerificationTransaction. + */ + fun transactionUpdated(tx: VerificationTransaction) {} + + /** + * Inform the the deviceId of the userId has been marked as manually verified by the SDK. + * It will be called after VerificationService.markedLocallyAsManuallyVerified() is called. + * + */ + fun markedAsManuallyVerified(userId: String, deviceId: String) {} + } + + companion object { + + private const val TEN_MINUTES_IN_MILLIS = 10 * 60 * 1000 + private const val FIVE_MINUTES_IN_MILLIS = 5 * 60 * 1000 + + fun isValidRequest(age: Long?): Boolean { + if (age == null) return false + val now = System.currentTimeMillis() + val tooInThePast = now - TEN_MINUTES_IN_MILLIS + val tooInTheFuture = now + FIVE_MINUTES_IN_MILLIS + return age in tooInThePast..tooInTheFuture + } + } + + fun onPotentiallyInterestingEventRoomFailToDecrypt(event: Event) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationTransaction.kt new file mode 100644 index 0000000000..7e7dcb6d90 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationTransaction.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.crypto.verification + +interface VerificationTransaction { + + var state: VerificationTxState + + val transactionId: String + val otherUserId: String + var otherDeviceId: String? + + // TODO Not used. Remove? + val isIncoming: Boolean + + /** + * User wants to cancel the transaction + */ + fun cancel() + + fun cancel(code: CancelCode) + + fun isToDeviceTransport(): Boolean +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationTxState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationTxState.kt new file mode 100644 index 0000000000..a8ae81bc30 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationTxState.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.crypto.verification + +sealed class VerificationTxState { + // Uninitialized state + object None : VerificationTxState() + + // Specific for SAS + abstract class VerificationSasTxState : VerificationTxState() + + object SendingStart : VerificationSasTxState() + object Started : VerificationSasTxState() + object OnStarted : VerificationSasTxState() + object SendingAccept : VerificationSasTxState() + object Accepted : VerificationSasTxState() + object OnAccepted : VerificationSasTxState() + object SendingKey : VerificationSasTxState() + object KeySent : VerificationSasTxState() + object OnKeyReceived : VerificationSasTxState() + object ShortCodeReady : VerificationSasTxState() + object ShortCodeAccepted : VerificationSasTxState() + object SendingMac : VerificationSasTxState() + object MacSent : VerificationSasTxState() + object Verifying : VerificationSasTxState() + + // Specific for QR code + abstract class VerificationQrTxState : VerificationTxState() + + // Will be used to ask the user if the other user has correctly scanned + object QrScannedByOther : VerificationQrTxState() + object WaitingOtherReciprocateConfirm : VerificationQrTxState() + + // Terminal states + abstract class TerminalTxState : VerificationTxState() + + object Verified : TerminalTxState() + + // Cancelled by me or by other + data class Cancelled(val cancelCode: CancelCode, val byMe: Boolean) : TerminalTxState() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedAnnotation.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedAnnotation.kt new file mode 100644 index 0000000000..30a1e29d81 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedAnnotation.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.session.events.model + +import com.squareup.moshi.JsonClass + +/** + * + * { + * "chunk": [ + * { + * "type": "m.reaction", + * "key": "👍", + * "count": 3 + * } + * ], + * "limited": false, + * "count": 1 + * }, + * + */ + +@JsonClass(generateAdapter = true) +data class AggregatedAnnotation( + override val limited: Boolean? = false, + override val count: Int? = 0, + val chunk: List? = null + +) : UnsignedRelationInfo diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt new file mode 100644 index 0000000000..8bc1af25e0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.session.events.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * + * { + * "m.annotation": { + * "chunk": [ + * { + * "type": "m.reaction", + * "key": "👍", + * "count": 3 + * } + * ], + * "limited": false, + * "count": 1 + * }, + * "m.reference": { + * "chunk": [ + * { + * "type": "m.room.message", + * "event_id": "$some_event_id" + * } + * ], + * "limited": false, + * "count": 1 + * } + * } + * + */ + +@JsonClass(generateAdapter = true) +data class AggregatedRelations( + @Json(name = "m.annotation") val annotations: AggregatedAnnotation? = null, + @Json(name = "m.reference") val references: DefaultUnsignedRelationInfo? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/DefaultUnsignedRelationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/DefaultUnsignedRelationInfo.kt new file mode 100644 index 0000000000..f8be9e26a0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/DefaultUnsignedRelationInfo.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.session.events.model + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class DefaultUnsignedRelationInfo( + override val limited: Boolean? = false, + override val count: Int? = 0, + val chunk: List>? = null + +) : UnsignedRelationInfo diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt new file mode 100644 index 0000000000..fdd3e66703 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt @@ -0,0 +1,260 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.events.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageType +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult +import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.json.JSONObject +import timber.log.Timber + +typealias Content = JsonDict + +/** + * This methods is a facility method to map a json content to a model. + */ +inline fun Content?.toModel(catchError: Boolean = true): T? { + val moshi = MoshiProvider.providesMoshi() + val moshiAdapter = moshi.adapter(T::class.java) + return try { + moshiAdapter.fromJsonValue(this) + } catch (e: Exception) { + if (catchError) { + Timber.e(e, "To model failed : $e") + null + } else { + throw e + } + } +} + +/** + * This methods is a facility method to map a model to a json Content + */ +@Suppress("UNCHECKED_CAST") +inline fun T.toContent(): Content { + val moshi = MoshiProvider.providesMoshi() + val moshiAdapter = moshi.adapter(T::class.java) + return moshiAdapter.toJsonValue(this) as Content +} + +/** + * Generic event class with all possible fields for events. + * The content and prevContent json fields can easily be mapped to a model with [toModel] method. + */ +@JsonClass(generateAdapter = true) +data class Event( + @Json(name = "type") val type: String, + @Json(name = "event_id") val eventId: String? = null, + @Json(name = "content") val content: Content? = null, + @Json(name = "prev_content") val prevContent: Content? = null, + @Json(name = "origin_server_ts") val originServerTs: Long? = null, + @Json(name = "sender") val senderId: String? = null, + @Json(name = "state_key") val stateKey: String? = null, + @Json(name = "room_id") val roomId: String? = null, + @Json(name = "unsigned") val unsignedData: UnsignedData? = null, + @Json(name = "redacts") val redacts: String? = null +) { + + @Transient + var mxDecryptionResult: OlmDecryptionResult? = null + + @Transient + var mCryptoError: MXCryptoError.ErrorType? = null + + @Transient + var mCryptoErrorReason: String? = null + + @Transient + var sendState: SendState = SendState.UNKNOWN + + /** + * The `age` value transcoded in a timestamp based on the device clock when the SDK received + * the event from the home server. + * Unlike `age`, this value is static. + */ + @Transient + var ageLocalTs: Long? = null + + /** + * Check if event is a state event. + * @return true if event is state event. + */ + fun isStateEvent(): Boolean { + return stateKey != null + } + + // ============================================================================================================== + // Crypto + // ============================================================================================================== + + /** + * @return true if this event is encrypted. + */ + fun isEncrypted(): Boolean { + return type == EventType.ENCRYPTED + } + + /** + * @return The curve25519 key that sent this event. + */ + fun getSenderKey(): String? { + return mxDecryptionResult?.senderKey + } + + /** + * @return The additional keys the sender of this encrypted event claims to possess. + */ + fun getKeysClaimed(): Map { + return mxDecryptionResult?.keysClaimed ?: HashMap() + } + + /** + * @return the event type + */ + fun getClearType(): String { + return mxDecryptionResult?.payload?.get("type")?.toString() ?: type + } + + /** + * @return the event content + */ + fun getClearContent(): Content? { + @Suppress("UNCHECKED_CAST") + return mxDecryptionResult?.payload?.get("content") as? Content ?: content + } + + fun toContentStringWithIndent(): String { + val contentMap = toContent() + return JSONObject(contentMap).toString(4) + } + + fun toClearContentStringWithIndent(): String? { + val contentMap = this.mxDecryptionResult?.payload + val adapter = MoshiProvider.providesMoshi().adapter(Map::class.java) + return contentMap?.let { JSONObject(adapter.toJson(it)).toString(4) } + } + + /** + * Tells if the event is redacted + */ + fun isRedacted() = unsignedData?.redactedEvent != null + + /** + * Tells if the event is redacted by the user himself. + */ + fun isRedactedBySameUser() = senderId == unsignedData?.redactedEvent?.senderId + + fun resolvedPrevContent(): Content? = prevContent ?: unsignedData?.prevContent + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Event + + if (type != other.type) return false + if (eventId != other.eventId) return false + if (content != other.content) return false + if (prevContent != other.prevContent) return false + if (originServerTs != other.originServerTs) return false + if (senderId != other.senderId) return false + if (stateKey != other.stateKey) return false + if (roomId != other.roomId) return false + if (unsignedData != other.unsignedData) return false + if (redacts != other.redacts) return false + if (mxDecryptionResult != other.mxDecryptionResult) return false + if (mCryptoError != other.mCryptoError) return false + if (mCryptoErrorReason != other.mCryptoErrorReason) return false + if (sendState != other.sendState) return false + + return true + } + + override fun hashCode(): Int { + var result = type.hashCode() + result = 31 * result + (eventId?.hashCode() ?: 0) + result = 31 * result + (content?.hashCode() ?: 0) + result = 31 * result + (prevContent?.hashCode() ?: 0) + result = 31 * result + (originServerTs?.hashCode() ?: 0) + result = 31 * result + (senderId?.hashCode() ?: 0) + result = 31 * result + (stateKey?.hashCode() ?: 0) + result = 31 * result + (roomId?.hashCode() ?: 0) + result = 31 * result + (unsignedData?.hashCode() ?: 0) + result = 31 * result + (redacts?.hashCode() ?: 0) + result = 31 * result + (mxDecryptionResult?.hashCode() ?: 0) + result = 31 * result + (mCryptoError?.hashCode() ?: 0) + result = 31 * result + (mCryptoErrorReason?.hashCode() ?: 0) + result = 31 * result + sendState.hashCode() + return result + } +} + +fun Event.isTextMessage(): Boolean { + return getClearType() == EventType.MESSAGE + && when (getClearContent()?.toModel()?.msgType) { + MessageType.MSGTYPE_TEXT, + MessageType.MSGTYPE_EMOTE, + MessageType.MSGTYPE_NOTICE -> true + else -> false + } +} + +fun Event.isImageMessage(): Boolean { + return getClearType() == EventType.MESSAGE + && when (getClearContent()?.toModel()?.msgType) { + MessageType.MSGTYPE_IMAGE -> true + else -> false + } +} + +fun Event.isVideoMessage(): Boolean { + return getClearType() == EventType.MESSAGE + && when (getClearContent()?.toModel()?.msgType) { + MessageType.MSGTYPE_VIDEO -> true + else -> false + } +} + +fun Event.isFileMessage(): Boolean { + return getClearType() == EventType.MESSAGE + && when (getClearContent()?.toModel()?.msgType) { + MessageType.MSGTYPE_FILE -> true + else -> false + } +} + +fun Event.getRelationContent(): RelationDefaultContent? { + return if (isEncrypted()) { + content.toModel()?.relatesTo + } else { + content.toModel()?.relatesTo + } +} + +fun Event.isReply(): Boolean { + return getRelationContent()?.inReplyTo?.eventId != null +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt new file mode 100644 index 0000000000..f9f2e10af4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt @@ -0,0 +1,96 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.events.model + +/** + * Constants defining known event types from Matrix specifications. + */ +object EventType { + + const val PRESENCE = "m.presence" + const val MESSAGE = "m.room.message" + const val STICKER = "m.sticker" + const val ENCRYPTED = "m.room.encrypted" + const val FEEDBACK = "m.room.message.feedback" + const val TYPING = "m.typing" + const val REDACTION = "m.room.redaction" + const val RECEIPT = "m.receipt" + const val TAG = "m.tag" + const val ROOM_KEY = "m.room_key" + const val FULLY_READ = "m.fully_read" + const val PLUMBING = "m.room.plumbing" + const val BOT_OPTIONS = "m.room.bot.options" + const val PREVIEW_URLS = "org.matrix.room.preview_urls" + + // State Events + + const val STATE_ROOM_WIDGET_LEGACY = "im.vector.modular.widgets" + const val STATE_ROOM_WIDGET = "m.widget" + const val STATE_ROOM_NAME = "m.room.name" + const val STATE_ROOM_TOPIC = "m.room.topic" + const val STATE_ROOM_AVATAR = "m.room.avatar" + const val STATE_ROOM_MEMBER = "m.room.member" + const val STATE_ROOM_THIRD_PARTY_INVITE = "m.room.third_party_invite" + const val STATE_ROOM_CREATE = "m.room.create" + const val STATE_ROOM_JOIN_RULES = "m.room.join_rules" + const val STATE_ROOM_GUEST_ACCESS = "m.room.guest_access" + const val STATE_ROOM_POWER_LEVELS = "m.room.power_levels" + const val STATE_ROOM_ALIASES = "m.room.aliases" + const val STATE_ROOM_TOMBSTONE = "m.room.tombstone" + const val STATE_ROOM_CANONICAL_ALIAS = "m.room.canonical_alias" + const val STATE_ROOM_HISTORY_VISIBILITY = "m.room.history_visibility" + const val STATE_ROOM_RELATED_GROUPS = "m.room.related_groups" + const val STATE_ROOM_PINNED_EVENT = "m.room.pinned_events" + const val STATE_ROOM_ENCRYPTION = "m.room.encryption" + + // Call Events + const val CALL_INVITE = "m.call.invite" + const val CALL_CANDIDATES = "m.call.candidates" + const val CALL_ANSWER = "m.call.answer" + const val CALL_HANGUP = "m.call.hangup" + + // Key share events + const val ROOM_KEY_REQUEST = "m.room_key_request" + const val FORWARDED_ROOM_KEY = "m.forwarded_room_key" + const val ROOM_KEY_WITHHELD = "org.matrix.room_key.withheld" + + const val REQUEST_SECRET = "m.secret.request" + const val SEND_SECRET = "m.secret.send" + + // Interactive key verification + const val KEY_VERIFICATION_START = "m.key.verification.start" + const val KEY_VERIFICATION_ACCEPT = "m.key.verification.accept" + const val KEY_VERIFICATION_KEY = "m.key.verification.key" + const val KEY_VERIFICATION_MAC = "m.key.verification.mac" + const val KEY_VERIFICATION_CANCEL = "m.key.verification.cancel" + const val KEY_VERIFICATION_DONE = "m.key.verification.done" + const val KEY_VERIFICATION_READY = "m.key.verification.ready" + + // Relation Events + const val REACTION = "m.reaction" + + // Unwedging + internal const val DUMMY = "m.dummy" + + fun isCallEvent(type: String): Boolean { + return type == CALL_INVITE + || type == CALL_CANDIDATES + || type == CALL_ANSWER + || type == CALL_HANGUP + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/LocalEcho.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/LocalEcho.kt new file mode 100644 index 0000000000..aa3726d49e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/LocalEcho.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.events.model + +import java.util.UUID + +object LocalEcho { + + private const val PREFIX = "\$local." + + fun isLocalEchoId(eventId: String) = eventId.startsWith(PREFIX) + + fun createLocalEchoId() = "${PREFIX}${UUID.randomUUID()}" +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationChunkInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationChunkInfo.kt new file mode 100644 index 0000000000..72ab3e5c0e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationChunkInfo.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.session.events.model + +import com.squareup.moshi.JsonClass + +/** + * + * { + * "type": "m.reaction", + * "key": "👍", + * "count": 3 + * } + * + */ + +@JsonClass(generateAdapter = true) +data class RelationChunkInfo( + val type: String, + val key: String, + val count: Int +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationType.kt new file mode 100644 index 0000000000..2c18bd6c8a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationType.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.session.events.model + +/** + * Constants defining known event relation types from Matrix specifications + */ +object RelationType { + /** Lets you define an event which annotates an existing event.*/ + const val ANNOTATION = "m.annotation" + /** Lets you define an event which replaces an existing event.*/ + const val REPLACE = "m.replace" + /** Lets you define an event which references an existing event.*/ + const val REFERENCE = "m.reference" + /** Lets you define an event which adds a response to an existing event.*/ + const val RESPONSE = "org.matrix.response" +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/UnsignedData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/UnsignedData.kt new file mode 100644 index 0000000000..a16d9ec5bd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/UnsignedData.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.events.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class UnsignedData( + /** + * The time in milliseconds that has elapsed since the event was sent. + * This field is generated by the local homeserver, and may be incorrect if the local time on at least one of the two servers + * is out of sync, which can cause the age to either be negative or greater than it actually is. + */ + @Json(name = "age") val age: Long?, + /** + * Optional. The event that redacted this event, if any. + */ + @Json(name = "redacted_because") val redactedEvent: Event? = null, + /** + * The client-supplied transaction ID, if the client being given the event is the same one which sent it. + */ + @Json(name = "transaction_id") val transactionId: String? = null, + /** + * Optional. The previous content for this event. If there is no previous content, this key will be missing. + */ + @Json(name = "prev_content") val prevContent: Map? = null, + @Json(name = "m.relations") val relations: AggregatedRelations? = null, + /** + * Optional. The eventId of the previous state event being replaced. + */ + @Json(name = "replaces_state") val replacesState: String? = null + +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/UnsignedRelationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/UnsignedRelationInfo.kt new file mode 100644 index 0000000000..04371ae54b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/UnsignedRelationInfo.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.session.events.model + +interface UnsignedRelationInfo { + val limited : Boolean? + val count: Int? +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/ContentDownloadStateTracker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/ContentDownloadStateTracker.kt new file mode 100644 index 0000000000..52bf0ed05c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/ContentDownloadStateTracker.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.file + +interface ContentDownloadStateTracker { + fun track(key: String, updateListener: UpdateListener) + fun unTrack(key: String, updateListener: UpdateListener) + fun clear() + + sealed class State { + object Idle : State() + data class Downloading(val current: Long, val total: Long, val indeterminate: Boolean) : State() + object Decrypting : State() + object Success : State() + data class Failure(val errorCode: Int) : State() + } + + interface UpdateListener { + fun onDownloadStateUpdate(state: State) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt new file mode 100644 index 0000000000..da42bfa485 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.file + +import android.net.Uri +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt +import java.io.File + +/** + * This interface defines methods to get files. + */ +interface FileService { + + enum class DownloadMode { + /** + * Download file in external storage + */ + TO_EXPORT, + + /** + * Download file in cache + */ + FOR_INTERNAL_USE, + + /** + * Download file in file provider path + */ + FOR_EXTERNAL_SHARE + } + + enum class FileState { + IN_CACHE, + DOWNLOADING, + UNKNOWN + } + + /** + * Download a file. + * Result will be a decrypted file, stored in the cache folder. url parameter will be used to create unique filename to avoid name collision. + */ + fun downloadFile( + downloadMode: DownloadMode, + id: String, + fileName: String, + mimeType: String?, + url: String?, + elementToDecrypt: ElementToDecrypt?, + callback: MatrixCallback): Cancelable + + fun isFileInCache(mxcUrl: String, mimeType: String?): Boolean + + /** + * Use this URI and pass it to intent using flag Intent.FLAG_GRANT_READ_URI_PERMISSION + * (if not other app won't be able to access it) + */ + fun getTemporarySharableURI(mxcUrl: String, mimeType: String?): Uri? + + /** + * Get information on the given file. + * Mimetype should be the same one as passed to downloadFile (limitation for now) + */ + fun fileState(mxcUrl: String, mimeType: String?): FileState + + /** + * Clears all the files downloaded by the service + */ + fun clearCache() + + /** + * Get size of cached files + */ + fun getCacheSize(): Int +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/MatrixSDKFileProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/MatrixSDKFileProvider.kt new file mode 100644 index 0000000000..b456626ef7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/MatrixSDKFileProvider.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.file + +import android.net.Uri +import androidx.core.content.FileProvider + +/** + * We have to declare our own file provider to avoid collision with apps using the sdk + * and having their own + */ +class MatrixSDKFileProvider : FileProvider() { + override fun getType(uri: Uri): String? { + return super.getType(uri) ?: "plain/text" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/Group.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/Group.kt new file mode 100644 index 0000000000..10435ee054 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/Group.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.group + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.util.Cancelable + +/** + * This interface defines methods to interact within a group. + */ +interface Group { + val groupId: String + + /** + * This methods allows you to refresh data about this group. It will be reflected on the GroupSummary. + * The SDK also takes care of refreshing group data every hour. + * @param callback : the matrix callback to be notified of success or failure + * @return a Cancelable to be able to cancel requests. + */ + fun fetchGroupData(callback: MatrixCallback): Cancelable +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/GroupService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/GroupService.kt new file mode 100644 index 0000000000..6858db4646 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/GroupService.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.group + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.session.group.model.GroupSummary + +/** + * This interface defines methods to get groups. It's implemented at the session level. + */ +interface GroupService { + + /** + * Get a group from a groupId + * @param groupId the groupId to look for. + * @return the group with groupId or null + */ + fun getGroup(groupId: String): Group? + + /** + * Get a groupSummary from a groupId + * @param groupId the groupId to look for. + * @return the groupSummary with groupId or null + */ + fun getGroupSummary(groupId: String): GroupSummary? + + /** + * Get a list of group summaries. This list is a snapshot of the data. + * @return the list of [GroupSummary] + */ + fun getGroupSummaries(groupSummaryQueryParams: GroupSummaryQueryParams): List + + /** + * Get a live list of group summaries. This list is refreshed as soon as the data changes. + * @return the [LiveData] of [GroupSummary] + */ + fun getGroupSummariesLive(groupSummaryQueryParams: GroupSummaryQueryParams): LiveData> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/GroupSummaryQueryParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/GroupSummaryQueryParams.kt new file mode 100644 index 0000000000..bf9535f271 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/GroupSummaryQueryParams.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.group + +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.room.model.Membership + +fun groupSummaryQueryParams(init: (GroupSummaryQueryParams.Builder.() -> Unit) = {}): GroupSummaryQueryParams { + return GroupSummaryQueryParams.Builder().apply(init).build() +} + +/** + * This class can be used to filter group summaries + */ +data class GroupSummaryQueryParams( + val displayName: QueryStringValue, + val memberships: List +) { + + class Builder { + + var displayName: QueryStringValue = QueryStringValue.IsNotEmpty + var memberships: List = Membership.all() + + fun build() = GroupSummaryQueryParams( + displayName = displayName, + memberships = memberships + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/model/GroupSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/model/GroupSummary.kt new file mode 100644 index 0000000000..2633bdcdeb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/model/GroupSummary.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.group.model + +import org.matrix.android.sdk.api.session.room.model.Membership + +/** + * This class holds some data of a group. + * It can be retrieved through [org.matrix.android.sdk.api.session.group.GroupService] + */ +data class GroupSummary( + val groupId: String, + val membership: Membership, + val displayName: String = "", + val shortDescription: String = "", + val avatarUrl: String = "", + val roomIds: List = emptyList(), + val userIds: List = emptyList() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt new file mode 100644 index 0000000000..c463fe9e72 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.homeserver + +data class HomeServerCapabilities( + /** + * True if it is possible to change the password of the account. + */ + val canChangePassword: Boolean = true, + /** + * Max size of file which can be uploaded to the homeserver in bytes. [MAX_UPLOAD_FILE_SIZE_UNKNOWN] if unknown or not retrieved yet + */ + val maxUploadFileSize: Long = MAX_UPLOAD_FILE_SIZE_UNKNOWN, + /** + * Last version identity server and binding supported + */ + val lastVersionIdentityServerSupported: Boolean = false, + /** + * Default identity server url, provided in Wellknown + */ + val defaultIdentityServerUrl: String? = null, + /** + * Option to allow homeserver admins to set the default E2EE behaviour back to disabled for DMs / private rooms + * (as it was before) for various environments where this is desired. + */ + val adminE2EByDefault: Boolean = true +) { + companion object { + const val MAX_UPLOAD_FILE_SIZE_UNKNOWN = -1L + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilitiesService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilitiesService.kt new file mode 100644 index 0000000000..bcf1052b98 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilitiesService.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.homeserver + +/** + * This interface defines a method to retrieve the homeserver capabilities. + */ +interface HomeServerCapabilitiesService { + + /** + * Get the HomeServer capabilities + */ + fun getHomeServerCapabilities(): HomeServerCapabilities +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/FoundThreePid.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/FoundThreePid.kt new file mode 100644 index 0000000000..2ac1720400 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/FoundThreePid.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.identity + +data class FoundThreePid( + val threePid: ThreePid, + val matrixId: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityService.kt new file mode 100644 index 0000000000..2a4054114e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityService.kt @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.identity + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.util.Cancelable + +/** + * Provides access to the identity server configuration and services identity server can provide + */ +interface IdentityService { + /** + * Return the default identity server of the user, which may have been provided at login time by the homeserver, + * or by the Well-known setup of the homeserver + * It may be different from the current configured identity server + */ + fun getDefaultIdentityServer(): String? + + /** + * Return the current identity server URL used by this account. Returns null if no identity server is configured. + */ + fun getCurrentIdentityServerUrl(): String? + + /** + * Check if the identity server is valid + * See https://matrix.org/docs/spec/identity_service/latest#status-check + * RiotX SDK only supports identity server API v2 + */ + fun isValidIdentityServer(url: String, callback: MatrixCallback): Cancelable + + /** + * Update the identity server url. + * If successful, any previous identity server will be disconnected. + * In case of error, any previous identity server will remain configured. + * @param url the new url. + * @param callback will notify the user if change is successful. The String will be the final url of the identity server. + * The SDK can prepend "https://" for instance. + */ + fun setNewIdentityServer(url: String, callback: MatrixCallback): Cancelable + + /** + * Disconnect (logout) from the current identity server + */ + fun disconnect(callback: MatrixCallback): Cancelable + + /** + * This will ask the identity server to send an email or an SMS to let the user confirm he owns the ThreePid + */ + fun startBindThreePid(threePid: ThreePid, callback: MatrixCallback): Cancelable + + /** + * This will cancel a pending binding of threePid. + */ + fun cancelBindThreePid(threePid: ThreePid, callback: MatrixCallback): Cancelable + + /** + * This will ask the identity server to send an new email or a new SMS to let the user confirm he owns the ThreePid + */ + fun sendAgainValidationCode(threePid: ThreePid, callback: MatrixCallback): Cancelable + + /** + * Submit the code that the identity server has sent to the user (in email or SMS) + * Once successful, you will have to call [finalizeBindThreePid] + * @param code the code sent to the user + */ + fun submitValidationToken(threePid: ThreePid, code: String, callback: MatrixCallback): Cancelable + + /** + * This will perform the actual association of ThreePid and Matrix account + */ + fun finalizeBindThreePid(threePid: ThreePid, callback: MatrixCallback): Cancelable + + /** + * Unbind a threePid + * The request will actually be done on the homeserver + */ + fun unbindThreePid(threePid: ThreePid, callback: MatrixCallback): Cancelable + + /** + * Search MatrixId of users providing email and phone numbers + */ + fun lookUp(threePids: List, callback: MatrixCallback>): Cancelable + + /** + * Get the status of the current user's threePid + * A lookup will be performed, but also pending binding state will be restored + * + * @param threePids the list of threePid the user owns (retrieved form the homeserver) + * @param callback onSuccess will be called with a map of ThreePid -> SharedState + */ + fun getShareStatus(threePids: List, callback: MatrixCallback>): Cancelable + + fun addListener(listener: IdentityServiceListener) + fun removeListener(listener: IdentityServiceListener) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityServiceError.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityServiceError.kt new file mode 100644 index 0000000000..a9f8ccb9d3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityServiceError.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.identity + +import org.matrix.android.sdk.api.failure.Failure + +sealed class IdentityServiceError : Failure.FeatureFailure() { + object OutdatedIdentityServer : IdentityServiceError() + object OutdatedHomeServer : IdentityServiceError() + object NoIdentityServerConfigured : IdentityServiceError() + object TermsNotSignedException : IdentityServiceError() + object BulkLookupSha256NotSupported : IdentityServiceError() + object BindingError : IdentityServiceError() + object NoCurrentBindingError : IdentityServiceError() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityServiceListener.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityServiceListener.kt new file mode 100644 index 0000000000..f01d4e97c3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityServiceListener.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.identity + +interface IdentityServiceListener { + fun onIdentityServerChange() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/SharedState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/SharedState.kt new file mode 100644 index 0000000000..3dae4b43ee --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/SharedState.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.identity + +enum class SharedState { + SHARED, + NOT_SHARED, + BINDING_IN_PROGRESS +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/ThreePid.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/ThreePid.kt new file mode 100644 index 0000000000..de4e0a9a5a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/ThreePid.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.identity + +import com.google.i18n.phonenumbers.NumberParseException +import com.google.i18n.phonenumbers.PhoneNumberUtil +import org.matrix.android.sdk.internal.session.profile.ThirdPartyIdentifier + +sealed class ThreePid(open val value: String) { + data class Email(val email: String) : ThreePid(email) + data class Msisdn(val msisdn: String) : ThreePid(msisdn) +} + +internal fun ThreePid.toMedium(): String { + return when (this) { + is ThreePid.Email -> ThirdPartyIdentifier.MEDIUM_EMAIL + is ThreePid.Msisdn -> ThirdPartyIdentifier.MEDIUM_MSISDN + } +} + +@Throws(NumberParseException::class) +internal fun ThreePid.Msisdn.getCountryCode(): String { + return with(PhoneNumberUtil.getInstance()) { + getRegionCodeForCountryCode(parse("+$msisdn", null).countryCode) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/integrationmanager/IntegrationManagerConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/integrationmanager/IntegrationManagerConfig.kt new file mode 100644 index 0000000000..0bee245537 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/integrationmanager/IntegrationManagerConfig.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.session.integrationmanager + +/** + * This class holds configuration of integration manager. + */ +data class IntegrationManagerConfig( + val uiUrl: String, + val restUrl: String, + val kind: Kind +) { + + // Order matters, first is preferred + /** + * The kind of config, it will reflect where the data is coming from. + */ + enum class Kind { + /** + * Defined in UserAccountData + */ + ACCOUNT, + /** + * Defined in Wellknown + */ + HOMESERVER, + /** + * Fallback value, hardcoded by the SDK + */ + DEFAULT + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/integrationmanager/IntegrationManagerService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/integrationmanager/IntegrationManagerService.kt new file mode 100644 index 0000000000..003e8bc9aa --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/integrationmanager/IntegrationManagerService.kt @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.integrationmanager + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.util.Cancelable + +/** + * This is the entry point to manage integration. You can grab an instance of this service through an active session. + */ +interface IntegrationManagerService { + + /** + * This listener allow you to observe change related to integrations. + */ + interface Listener { + /** + * Is called whenever integration is enabled or disabled, comes from user account data. + */ + fun onIsEnabledChanged(enabled: Boolean) { + // No-op + } + + /** + * Is called whenever configs from user account data or wellknown are updated. + */ + fun onConfigurationChanged(configs: List) { + // No-op + } + + /** + * Is called whenever widget permissions from user account data are updated. + */ + fun onWidgetPermissionsChanged(widgets: Map) { + // No-op + } + } + + /** + * Adds a listener to observe changes. + */ + fun addListener(listener: Listener) + + /** + * Removes a previously added listener. + */ + fun removeListener(listener: Listener) + + /** + * Return the list of current configurations, sorted by kind. First one is preferred. + * See [IntegrationManagerConfig.Kind] + */ + fun getOrderedConfigs(): List + + /** + * Return the preferred current configuration. + * See [IntegrationManagerConfig.Kind] + */ + fun getPreferredConfig(): IntegrationManagerConfig + + /** + * Returns true if integration is enabled, false otherwise. + */ + fun isIntegrationEnabled(): Boolean + + /** + * Offers to enable or disable the integration. + * @param enable the param to change + * @param callback the matrix callback to listen for result. + * @return Cancelable + */ + fun setIntegrationEnabled(enable: Boolean, callback: MatrixCallback): Cancelable + + /** + * Offers to allow or disallow a widget. + * @param stateEventId the eventId of the state event defining the widget. + * @param allowed the param to change + * @param callback the matrix callback to listen for result. + * @return Cancelable + */ + fun setWidgetAllowed(stateEventId: String, allowed: Boolean, callback: MatrixCallback): Cancelable + + /** + * Returns true if the widget is allowed, false otherwise. + * @param stateEventId the eventId of the state event defining the widget. + */ + fun isWidgetAllowed(stateEventId: String): Boolean + + /** + * Offers to allow or disallow a native widget domain. + * @param widgetType the widget type to check for + * @param domain the domain to check for + */ + fun setNativeWidgetDomainAllowed(widgetType: String, domain: String, allowed: Boolean, callback: MatrixCallback): Cancelable + + /** + * Returns true if the widget domain is allowed, false otherwise. + * @param widgetType the widget type to check for + * @param domain the domain to check for + */ + fun isNativeWidgetDomainAllowed(widgetType: String, domain: String): Boolean +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt new file mode 100644 index 0000000000..449c670983 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + * + */ + +package org.matrix.android.sdk.api.session.profile + +import android.net.Uri +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.identity.ThreePid +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.api.util.Optional + +/** + * This interface defines methods to handling profile information. It's implemented at the session level. + */ +interface ProfileService { + + companion object Constants { + const val DISPLAY_NAME_KEY = "displayname" + const val AVATAR_URL_KEY = "avatar_url" + } + + /** + * Return the current display name for this user + * @param userId the userId param to look for + * + */ + fun getDisplayName(userId: String, matrixCallback: MatrixCallback>): Cancelable + + /** + * Update the display name for this user + * @param userId the userId to update the display name of + * @param newDisplayName the new display name of the user + */ + fun setDisplayName(userId: String, newDisplayName: String, matrixCallback: MatrixCallback): Cancelable + + /** + * Update the avatar for this user + * @param userId the userId to update the avatar of + * @param newAvatarUri the new avatar uri of the user + * @param fileName the fileName of selected image + */ + fun updateAvatar(userId: String, newAvatarUri: Uri, fileName: String, matrixCallback: MatrixCallback): Cancelable + + /** + * Return the current avatarUrl for this user. + * @param userId the userId param to look for + * + */ + fun getAvatarUrl(userId: String, matrixCallback: MatrixCallback>): Cancelable + + /** + * Get the combined profile information for this user. + * This may return keys which are not limited to displayname or avatar_url. + * @param userId the userId param to look for + * + */ + fun getProfile(userId: String, matrixCallback: MatrixCallback): Cancelable + + /** + * Get the current user 3Pids + */ + fun getThreePids(): List + + /** + * Get the current user 3Pids Live + * @param refreshData set to true to fetch data from the homeserver + */ + fun getThreePidsLive(refreshData: Boolean): LiveData> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/Pusher.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/Pusher.kt new file mode 100644 index 0000000000..6cc089e152 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/Pusher.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.session.pushers + +data class Pusher( + val pushKey: String, + val kind: String, + val appId: String, + val appDisplayName: String?, + val deviceDisplayName: String?, + val profileTag: String? = null, + val lang: String?, + val data: PusherData, + + val state: PusherState +) + +enum class PusherState { + UNREGISTERED, + REGISTERING, + UNREGISTERING, + REGISTERED, + FAILED_TO_REGISTER +} + +data class PusherData( + val url: String? = null, + val format: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt new file mode 100644 index 0000000000..f42721f485 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.session.pushers + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.util.Cancelable +import java.util.UUID + +interface PushersService { + + /** + * Refresh pushers from server state + */ + fun refreshPushers() + + /** + * Add a new HTTP pusher. + * Note that only `http` kind is supported by the SDK for now. + * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-pushers-set + * + * @param pushkey This is a unique identifier for this pusher. The value you should use for + * this is the routing or destination address information for the notification, + * for example, the APNS token for APNS or the Registration ID for GCM. If your + * notification client has no such concept, use any unique identifier. Max length, 512 chars. + * If the kind is "email", this is the email address to send notifications to. + * @param appId the application id + * This is a reverse-DNS style identifier for the application. It is recommended + * that this end with the platform, such that different platform versions get + * different app identifiers. Max length, 64 chars. + * @param profileTag This string determines which set of device specific rules this pusher executes. + * @param lang The preferred language for receiving notifications (e.g. "en" or "en-US"). + * @param appDisplayName A human readable string that will allow the user to identify what application owns this pusher. + * @param deviceDisplayName A human readable string that will allow the user to identify what device owns this pusher. + * @param url The URL to use to send notifications to. MUST be an HTTPS URL with a path of /_matrix/push/v1/notify. + * @param append If true, the homeserver should add another pusher with the given pushkey and App ID in addition + * to any others with different user IDs. Otherwise, the homeserver must remove any other pushers + * with the same App ID and pushkey for different users. + * @param withEventIdOnly true to limit the push content to only id and not message content + * Ref: https://matrix.org/docs/spec/push_gateway/r0.1.1#homeserver-behaviour + * + * @return A work request uuid. Can be used to listen to the status + * (LiveData status = workManager.getWorkInfoByIdLiveData()) + * @throws [InvalidParameterException] if a parameter is not correct + */ + fun addHttpPusher(pushkey: String, + appId: String, + profileTag: String, + lang: String, + appDisplayName: String, + deviceDisplayName: String, + url: String, + append: Boolean, + withEventIdOnly: Boolean): UUID + + /** + * Remove the http pusher + */ + fun removeHttpPusher(pushkey: String, appId: String, callback: MatrixCallback): Cancelable + + /** + * Get the current pushers, as a LiveData + */ + fun getPushersLive(): LiveData> + + /** + * Get the current pushers + */ + fun getPushers(): List +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt new file mode 100644 index 0000000000..3d8550adf0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.session.room.call.RoomCallService +import org.matrix.android.sdk.api.session.room.crypto.RoomCryptoService +import org.matrix.android.sdk.api.session.room.members.MembershipService +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.relation.RelationService +import org.matrix.android.sdk.api.session.room.notification.RoomPushRuleService +import org.matrix.android.sdk.api.session.room.read.ReadService +import org.matrix.android.sdk.api.session.room.reporting.ReportingService +import org.matrix.android.sdk.api.session.room.send.DraftService +import org.matrix.android.sdk.api.session.room.send.SendService +import org.matrix.android.sdk.api.session.room.state.StateService +import org.matrix.android.sdk.api.session.room.tags.TagsService +import org.matrix.android.sdk.api.session.room.timeline.TimelineService +import org.matrix.android.sdk.api.session.room.typing.TypingService +import org.matrix.android.sdk.api.session.room.uploads.UploadsService +import org.matrix.android.sdk.api.util.Optional + +/** + * This interface defines methods to interact within a room. + */ +interface Room : + TimelineService, + SendService, + DraftService, + ReadService, + TypingService, + TagsService, + MembershipService, + StateService, + UploadsService, + ReportingService, + RoomCallService, + RelationService, + RoomCryptoService, + RoomPushRuleService { + + /** + * The roomId of this room + */ + val roomId: String + + /** + * A live [RoomSummary] associated with the room + * You can observe this summary to get dynamic data from this room. + */ + fun getRoomSummaryLive(): LiveData> + + /** + * A current snapshot of [RoomSummary] associated with the room + */ + fun roomSummary(): RoomSummary? +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomDirectoryService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomDirectoryService.kt new file mode 100644 index 0000000000..17d3a2a95a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomDirectoryService.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams +import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsResponse +import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol +import org.matrix.android.sdk.api.util.Cancelable + +/** + * This interface defines methods to get and join public rooms. It's implemented at the session level. + */ +interface RoomDirectoryService { + + /** + * Get rooms from directory + */ + fun getPublicRooms(server: String?, + publicRoomsParams: PublicRoomsParams, + callback: MatrixCallback): Cancelable + + /** + * Fetches the overall metadata about protocols supported by the homeserver. + * Includes both the available protocols and all fields required for queries against each protocol. + */ + fun getThirdPartyProtocol(callback: MatrixCallback>): Cancelable +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt new file mode 100644 index 0000000000..1161dce518 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt @@ -0,0 +1,118 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.Optional + +/** + * This interface defines methods to get rooms. It's implemented at the session level. + */ +interface RoomService { + + /** + * Create a room asynchronously + */ + fun createRoom(createRoomParams: CreateRoomParams, + callback: MatrixCallback): Cancelable + + /** + * Join a room by id + * @param roomIdOrAlias the roomId or the room alias of the room to join + * @param reason optional reason for joining the room + * @param viaServers the servers to attempt to join the room through. One of the servers must be participating in the room. + */ + fun joinRoom(roomIdOrAlias: String, + reason: String? = null, + viaServers: List = emptyList(), + callback: MatrixCallback): Cancelable + + /** + * Get a room from a roomId + * @param roomId the roomId to look for. + * @return a room with roomId or null + */ + fun getRoom(roomId: String): Room? + + /** + * Get a roomSummary from a roomId or a room alias + * @param roomIdOrAlias the roomId or the alias of a room to look for. + * @return a matching room summary or null + */ + fun getRoomSummary(roomIdOrAlias: String): RoomSummary? + + /** + * Get a snapshot list of room summaries. + * @return the immutable list of [RoomSummary] + */ + fun getRoomSummaries(queryParams: RoomSummaryQueryParams): List + + /** + * Get a live list of room summaries. This list is refreshed as soon as the data changes. + * @return the [LiveData] of List[RoomSummary] + */ + fun getRoomSummariesLive(queryParams: RoomSummaryQueryParams): LiveData> + + /** + * Get a snapshot list of Breadcrumbs + * @param queryParams parameters to query the room summaries. It can be use to keep only joined rooms, for instance. + * @return the immutable list of [RoomSummary] + */ + fun getBreadcrumbs(queryParams: RoomSummaryQueryParams): List + + /** + * Get a live list of Breadcrumbs + * @param queryParams parameters to query the room summaries. It can be use to keep only joined rooms, for instance. + * @return the [LiveData] of [RoomSummary] + */ + fun getBreadcrumbsLive(queryParams: RoomSummaryQueryParams): LiveData> + + /** + * Inform the Matrix SDK that a room is displayed. + * The SDK will update the breadcrumbs in the user account data + */ + fun onRoomDisplayed(roomId: String): Cancelable + + /** + * Mark all rooms as read + */ + fun markAllAsRead(roomIds: List, + callback: MatrixCallback): Cancelable + + /** + * Resolve a room alias to a room ID. + */ + fun getRoomIdByAlias(roomAlias: String, + searchOnServer: Boolean, + callback: MatrixCallback>): Cancelable + + /** + * Return a live data of all local changes membership that happened since the session has been opened. + * It allows you to track this in your client to known what is currently being processed by the SDK. + * It won't know anything about change being done in other client. + * Keys are roomId or roomAlias, depending of what you used as parameter for the join/leave action + */ + fun getChangeMembershipsLive(): LiveData> + + fun getExistingDirectRoomWithUser(otherUserId: String): Room? +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt new file mode 100644 index 0000000000..5af23f8e24 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room + +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.room.model.Membership + +fun roomSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = {}): RoomSummaryQueryParams { + return RoomSummaryQueryParams.Builder().apply(init).build() +} + +/** + * This class can be used to filter room summaries to use with: + * [org.matrix.android.sdk.api.session.room.Room] and [org.matrix.android.sdk.api.session.room.RoomService] + */ +data class RoomSummaryQueryParams( + val roomId: QueryStringValue, + val displayName: QueryStringValue, + val canonicalAlias: QueryStringValue, + val memberships: List +) { + + class Builder { + + var roomId: QueryStringValue = QueryStringValue.IsNotEmpty + var displayName: QueryStringValue = QueryStringValue.IsNotEmpty + var canonicalAlias: QueryStringValue = QueryStringValue.NoCondition + var memberships: List = Membership.all() + + fun build() = RoomSummaryQueryParams( + roomId = roomId, + displayName = displayName, + canonicalAlias = canonicalAlias, + memberships = memberships + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/call/RoomCallService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/call/RoomCallService.kt new file mode 100644 index 0000000000..0ec27fdd5d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/call/RoomCallService.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.call + +/** + * This interface defines methods to handle calls in a room. It's implemented at the room level. + */ +interface RoomCallService { + /** + * Return true if calls (audio or video) can be performed on this Room + */ + fun canStartCall(): Boolean +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/crypto/RoomCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/crypto/RoomCryptoService.kt new file mode 100644 index 0000000000..b7f018bda8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/crypto/RoomCryptoService.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.crypto + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM + +interface RoomCryptoService { + + fun isEncrypted(): Boolean + + fun encryptionAlgorithm(): String? + + fun shouldEncryptForInvitedMembers(): Boolean + + /** + * Enable encryption of the room + */ + fun enableEncryption(algorithm: String = MXCRYPTO_ALGORITHM_MEGOLM, + callback: MatrixCallback) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/failure/CreateRoomFailure.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/failure/CreateRoomFailure.kt new file mode 100644 index 0000000000..d70dae3454 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/failure/CreateRoomFailure.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.failure + +import org.matrix.android.sdk.api.failure.Failure + +sealed class CreateRoomFailure : Failure.FeatureFailure() { + + object CreatedWithTimeout: CreateRoomFailure() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/failure/JoinRoomFailure.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/failure/JoinRoomFailure.kt new file mode 100644 index 0000000000..ef15fbc7c1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/failure/JoinRoomFailure.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.failure + +import org.matrix.android.sdk.api.failure.Failure + +sealed class JoinRoomFailure : Failure.FeatureFailure() { + + object JoinedWithTimeout : JoinRoomFailure() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/ChangeMembershipState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/ChangeMembershipState.kt new file mode 100644 index 0000000000..6d13b0bf94 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/ChangeMembershipState.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.members + +sealed class ChangeMembershipState() { + object Unknown : ChangeMembershipState() + object Joining : ChangeMembershipState() + data class FailedJoining(val throwable: Throwable) : ChangeMembershipState() + object Joined : ChangeMembershipState() + object Leaving : ChangeMembershipState() + data class FailedLeaving(val throwable: Throwable) : ChangeMembershipState() + object Left : ChangeMembershipState() + + fun isInProgress() = this is Joining || this is Leaving + + fun isSuccessful() = this is Joined || this is Left + + fun isFailed() = this is FailedJoining || this is FailedLeaving +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/MembershipService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/MembershipService.kt new file mode 100644 index 0000000000..5c9a50dc0c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/MembershipService.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.members + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.identity.ThreePid +import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary +import org.matrix.android.sdk.api.util.Cancelable + +/** + * This interface defines methods to handling membership. It's implemented at the room level. + */ +interface MembershipService { + + /** + * This methods load all room members if it was done yet. + * @return a [Cancelable] + */ + fun loadRoomMembersIfNeeded(matrixCallback: MatrixCallback): Cancelable + + /** + * Return the roomMember with userId or null. + * @param userId the userId param to look for + * + * @return the roomMember with userId or null + */ + fun getRoomMember(userId: String): RoomMemberSummary? + + /** + * Return all the roomMembers of the room with params + * @param queryParams the params to query for + * @return a roomMember list. + */ + fun getRoomMembers(queryParams: RoomMemberQueryParams): List + + /** + * Return all the roomMembers of the room filtered by memberships + * @param queryParams the params to query for + * @return a [LiveData] of roomMember list. + */ + fun getRoomMembersLive(queryParams: RoomMemberQueryParams): LiveData> + + fun getNumberOfJoinedMembers(): Int + + /** + * Invite a user in the room + */ + fun invite(userId: String, + reason: String? = null, + callback: MatrixCallback): Cancelable + + /** + * Invite a user with email or phone number in the room + */ + fun invite3pid(threePid: ThreePid, + callback: MatrixCallback): Cancelable + + /** + * Ban a user from the room + */ + fun ban(userId: String, + reason: String? = null, + callback: MatrixCallback): Cancelable + + /** + * Unban a user from the room + */ + fun unban(userId: String, + reason: String? = null, + callback: MatrixCallback): Cancelable + + /** + * Kick a user from the room + */ + fun kick(userId: String, + reason: String? = null, + callback: MatrixCallback): Cancelable + + /** + * Join the room, or accept an invitation. + */ + fun join(reason: String? = null, + viaServers: List = emptyList(), + callback: MatrixCallback): Cancelable + + /** + * Leave the room, or reject an invitation. + */ + fun leave(reason: String? = null, + callback: MatrixCallback): Cancelable +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/RoomMemberQueryParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/RoomMemberQueryParams.kt new file mode 100644 index 0000000000..39fc7598f6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/RoomMemberQueryParams.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.members + +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.room.model.Membership + +fun roomMemberQueryParams(init: (RoomMemberQueryParams.Builder.() -> Unit) = {}): RoomMemberQueryParams { + return RoomMemberQueryParams.Builder().apply(init).build() +} + +/** + * This class can be used to filter room members + */ +data class RoomMemberQueryParams( + val displayName: QueryStringValue, + val memberships: List, + val userId: QueryStringValue, + val excludeSelf: Boolean +) { + + class Builder { + + var userId: QueryStringValue = QueryStringValue.NoCondition + var displayName: QueryStringValue = QueryStringValue.IsNotEmpty + var memberships: List = Membership.all() + var excludeSelf: Boolean = false + + fun build() = RoomMemberQueryParams( + displayName = displayName, + memberships = memberships, + userId = userId, + excludeSelf = excludeSelf + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/EditAggregatedSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/EditAggregatedSummary.kt new file mode 100644 index 0000000000..721dcf4f2e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/EditAggregatedSummary.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.session.room.model + +import org.matrix.android.sdk.api.session.events.model.Content + +data class EditAggregatedSummary( + val aggregatedContent: Content? = null, + // The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk) + val sourceEvents: List, + val localEchos: List, + val lastEditTs: Long = 0 +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/EventAnnotationsSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/EventAnnotationsSummary.kt new file mode 100644 index 0000000000..d1b0c89410 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/EventAnnotationsSummary.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.session.room.model + +data class EventAnnotationsSummary( + var eventId: String, + var reactionsSummary: List = emptyList(), + var editSummary: EditAggregatedSummary? = null, + var pollResponseSummary: PollResponseAggregatedSummary? = null, + var referencesAggregatedSummary: ReferencesAggregatedSummary? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/Invite.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/Invite.kt new file mode 100644 index 0000000000..6b3a333672 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/Invite.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Subclass representing a search API response + */ +@JsonClass(generateAdapter = true) +data class Invite( + @Json(name = "display_name") val displayName: String, + @Json(name = "signed") val signed: Signed + +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/Membership.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/Membership.kt new file mode 100644 index 0000000000..fc89ff06df --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/Membership.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Represents the membership of a user on a room + */ +@JsonClass(generateAdapter = false) +enum class Membership(val value: String) { + + NONE("none"), + + @Json(name = "invite") + INVITE("invite"), + + @Json(name = "join") + JOIN("join"), + + @Json(name = "knock") + KNOCK("knock"), + + @Json(name = "leave") + LEAVE("leave"), + + @Json(name = "ban") + BAN("ban"); + + fun isLeft(): Boolean { + return this == KNOCK || this == LEAVE || this == BAN + } + + fun isActive(): Boolean { + return activeMemberships().contains(this) + } + + companion object { + fun activeMemberships(): List { + return listOf(INVITE, JOIN) + } + + fun all(): List { + return values().asList() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PollResponseAggregatedSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PollResponseAggregatedSummary.kt new file mode 100644 index 0000000000..695a3353d5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PollResponseAggregatedSummary.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.session.room.model + +data class PollResponseAggregatedSummary( + + var aggregatedContent: PollSummaryContent? = null, + + // If set the poll is closed (Clients SHOULD NOT consider responses after the close event) + var closedTime: Long? = null, + // Clients SHOULD validate that the option in the relationship is a valid option, and ignore the response if invalid + var nbOptions: Int = 0, + // The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk) + val sourceEvents: List, + val localEchos: List +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PollSummaryContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PollSummaryContent.kt new file mode 100644 index 0000000000..07d62a173c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PollSummaryContent.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.session.room.model + +import com.squareup.moshi.JsonClass + +/** + * Contains an aggregated summary info of the poll response. + * Put pre-computed info that you want to access quickly without having + * to go through all references events + */ +@JsonClass(generateAdapter = true) +data class PollSummaryContent( + // Index of my vote + var myVote: Int? = null, + // Array of VoteInfo, list is constructed so that there is only one vote by user + // And that optionIndex is valid + var votes: List? = null +) { + + fun voteCount(): Int { + return votes?.size ?: 0 + } + + fun voteCountForOption(optionIndex: Int) : Int { + return votes?.filter { it.optionIndex == optionIndex }?.count() ?: 0 + } +} + +@JsonClass(generateAdapter = true) +data class VoteInfo( + val userId: String, + val optionIndex: Int, + val voteTimestamp: Long +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PowerLevelsContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PowerLevelsContent.kt new file mode 100644 index 0000000000..e55508c9db --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PowerLevelsContent.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.room.powerlevels.Role + +/** + * Class representing the EventType.EVENT_TYPE_STATE_ROOM_POWER_LEVELS state event content. + */ +@JsonClass(generateAdapter = true) +data class PowerLevelsContent( + @Json(name = "ban") val ban: Int = Role.Moderator.value, + @Json(name = "kick") val kick: Int = Role.Moderator.value, + @Json(name = "invite") val invite: Int = Role.Moderator.value, + @Json(name = "redact") val redact: Int = Role.Moderator.value, + @Json(name = "events_default") val eventsDefault: Int = Role.Default.value, + @Json(name = "events") val events: MutableMap = HashMap(), + @Json(name = "users_default") val usersDefault: Int = Role.Default.value, + @Json(name = "users") val users: MutableMap = HashMap(), + @Json(name = "state_default") val stateDefault: Int = Role.Moderator.value, + @Json(name = "notifications") val notifications: Map = HashMap() +) { + /** + * Alter this content with a new power level for the specified user + * + * @param userId the userId to alter the power level of + * @param powerLevel the new power level, or null to set the default value. + */ + fun setUserPowerLevel(userId: String, powerLevel: Int?) { + if (powerLevel == null || powerLevel == usersDefault) { + users.remove(userId) + } else { + users[userId] = powerLevel + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReactionAggregatedSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReactionAggregatedSummary.kt new file mode 100644 index 0000000000..97fd0a16ab --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReactionAggregatedSummary.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model + +data class ReactionAggregatedSummary( + val key: String, // "👍" + val count: Int, // 8 + val addedByMe: Boolean, // true + val firstTimestamp: Long, // unix timestamp + val sourceEvents: List, + val localEchoEvents: List +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReadReceipt.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReadReceipt.kt new file mode 100644 index 0000000000..d6ced198d7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReadReceipt.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model + +import org.matrix.android.sdk.api.session.user.model.User + +data class ReadReceipt( + val user: User, + val originServerTs: Long +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReferencesAggregatedContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReferencesAggregatedContent.kt new file mode 100644 index 0000000000..82291fa062 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReferencesAggregatedContent.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.session.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.session.room.VerificationState + +/** + * Contains an aggregated summary info of the references. + * Put pre-computed info that you want to access quickly without having + * to go through all references events + */ +@JsonClass(generateAdapter = true) +data class ReferencesAggregatedContent( + // Verification status info for m.key.verification.request msgType events + @Json(name = "verif_sum") val verificationState: VerificationState + // Add more fields for future summary info. +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReferencesAggregatedSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReferencesAggregatedSummary.kt new file mode 100644 index 0000000000..298567262a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReferencesAggregatedSummary.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.session.room.model + +import org.matrix.android.sdk.api.session.events.model.Content + +/** + * Events can relates to other events, this object keeps a summary + * of all events that are referencing the 'eventId' event via the RelationType.REFERENCE + */ +data class ReferencesAggregatedSummary( + val eventId: String, + val content: Content?, + val sourceEvents: List, + val localEchos: List +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomAliasesContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomAliasesContent.kt new file mode 100644 index 0000000000..94628e6987 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomAliasesContent.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing the EventType.STATE_ROOM_ALIASES state event content + */ +@JsonClass(generateAdapter = true) +data class RoomAliasesContent( + @Json(name = "aliases") val aliases: List = emptyList() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomAvatarContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomAvatarContent.kt new file mode 100644 index 0000000000..ded2e49657 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomAvatarContent.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing the EventType.STATE_ROOM_AVATAR state event content + */ +@JsonClass(generateAdapter = true) +data class RoomAvatarContent( + @Json(name = "url") val avatarUrl: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomCanonicalAliasContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomCanonicalAliasContent.kt new file mode 100644 index 0000000000..d5f41b66dc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomCanonicalAliasContent.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing the EventType.STATE_ROOM_CANONICAL_ALIAS state event content + */ +@JsonClass(generateAdapter = true) +data class RoomCanonicalAliasContent( + @Json(name = "alias") val canonicalAlias: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomDirectoryVisibility.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomDirectoryVisibility.kt new file mode 100644 index 0000000000..bd37884407 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomDirectoryVisibility.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = false) +enum class RoomDirectoryVisibility { + @Json(name = "private") PRIVATE, + @Json(name = "public") PUBLIC +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomGuestAccessContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomGuestAccessContent.kt new file mode 100644 index 0000000000..d2b944c0eb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomGuestAccessContent.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing the EventType.STATE_ROOM_GUEST_ACCESS state event content + * Ref: https://matrix.org/docs/spec/client_server/latest#m-room-guest-access + */ +@JsonClass(generateAdapter = true) +data class RoomGuestAccessContent( + // Required. Whether guests can join the room. One of: ["can_join", "forbidden"] + @Json(name = "guest_access") val guestAccess: GuestAccess? = null +) + +@JsonClass(generateAdapter = false) +enum class GuestAccess(val value: String) { + @Json(name = "can_join") + CanJoin("can_join"), + @Json(name = "forbidden") + Forbidden("forbidden") +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomHistoryVisibility.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomHistoryVisibility.kt new file mode 100644 index 0000000000..d6546b1065 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomHistoryVisibility.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Ref: https://matrix.org/docs/spec/client_server/latest#room-history-visibility + */ +@JsonClass(generateAdapter = false) +enum class RoomHistoryVisibility { + /** + * All events while this is the m.room.history_visibility value may be shared by any + * participating homeserver with anyone, regardless of whether they have ever joined the room. + */ + @Json(name = "world_readable") WORLD_READABLE, + /** + * Previous events are always accessible to newly joined members. All events in the + * room are accessible, even those sent when the member was not a part of the room. + */ + @Json(name = "shared") SHARED, + /** + * Events are accessible to newly joined members from the point they were invited onwards. + * Events stop being accessible when the member's state changes to something other than invite or join. + */ + @Json(name = "invited") INVITED, + /** + * Events are accessible to newly joined members from the point they joined the room onwards. + * Events stop being accessible when the member's state changes to something other than join. + */ + @Json(name = "joined") JOINED +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomHistoryVisibilityContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomHistoryVisibilityContent.kt new file mode 100644 index 0000000000..8955320d89 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomHistoryVisibilityContent.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class RoomHistoryVisibilityContent( + @Json(name = "history_visibility") val historyVisibility: RoomHistoryVisibility? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRules.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRules.kt new file mode 100644 index 0000000000..d6c947b753 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRules.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + * + */ + +package org.matrix.android.sdk.api.session.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Enum for [RoomJoinRulesContent] : https://matrix.org/docs/spec/client_server/r0.4.0#m-room-join-rules + */ +@JsonClass(generateAdapter = false) +enum class RoomJoinRules(val value: String) { + + @Json(name = "public") + PUBLIC("public"), + + @Json(name = "invite") + INVITE("invite"), + + @Json(name = "knock") + KNOCK("knock"), + + @Json(name = "private") + PRIVATE("private") +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRulesContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRulesContent.kt new file mode 100644 index 0000000000..14a88885b6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRulesContent.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + * + */ + +package org.matrix.android.sdk.api.session.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing the EventType.STATE_ROOM_JOIN_RULES state event content + */ +@JsonClass(generateAdapter = true) +data class RoomJoinRulesContent( + @Json(name = "join_rule") val joinRules: RoomJoinRules? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomMemberContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomMemberContent.kt new file mode 100644 index 0000000000..278db67a0e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomMemberContent.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.UnsignedData + +/** + * Class representing the EventType.STATE_ROOM_MEMBER state event content + */ +@JsonClass(generateAdapter = true) +data class RoomMemberContent( + @Json(name = "membership") val membership: Membership, + @Json(name = "reason") val reason: String? = null, + @Json(name = "displayname") val displayName: String? = null, + @Json(name = "avatar_url") val avatarUrl: String? = null, + @Json(name = "is_direct") val isDirect: Boolean = false, + @Json(name = "third_party_invite") val thirdPartyInvite: Invite? = null, + @Json(name = "unsigned") val unsignedData: UnsignedData? = null +) { + val safeReason + get() = reason?.takeIf { it.isNotBlank() } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomMemberSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomMemberSummary.kt new file mode 100644 index 0000000000..17b0cf30b1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomMemberSummary.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model + +/** + * Class representing a simplified version of EventType.STATE_ROOM_MEMBER state event content + */ +data class RoomMemberSummary constructor( + val membership: Membership, + val userId: String, + val displayName: String? = null, + val avatarUrl: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomNameContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomNameContent.kt new file mode 100644 index 0000000000..c3d93a5a16 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomNameContent.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing the EventType.STATE_ROOM_NAME state event content + */ +@JsonClass(generateAdapter = true) +data class RoomNameContent( + @Json(name = "name") val name: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt new file mode 100644 index 0000000000..0df86e09d7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model + +import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel +import org.matrix.android.sdk.api.session.room.model.tag.RoomTag +import org.matrix.android.sdk.api.session.room.send.UserDraft +import org.matrix.android.sdk.api.session.room.sender.SenderInfo +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent + +/** + * This class holds some data of a room. + * It can be retrieved by [org.matrix.android.sdk.api.session.room.Room] and [org.matrix.android.sdk.api.session.room.RoomService] + */ +data class RoomSummary constructor( + val roomId: String, + // Computed display name + val displayName: String = "", + val name: String = "", + val topic: String = "", + val avatarUrl: String = "", + val canonicalAlias: String? = null, + val aliases: List = emptyList(), + val isDirect: Boolean = false, + val joinedMembersCount: Int? = 0, + val invitedMembersCount: Int? = 0, + val latestPreviewableEvent: TimelineEvent? = null, + val otherMemberIds: List = emptyList(), + val notificationCount: Int = 0, + val highlightCount: Int = 0, + val hasUnreadMessages: Boolean = false, + val tags: List = emptyList(), + val membership: Membership = Membership.NONE, + val versioningState: VersioningState = VersioningState.NONE, + val readMarkerId: String? = null, + val userDrafts: List = emptyList(), + val isEncrypted: Boolean, + val encryptionEventTs: Long?, + val typingUsers: List, + val inviterId: String? = null, + val breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS, + val roomEncryptionTrustLevel: RoomEncryptionTrustLevel? = null, + val hasFailedSending: Boolean = false +) { + + val isVersioned: Boolean + get() = versioningState != VersioningState.NONE + + val hasNewMessages: Boolean + get() = notificationCount != 0 + + val isFavorite: Boolean + get() = tags.any { it.name == RoomTag.ROOM_TAG_FAVOURITE } + + val canStartCall: Boolean + get() = joinedMembersCount == 2 + + companion object { + const val NOT_IN_BREADCRUMBS = -1 + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomThirdPartyInviteContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomThirdPartyInviteContent.kt new file mode 100644 index 0000000000..f8372f3142 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomThirdPartyInviteContent.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing the EventType.STATE_ROOM_THIRD_PARTY_INVITE state event content + * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#m-room-third-party-invite + */ +@JsonClass(generateAdapter = true) +data class RoomThirdPartyInviteContent( + /** + * Required. A user-readable string which represents the user who has been invited. + * This should not contain the user's third party ID, as otherwise when the invite + * is accepted it would leak the association between the matrix ID and the third party ID. + */ + @Json(name = "display_name") val displayName: String, + + /** + * Required. A URL which can be fetched, with querystring public_key=public_key, to validate + * whether the key has been revoked. The URL must return a JSON object containing a boolean property named 'valid'. + */ + @Json(name = "key_validity_url") val keyValidityUrl: String, + + /** + * Required. A base64-encoded ed25519 key with which token must be signed (though a signature from any entry in + * public_keys is also sufficient). This exists for backwards compatibility. + */ + @Json(name = "public_key") val publicKey: String, + + /** + * Keys with which the token may be signed. + */ + @Json(name = "public_keys") val publicKeys: List = emptyList() +) + +@JsonClass(generateAdapter = true) +data class PublicKeys( + /** + * An optional URL which can be fetched, with querystring public_key=public_key, to validate whether the key + * has been revoked. The URL must return a JSON object containing a boolean property named 'valid'. If this URL + * is absent, the key must be considered valid indefinitely. + */ + @Json(name = "key_validity_url") val keyValidityUrl: String? = null, + + /** + * Required. A base-64 encoded ed25519 key with which token may be signed. + */ + @Json(name = "public_key") val publicKey: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomTopicContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomTopicContent.kt new file mode 100644 index 0000000000..38d3ca93f4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomTopicContent.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing the EventType.STATE_ROOM_TOPIC state event content + */ +@JsonClass(generateAdapter = true) +data class RoomTopicContent( + @Json(name = "topic") val topic: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/Signed.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/Signed.kt new file mode 100644 index 0000000000..9c14275b3e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/Signed.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model + +import com.squareup.moshi.Json + +data class Signed( + @Json(name = "token") val token: String, + @Json(name = "signatures") val signatures: Any, + @Json(name = "mxid") val mxid: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/VersioningState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/VersioningState.kt new file mode 100644 index 0000000000..202d64d621 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/VersioningState.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model + +enum class VersioningState { + NONE, + UPGRADED_ROOM_NOT_JOINED, + UPGRADED_ROOM_JOINED +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallAnswerContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallAnswerContent.kt new file mode 100644 index 0000000000..2e21ebea86 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallAnswerContent.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model.call + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This event is sent by the callee when they wish to answer the call. + */ +@JsonClass(generateAdapter = true) +data class CallAnswerContent( + /** + * Required. The ID of the call this event relates to. + */ + @Json(name = "call_id") val callId: String, + /** + * Required. The session description object + */ + @Json(name = "answer") val answer: Answer, + /** + * Required. The version of the VoIP specification this messages adheres to. This specification is version 0. + */ + @Json(name = "version") val version: Int = 0 +) { + + @JsonClass(generateAdapter = true) + data class Answer( + /** + * Required. The type of session description. Must be 'answer'. + */ + @Json(name = "type") val type: SdpType = SdpType.ANSWER, + /** + * Required. The SDP text of the session description. + */ + @Json(name = "sdp") val sdp: String + ) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallCandidatesContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallCandidatesContent.kt new file mode 100644 index 0000000000..6a6f1c82c3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallCandidatesContent.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model.call + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This event is sent by callers after sending an invite and by the callee after answering. + * Its purpose is to give the other party additional ICE candidates to try using to communicate. + */ +@JsonClass(generateAdapter = true) +data class CallCandidatesContent( + /** + * Required. The ID of the call this event relates to. + */ + @Json(name = "call_id") val callId: String, + /** + * Required. Array of objects describing the candidates. + */ + @Json(name = "candidates") val candidates: List = emptyList(), + /** + * Required. The version of the VoIP specification this messages adheres to. This specification is version 0. + */ + @Json(name = "version") val version: Int = 0 +) { + + @JsonClass(generateAdapter = true) + data class Candidate( + /** + * Required. The SDP media type this candidate is intended for. + */ + @Json(name = "sdpMid") val sdpMid: String, + /** + * Required. The index of the SDP 'm' line this candidate is intended for. + */ + @Json(name = "sdpMLineIndex") val sdpMLineIndex: Int, + /** + * Required. The SDP 'a' line of the candidate. + */ + @Json(name = "candidate") val candidate: String + ) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallHangupContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallHangupContent.kt new file mode 100644 index 0000000000..aef774008c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallHangupContent.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model.call + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Sent by either party to signal their termination of the call. This can be sent either once + * the call has been established or before to abort the call. + */ +@JsonClass(generateAdapter = true) +data class CallHangupContent( + /** + * Required. The ID of the call this event relates to. + */ + @Json(name = "call_id") val callId: String, + /** + * Required. The version of the VoIP specification this message adheres to. This specification is version 0. + */ + @Json(name = "version") val version: Int = 0, + /** + * Optional error reason for the hangup. This should not be provided when the user naturally ends or rejects the call. + * When there was an error in the call negotiation, this should be `ice_failed` for when ICE negotiation fails + * or `invite_timeout` for when the other party did not answer in time. One of: ["ice_failed", "invite_timeout"] + */ + @Json(name = "reason") val reason: Reason? = null +) { + @JsonClass(generateAdapter = false) + enum class Reason { + @Json(name = "ice_failed") + ICE_FAILED, + + @Json(name = "invite_timeout") + INVITE_TIMEOUT + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallInviteContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallInviteContent.kt new file mode 100644 index 0000000000..6baef034c2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallInviteContent.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model.call + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This event is sent by the caller when they wish to establish a call. + */ +@JsonClass(generateAdapter = true) +data class CallInviteContent( + /** + * Required. A unique identifier for the call. + */ + @Json(name = "call_id") val callId: String?, + /** + * Required. The session description object + */ + @Json(name = "offer") val offer: Offer?, + /** + * Required. The version of the VoIP specification this message adheres to. This specification is version 0. + */ + @Json(name = "version") val version: Int? = 0, + /** + * Required. The time in milliseconds that the invite is valid for. + * Once the invite age exceeds this value, clients should discard it. + * They should also no longer show the call as awaiting an answer in the UI. + */ + @Json(name = "lifetime") val lifetime: Int? +) { + @JsonClass(generateAdapter = true) + data class Offer( + /** + * Required. The type of session description. Must be 'offer'. + */ + @Json(name = "type") val type: SdpType? = SdpType.OFFER, + /** + * Required. The SDP text of the session description. + */ + @Json(name = "sdp") val sdp: String? + ) { + companion object { + const val SDP_VIDEO = "m=video" + } + } + + fun isVideo() = offer?.sdp?.contains(Offer.SDP_VIDEO) == true +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/SdpType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/SdpType.kt new file mode 100644 index 0000000000..a760e6ef93 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/SdpType.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model.call + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = false) +enum class SdpType { + @Json(name = "offer") + OFFER, + + @Json(name = "answer") + ANSWER +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt new file mode 100644 index 0000000000..2e78ae10f9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model.create + +import org.matrix.android.sdk.api.session.identity.ThreePid +import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent +import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility +import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility +import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM + +// TODO Give a way to include other initial states +class CreateRoomParams { + /** + * A public visibility indicates that the room will be shown in the published room list. + * A private visibility will hide the room from the published room list. + * Rooms default to private visibility if this key is not included. + * NB: This should not be confused with join_rules which also uses the word public. One of: ["public", "private"] + */ + var visibility: RoomDirectoryVisibility? = null + + /** + * The desired room alias local part. If this is included, a room alias will be created and mapped to the newly created room. + * The alias will belong on the same homeserver which created the room. + * For example, if this was set to "foo" and sent to the homeserver "example.com" the complete room alias would be #foo:example.com. + */ + var roomAliasName: String? = null + + /** + * If this is not null, an m.room.name event will be sent into the room to indicate the name of the room. + * See Room Events for more information on m.room.name. + */ + var name: String? = null + + /** + * If this is not null, an m.room.topic event will be sent into the room to indicate the topic for the room. + * See Room Events for more information on m.room.topic. + */ + var topic: String? = null + + /** + * A list of user IDs to invite to the room. + * This will tell the server to invite everyone in the list to the newly created room. + */ + val invitedUserIds = mutableListOf() + + /** + * A list of objects representing third party IDs to invite into the room. + */ + val invite3pids = mutableListOf() + + /** + * If set to true, when the room will be created, if cross-signing is enabled and we can get keys for every invited users, + * the encryption will be enabled on the created room + */ + var enableEncryptionIfInvitedUsersSupportIt: Boolean = false + + /** + * Convenience parameter for setting various default state events based on a preset. Must be either: + * private_chat => join_rules is set to invite. history_visibility is set to shared. + * trusted_private_chat => join_rules is set to invite. history_visibility is set to shared. All invitees are given the same power level as the + * room creator. + * public_chat: => join_rules is set to public. history_visibility is set to shared. + */ + var preset: CreateRoomPreset? = null + + /** + * This flag makes the server set the is_direct flag on the m.room.member events sent to the users in invite and invite_3pid. + * See Direct Messaging for more information. + */ + var isDirect: Boolean? = null + + /** + * Extra keys to be added to the content of the m.room.create. + * The server will clobber the following keys: creator. + * Future versions of the specification may allow the server to clobber other keys. + */ + var creationContent: Any? = null + + /** + * The power level content to override in the default power level event + */ + var powerLevelContentOverride: PowerLevelsContent? = null + + /** + * Mark as a direct message room. + */ + fun setDirectMessage() { + preset = CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT + isDirect = true + } + + /** + * Supported value: MXCRYPTO_ALGORITHM_MEGOLM + */ + var algorithm: String? = null + private set + + var historyVisibility: RoomHistoryVisibility? = null + + fun enableEncryption() { + algorithm = MXCRYPTO_ALGORITHM_MEGOLM + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomPreset.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomPreset.kt new file mode 100644 index 0000000000..7bc4f664c5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomPreset.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model.create + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = false) +enum class CreateRoomPreset { + @Json(name = "private_chat") + PRESET_PRIVATE_CHAT, + + @Json(name = "public_chat") + PRESET_PUBLIC_CHAT, + + @Json(name = "trusted_private_chat") + PRESET_TRUSTED_PRIVATE_CHAT +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/Predecessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/Predecessor.kt new file mode 100644 index 0000000000..fb726808e9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/Predecessor.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.session.room.model.create + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * A link to an old room in case of room versioning + */ +@JsonClass(generateAdapter = true) +data class Predecessor( + @Json(name = "room_id") val roomId: String? = null, + @Json(name = "event_id") val eventId: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/RoomCreateContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/RoomCreateContent.kt new file mode 100644 index 0000000000..2a9b4ca9cf --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/RoomCreateContent.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model.create + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Content of a m.room.create type event + */ +@JsonClass(generateAdapter = true) +data class RoomCreateContent( + @Json(name = "creator") val creator: String? = null, + @Json(name = "room_version") val roomVersion: String? = null, + @Json(name = "predecessor") val predecessor: Predecessor? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/AudioInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/AudioInfo.kt new file mode 100644 index 0000000000..7b0097db2c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/AudioInfo.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class AudioInfo( + /** + * The mimetype of the audio e.g. "audio/aac". + */ + @Json(name = "mimetype") val mimeType: String?, + + /** + * The size of the audio clip in bytes. + */ + @Json(name = "size") val size: Long = 0, + + /** + * The duration of the audio in milliseconds. + */ + @Json(name = "duration") val duration: Int = 0 +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/FileInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/FileInfo.kt new file mode 100644 index 0000000000..290909ded1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/FileInfo.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo + +@JsonClass(generateAdapter = true) +data class FileInfo( + /** + * The mimetype of the file e.g. application/msword. + */ + @Json(name = "mimetype") val mimeType: String?, + + /** + * The size of the file in bytes. + */ + @Json(name = "size") val size: Long = 0, + + /** + * Metadata about the image referred to in thumbnail_url. + */ + @Json(name = "thumbnail_info") val thumbnailInfo: ThumbnailInfo? = null, + + /** + * The URL to the thumbnail of the file. Only present if the thumbnail is unencrypted. + */ + @Json(name = "thumbnail_url") val thumbnailUrl: String? = null, + + /** + * Information on the encrypted thumbnail file, as specified in End-to-end encryption. Only present if the thumbnail is encrypted. + */ + @Json(name = "thumbnail_file") val thumbnailFile: EncryptedFileInfo? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/ImageInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/ImageInfo.kt new file mode 100644 index 0000000000..26c196bb14 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/ImageInfo.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo + +@JsonClass(generateAdapter = true) +data class ImageInfo( + /** + * The mimetype of the image, e.g. "image/jpeg". + */ + @Json(name = "mimetype") val mimeType: String?, + + /** + * The intended display width of the image in pixels. This may differ from the intrinsic dimensions of the image file. + */ + @Json(name = "w") val width: Int = 0, + + /** + * The intended display height of the image in pixels. This may differ from the intrinsic dimensions of the image file. + */ + @Json(name = "h") val height: Int = 0, + + /** + * Size of the image in bytes. + */ + @Json(name = "size") val size: Int = 0, + + /** + * Metadata about the image referred to in thumbnail_url. + */ + @Json(name = "thumbnail_info") val thumbnailInfo: ThumbnailInfo? = null, + + /** + * The URL (typically MXC URI) to a thumbnail of the image. Only present if the thumbnail is unencrypted. + */ + @Json(name = "thumbnail_url") val thumbnailUrl: String? = null, + + /** + * Information on the encrypted thumbnail file, as specified in End-to-end encryption. Only present if the thumbnail is encrypted. + */ + @Json(name = "thumbnail_file") val thumbnailFile: EncryptedFileInfo? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationInfo.kt new file mode 100644 index 0000000000..258fd75c94 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationInfo.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo + +@JsonClass(generateAdapter = true) +data class LocationInfo( + /** + * The URL to the thumbnail of the file. Only present if the thumbnail is unencrypted. + */ + @Json(name = "thumbnail_url") val thumbnailUrl: String? = null, + + /** + * Metadata about the image referred to in thumbnail_url. + */ + @Json(name = "thumbnail_info") val thumbnailInfo: ThumbnailInfo? = null, + + /** + * Information on the encrypted thumbnail file, as specified in End-to-end encryption. Only present if the thumbnail is encrypted. + */ + @Json(name = "thumbnail_file") val thumbnailFile: EncryptedFileInfo? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageAudioContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageAudioContent.kt new file mode 100644 index 0000000000..14022075c2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageAudioContent.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo + +@JsonClass(generateAdapter = true) +data class MessageAudioContent( + /** + * Required. Must be 'm.audio'. + */ + @Json(name = "msgtype") override val msgType: String, + + /** + * Required. A description of the audio e.g. 'Bee Gees - Stayin' Alive', or some kind of content description for accessibility e.g. 'audio attachment'. + */ + @Json(name = "body") override val body: String, + + /** + * Metadata for the audio clip referred to in url. + */ + @Json(name = "info") val audioInfo: AudioInfo? = null, + + /** + * Required if the file is not encrypted. The URL (typically MXC URI) to the audio clip. + */ + @Json(name = "url") override val url: String? = null, + + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, + @Json(name = "m.new_content") override val newContent: Content? = null, + + /** + * Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption. + */ + @Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null +) : MessageWithAttachmentContent { + + override val mimeType: String? + get() = encryptedFileInfo?.mimetype ?: audioInfo?.mimeType +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageContent.kt new file mode 100644 index 0000000000..8b5c98d250 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageContent.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent + +interface MessageContent { + val msgType: String + val body: String + val relatesTo: RelationDefaultContent? + val newContent: Content? +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageContentWithFormattedBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageContentWithFormattedBody.kt new file mode 100644 index 0000000000..15609dca3b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageContentWithFormattedBody.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +interface MessageContentWithFormattedBody : MessageContent { + /** + * The format used in the formatted_body. Currently only "org.matrix.custom.html" is supported. + */ + val format: String? + + /** + * The formatted version of the body. This is required if format is specified. + */ + val formattedBody: String? + + /** + * Get the formattedBody, only if not blank and if the format is equal to "org.matrix.custom.html" + */ + val matrixFormattedBody: String? + get() = formattedBody?.takeIf { it.isNotBlank() && format == MessageFormat.FORMAT_MATRIX_HTML } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageDefaultContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageDefaultContent.kt new file mode 100644 index 0000000000..2b033755bd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageDefaultContent.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent + +@JsonClass(generateAdapter = true) +data class MessageDefaultContent( + @Json(name = "msgtype") override val msgType: String, + @Json(name = "body") override val body: String, + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, + @Json(name = "m.new_content") override val newContent: Content? = null +) : MessageContent diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageEmoteContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageEmoteContent.kt new file mode 100644 index 0000000000..36ec85ebf0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageEmoteContent.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent + +@JsonClass(generateAdapter = true) +data class MessageEmoteContent( + /** + * Required. Must be 'm.emote'. + */ + @Json(name = "msgtype") override val msgType: String, + + /** + * Required. The emote action to perform. + */ + @Json(name = "body") override val body: String, + + /** + * The format used in the formatted_body. Currently only "org.matrix.custom.html" is supported. + */ + @Json(name = "format") override val format: String? = null, + + /** + * The formatted version of the body. This is required if format is specified. + */ + @Json(name = "formatted_body") override val formattedBody: String? = null, + + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, + @Json(name = "m.new_content") override val newContent: Content? = null +) : MessageContentWithFormattedBody diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageFileContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageFileContent.kt new file mode 100644 index 0000000000..bbdb2835b1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageFileContent.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +import android.webkit.MimeTypeMap +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo + +@JsonClass(generateAdapter = true) +data class MessageFileContent( + /** + * Required. Must be 'm.file'. + */ + @Json(name = "msgtype") override val msgType: String, + + /** + * Required. A human-readable description of the file. This is recommended to be the filename of the original upload. + */ + @Json(name = "body") override val body: String, + + /** + * The original filename of the uploaded file. + */ + @Json(name = "filename") val filename: String? = null, + + /** + * Information about the file referred to in url. + */ + @Json(name = "info") val info: FileInfo? = null, + + /** + * Required if the file is unencrypted. The URL (typically MXC URI) to the file. + */ + @Json(name = "url") override val url: String? = null, + + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, + @Json(name = "m.new_content") override val newContent: Content? = null, + + /** + * Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption. + */ + @Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null +) : MessageWithAttachmentContent { + + override val mimeType: String? + get() = encryptedFileInfo?.mimetype + ?: info?.mimeType + ?: MimeTypeMap.getFileExtensionFromUrl(filename ?: body)?.let { extension -> + MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) + } + + fun getFileName(): String { + return filename ?: body + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageFormat.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageFormat.kt new file mode 100644 index 0000000000..c32b0586ea --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageFormat.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +object MessageFormat { + const val FORMAT_MATRIX_HTML = "org.matrix.custom.html" +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageImageContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageImageContent.kt new file mode 100644 index 0000000000..48e30508bc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageImageContent.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo + +@JsonClass(generateAdapter = true) +data class MessageImageContent( + /** + * Required. Must be 'm.image'. + */ + @Json(name = "msgtype") override val msgType: String, + + /** + * Required. A textual representation of the image. This could be the alt text of the image, the filename of the image, + * or some kind of content description for accessibility e.g. 'image attachment'. + */ + @Json(name = "body") override val body: String, + + /** + * Metadata about the image referred to in url. + */ + @Json(name = "info") override val info: ImageInfo? = null, + + /** + * Required if the file is unencrypted. The URL (typically MXC URI) to the image. + */ + @Json(name = "url") override val url: String? = null, + + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, + @Json(name = "m.new_content") override val newContent: Content? = null, + + /** + * Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption. + */ + @Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null +) : MessageImageInfoContent { + override val mimeType: String? + get() = encryptedFileInfo?.mimetype ?: info?.mimeType ?: "image/*" +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageImageInfoContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageImageInfoContent.kt new file mode 100644 index 0000000000..e14d531a4f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageImageInfoContent.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + * + */ + +package org.matrix.android.sdk.api.session.room.model.message + +/** + * A content with image information + */ +interface MessageImageInfoContent : MessageWithAttachmentContent { + val info: ImageInfo? +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt new file mode 100644 index 0000000000..3452e291eb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent + +@JsonClass(generateAdapter = true) +data class MessageLocationContent( + /** + * Required. Must be 'm.location'. + */ + @Json(name = "msgtype") override val msgType: String, + + /** + * Required. A description of the location e.g. 'Big Ben, London, UK', or some kind + * of content description for accessibility e.g. 'location attachment'. + */ + @Json(name = "body") override val body: String, + + /** + * Required. A geo URI representing this location. + */ + @Json(name = "geo_uri") val geoUri: String, + + /** + * + */ + @Json(name = "info") val locationInfo: LocationInfo? = null, + + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, + @Json(name = "m.new_content") override val newContent: Content? = null +) : MessageContent diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageNoticeContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageNoticeContent.kt new file mode 100644 index 0000000000..7c2dd2a196 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageNoticeContent.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent + +@JsonClass(generateAdapter = true) +data class MessageNoticeContent( + /** + * Required. Must be 'm.notice'. + */ + @Json(name = "msgtype") override val msgType: String, + + /** + * Required. The notice text to send. + */ + @Json(name = "body") override val body: String, + + /** + * The format used in the formatted_body. Currently only "org.matrix.custom.html" is supported. + */ + @Json(name = "format") override val format: String? = null, + + /** + * The formatted version of the body. This is required if format is specified. + */ + @Json(name = "formatted_body") override val formattedBody: String? = null, + + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, + @Json(name = "m.new_content") override val newContent: Content? = null +) : MessageContentWithFormattedBody diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageOptionsContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageOptionsContent.kt new file mode 100644 index 0000000000..caaf5151af --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageOptionsContent.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent + +// Possible values for optionType +const val OPTION_TYPE_POLL = "org.matrix.poll" +const val OPTION_TYPE_BUTTONS = "org.matrix.buttons" + +/** + * Polls and bot buttons are m.room.message events with a msgtype of m.options, + * Ref: https://github.com/matrix-org/matrix-doc/pull/2192 + */ +@JsonClass(generateAdapter = true) +data class MessageOptionsContent( + @Json(name = "msgtype") override val msgType: String = MessageType.MSGTYPE_OPTIONS, + @Json(name = "type") val optionType: String? = null, + @Json(name = "body") override val body: String, + @Json(name = "label") val label: String?, + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, + @Json(name = "options") val options: List? = null, + @Json(name = "m.new_content") override val newContent: Content? = null +) : MessageContent diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollResponseContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollResponseContent.kt new file mode 100644 index 0000000000..a7dd0160cb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollResponseContent.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent + +/** + * Ref: https://github.com/matrix-org/matrix-doc/pull/2192 + */ +@JsonClass(generateAdapter = true) +data class MessagePollResponseContent( + @Json(name = "msgtype") override val msgType: String = MessageType.MSGTYPE_RESPONSE, + @Json(name = "body") override val body: String, + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, + @Json(name = "m.new_content") override val newContent: Content? = null +) : MessageContent diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageRelationContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageRelationContent.kt new file mode 100644 index 0000000000..a97c0f86e3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageRelationContent.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + * + */ +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent + +@JsonClass(generateAdapter = true) +data class MessageRelationContent( + @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageStickerContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageStickerContent.kt new file mode 100644 index 0000000000..fad04941f7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageStickerContent.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo + +@JsonClass(generateAdapter = true) +data class MessageStickerContent( + /** + * Set in local, not from server + */ + override val msgType: String = MessageType.MSGTYPE_STICKER_LOCAL, + + /** + * Required. A textual representation of the image. This could be the alt text of the image, the filename of the image, + * or some kind of content description for accessibility e.g. 'image attachment'. + */ + @Json(name = "body") override val body: String, + + /** + * Metadata about the image referred to in url. + */ + @Json(name = "info") override val info: ImageInfo? = null, + + /** + * Required if the file is unencrypted. The URL (typically MXC URI) to the image. + */ + @Json(name = "url") override val url: String? = null, + + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, + @Json(name = "m.new_content") override val newContent: Content? = null, + + /** + * Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption. + */ + @Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null +) : MessageImageInfoContent { + override val mimeType: String? + get() = encryptedFileInfo?.mimetype ?: info?.mimeType +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageTextContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageTextContent.kt new file mode 100644 index 0000000000..ea8685ae71 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageTextContent.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent + +@JsonClass(generateAdapter = true) +data class MessageTextContent( + /** + * Required. Must be 'm.text'. + */ + @Json(name = "msgtype") override val msgType: String, + + /** + * Required. The body of the message. + */ + @Json(name = "body") override val body: String, + + /** + * The format used in the formatted_body. Currently only "org.matrix.custom.html" is supported. + */ + @Json(name = "format") override val format: String? = null, + + /** + * The formatted version of the body. This is required if format is specified. + */ + @Json(name = "formatted_body") override val formattedBody: String? = null, + + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, + @Json(name = "m.new_content") override val newContent: Content? = null +) : MessageContentWithFormattedBody diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt new file mode 100644 index 0000000000..026132b7c5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +object MessageType { + const val MSGTYPE_TEXT = "m.text" + const val MSGTYPE_EMOTE = "m.emote" + const val MSGTYPE_NOTICE = "m.notice" + const val MSGTYPE_IMAGE = "m.image" + const val MSGTYPE_AUDIO = "m.audio" + const val MSGTYPE_VIDEO = "m.video" + const val MSGTYPE_LOCATION = "m.location" + const val MSGTYPE_FILE = "m.file" + const val MSGTYPE_OPTIONS = "org.matrix.options" + const val MSGTYPE_RESPONSE = "org.matrix.response" + const val MSGTYPE_POLL_CLOSED = "org.matrix.poll_closed" + const val MSGTYPE_VERIFICATION_REQUEST = "m.key.verification.request" + // Add, in local, a fake message type in order to StickerMessage can inherit Message class + // Because sticker isn't a message type but a event type without msgtype field + const val MSGTYPE_STICKER_LOCAL = "org.matrix.android.sdk.sticker" +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationAcceptContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationAcceptContent.kt new file mode 100644 index 0000000000..d7a8a4a6f8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationAcceptContent.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoAccept +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoAcceptFactory + +@JsonClass(generateAdapter = true) +internal data class MessageVerificationAcceptContent( + @Json(name = "hash") override val hash: String?, + @Json(name = "key_agreement_protocol") override val keyAgreementProtocol: String?, + @Json(name = "message_authentication_code") override val messageAuthenticationCode: String?, + @Json(name = "short_authentication_string") override val shortAuthenticationStrings: List?, + @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?, + @Json(name = "commitment") override var commitment: String? = null +) : VerificationInfoAccept { + + override val transactionId: String? + get() = relatesTo?.eventId + + override fun toEventContent() = toContent() + + companion object : VerificationInfoAcceptFactory { + + override fun create(tid: String, + keyAgreementProtocol: String, + hash: String, + commitment: String, + messageAuthenticationCode: String, + shortAuthenticationStrings: List): VerificationInfoAccept { + return MessageVerificationAcceptContent( + hash, + keyAgreementProtocol, + messageAuthenticationCode, + shortAuthenticationStrings, + RelationDefaultContent( + RelationType.REFERENCE, + tid + ), + commitment + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationCancelContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationCancelContent.kt new file mode 100644 index 0000000000..944599a153 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationCancelContent.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.crypto.verification.CancelCode +import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoCancel + +@JsonClass(generateAdapter = true) +data class MessageVerificationCancelContent( + @Json(name = "code") override val code: String? = null, + @Json(name = "reason") override val reason: String? = null, + @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? +) : VerificationInfoCancel { + + override val transactionId: String? + get() = relatesTo?.eventId + + override fun toEventContent() = toContent() + + companion object { + fun create(transactionId: String, reason: CancelCode): MessageVerificationCancelContent { + return MessageVerificationCancelContent( + reason.value, + reason.humanReadable, + RelationDefaultContent( + RelationType.REFERENCE, + transactionId + ) + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationDoneContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationDoneContent.kt new file mode 100644 index 0000000000..13593b60b8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationDoneContent.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfo + +@JsonClass(generateAdapter = true) +internal data class MessageVerificationDoneContent( + @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? +) : VerificationInfo { + + override val transactionId: String? + get() = relatesTo?.eventId + + override fun toEventContent(): Content? = toContent() + + override fun asValidObject(): ValidVerificationDone? { + val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null + + return ValidVerificationDone( + validTransactionId + ) + } +} + +internal data class ValidVerificationDone( + val transactionId: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationKeyContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationKeyContent.kt new file mode 100644 index 0000000000..00d4e2cd0b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationKeyContent.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoKey +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoKeyFactory + +@JsonClass(generateAdapter = true) +internal data class MessageVerificationKeyContent( + /** + * The device’s ephemeral public key, as an unpadded base64 string + */ + @Json(name = "key") override val key: String? = null, + @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? +) : VerificationInfoKey { + + override val transactionId: String? + get() = relatesTo?.eventId + + override fun toEventContent() = toContent() + + companion object : VerificationInfoKeyFactory { + + override fun create(tid: String, pubKey: String): VerificationInfoKey { + return MessageVerificationKeyContent( + pubKey, + RelationDefaultContent( + RelationType.REFERENCE, + tid + ) + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationMacContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationMacContent.kt new file mode 100644 index 0000000000..9ac43e49d0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationMacContent.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoMac +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoMacFactory + +@JsonClass(generateAdapter = true) +internal data class MessageVerificationMacContent( + @Json(name = "mac") override val mac: Map? = null, + @Json(name = "keys") override val keys: String? = null, + @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? +) : VerificationInfoMac { + + override val transactionId: String? + get() = relatesTo?.eventId + + override fun toEventContent() = toContent() + + companion object : VerificationInfoMacFactory { + override fun create(tid: String, mac: Map, keys: String): VerificationInfoMac { + return MessageVerificationMacContent( + mac, + keys, + RelationDefaultContent( + RelationType.REFERENCE, + tid + ) + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationReadyContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationReadyContent.kt new file mode 100644 index 0000000000..eafdb30ecf --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationReadyContent.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.internal.crypto.verification.MessageVerificationReadyFactory +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoReady + +@JsonClass(generateAdapter = true) +internal data class MessageVerificationReadyContent( + @Json(name = "from_device") override val fromDevice: String? = null, + @Json(name = "methods") override val methods: List? = null, + @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? +) : VerificationInfoReady { + + override val transactionId: String? + get() = relatesTo?.eventId + + override fun toEventContent() = toContent() + + companion object : MessageVerificationReadyFactory { + override fun create(tid: String, methods: List, fromDevice: String): VerificationInfoReady { + return MessageVerificationReadyContent( + fromDevice = fromDevice, + methods = methods, + relatesTo = RelationDefaultContent( + RelationType.REFERENCE, + tid + ) + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationRequestContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationRequestContent.kt new file mode 100644 index 0000000000..b89ff07552 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationRequestContent.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoRequest + +@JsonClass(generateAdapter = true) +data class MessageVerificationRequestContent( + @Json(name = "msgtype") override val msgType: String = MessageType.MSGTYPE_VERIFICATION_REQUEST, + @Json(name = "body") override val body: String, + @Json(name = "from_device") override val fromDevice: String?, + @Json(name = "methods") override val methods: List, + @Json(name = "to") val toUserId: String, + @Json(name = "timestamp") override val timestamp: Long?, + @Json(name = "format") val format: String? = null, + @Json(name = "formatted_body") val formattedBody: String? = null, + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, + @Json(name = "m.new_content") override val newContent: Content? = null, + // Not parsed, but set after, using the eventId + override val transactionId: String? = null +) : MessageContent, VerificationInfoRequest { + + override fun toEventContent() = toContent() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationStartContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationStartContent.kt new file mode 100644 index 0000000000..c6c5cb6208 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationStartContent.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoStart +import org.matrix.android.sdk.internal.util.JsonCanonicalizer + +@JsonClass(generateAdapter = true) +internal data class MessageVerificationStartContent( + @Json(name = "from_device") override val fromDevice: String?, + @Json(name = "hashes") override val hashes: List?, + @Json(name = "key_agreement_protocols") override val keyAgreementProtocols: List?, + @Json(name = "message_authentication_codes") override val messageAuthenticationCodes: List?, + @Json(name = "short_authentication_string") override val shortAuthenticationStrings: List?, + @Json(name = "method") override val method: String?, + @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?, + @Json(name = "secret") override val sharedSecret: String? +) : VerificationInfoStart { + + override fun toCanonicalJson(): String { + return JsonCanonicalizer.getCanonicalJson(MessageVerificationStartContent::class.java, this) + } + + override val transactionId: String? + get() = relatesTo?.eventId + + override fun toEventContent() = toContent() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVideoContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVideoContent.kt new file mode 100644 index 0000000000..02456e6b0f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVideoContent.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo + +@JsonClass(generateAdapter = true) +data class MessageVideoContent( + /** + * Required. Must be 'm.video'. + */ + @Json(name = "msgtype") override val msgType: String, + + /** + * Required. A description of the video e.g. 'Gangnam style', or some kind of content description for accessibility e.g. 'video attachment'. + */ + @Json(name = "body") override val body: String, + + /** + * Metadata about the video clip referred to in url. + */ + @Json(name = "info") val videoInfo: VideoInfo? = null, + + /** + * Required if the file is unencrypted. The URL (typically MXC URI) to the video clip. + */ + @Json(name = "url") override val url: String? = null, + + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, + @Json(name = "m.new_content") override val newContent: Content? = null, + + /** + * Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption. + */ + @Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null +) : MessageWithAttachmentContent { + override val mimeType: String? + get() = encryptedFileInfo?.mimetype ?: videoInfo?.mimeType +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageWithAttachmentContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageWithAttachmentContent.kt new file mode 100644 index 0000000000..af07d37efd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageWithAttachmentContent.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo + +/** + * Interface for message which can contains an encrypted file + */ +interface MessageWithAttachmentContent : MessageContent { + /** + * Required if the file is unencrypted. The URL (typically MXC URI) to the image. + */ + val url: String? + + /** + * Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption. + */ + val encryptedFileInfo: EncryptedFileInfo? + + val mimeType: String? +} + +/** + * Get the url of the encrypted file or of the file + */ +fun MessageWithAttachmentContent.getFileUrl() = encryptedFileInfo?.url ?: url + +fun MessageWithAttachmentContent.getFileName() = (this as? MessageFileContent)?.getFileName() ?: body diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/OptionItem.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/OptionItem.kt new file mode 100644 index 0000000000..b6e7979040 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/OptionItem.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Ref: https://github.com/matrix-org/matrix-doc/pull/2192 + */ +@JsonClass(generateAdapter = true) +data class OptionItem( + @Json(name = "label") val label: String?, + @Json(name = "value") val value: String? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/ThumbnailInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/ThumbnailInfo.kt new file mode 100644 index 0000000000..bedbe52641 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/ThumbnailInfo.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ThumbnailInfo( + /** + * The intended display width of the image in pixels. This may differ from the intrinsic dimensions of the image file. + */ + @Json(name = "w") val width: Int = 0, + + /** + * The intended display height of the image in pixels. This may differ from the intrinsic dimensions of the image file. + */ + @Json(name = "h") val height: Int = 0, + + /** + * Size of the image in bytes. + */ + @Json(name = "size") val size: Long = 0, + + /** + * The mimetype of the image, e.g. "image/jpeg". + */ + @Json(name = "mimetype") val mimeType: String? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/VideoInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/VideoInfo.kt new file mode 100644 index 0000000000..b16c3dd823 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/VideoInfo.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo + +@JsonClass(generateAdapter = true) +data class VideoInfo( + /** + * The mimetype of the video e.g. "video/mp4". + */ + @Json(name = "mimetype") val mimeType: String?, + + /** + * The width of the video in pixels. + */ + @Json(name = "w") val width: Int = 0, + + /** + * The height of the video in pixels. + */ + @Json(name = "h") val height: Int = 0, + + /** + * The size of the video in bytes. + */ + @Json(name = "size") val size: Long = 0, + + /** + * The duration of the video in milliseconds. + */ + @Json(name = "duration") val duration: Int = 0, + + /** + * Metadata about the image referred to in thumbnail_url. + */ + @Json(name = "thumbnail_info") val thumbnailInfo: ThumbnailInfo? = null, + + /** + * The URL (typically MXC URI) to an image thumbnail of the video clip. Only present if the thumbnail is unencrypted. + */ + @Json(name = "thumbnail_url") val thumbnailUrl: String? = null, + + /** + * Information on the encrypted thumbnail file, as specified in End-to-end encryption. Only present if the thumbnail is encrypted. + */ + @Json(name = "thumbnail_file") val thumbnailFile: EncryptedFileInfo? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReactionContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReactionContent.kt new file mode 100644 index 0000000000..8f2b2c7fac --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReactionContent.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model.relation + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ReactionContent( + @Json(name = "m.relates_to") val relatesTo: ReactionInfo? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReactionInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReactionInfo.kt new file mode 100644 index 0000000000..97577c90d1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReactionInfo.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model.relation + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ReactionInfo( + @Json(name = "rel_type") override val type: String?, + @Json(name = "event_id") override val eventId: String, + val key: String, + // always null for reaction + @Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null, + @Json(name = "option") override val option: Int? = null +) : RelationContent diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationContent.kt new file mode 100644 index 0000000000..a3822118c0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationContent.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model.relation + +import org.matrix.android.sdk.api.session.events.model.RelationType + +interface RelationContent { + /** See [RelationType] for known possible values */ + val type: String? + val eventId: String? + val inReplyTo: ReplyToContent? + val option: Int? +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationDefaultContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationDefaultContent.kt new file mode 100644 index 0000000000..eedc23518f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationDefaultContent.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.session.room.model.relation + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class RelationDefaultContent( + @Json(name = "rel_type") override val type: String?, + @Json(name = "event_id") override val eventId: String?, + @Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null, + @Json(name = "option") override val option: Int? = null +) : RelationContent diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt new file mode 100644 index 0000000000..b3d739fc62 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt @@ -0,0 +1,125 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.session.room.model.relation + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.Optional + +/** + * In some cases, events may wish to reference other events. + * This could be to form a thread of messages for the user to follow along with, + * or to provide more context as to what a particular event is describing. + * Relation are used to associate new information with an existing event. + * + * Relations are events which have an m.relates_to mixin in their contents, + * and the new information they convey is expressed in their usual event type and content. + * + * Three types of relations are defined, each defining different behaviour when aggregated: + * + * m.annotation - lets you define an event which annotates an existing event. + * When aggregated, groups events together based on key and returns a count. + * (aka SQL's COUNT) These are primarily intended for handling reactions. + * + * m.replace - lets you define an event which replaces an existing event. + * When aggregated, returns the most recent replacement event. (aka SQL's MAX) + * These are primarily intended for handling edits. + * + * m.reference - lets you define an event which references an existing event. + * When aggregated, currently doesn't do anything special, but in future could bundle chains of references (i.e. threads). + * These are primarily intended for handling replies (and in future threads). + */ +interface RelationService { + + /** + * Sends a reaction (emoji) to the targetedEvent. + * It has no effect if the user has already added the same reaction to the event. + * @param targetEventId the id of the event being reacted + * @param reaction the reaction (preferably emoji) + */ + fun sendReaction(targetEventId: String, + reaction: String): Cancelable + + /** + * Undo a reaction (emoji) to the targetedEvent. + * @param targetEventId the id of the event being reacted + * @param reaction the reaction (preferably emoji) + */ + fun undoReaction(targetEventId: String, + reaction: String): Cancelable + + /** + * Edit a text message body. Limited to "m.text" contentType + * @param targetEventId The event to edit + * @param newBodyText The edited body + * @param compatibilityBodyText The text that will appear on clients that don't support yet edition + */ + fun editTextMessage(targetEventId: String, + msgType: String, + newBodyText: CharSequence, + newBodyAutoMarkdown: Boolean, + compatibilityBodyText: String = "* $newBodyText"): Cancelable + + /** + * Edit a reply. This is a special case because replies contains fallback text as a prefix. + * This method will take the new body (stripped from fallbacks) and re-add them before sending. + * @param replyToEdit The event to edit + * @param originalTimelineEvent the message that this reply (being edited) is relating to + * @param newBodyText The edited body (stripped from in reply to content) + * @param compatibilityBodyText The text that will appear on clients that don't support yet edition + */ + fun editReply(replyToEdit: TimelineEvent, + originalTimelineEvent: TimelineEvent, + newBodyText: String, + compatibilityBodyText: String = "* $newBodyText"): Cancelable + + /** + * Get the edit history of the given event + */ + fun fetchEditHistory(eventId: String, callback: MatrixCallback>) + + /** + * Reply to an event in the timeline (must be in same room) + * https://matrix.org/docs/spec/client_server/r0.4.0.html#id350 + * The replyText can be a Spannable and contains special spans (MatrixItemSpan) that will be translated + * by the sdk into pills. + * @param eventReplied the event referenced by the reply + * @param replyText the reply text + * @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present + */ + fun replyToMessage(eventReplied: TimelineEvent, + replyText: CharSequence, + autoMarkdown: Boolean = false): Cancelable? + + /** + * Get the current EventAnnotationsSummary + * @param eventId the eventId to look for EventAnnotationsSummary + * @return the EventAnnotationsSummary found + */ + fun getEventAnnotationsSummary(eventId: String): EventAnnotationsSummary? + + /** + * Get a LiveData of EventAnnotationsSummary for the specified eventId + * @param eventId the eventId to look for EventAnnotationsSummary + * @return the LiveData of EventAnnotationsSummary + */ + fun getEventAnnotationsSummaryLive(eventId: String): LiveData> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReplyToContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReplyToContent.kt new file mode 100644 index 0000000000..57d8adb6d6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReplyToContent.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model.relation + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ReplyToContent( + @Json(name = "event_id") val eventId: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/roomdirectory/PublicRoom.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/roomdirectory/PublicRoom.kt new file mode 100644 index 0000000000..0429eba1b0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/roomdirectory/PublicRoom.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2014 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.session.room.model.roomdirectory + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing the objects returned by /publicRooms call. + */ +@JsonClass(generateAdapter = true) +data class PublicRoom( + /** + * Aliases of the room. May be empty. + */ + @Json(name = "aliases") + val aliases: List? = null, + + /** + * The canonical alias of the room, if any. + */ + @Json(name = "canonical_alias") + val canonicalAlias: String? = null, + + /** + * The name of the room, if any. + */ + @Json(name = "name") + val name: String? = null, + + /** + * Required. The number of members joined to the room. + */ + @Json(name = "num_joined_members") + val numJoinedMembers: Int = 0, + + /** + * Required. The ID of the room. + */ + @Json(name = "room_id") + val roomId: String, + + /** + * The topic of the room, if any. + */ + @Json(name = "topic") + val topic: String? = null, + + /** + * Required. Whether the room may be viewed by guest users without joining. + */ + @Json(name = "world_readable") + val worldReadable: Boolean = false, + + /** + * Required. Whether guest users may join the room and participate in it. If they can, + * they will be subject to ordinary power level rules like any other user. + */ + @Json(name = "guest_can_join") + val guestCanJoin: Boolean = false, + + /** + * The URL for the room's avatar, if one is set. + */ + @Json(name = "avatar_url") + val avatarUrl: String? = null, + + /** + * Undocumented item + */ + @Json(name = "m.federate") + val isFederated: Boolean = false +) { + /** + * Return the canonical alias, or the first alias from the list of aliases, or null + */ + fun getPrimaryAlias(): String? { + return canonicalAlias ?: aliases?.firstOrNull() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/roomdirectory/PublicRoomsFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/roomdirectory/PublicRoomsFilter.kt new file mode 100644 index 0000000000..041b804576 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/roomdirectory/PublicRoomsFilter.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2014 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.session.room.model.roomdirectory + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class to define a filter to retrieve public rooms + */ +@JsonClass(generateAdapter = true) +data class PublicRoomsFilter( + /** + * A string to search for in the room metadata, e.g. name, topic, canonical alias etc. (Optional). + */ + @Json(name = "generic_search_term") + val searchTerm: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/roomdirectory/PublicRoomsParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/roomdirectory/PublicRoomsParams.kt new file mode 100644 index 0000000000..c69cde72b7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/roomdirectory/PublicRoomsParams.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.session.room.model.roomdirectory + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class to pass parameters to get the public rooms list + */ +@JsonClass(generateAdapter = true) +data class PublicRoomsParams( + /** + * Limit the number of results returned. + */ + @Json(name = "limit") + val limit: Int? = null, + + /** + * A pagination token from a previous request, allowing clients to get the next (or previous) batch of rooms. + * The direction of pagination is specified solely by which token is supplied, rather than via an explicit flag. + */ + @Json(name = "since") + val since: String? = null, + + /** + * Filter to apply to the results. + */ + @Json(name = "filter") + val filter: PublicRoomsFilter? = null, + + /** + * Whether or not to include all known networks/protocols from application services on the homeserver. Defaults to false. + */ + @Json(name = "include_all_networks") + val includeAllNetworks: Boolean = false, + + /** + * The specific third party network/protocol to request from the homeserver. Can only be used if include_all_networks is false. + */ + @Json(name = "third_party_instance_id") + val thirdPartyInstanceId: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/roomdirectory/PublicRoomsResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/roomdirectory/PublicRoomsResponse.kt new file mode 100644 index 0000000000..39f24d658c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/roomdirectory/PublicRoomsResponse.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2014 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.session.room.model.roomdirectory + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing the public rooms request response + */ +@JsonClass(generateAdapter = true) +data class PublicRoomsResponse( + /** + * A pagination token for the response. The absence of this token means there are no more results to fetch and the client should stop paginating. + */ + @Json(name = "next_batch") + val nextBatch: String? = null, + + /** + * A pagination token that allows fetching previous results. The absence of this token means there are no results before this batch, + * i.e. this is the first batch. + */ + @Json(name = "prev_batch") + val prevBatch: String? = null, + + /** + * A paginated chunk of public rooms. + */ + @Json(name = "chunk") + val chunk: List? = null, + + /** + * An estimate on the total number of public rooms, if the server has an estimate. + */ + @Json(name = "total_room_count_estimate") + val totalRoomCountEstimate: Int? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/tag/RoomTag.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/tag/RoomTag.kt new file mode 100644 index 0000000000..f5303773bc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/tag/RoomTag.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model.tag + +data class RoomTag( + val name: String, + val order: Double? +) { + + companion object { + const val ROOM_TAG_FAVOURITE = "m.favourite" + const val ROOM_TAG_LOW_PRIORITY = "m.lowpriority" + const val ROOM_TAG_SERVER_NOTICE = "m.server_notice" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/tag/RoomTagContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/tag/RoomTagContent.kt new file mode 100644 index 0000000000..15b83a4af1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/tag/RoomTagContent.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model.tag + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class RoomTagContent( + @Json(name = "tags") val tags: Map> = emptyMap() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/thirdparty/FieldType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/thirdparty/FieldType.kt new file mode 100644 index 0000000000..36bc949606 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/thirdparty/FieldType.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model.thirdparty + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class FieldType( + /** + * Required. A regular expression for validation of a field's value. This may be relatively coarse to verify the value as the application + * service providing this protocol may apply additional + */ + @Json(name = "regexp") + val regexp: String? = null, + + /** + * Required. An placeholder serving as a valid example of the field value. + */ + @Json(name = "placeholder") + val placeholder: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/thirdparty/RoomDirectoryData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/thirdparty/RoomDirectoryData.kt new file mode 100644 index 0000000000..e2cdd25b7c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/thirdparty/RoomDirectoryData.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model.thirdparty + +/** + * This class describes a rooms directory server. + */ +data class RoomDirectoryData( + + /** + * The server name (might be null) + * Set null when the server is the current user's home server. + */ + val homeServer: String? = null, + + /** + * The display name (the server description) + */ + val displayName: String = DEFAULT_HOME_SERVER_NAME, + + /** + * The third party server identifier + */ + val thirdPartyInstanceId: String? = null, + + /** + * Tell if all the federated servers must be included + */ + val includeAllNetworks: Boolean = false, + + /** + * the avatar url + */ + val avatarUrl: String? = null +) { + + companion object { + const val DEFAULT_HOME_SERVER_NAME = "Matrix" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/thirdparty/ThirdPartyProtocol.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/thirdparty/ThirdPartyProtocol.kt new file mode 100644 index 0000000000..ebc147e69f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/thirdparty/ThirdPartyProtocol.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model.thirdparty + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ThirdPartyProtocol( + /** + * Required. Fields which may be used to identify a third party user. These should be ordered to suggest the way that entities may be grouped, + * where higher groupings are ordered first. For example, the name of a network should be searched before the nickname of a user. + */ + @Json(name = "user_fields") + val userFields: List? = null, + + /** + * Required. Fields which may be used to identify a third party location. These should be ordered to suggest the way that + * entities may be grouped, where higher groupings are ordered first. For example, the name of a network should be + * searched before the name of a channel. + */ + @Json(name = "location_fields") + val locationFields: List? = null, + + /** + * Required. A content URI representing an icon for the third party protocol. + * + * FIXDOC: This field was not present in legacy Riot, and it is sometimes sent by the server (so not Required?) + */ + @Json(name = "icon") + val icon: String? = null, + + /** + * Required. The type definitions for the fields defined in the user_fields and location_fields. Each entry in those arrays MUST have an entry here. + * The string key for this object is field name itself. + * + * May be an empty object if no fields are defined. + */ + @Json(name = "field_types") + val fieldTypes: Map? = null, + + /** + * Required. A list of objects representing independent instances of configuration. For example, multiple networks on IRC + * if multiple are provided by the same application service. + */ + @Json(name = "instances") + val instances: List? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/thirdparty/ThirdPartyProtocolInstance.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/thirdparty/ThirdPartyProtocolInstance.kt new file mode 100644 index 0000000000..04e5481259 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/thirdparty/ThirdPartyProtocolInstance.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.model.thirdparty + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ThirdPartyProtocolInstance( + /** + * Required. A human-readable description for the protocol, such as the name. + */ + @Json(name = "desc") + val desc: String? = null, + + /** + * An optional content URI representing the protocol. Overrides the one provided at the higher level Protocol object. + */ + @Json(name = "icon") + val icon: String? = null, + + /** + * Required. Preset values for fields the client may use to search by. + */ + @Json(name = "fields") + val fields: Map? = null, + + /** + * Required. A unique identifier across all instances. + */ + @Json(name = "network_id") + val networkId: String? = null, + + /** + * FIXDOC Not documented on matrix.org doc + */ + @Json(name = "instance_id") + val instanceId: String? = null, + + /** + * FIXDOC Not documented on matrix.org doc + */ + @Json(name = "bot_user_id") + val botUserId: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/tombstone/RoomTombstoneContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/tombstone/RoomTombstoneContent.kt new file mode 100644 index 0000000000..43b56c8b9d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/tombstone/RoomTombstoneContent.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.session.room.model.tombstone + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class to contains Tombstone information + */ +@JsonClass(generateAdapter = true) +data class RoomTombstoneContent( + /** + * Required. A server-defined message. + */ + @Json(name = "body") val body: String? = null, + + /** + * Required. The new room the client should be visiting. + */ + @Json(name = "replacement_room") val replacementRoomId: String? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/notification/RoomNotificationState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/notification/RoomNotificationState.kt new file mode 100644 index 0000000000..42971e874a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/notification/RoomNotificationState.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.notification + +/** + * Defines the room notification state + */ +enum class RoomNotificationState { + /** + * All the messages will trigger a noisy notification + */ + ALL_MESSAGES_NOISY, + + /** + * All the messages will trigger a notification + */ + ALL_MESSAGES, + + /** + * Only the messages with user display name / user name will trigger notifications + */ + MENTIONS_ONLY, + + /** + * No notifications + */ + MUTE +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/notification/RoomPushRuleService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/notification/RoomPushRuleService.kt new file mode 100644 index 0000000000..79070adea3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/notification/RoomPushRuleService.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.notification + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.util.Cancelable + +interface RoomPushRuleService { + + fun getLiveRoomNotificationState(): LiveData + + fun setRoomNotificationState(roomNotificationState: RoomNotificationState, matrixCallback: MatrixCallback): Cancelable +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/PowerLevelsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/PowerLevelsHelper.kt new file mode 100644 index 0000000000..34e3168ce1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/PowerLevelsHelper.kt @@ -0,0 +1,127 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + * + */ + +package org.matrix.android.sdk.api.session.room.powerlevels + +import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent + +/** + * This class is an helper around PowerLevelsContent. + */ +class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) { + + /** + * Returns the user power level of a dedicated user Id + * + * @param userId the user id + * @return the power level + */ + fun getUserPowerLevelValue(userId: String): Int { + return powerLevelsContent.users.getOrElse(userId) { + powerLevelsContent.usersDefault + } + } + + /** + * Returns the user power level of a dedicated user Id + * + * @param userId the user id + * @return the power level + */ + fun getUserRole(userId: String): Role { + val value = getUserPowerLevelValue(userId) + // I think we should use powerLevelsContent.usersDefault, but Ganfra told me that it was like that on riot-Web + return Role.fromValue(value, powerLevelsContent.eventsDefault) + } + + /** + * Tell if an user can send an event of a certain type + * + * @param userId the id of the user to check for. + * @param isState true if the event is a state event (ie. state key is not null) + * @param eventType the event type to check for + * @return true if the user can send this type of event + */ + fun isUserAllowedToSend(userId: String, isState: Boolean, eventType: String?): Boolean { + return if (userId.isNotEmpty()) { + val powerLevel = getUserPowerLevelValue(userId) + val minimumPowerLevel = powerLevelsContent.events[eventType] + ?: if (isState) { + powerLevelsContent.stateDefault + } else { + powerLevelsContent.eventsDefault + } + powerLevel >= minimumPowerLevel + } else false + } + + /** + * Check if the user have the necessary power level to invite + * @param userId the id of the user to check for. + * @return true if able to invite + */ + fun isUserAbleToInvite(userId: String): Boolean { + val powerLevel = getUserPowerLevelValue(userId) + return powerLevel >= powerLevelsContent.invite + } + + /** + * Check if the user have the necessary power level to ban + * @param userId the id of the user to check for. + * @return true if able to ban + */ + fun isUserAbleToBan(userId: String): Boolean { + val powerLevel = getUserPowerLevelValue(userId) + return powerLevel >= powerLevelsContent.ban + } + + /** + * Check if the user have the necessary power level to kick + * @param userId the id of the user to check for. + * @return true if able to kick + */ + fun isUserAbleToKick(userId: String): Boolean { + val powerLevel = getUserPowerLevelValue(userId) + return powerLevel >= powerLevelsContent.kick + } + + /** + * Check if the user have the necessary power level to redact + * @param userId the id of the user to check for. + * @return true if able to redact + */ + fun isUserAbleToRedact(userId: String): Boolean { + val powerLevel = getUserPowerLevelValue(userId) + return powerLevel >= powerLevelsContent.redact + } + + /** + * Get the notification level for a dedicated key. + * + * @param key the notification key + * @return the level + */ + fun notificationLevel(key: String): Int { + return when (val value = powerLevelsContent.notifications[key]) { + // the first implementation was a string value + is String -> value.toInt() + is Int -> value + else -> Role.Moderator.value + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/Role.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/Role.kt new file mode 100644 index 0000000000..5ac479786e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/Role.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + * + */ + +package org.matrix.android.sdk.api.session.room.powerlevels + +import androidx.annotation.StringRes +import org.matrix.android.sdk.R + +sealed class Role(open val value: Int, @StringRes val res: Int) : Comparable { + object Admin : Role(100, R.string.power_level_admin) + object Moderator : Role(50, R.string.power_level_moderator) + object Default : Role(0, R.string.power_level_default) + data class Custom(override val value: Int) : Role(value, R.string.power_level_custom) + + override fun compareTo(other: Role): Int { + return value.compareTo(other.value) + } + + companion object { + + // Order matters, default value should be checked after defined roles + fun fromValue(value: Int, default: Int): Role { + return when (value) { + Admin.value -> Admin + Moderator.value -> Moderator + Default.value, + default -> Default + else -> Custom(value) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt new file mode 100644 index 0000000000..3aa9d60e6a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.read + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.room.model.ReadReceipt +import org.matrix.android.sdk.api.util.Optional + +/** + * This interface defines methods to handle read receipts and read marker in a room. It's implemented at the room level. + */ +interface ReadService { + + enum class MarkAsReadParams { + READ_RECEIPT, + READ_MARKER, + BOTH + } + + /** + * Force the read marker to be set on the latest event. + */ + fun markAsRead(params: MarkAsReadParams = MarkAsReadParams.BOTH, callback: MatrixCallback) + + /** + * Set the read receipt on the event with provided eventId. + */ + fun setReadReceipt(eventId: String, callback: MatrixCallback) + + /** + * Set the read marker on the event with provided eventId. + */ + fun setReadMarker(fullyReadEventId: String, callback: MatrixCallback) + + /** + * Check if an event is already read, ie. your read receipt is set on a more recent event. + */ + fun isEventRead(eventId: String): Boolean + + /** + * Returns a live read marker id for the room. + */ + fun getReadMarkerLive(): LiveData> + + /** + * Returns a live read receipt id for the room. + */ + fun getMyReadReceiptLive(): LiveData> + + /** + * Returns a live list of read receipts for a given event + * @param eventId: the event + */ + fun getEventReadReceiptsLive(eventId: String): LiveData> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/reporting/ReportingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/reporting/ReportingService.kt new file mode 100644 index 0000000000..42a21fab90 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/reporting/ReportingService.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.reporting + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.util.Cancelable + +/** + * This interface defines methods to report content of an event. + */ +interface ReportingService { + + /** + * Report content + * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-rooms-roomid-report-eventid + */ + fun reportContent(eventId: String, score: Int, reason: String, callback: MatrixCallback): Cancelable +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/DraftService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/DraftService.kt new file mode 100644 index 0000000000..dc91d5177f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/DraftService.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.send + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.util.Cancelable + +interface DraftService { + + /** + * Save or update a draft to the room + */ + fun saveDraft(draft: UserDraft, callback: MatrixCallback): Cancelable + + /** + * Delete the last draft, basically just after sending the message + */ + fun deleteDraft(callback: MatrixCallback): Cancelable + + /** + * Return the current drafts if any, as a live data + * The draft list can contain one draft for {regular, reply, quote} and an arbitrary number of {edit} drafts + */ + fun getDraftsLive(): LiveData> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/MatrixItemSpan.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/MatrixItemSpan.kt new file mode 100644 index 0000000000..a96339a111 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/MatrixItemSpan.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.send + +import org.matrix.android.sdk.api.util.MatrixItem + +/** + * Tag class for spans that should mention a matrix item. + * These Spans will be transformed into pills when detected in message to send + */ +interface MatrixItemSpan { + val matrixItem: MatrixItem +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt new file mode 100644 index 0000000000..e84b75d0af --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt @@ -0,0 +1,133 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.send + +import org.matrix.android.sdk.api.session.content.ContentAttachmentData +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.model.message.MessageType +import org.matrix.android.sdk.api.session.room.model.message.OptionItem +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.util.Cancelable + +/** + * This interface defines methods to send events in a room. It's implemented at the room level. + */ +interface SendService { + + /** + * Method to send a generic event asynchronously. If you want to send a state event, please use [StateService] instead. + * @param eventType the type of the event + * @param content the optional body as a json dict. + * @return a [Cancelable] + */ + fun sendEvent(eventType: String, content: Content?): Cancelable + + /** + * Method to send a text message asynchronously. + * The text to send can be a Spannable and contains special spans (MatrixItemSpan) that will be translated + * by the sdk into pills. + * @param text the text message to send + * @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE + * @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present + * @return a [Cancelable] + */ + fun sendTextMessage(text: CharSequence, msgType: String = MessageType.MSGTYPE_TEXT, autoMarkdown: Boolean = false): Cancelable + + /** + * Method to send a text message with a formatted body. + * @param text the text message to send + * @param formattedText The formatted body using MessageType#FORMAT_MATRIX_HTML + * @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE + * @return a [Cancelable] + */ + fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String = MessageType.MSGTYPE_TEXT): Cancelable + + /** + * Method to send a media asynchronously. + * @param attachment the media to send + * @param compressBeforeSending set to true to compress images before sending them + * @param roomIds set of roomIds to where the media will be sent. The current roomId will be add to this set if not present. + * It can be useful to send media to multiple room. It's safe to include the current roomId in this set + * @return a [Cancelable] + */ + fun sendMedia(attachment: ContentAttachmentData, + compressBeforeSending: Boolean, + roomIds: Set): Cancelable + + /** + * Method to send a list of media asynchronously. + * @param attachments the list of media to send + * @param compressBeforeSending set to true to compress images before sending them + * @param roomIds set of roomIds to where the media will be sent. The current roomId will be add to this set if not present. + * It can be useful to send media to multiple room. It's safe to include the current roomId in this set + * @return a [Cancelable] + */ + fun sendMedias(attachments: List, + compressBeforeSending: Boolean, + roomIds: Set): Cancelable + + /** + * Send a poll to the room. + * @param question the question + * @param options list of (label, value) + * @return a [Cancelable] + */ + fun sendPoll(question: String, options: List): Cancelable + + /** + * Method to send a poll response. + * @param pollEventId the poll currently replied to + * @param optionIndex The reply index + * @param optionValue The option value (for compatibility) + * @return a [Cancelable] + */ + fun sendOptionsReply(pollEventId: String, optionIndex: Int, optionValue: String): Cancelable + + /** + * Redact (delete) the given event. + * @param event The event to redact + * @param reason Optional reason string + */ + fun redactEvent(event: Event, reason: String?): Cancelable + + /** + * Schedule this message to be resent + * @param localEcho the unsent local echo + */ + fun resendTextMessage(localEcho: TimelineEvent): Cancelable? + + /** + * Schedule this message to be resent + * @param localEcho the unsent local echo + */ + fun resendMediaMessage(localEcho: TimelineEvent): Cancelable? + + /** + * Remove this failed message from the timeline + * @param localEcho the unsent local echo + */ + fun deleteFailedEcho(localEcho: TimelineEvent) + + fun clearSendingQueue() + + /** + * Resend all failed messages one by one (and keep order) + */ + fun resendAllFailedMessages() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendState.kt new file mode 100644 index 0000000000..f0dd2f3025 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendState.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.send + +enum class SendState { + UNKNOWN, + // the event has not been sent + UNSENT, + // the event is encrypting + ENCRYPTING, + // the event is currently sending + SENDING, + // the event has been sent + SENT, + // the event has been received from server + SYNCED, + // The event failed to be sent + UNDELIVERED, + // the event failed to be sent because some unknown devices have been found while encrypting it + FAILED_UNKNOWN_DEVICES; + + internal companion object { + val HAS_FAILED_STATES = listOf(UNDELIVERED, FAILED_UNKNOWN_DEVICES) + val IS_SENT_STATES = listOf(SENT, SYNCED) + val IS_SENDING_STATES = listOf(UNSENT, ENCRYPTING, SENDING) + val PENDING_STATES = IS_SENDING_STATES + HAS_FAILED_STATES + } + + fun isSent() = IS_SENT_STATES.contains(this) + + fun hasFailed() = HAS_FAILED_STATES.contains(this) + + fun isSending() = IS_SENDING_STATES.contains(this) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/UserDraft.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/UserDraft.kt new file mode 100644 index 0000000000..b5542b7d63 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/UserDraft.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.send + +/** + * Describes a user draft: + * REGULAR: draft of a classical message + * QUOTE: draft of a message which quotes another message + * EDIT: draft of an edition of a message + * REPLY: draft of a reply of another message + */ +sealed class UserDraft(open val text: String) { + data class REGULAR(override val text: String) : UserDraft(text) + data class QUOTE(val linkedEventId: String, override val text: String) : UserDraft(text) + data class EDIT(val linkedEventId: String, override val text: String) : UserDraft(text) + data class REPLY(val linkedEventId: String, override val text: String) : UserDraft(text) + + fun isValid(): Boolean { + return when (this) { + is REGULAR -> text.isNotBlank() + else -> true + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/sender/SenderInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/sender/SenderInfo.kt new file mode 100644 index 0000000000..b2836ecaae --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/sender/SenderInfo.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.sender + +data class SenderInfo( + val userId: String, + /** + * Consider using [disambiguatedDisplayName] + */ + val displayName: String?, + val isUniqueDisplayName: Boolean, + val avatarUrl: String? +) { + val disambiguatedDisplayName: String + get() = when { + displayName.isNullOrBlank() -> userId + isUniqueDisplayName -> displayName + else -> "$displayName ($userId)" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt new file mode 100644 index 0000000000..f887a8b854 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.state + +import android.net.Uri +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.api.util.Optional + +interface StateService { + + /** + * Update the topic of the room + */ + fun updateTopic(topic: String, callback: MatrixCallback): Cancelable + + /** + * Update the name of the room + */ + fun updateName(name: String, callback: MatrixCallback): Cancelable + + /** + * Add new alias to the room. + */ + fun addRoomAlias(roomAlias: String, callback: MatrixCallback): Cancelable + + /** + * Update the canonical alias of the room + */ + fun updateCanonicalAlias(alias: String, callback: MatrixCallback): Cancelable + + /** + * Update the history readability of the room + */ + fun updateHistoryReadability(readability: RoomHistoryVisibility, callback: MatrixCallback): Cancelable + + /** + * Update the avatar of the room + */ + fun updateAvatar(avatarUri: Uri, fileName: String, callback: MatrixCallback): Cancelable + + fun sendStateEvent(eventType: String, stateKey: String?, body: JsonDict, callback: MatrixCallback): Cancelable + + fun getStateEvent(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): Event? + + fun getStateEventLive(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): LiveData> + + fun getStateEvents(eventTypes: Set, stateKey: QueryStringValue = QueryStringValue.NoCondition): List + + fun getStateEventsLive(eventTypes: Set, stateKey: QueryStringValue = QueryStringValue.NoCondition): LiveData> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/tags/TagsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/tags/TagsService.kt new file mode 100644 index 0000000000..62f9560315 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/tags/TagsService.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.tags + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.util.Cancelable + +/** + * This interface defines methods to handle tags of a room. It's implemented at the room level. + */ +interface TagsService { + /** + * Add a tag to a room + */ + fun addTag(tag: String, order: Double?, callback: MatrixCallback): Cancelable + + /** + * Remove tag from a room + */ + fun deleteTag(tag: String, callback: MatrixCallback): Cancelable +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt new file mode 100644 index 0000000000..8920689d97 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt @@ -0,0 +1,137 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.timeline + +/** + * A Timeline instance represents a contiguous sequence of events in a room. + *

+ * There are two kinds of timeline: + *

+ * - live timelines: they process live events from the sync. You can paginate + * backwards but not forwards. + *

+ * - past timelines: they start in the past from an `initialEventId`. You can paginate + * backwards and forwards. + * + */ +interface Timeline { + + val timelineID: String + + val isLive: Boolean + + fun addListener(listener: Listener): Boolean + + fun removeListener(listener: Listener): Boolean + + fun removeAllListeners() + + /** + * This must be called before any other method after creating the timeline. It ensures the underlying database is open + */ + fun start() + + /** + * This must be called when you don't need the timeline. It ensures the underlying database get closed. + */ + fun dispose() + + /** + * This method restarts the timeline, erases all built events and pagination states. + * It then loads events around the eventId. If eventId is null, it does restart the live timeline. + */ + fun restartWithEventId(eventId: String?) + + /** + * Check if the timeline can be enriched by paginating. + * @param direction the direction to check in + * @return true if timeline can be enriched + */ + fun hasMoreToLoad(direction: Direction): Boolean + + /** + * This is the main method to enrich the timeline with new data. + * It will call the onTimelineUpdated method from [Listener] when the data will be processed. + * It also ensures only one pagination by direction is launched at a time, so you can safely call this multiple time in a row. + */ + fun paginate(direction: Direction, count: Int) + + /** + * Returns the number of sending events + */ + fun pendingEventCount(): Int + + /** + * Returns the number of failed sending events. + */ + fun failedToDeliverEventCount(): Int + + /** + * Returns the index of a built event or null. + */ + fun getIndexOfEvent(eventId: String?): Int? + + /** + * Returns the built [TimelineEvent] at index or null + */ + fun getTimelineEventAtIndex(index: Int): TimelineEvent? + + /** + * Returns the built [TimelineEvent] with eventId or null + */ + fun getTimelineEventWithId(eventId: String?): TimelineEvent? + + /** + * Returns the first displayable events starting from eventId. + * It does depend on the provided [TimelineSettings]. + */ + fun getFirstDisplayableEventId(eventId: String): String? + + interface Listener { + /** + * Call when the timeline has been updated through pagination or sync. + * The latest event is the first in the list + * @param snapshot the most up to date snapshot + */ + fun onTimelineUpdated(snapshot: List) + + /** + * Called whenever an error we can't recover from occurred + */ + fun onTimelineFailure(throwable: Throwable) + + /** + * Called when new events come through the sync + */ + fun onNewTimelineEvents(eventIds: List) + } + + /** + * This is used to paginate in one or another direction. + */ + enum class Direction { + /** + * It represents future events. + */ + FORWARDS, + /** + * It represents past events. + */ + BACKWARDS + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt new file mode 100644 index 0000000000..1f3c85afe6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt @@ -0,0 +1,147 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.timeline + +import org.matrix.android.sdk.BuildConfig +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.events.model.getRelationContent +import org.matrix.android.sdk.api.session.events.model.isReply +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary +import org.matrix.android.sdk.api.session.room.model.ReadReceipt +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.api.session.room.sender.SenderInfo +import org.matrix.android.sdk.api.util.ContentUtils.extractUsefulTextFromReply + +/** + * This data class is a wrapper around an Event. It allows to get useful data in the context of a timeline. + * This class is used by [TimelineService] + * Users can also enrich it with metadata. + */ +data class TimelineEvent( + val root: Event, + val localId: Long, + val eventId: String, + val displayIndex: Int, + val senderInfo: SenderInfo, + val annotations: EventAnnotationsSummary? = null, + val readReceipts: List = emptyList() +) { + + init { + if (BuildConfig.DEBUG) { + assert(eventId == root.eventId) + } + } + + val metadata = HashMap() + + /** + * The method to enrich this timeline event. + * If you provides multiple data with the same key, only first one will be kept. + * @param key the key to associate data with. + * @param data the data to enrich with. + */ + fun enrichWith(key: String?, data: Any?) { + if (key == null || data == null) { + return + } + if (!metadata.containsKey(key)) { + metadata[key] = data + } + } + + /** + * Get the metadata associated with a key. + * @param key the key to get the metadata + * @return the metadata + */ + inline fun getMetadata(key: String): T? { + return metadata[key] as T? + } + + fun isEncrypted(): Boolean { + // warning: Do not use getClearType here + return EventType.ENCRYPTED == root.type + } +} + +/** + * Tells if the event has been edited + */ +fun TimelineEvent.hasBeenEdited() = annotations?.editSummary != null + +/** + * Get the relation content if any + */ +fun TimelineEvent.getRelationContent(): RelationDefaultContent? { + return root.getRelationContent() +} + +/** + * Get the eventId which was edited by this event if any + */ +fun TimelineEvent.getEditedEventId(): String? { + return getRelationContent()?.takeIf { it.type == RelationType.REPLACE }?.eventId +} + +/** + * Get last MessageContent, after a possible edition + */ +fun TimelineEvent.getLastMessageContent(): MessageContent? { + return if (root.getClearType() == EventType.STICKER) { + root.getClearContent().toModel() + } else { + annotations?.editSummary?.aggregatedContent?.toModel() + ?: root.getClearContent().toModel() + } +} + +/** + * Get last Message body, after a possible edition + */ +fun TimelineEvent.getLastMessageBody(): String? { + val lastMessageContent = getLastMessageContent() + + if (lastMessageContent != null) { + return lastMessageContent.newContent?.toModel()?.body + ?: lastMessageContent.body + } + + return null +} + +/** + * Returns true if it's a reply + */ +fun TimelineEvent.isReply(): Boolean { + return root.isReply() +} + +fun TimelineEvent.getTextEditableContent(): String? { + val lastContent = getLastMessageContent() + return if (isReply()) { + return extractUsefulTextFromReply(lastContent?.body ?: "") + } else { + lastContent?.body ?: "" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt new file mode 100644 index 0000000000..473e505302 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.timeline + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.util.Optional + +/** + * This interface defines methods to interact with the timeline. It's implemented at the room level. + */ +interface TimelineService { + + /** + * Instantiate a [Timeline] with an optional initial eventId, to be used with permalink. + * You can also configure some settings with the [settings] param. + * + * Important: the returned Timeline has to be started + * + * @param eventId the optional initial eventId. + * @param settings settings to configure the timeline. + * @return the instantiated timeline + */ + fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline + + fun getTimeLineEvent(eventId: String): TimelineEvent? + + fun getTimeLineEventLive(eventId: String): LiveData> + + fun getAttachmentMessages() : List +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineSettings.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineSettings.kt new file mode 100644 index 0000000000..4f915cb907 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineSettings.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.timeline + +/** + * Data class holding setting values for a [Timeline] instance. + */ +data class TimelineSettings( + /** + * The initial number of events to retrieve from cache. You might get less events if you don't have loaded enough yet. + */ + val initialSize: Int, + /** + * A flag to filter edit events + */ + val filterEdits: Boolean = false, + /** + * A flag to filter redacted events + */ + val filterRedacted: Boolean = false, + /** + * A flag to filter useless events, such as membership events without any change + */ + val filterUseless: Boolean = false, + /** + * A flag to filter by types. It should be used with [allowedTypes] field + */ + val filterTypes: Boolean = false, + /** + * If [filterTypes] is true, the list of types allowed by the list. + */ + val allowedTypes: List = emptyList(), + /** + * If true, will build read receipts for each event. + */ + val buildReadReceipts: Boolean = true +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/typing/TypingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/typing/TypingService.kt new file mode 100644 index 0000000000..eaa8d5c3df --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/typing/TypingService.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.typing + +/** + * This interface defines methods to handle typing data. It's implemented at the room level. + */ +interface TypingService { + + /** + * To call when user is typing a message in the room + * The SDK will handle the requests scheduling to the homeserver: + * - No more than one typing request per 10s + * - If not called after 10s, the SDK will notify the homeserver that the user is not typing anymore + */ + fun userIsTyping() + + /** + * To call when user stops typing in the room + * Notify immediately the homeserver that the user is not typing anymore in the room, for + * instance when user has emptied the composer, or when the user quits the timeline screen. + */ + fun userStopsTyping() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/uploads/GetUploadsResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/uploads/GetUploadsResult.kt new file mode 100644 index 0000000000..09b885e24d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/uploads/GetUploadsResult.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.uploads + +data class GetUploadsResult( + // List of fetched Events, most recent first + val uploadEvents: List, + // token to get more events + val nextToken: String, + // True if there are more event to load + val hasMore: Boolean +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/uploads/UploadEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/uploads/UploadEvent.kt new file mode 100644 index 0000000000..16423cf3c5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/uploads/UploadEvent.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.uploads + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent +import org.matrix.android.sdk.api.session.room.sender.SenderInfo + +/** + * Wrapper around on Event. + * Similar to [org.matrix.android.sdk.api.session.room.timeline.TimelineEvent], contains an Event with extra useful data + */ +data class UploadEvent( + val root: Event, + val eventId: String, + val contentWithAttachmentContent: MessageWithAttachmentContent, + val senderInfo: SenderInfo +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/uploads/UploadsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/uploads/UploadsService.kt new file mode 100644 index 0000000000..1cabdfc92a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/uploads/UploadsService.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.room.uploads + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.util.Cancelable + +/** + * This interface defines methods to get event with uploads (= attachments) sent to a room. It's implemented at the room level. + */ +interface UploadsService { + + /** + * Get a list of events containing URL sent to a room, from most recent to oldest one + * @param numberOfEvents the expected number of events to retrieve. The result can contain less events. + * @param since token to get next page, or null to get the first page + */ + fun getUploads(numberOfEvents: Int, + since: String?, + callback: MatrixCallback): Cancelable +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/EncryptedSecretContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/EncryptedSecretContent.kt new file mode 100644 index 0000000000..8c062b4203 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/EncryptedSecretContent.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.securestorage + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.android.sdk.internal.session.user.accountdata.AccountDataContent + +/** + * The account_data will have an encrypted property that is a map from key ID to an object. + * The algorithm from the m.secret_storage.key.[key ID] data for the given key defines how the other properties are interpreted, + * though it's expected that most encryption schemes would have ciphertext and mac properties, + * where the ciphertext property is the unpadded base64-encoded ciphertext, and the mac is used to ensure the integrity of the data. + */ +@JsonClass(generateAdapter = true) +data class EncryptedSecretContent( + /** unpadded base64-encoded ciphertext */ + @Json(name = "ciphertext") val ciphertext: String? = null, + @Json(name = "mac") val mac: String? = null, + @Json(name = "ephemeral") val ephemeral: String? = null, + @Json(name = "iv") val initializationVector: String? = null +) : AccountDataContent { + companion object { + /** + * Facility method to convert from object which must be comprised of maps, lists, + * strings, numbers, booleans and nulls. + */ + fun fromJson(obj: Any?): EncryptedSecretContent? { + return MoshiProvider.providesMoshi() + .adapter(EncryptedSecretContent::class.java) + .fromJsonValue(obj) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/IntegrityResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/IntegrityResult.kt new file mode 100644 index 0000000000..096f9f34a2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/IntegrityResult.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.securestorage + +sealed class IntegrityResult { + data class Success(val passphraseBased: Boolean) : IntegrityResult() + data class Error(val cause: SharedSecretStorageError) : IntegrityResult() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/KeyInfoResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/KeyInfoResult.kt new file mode 100644 index 0000000000..287555ae95 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/KeyInfoResult.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.securestorage + +sealed class KeyInfoResult { + data class Success(val keyInfo: KeyInfo) : KeyInfoResult() + data class Error(val error: SharedSecretStorageError) : KeyInfoResult() + + fun isSuccess(): Boolean = this is Success +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/KeySigner.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/KeySigner.kt new file mode 100644 index 0000000000..2d56fb81f3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/KeySigner.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.securestorage + +interface KeySigner { + fun sign(canonicalJson: String): Map>? +} + +class EmptyKeySigner : KeySigner { + override fun sign(canonicalJson: String): Map>? = null +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SecretStorageKeyContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SecretStorageKeyContent.kt new file mode 100644 index 0000000000..f960a43675 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SecretStorageKeyContent.kt @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.securestorage + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.android.sdk.internal.util.JsonCanonicalizer + +/** + * + * The contents of the account data for the key will include an algorithm property, which indicates the encryption algorithm used, as well as a name property, + * which is a human-readable name. + * The contents will be signed as signed JSON using the user's master cross-signing key. Other properties depend on the encryption algorithm. + * + * + * "content": { + * "algorithm": "m.secret_storage.v1.curve25519-aes-sha2", + * "passphrase": { + * "algorithm": "m.pbkdf2", + * "iterations": 500000, + * "salt": "IrswcMWnYieBALCAOMBw9k93xSzlc2su" + * }, + * "pubkey": "qql1q3IvBbwMU97zLnyh9HYW5x/zqTy5eoK1n+9fm1Y", + * "signatures": { + * "@valere35:matrix.org": { + * "ed25519:nOUQYiH9L8uKp5JajqiQyv+Loa3+lsdil7UBverz/Ko": "QtePmwfUL7+SHYRJT/HaTgF7gUFog1E/wtUCt0qc5aB8N+Sz5iCOvQ0KtaFHQ5SJzsBlYH8k7ejoBc0RcnU7BA" + * } + * } + * } + */ + +data class KeyInfo( + val id: String, + val content: SecretStorageKeyContent +) + +@JsonClass(generateAdapter = true) +data class SecretStorageKeyContent( + /** Currently support m.secret_storage.v1.curve25519-aes-sha2 */ + @Json(name = "algorithm") val algorithm: String? = null, + @Json(name = "name") val name: String? = null, + @Json(name = "passphrase") val passphrase: SsssPassphrase? = null, + @Json(name = "pubkey") val publicKey: String? = null, + @Json(name = "signatures") val signatures: Map>? = null +) { + + private fun signalableJSONDictionary(): Map { + return mutableMapOf().apply { + algorithm + ?.let { this["algorithm"] = it } + name + ?.let { this["name"] = it } + publicKey + ?.let { this["pubkey"] = it } + passphrase + ?.let { ssssPassphrase -> + this["passphrase"] = mapOf( + "algorithm" to ssssPassphrase.algorithm, + "iterations" to ssssPassphrase.iterations, + "salt" to ssssPassphrase.salt + ) + } + } + } + + fun canonicalSignable(): String { + return JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableJSONDictionary()) + } + + companion object { + /** + * Facility method to convert from object which must be comprised of maps, lists, + * strings, numbers, booleans and nulls. + */ + fun fromJson(obj: Any?): SecretStorageKeyContent? { + return MoshiProvider.providesMoshi() + .adapter(SecretStorageKeyContent::class.java) + .fromJsonValue(obj) + } + } +} + +@JsonClass(generateAdapter = true) +data class SsssPassphrase( + @Json(name = "algorithm") val algorithm: String?, + @Json(name = "iterations") val iterations: Int, + @Json(name = "salt") val salt: String? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SecureStorageService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SecureStorageService.kt new file mode 100644 index 0000000000..89095268b3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SecureStorageService.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.securestorage + +import java.io.InputStream +import java.io.OutputStream + +interface SecureStorageService { + + fun securelyStoreObject(any: Any, keyAlias: String, outputStream: OutputStream) + + fun loadSecureSecret(inputStream: InputStream, keyAlias: String): T? +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SharedSecretStorageError.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SharedSecretStorageError.kt new file mode 100644 index 0000000000..79e7fa51fe --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SharedSecretStorageError.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.securestorage + +sealed class SharedSecretStorageError(message: String?) : Throwable(message) { + data class UnknownSecret(val secretName: String) : SharedSecretStorageError("Unknown Secret $secretName") + data class UnknownKey(val keyId: String) : SharedSecretStorageError("Unknown key $keyId") + data class UnknownAlgorithm(val keyId: String) : SharedSecretStorageError("Unknown algorithm $keyId") + data class UnsupportedAlgorithm(val algorithm: String) : SharedSecretStorageError("Unknown algorithm $algorithm") + data class SecretNotEncrypted(val secretName: String) : SharedSecretStorageError("Missing content for secret $secretName") + data class SecretNotEncryptedWithKey(val secretName: String, val keyId: String) + : SharedSecretStorageError("Missing content for secret $secretName with key $keyId") + + object BadKeyFormat : SharedSecretStorageError("Bad Key Format") + object ParsingError : SharedSecretStorageError("parsing Error") + object BadMac : SharedSecretStorageError("Bad mac") + object BadCipherText : SharedSecretStorageError("Bad cipher text") + + data class OtherError(val reason: Throwable) : SharedSecretStorageError(reason.localizedMessage) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SharedSecretStorageService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SharedSecretStorageService.kt new file mode 100644 index 0000000000..ffc7e3889d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SharedSecretStorageService.kt @@ -0,0 +1,144 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.securestorage + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.listeners.ProgressListener +import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME + +/** + * Some features may require clients to store encrypted data on the server so that it can be shared securely between clients. + * Clients may also wish to securely send such data directly to each other. + * For example, key backups (MSC1219) can store the decryption key for the backups on the server, or cross-signing (MSC1756) can store the signing keys. + * + * https://github.com/matrix-org/matrix-doc/pull/1946 + * + */ + +interface SharedSecretStorageService { + + /** + * Generates a SSSS key for encrypting secrets. + * Use the SsssKeyCreationInfo object returned by the callback to get more information about the created key (recovery key ...) + * + * @param keyId the ID of the key + * @param key keep null if you want to generate a random key + * @param keyName a human readable name + * @param keySigner Used to add a signature to the key (client should check key signature before storing secret) + * + * @param callback Get key creation info + */ + fun generateKey(keyId: String, + key: SsssKeySpec?, + keyName: String, + keySigner: KeySigner?, + callback: MatrixCallback) + + /** + * Generates a SSSS key using the given passphrase. + * Use the SsssKeyCreationInfo object returned by the callback to get more information about the created key (recovery key, salt, iteration ...) + * + * @param keyId the ID of the key + * @param keyName human readable key name + * @param passphrase The passphrase used to generate the key + * @param keySigner Used to add a signature to the key (client should check key signature before retrieving secret) + * @param progressListener The derivation of the passphrase may take long depending on the device, use this to report progress + * + * @param callback Get key creation info + */ + fun generateKeyWithPassphrase(keyId: String, + keyName: String, + passphrase: String, + keySigner: KeySigner, + progressListener: ProgressListener?, + callback: MatrixCallback) + + fun getKey(keyId: String): KeyInfoResult + + /** + * A key can be marked as the "default" key by setting the user's account_data with event type m.secret_storage.default_key + * to an object that has the ID of the key as its key property. + * The default key will be used to encrypt all secrets that the user would expect to be available on all their clients. + * Unless the user specifies otherwise, clients will try to use the default key to decrypt secrets. + */ + fun getDefaultKey(): KeyInfoResult + + fun setDefaultKey(keyId: String, callback: MatrixCallback) + + /** + * Check whether we have a key with a given ID. + * + * @param keyId The ID of the key to check + * @return Whether we have the key. + */ + fun hasKey(keyId: String): Boolean + + /** + * Store an encrypted secret on the server + * Clients MUST ensure that the key is trusted before using it to encrypt secrets. + * + * @param name The name of the secret + * @param secret The secret contents. + * @param keys The list of (ID,privateKey) of the keys to use to encrypt the secret. + */ + fun storeSecret(name: String, secretBase64: String, keys: List, callback: MatrixCallback) + + /** + * Use this call to determine which SSSSKeySpec to use for requesting secret + */ + fun getAlgorithmsForSecret(name: String): List + + /** + * Get an encrypted secret from the shared storage + * + * @param name The name of the secret + * @param keyId The id of the key that should be used to decrypt (null for default key) + * @param secretKey the secret key to use (@see #RawBytesKeySpec) + * + */ + fun getSecret(name: String, keyId: String?, secretKey: SsssKeySpec, callback: MatrixCallback) + + /** + * Return true if SSSS is configured + */ + fun isRecoverySetup(): Boolean { + return checkShouldBeAbleToAccessSecrets( + secretNames = listOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME), + keyId = null + ) is IntegrityResult.Success + } + + fun isMegolmKeyInBackup(): Boolean { + return checkShouldBeAbleToAccessSecrets( + secretNames = listOf(KEYBACKUP_SECRET_SSSS_NAME), + keyId = null + ) is IntegrityResult.Success + } + + fun checkShouldBeAbleToAccessSecrets(secretNames: List, keyId: String?): IntegrityResult + + fun requestSecret(name: String, myOtherDeviceId: String) + + data class KeyRef( + val keyId: String?, + val keySpec: SsssKeySpec? + ) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SsssKeyCreationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SsssKeyCreationInfo.kt new file mode 100644 index 0000000000..17f0366d16 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SsssKeyCreationInfo.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.securestorage + +data class SsssKeyCreationInfo( + val keyId: String = "", + var content: SecretStorageKeyContent?, + val recoveryKey: String = "", + val keySpec: SsssKeySpec +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SsssKeySpec.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SsssKeySpec.kt new file mode 100644 index 0000000000..9ae181a44e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SsssKeySpec.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.securestorage + +import org.matrix.android.sdk.api.listeners.ProgressListener +import org.matrix.android.sdk.internal.crypto.keysbackup.deriveKey +import org.matrix.android.sdk.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey + +/** Tag class */ +interface SsssKeySpec + +data class RawBytesKeySpec( + val privateKey: ByteArray +) : SsssKeySpec { + + companion object { + + fun fromPassphrase(passphrase: String, salt: String, iterations: Int, progressListener: ProgressListener?): RawBytesKeySpec { + return RawBytesKeySpec( + privateKey = deriveKey( + passphrase, + salt, + iterations, + progressListener + ) + ) + } + + fun fromRecoveryKey(recoveryKey: String): RawBytesKeySpec? { + return extractCurveKeyFromRecoveryKey(recoveryKey)?.let { + RawBytesKeySpec( + privateKey = it + ) + } + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as RawBytesKeySpec + + if (!privateKey.contentEquals(other.privateKey)) return false + + return true + } + + override fun hashCode(): Int { + return privateKey.contentHashCode() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/signout/SignOutService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/signout/SignOutService.kt new file mode 100644 index 0000000000..d4061c5c7c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/signout/SignOutService.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.signout + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.util.Cancelable + +/** + * This interface defines a method to sign out, or to renew the token. It's implemented at the session level. + */ +interface SignOutService { + + /** + * Ask the homeserver for a new access token. + * The same deviceId will be used + */ + fun signInAgain(password: String, + callback: MatrixCallback): Cancelable + + /** + * Update the session with credentials received after SSO + */ + fun updateCredentials(credentials: Credentials, + callback: MatrixCallback): Cancelable + + /** + * Sign out, and release the session, clear all the session data, including crypto data + * @param signOutFromHomeserver true if the sign out request has to be done + */ + fun signOut(signOutFromHomeserver: Boolean, + callback: MatrixCallback): Cancelable +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/FilterService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/FilterService.kt new file mode 100644 index 0000000000..0389969948 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/FilterService.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.sync + +interface FilterService { + + enum class FilterPreset { + NoFilter, + /** + * Filter for Riot, will include only known event type + */ + RiotFilter + } + + /** + * Configure the filter for the sync + */ + fun setFilter(filterPreset: FilterPreset) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/SyncState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/SyncState.kt new file mode 100644 index 0000000000..08d8be699a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/SyncState.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.sync + +sealed class SyncState { + object Idle : SyncState() + data class Running(val afterPause: Boolean) : SyncState() + object Paused : SyncState() + object Killing : SyncState() + object Killed : SyncState() + object NoNetwork : SyncState() + object InvalidToken : SyncState() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/terms/GetTermsResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/terms/GetTermsResponse.kt new file mode 100644 index 0000000000..685f4ba9c3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/terms/GetTermsResponse.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.terms + +import org.matrix.android.sdk.internal.session.terms.TermsResponse + +data class GetTermsResponse( + val serverResponse: TermsResponse, + val alreadyAcceptedTermUrls: Set +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/terms/TermsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/terms/TermsService.kt new file mode 100644 index 0000000000..3e2201cb29 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/terms/TermsService.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.terms + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.util.Cancelable + +interface TermsService { + enum class ServiceType { + IntegrationManager, + IdentityService + } + + fun getTerms(serviceType: ServiceType, + baseUrl: String, + callback: MatrixCallback): Cancelable + + fun agreeToTerms(serviceType: ServiceType, + baseUrl: String, + agreedUrls: List, + token: String?, + callback: MatrixCallback): Cancelable +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/typing/TypingUsersTracker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/typing/TypingUsersTracker.kt new file mode 100644 index 0000000000..e51fa45d72 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/typing/TypingUsersTracker.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.typing + +import org.matrix.android.sdk.api.session.room.sender.SenderInfo + +/** + * Responsible for tracking typing users from each room. + * It's ephemeral data and it's only saved in memory. + */ +interface TypingUsersTracker { + + /** + * Returns the sender information of all currently typing users in a room, excluding yourself. + */ + fun getTypingUsers(roomId: String): List +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/UserService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/UserService.kt new file mode 100644 index 0000000000..b5617a206f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/UserService.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.user + +import androidx.lifecycle.LiveData +import androidx.paging.PagedList +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.user.model.User +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.Optional + +/** + * This interface defines methods to get users. It's implemented at the session level. + */ +interface UserService { + + /** + * Get a user from a userId + * @param userId the userId to look for. + * @return a user with userId or null + */ + fun getUser(userId: String): User? + + /** + * Search list of users on server directory. + * @param search the searched term + * @param limit the max number of users to return + * @param excludedUserIds the user ids to filter from the search + * @param callback the async callback + * @return Cancelable + */ + fun searchUsersDirectory(search: String, limit: Int, excludedUserIds: Set, callback: MatrixCallback>): Cancelable + + /** + * Observe a live user from a userId + * @param userId the userId to look for. + * @return a LiveData of user with userId + */ + fun getUserLive(userId: String): LiveData> + + /** + * Observe a live list of users sorted alphabetically + * @return a Livedata of users + */ + fun getUsersLive(): LiveData> + + /** + * Observe a live [PagedList] of users sorted alphabetically. You can filter the users. + * @param filter the filter. It will look into userId and displayName. + * @param excludedUserIds userId list which will be excluded from the result list. + * @return a Livedata of users + */ + fun getPagedUsersLive(filter: String? = null, excludedUserIds: Set? = null): LiveData> + + /** + * Get list of ignored users + */ + fun getIgnoredUsersLive(): LiveData> + + /** + * Ignore users + */ + fun ignoreUserIds(userIds: List, callback: MatrixCallback): Cancelable + + /** + * Un-ignore some users + */ + fun unIgnoreUserIds(userIds: List, callback: MatrixCallback): Cancelable +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/model/User.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/model/User.kt new file mode 100644 index 0000000000..bf8551588e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/model/User.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.user.model + +/** + * Data class which holds information about a user. + * It can be retrieved with [org.matrix.android.sdk.api.session.user.UserService] + */ +data class User( + val userId: String, + /** + * For usage in UI, consider using [getBestName] + */ + val displayName: String? = null, + val avatarUrl: String? = null +) { + /** + * Return the display name or the user id + */ + fun getBestName() = displayName?.takeIf { it.isNotEmpty() } ?: userId +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetManagementFailure.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetManagementFailure.kt new file mode 100644 index 0000000000..abbbf040ab --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetManagementFailure.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.widgets + +import org.matrix.android.sdk.api.failure.Failure + +sealed class WidgetManagementFailure : Failure.FeatureFailure() { + object NotEnoughPower : WidgetManagementFailure() + object CreationFailed : WidgetManagementFailure() + data class TermsNotSignedException(val baseUrl: String, val token: String) : WidgetManagementFailure() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetPostAPIMediator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetPostAPIMediator.kt new file mode 100644 index 0000000000..4dba2a10e1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetPostAPIMediator.kt @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.widgets + +import android.webkit.WebView +import org.matrix.android.sdk.api.util.JsonDict +import java.lang.reflect.Type + +interface WidgetPostAPIMediator { + + /** + * This initialize the webview to handle. + * It will add a JavaScript Interface. + * Please call [clearWebView] method when finished to clean the provided webview + */ + fun setWebView(webView: WebView) + + /** + * Set handler to communicate with the widgetPostAPIMediator. + * Please remove the reference by passing null when finished. + */ + fun setHandler(handler: Handler?) + + /** + * This clear the mediator by removing the JavaScript Interface and cleaning references. + */ + fun clearWebView() + + /** + * Inject the necessary javascript into the configured WebView. + * Should be called after a web page has been loaded. + */ + fun injectAPI() + + /** + * Send a boolean response + * + * @param response the response + * @param eventData the modular data + */ + fun sendBoolResponse(response: Boolean, eventData: JsonDict) + + /** + * Send an integer response + * + * @param response the response + * @param eventData the modular data + */ + fun sendIntegerResponse(response: Int, eventData: JsonDict) + + /** + * Send an object response + * + * @param klass the class of the response + * @param response the response + * @param eventData the modular data + */ + fun sendObjectResponse(type: Type, response: T?, eventData: JsonDict) + + /** + * Send success + * + * @param eventData the modular data + */ + fun sendSuccess(eventData: JsonDict) + + /** + * Send an error + * + * @param message the error message + * @param eventData the modular data + */ + fun sendError(message: String, eventData: JsonDict) + + interface Handler { + /** + * Triggered when a widget is posting + */ + fun handleWidgetRequest(mediator: WidgetPostAPIMediator, eventData: JsonDict): Boolean + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetService.kt new file mode 100644 index 0000000000..444708d992 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetService.kt @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.widgets + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.session.widgets.model.Widget + +/** + * This is the entry point to manage widgets. You can grab an instance of this service through an active session. + */ +interface WidgetService { + + /** + * Returns an instance of [WidgetURLFormatter]. + */ + fun getWidgetURLFormatter(): WidgetURLFormatter + + /** + * Returns a new instance of [WidgetPostAPIMediator]. + * Be careful to call clearWebView method and setHandler to null to avoid memory leaks. + * This is to be used for "admin" widgets so you can interact through JS. + */ + fun getWidgetPostAPIMediator(): WidgetPostAPIMediator + + /** + * Returns the current room widgets defined through state events. + * Some widgets can be deactivated, so be sure to check for isActive if needed. + * + * @param roomId the room where you want to fetch widgets + * @param widgetId if you want to fetch for some particular widget + * @param widgetTypes if you want to filter some widget type. + * @param excludedTypes if you want to exclude some widget type. + */ + fun getRoomWidgets( + roomId: String, + widgetId: QueryStringValue = QueryStringValue.NoCondition, + widgetTypes: Set? = null, + excludedTypes: Set? = null + ): List + + /** + * Returns the live room widgets so you can listen to them. + * Some widgets can be deactivated, so be sure to check for isActive. + * + * @param roomId the room where you want to fetch widgets + * @param widgetId if you want to fetch for some particular widget + * @param widgetTypes if you want to filter some widget type. + * @param excludedTypes if you want to exclude some widget type. + */ + fun getRoomWidgetsLive( + roomId: String, + widgetId: QueryStringValue = QueryStringValue.NoCondition, + widgetTypes: Set? = null, + excludedTypes: Set? = null + ): LiveData> + + /** + * Returns the current user widgets. + * Some widgets can be deactivated, so be sure to check for isActive. + * + * @param widgetTypes if you want to filter some widget type. + * @param excludedTypes if you want to exclude some widget type. + */ + fun getUserWidgets( + widgetTypes: Set? = null, + excludedTypes: Set? = null + ): List + + /** + * Returns the live user widgets so you can listen to them. + * Some widgets can be deactivated, so be sure to check for isActive. + * + * @param widgetTypes if you want to filter some widget type. + * @param excludedTypes if you want to exclude some widget type. + */ + fun getUserWidgetsLive( + widgetTypes: Set? = null, + excludedTypes: Set? = null + ): LiveData> + + /** + * Creates a new widget in a room. It makes sure you have the rights to handle this. + * + * @param roomId: the room where you want to deactivate the widget. + * @param widgetId: the widget to deactivate. + * @param callback the matrix callback to listen for result. + * @return Cancelable + */ + fun createRoomWidget(roomId: String, widgetId: String, content: Content, callback: MatrixCallback): Cancelable + + /** + * Deactivate a widget in a room. It makes sure you have the rights to handle this. + * + * @param roomId: the room where you want to deactivate the widget. + * @param widgetId: the widget to deactivate. + * @param callback the matrix callback to listen for result. + * @return Cancelable + */ + fun destroyRoomWidget(roomId: String, widgetId: String, callback: MatrixCallback): Cancelable + + /** + * Returns true if you can add/remove widgets. It goes through + * @param roomId the room where you want to administrate widgets. + */ + fun hasPermissionsToHandleWidgets(roomId: String): Boolean +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetURLFormatter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetURLFormatter.kt new file mode 100644 index 0000000000..ad01679ee5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetURLFormatter.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.widgets + +interface WidgetURLFormatter { + /** + * Takes care of fetching a scalar token if required and build the final url. + * This methods can throw, you should take care of handling failure. + * + * @param baseUrl the baseUrl which will be checked for scalar token + * @param params additional params you want to append to the base url. + * @param forceFetchScalarToken if true, you will force to fetch a new scalar token + * from the server (only if the base url is whitelisted) + * @param bypassWhitelist if true, the base url will be considered as whitelisted + */ + suspend fun format( + baseUrl: String, + params: Map = emptyMap(), + forceFetchScalarToken: Boolean = false, + bypassWhitelist: Boolean + ): String +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/Widget.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/Widget.kt new file mode 100644 index 0000000000..9da2f224f7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/Widget.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.widgets.model + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.sender.SenderInfo + +data class Widget( + val widgetContent: WidgetContent, + val event: Event, + val widgetId: String, + val senderInfo: SenderInfo?, + val isAddedByMe: Boolean, + val computedUrl: String?, + val type: WidgetType +) { + + val isActive = widgetContent.isActive() + + val name = widgetContent.getHumanName() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetContent.kt new file mode 100644 index 0000000000..1a3d397376 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetContent.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.widgets.model + +import android.annotation.SuppressLint +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.util.JsonDict + +/** + * Ref: https://github.com/matrix-org/matrix-doc/issues/1236 + */ +@JsonClass(generateAdapter = true) +data class WidgetContent( + @Json(name = "creatorUserId") val creatorUserId: String? = null, + @Json(name = "id") val id: String? = null, + @Json(name = "type") val type: String? = null, + @Json(name = "url") val url: String? = null, + @Json(name = "name") val name: String? = null, + @Json(name = "data") val data: JsonDict = emptyMap(), + @Json(name = "waitForIframeLoad") val waitForIframeLoad: Boolean = false +) { + + fun isActive() = type != null && url != null + + @SuppressLint("DefaultLocale") + fun getHumanName(): String { + return (name ?: type ?: "").capitalize() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetType.kt new file mode 100644 index 0000000000..278a123699 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetType.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.session.widgets.model + +private val DEFINED_TYPES by lazy { + listOf( + WidgetType.Jitsi, + WidgetType.TradingView, + WidgetType.Spotify, + WidgetType.Video, + WidgetType.GoogleDoc, + WidgetType.GoogleCalendar, + WidgetType.Etherpad, + WidgetType.StickerPicker, + WidgetType.Grafana, + WidgetType.Custom, + WidgetType.IntegrationManager + ) +} + +/** + * Ref: https://github.com/matrix-org/matrix-doc/issues/1236 + */ +sealed class WidgetType(open val preferred: String, open val legacy: String = preferred) { + object Jitsi : WidgetType("m.jitsi", "jitsi") + object TradingView : WidgetType("m.tradingview") + object Spotify : WidgetType("m.spotify") + object Video : WidgetType("m.video") + object GoogleDoc : WidgetType("m.googledoc") + object GoogleCalendar : WidgetType("m.googlecalendar") + object Etherpad : WidgetType("m.etherpad") + object StickerPicker : WidgetType("m.stickerpicker") + object Grafana : WidgetType("m.grafana") + object Custom : WidgetType("m.custom") + object IntegrationManager : WidgetType("m.integration_manager") + data class Fallback(override val preferred: String) : WidgetType(preferred) + + fun matches(type: String): Boolean { + return type == preferred || type == legacy + } + + fun values(): Set { + return setOf(preferred, legacy) + } + + companion object { + + fun fromString(type: String): WidgetType { + val matchingType = DEFINED_TYPES.firstOrNull { + it.matches(type) + } + return matchingType ?: Fallback(type) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/Cancelable.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/Cancelable.kt new file mode 100644 index 0000000000..b1976f3921 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/Cancelable.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.util + +/** + * An interface defining a unique cancel method. + * It should be used with methods you want to be able to cancel, such as ones interacting with Web Services. + */ +interface Cancelable { + + /** + * The cancel method, it does nothing by default. + */ + fun cancel() { + // no-op + } +} + +object NoOpCancellable : Cancelable diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/CancelableBag.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/CancelableBag.kt new file mode 100644 index 0000000000..bc44e08c02 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/CancelableBag.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.util + +class CancelableBag : Cancelable, MutableList by ArrayList() { + override fun cancel() { + forEach { it.cancel() } + clear() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/ContentUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/ContentUtils.kt new file mode 100644 index 0000000000..a11be96297 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/ContentUtils.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.api.util + +object ContentUtils { + fun extractUsefulTextFromReply(repliedBody: String): String { + val lines = repliedBody.lines() + var wellFormed = repliedBody.startsWith(">") + var endOfPreviousFound = false + val usefullines = ArrayList() + lines.forEach { + if (it == "") { + endOfPreviousFound = true + return@forEach + } + if (!endOfPreviousFound) { + wellFormed = wellFormed && it.startsWith(">") + } else { + usefullines.add(it) + } + } + return usefullines.joinToString("\n").takeIf { wellFormed } ?: repliedBody + } + + fun extractUsefulTextFromHtmlReply(repliedBody: String): String { + if (repliedBody.startsWith("")) { + val closingTagIndex = repliedBody.lastIndexOf("") + if (closingTagIndex != -1) { + return repliedBody.substring(closingTagIndex + "".length).trim() + } + } + return repliedBody + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixCallbackDelegate.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixCallbackDelegate.kt new file mode 100644 index 0000000000..c72ae3d051 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixCallbackDelegate.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.util + +import org.matrix.android.sdk.api.MatrixCallback + +/** + * Simple MatrixCallback implementation which delegate its calls to another callback + */ +open class MatrixCallbackDelegate(private val callback: MatrixCallback) : MatrixCallback by callback diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt new file mode 100644 index 0000000000..3e99ae52b4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt @@ -0,0 +1,160 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.util + +import org.matrix.android.sdk.BuildConfig +import org.matrix.android.sdk.api.session.group.model.GroupSummary +import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoom +import org.matrix.android.sdk.api.session.room.sender.SenderInfo +import org.matrix.android.sdk.api.session.user.model.User +import java.util.Locale + +sealed class MatrixItem( + open val id: String, + open val displayName: String?, + open val avatarUrl: String? +) { + data class UserItem(override val id: String, + override val displayName: String? = null, + override val avatarUrl: String? = null) + : MatrixItem(id, displayName?.removeSuffix(ircPattern), avatarUrl) { + init { + if (BuildConfig.DEBUG) checkId() + } + } + + data class EventItem(override val id: String, + override val displayName: String? = null, + override val avatarUrl: String? = null) + : MatrixItem(id, displayName, avatarUrl) { + init { + if (BuildConfig.DEBUG) checkId() + } + } + + data class RoomItem(override val id: String, + override val displayName: String? = null, + override val avatarUrl: String? = null) + : MatrixItem(id, displayName, avatarUrl) { + init { + if (BuildConfig.DEBUG) checkId() + } + } + + data class RoomAliasItem(override val id: String, + override val displayName: String? = null, + override val avatarUrl: String? = null) + : MatrixItem(id, displayName, avatarUrl) { + init { + if (BuildConfig.DEBUG) checkId() + } + + // Best name is the id, and we keep the displayName of the room for the case we need the first letter + override fun getBestName() = id + } + + data class GroupItem(override val id: String, + override val displayName: String? = null, + override val avatarUrl: String? = null) + : MatrixItem(id, displayName, avatarUrl) { + init { + if (BuildConfig.DEBUG) checkId() + } + + // Best name is the id, and we keep the displayName of the room for the case we need the first letter + override fun getBestName() = id + } + + open fun getBestName(): String { + return displayName?.takeIf { it.isNotBlank() } ?: id + } + + protected fun checkId() { + if (!id.startsWith(getIdPrefix())) { + error("Wrong usage of MatrixItem: check the id $id should start with ${getIdPrefix()}") + } + } + + /** + * Return the prefix as defined in the matrix spec (and not extracted from the id) + */ + fun getIdPrefix() = when (this) { + is UserItem -> '@' + is EventItem -> '$' + is RoomItem -> '!' + is RoomAliasItem -> '#' + is GroupItem -> '+' + } + + fun firstLetterOfDisplayName(): String { + return (displayName?.takeIf { it.isNotBlank() } ?: id) + .let { dn -> + var startIndex = 0 + val initial = dn[startIndex] + + if (initial in listOf('@', '#', '+') && dn.length > 1) { + startIndex++ + } + + var length = 1 + var first = dn[startIndex] + + // LEFT-TO-RIGHT MARK + if (dn.length >= 2 && 0x200e == first.toInt()) { + startIndex++ + first = dn[startIndex] + } + + // check if it’s the start of a surrogate pair + if (first.toInt() in 0xD800..0xDBFF && dn.length > startIndex + 1) { + val second = dn[startIndex + 1] + if (second.toInt() in 0xDC00..0xDFFF) { + length++ + } + } + + dn.substring(startIndex, startIndex + length) + } + .toUpperCase(Locale.ROOT) + } + + companion object { + private const val ircPattern = " (IRC)" + } +} + +/* ========================================================================================== + * Extensions to create MatrixItem + * ========================================================================================== */ + +fun User.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl) + +fun GroupSummary.toMatrixItem() = MatrixItem.GroupItem(groupId, displayName, avatarUrl) + +fun RoomSummary.toMatrixItem() = MatrixItem.RoomItem(roomId, displayName, avatarUrl) + +fun RoomSummary.toRoomAliasMatrixItem() = MatrixItem.RoomAliasItem(canonicalAlias ?: roomId, displayName, avatarUrl) + +// If no name is available, use room alias as Riot-Web does +fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name ?: getPrimaryAlias() ?: "", avatarUrl) + +fun RoomMemberSummary.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl) + +fun SenderInfo.toMatrixItem() = MatrixItem.UserItem(userId, disambiguatedDisplayName, avatarUrl) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/Optional.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/Optional.kt new file mode 100644 index 0000000000..159f7149b9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/Optional.kt @@ -0,0 +1,58 @@ +/* + + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + + */ +package org.matrix.android.sdk.api.util + +data class Optional constructor(private val value: T?) { + + fun get(): T { + return value!! + } + + fun getOrNull(): T? { + return value + } + + fun map(fn: (T) -> U?): Optional { + return if (value == null) { + from(null) + } else { + from(fn(value)) + } + } + + fun getOrElse(fn: () -> T): T { + return value ?: fn() + } + + fun hasValue(): Boolean { + return value != null + } + + companion object { + fun from(value: T?): Optional { + return Optional(value) + } + + fun empty(): Optional { + return Optional(null) + } + } +} + +fun T?.toOptional() = Optional(this) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/Types.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/Types.kt new file mode 100644 index 0000000000..7344dab8d4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/Types.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.api.util + +import com.squareup.moshi.Types +import java.lang.reflect.ParameterizedType + +typealias JsonDict = Map + +val emptyJsonDict = emptyMap() + +internal val JSON_DICT_PARAMETERIZED_TYPE: ParameterizedType = Types.newParameterizedType(Map::class.java, String::class.java, Any::class.java) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/SessionManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/SessionManager.kt new file mode 100644 index 0000000000..24f5558b26 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/SessionManager.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal + +import org.matrix.android.sdk.api.auth.data.SessionParams +import org.matrix.android.sdk.api.auth.data.sessionId +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.internal.auth.SessionParamsStore +import org.matrix.android.sdk.internal.di.MatrixComponent +import org.matrix.android.sdk.internal.di.MatrixScope +import org.matrix.android.sdk.internal.session.DaggerSessionComponent +import org.matrix.android.sdk.internal.session.SessionComponent +import javax.inject.Inject + +@MatrixScope +internal class SessionManager @Inject constructor(private val matrixComponent: MatrixComponent, + private val sessionParamsStore: SessionParamsStore) { + + // SessionId -> SessionComponent + private val sessionComponents = HashMap() + + fun getSessionComponent(sessionId: String): SessionComponent? { + val sessionParams = sessionParamsStore.get(sessionId) ?: return null + return getOrCreateSessionComponent(sessionParams) + } + + fun getOrCreateSession(sessionParams: SessionParams): Session { + return getOrCreateSessionComponent(sessionParams).session() + } + + fun releaseSession(sessionId: String) { + if (sessionComponents.containsKey(sessionId).not()) { + throw RuntimeException("You don't have a session for id $sessionId") + } + sessionComponents.remove(sessionId)?.also { + it.session().close() + } + } + + private fun getOrCreateSessionComponent(sessionParams: SessionParams): SessionComponent { + return sessionComponents.getOrPut(sessionParams.credentials.sessionId()) { + DaggerSessionComponent + .factory() + .create(matrixComponent, sessionParams) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthAPI.kt new file mode 100644 index 0000000000..00eb7e8599 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthAPI.kt @@ -0,0 +1,114 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.auth + +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.internal.auth.data.LoginFlowResponse +import org.matrix.android.sdk.internal.auth.data.PasswordLoginParams +import org.matrix.android.sdk.internal.auth.data.RiotConfig +import org.matrix.android.sdk.internal.auth.data.TokenLoginParams +import org.matrix.android.sdk.internal.auth.login.ResetPasswordMailConfirmed +import org.matrix.android.sdk.internal.auth.registration.AddThreePidRegistrationParams +import org.matrix.android.sdk.internal.auth.registration.AddThreePidRegistrationResponse +import org.matrix.android.sdk.internal.auth.registration.RegistrationParams +import org.matrix.android.sdk.internal.auth.registration.SuccessResult +import org.matrix.android.sdk.internal.auth.registration.ValidationCodeBody +import org.matrix.android.sdk.internal.auth.version.Versions +import org.matrix.android.sdk.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.Headers +import retrofit2.http.POST +import retrofit2.http.Path +import retrofit2.http.Url + +/** + * The login REST API. + */ +internal interface AuthAPI { + + /** + * Get a Riot config file + */ + @GET("config.json") + fun getRiotConfig(): Call + + /** + * Get the version information of the homeserver + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_ + "versions") + fun versions(): Call + + /** + * Register to the homeserver, or get error 401 with a RegistrationFlowResponse object if registration is incomplete + * Ref: https://matrix.org/docs/spec/client_server/latest#account-registration-and-management + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "register") + fun register(@Body registrationParams: RegistrationParams): Call + + /** + * Add 3Pid during registration + * Ref: https://gist.github.com/jryans/839a09bf0c5a70e2f36ed990d50ed928 + * https://github.com/matrix-org/matrix-doc/pull/2290 + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "register/{threePid}/requestToken") + fun add3Pid(@Path("threePid") threePid: String, + @Body params: AddThreePidRegistrationParams): Call + + /** + * Validate 3pid + */ + @POST + fun validate3Pid(@Url url: String, + @Body params: ValidationCodeBody): Call + + /** + * Get the supported login flow + * Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-login + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login") + fun getLoginFlows(): Call + + /** + * Pass params to the server for the current login phase. + * Set all the timeouts to 1 minute + * + * @param loginParams the login parameters + */ + @Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000") + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login") + fun login(@Body loginParams: PasswordLoginParams): Call + + // Unfortunately we cannot use interface for @Body parameter, so I duplicate the method for the type TokenLoginParams + @Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000") + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login") + fun login(@Body loginParams: TokenLoginParams): Call + + /** + * Ask the homeserver to reset the password associated with the provided email. + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/password/email/requestToken") + fun resetPassword(@Body params: AddThreePidRegistrationParams): Call + + /** + * Ask the homeserver to reset the password with the provided new password once the email is validated. + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/password") + fun resetPasswordMailConfirmed(@Body params: ResetPasswordMailConfirmed): Call +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthModule.kt new file mode 100644 index 0000000000..229baac052 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthModule.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.auth + +import android.content.Context +import dagger.Binds +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.api.auth.AuthenticationService +import org.matrix.android.sdk.api.legacy.LegacySessionImporter +import org.matrix.android.sdk.internal.auth.db.AuthRealmMigration +import org.matrix.android.sdk.internal.auth.db.AuthRealmModule +import org.matrix.android.sdk.internal.auth.db.RealmPendingSessionStore +import org.matrix.android.sdk.internal.auth.db.RealmSessionParamsStore +import org.matrix.android.sdk.internal.auth.login.DefaultDirectLoginTask +import org.matrix.android.sdk.internal.auth.login.DirectLoginTask +import org.matrix.android.sdk.internal.database.RealmKeysUtils +import org.matrix.android.sdk.internal.di.AuthDatabase +import org.matrix.android.sdk.internal.legacy.DefaultLegacySessionImporter +import org.matrix.android.sdk.internal.wellknown.WellknownModule +import io.realm.RealmConfiguration +import java.io.File + +@Module(includes = [WellknownModule::class]) +internal abstract class AuthModule { + + @Module + companion object { + private const val DB_ALIAS = "matrix-sdk-auth" + + @JvmStatic + @Provides + @AuthDatabase + fun providesRealmConfiguration(context: Context, realmKeysUtils: RealmKeysUtils): RealmConfiguration { + val old = File(context.filesDir, "matrix-sdk-auth") + if (old.exists()) { + old.renameTo(File(context.filesDir, "matrix-sdk-auth.realm")) + } + + return RealmConfiguration.Builder() + .apply { + realmKeysUtils.configureEncryption(this, DB_ALIAS) + } + .name("matrix-sdk-auth.realm") + .modules(AuthRealmModule()) + .schemaVersion(AuthRealmMigration.SCHEMA_VERSION) + .migration(AuthRealmMigration) + .build() + } + } + + @Binds + abstract fun bindLegacySessionImporter(importer: DefaultLegacySessionImporter): LegacySessionImporter + + @Binds + abstract fun bindSessionParamsStore(store: RealmSessionParamsStore): SessionParamsStore + + @Binds + abstract fun bindPendingSessionStore(store: RealmPendingSessionStore): PendingSessionStore + + @Binds + abstract fun bindAuthenticationService(service: DefaultAuthenticationService): AuthenticationService + + @Binds + abstract fun bindSessionCreator(creator: DefaultSessionCreator): SessionCreator + + @Binds + abstract fun bindDirectLoginTask(task: DefaultDirectLoginTask): DirectLoginTask +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt new file mode 100644 index 0000000000..1294855b6e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt @@ -0,0 +1,369 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.auth + +import android.net.Uri +import dagger.Lazy +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.auth.AuthenticationService +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import org.matrix.android.sdk.api.auth.data.LoginFlowResult +import org.matrix.android.sdk.api.auth.login.LoginWizard +import org.matrix.android.sdk.api.auth.registration.RegistrationWizard +import org.matrix.android.sdk.api.auth.wellknown.WellknownResult +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.NoOpCancellable +import org.matrix.android.sdk.internal.SessionManager +import org.matrix.android.sdk.internal.auth.data.LoginFlowResponse +import org.matrix.android.sdk.internal.auth.data.RiotConfig +import org.matrix.android.sdk.internal.auth.db.PendingSessionData +import org.matrix.android.sdk.internal.auth.login.DefaultLoginWizard +import org.matrix.android.sdk.internal.auth.login.DirectLoginTask +import org.matrix.android.sdk.internal.auth.registration.DefaultRegistrationWizard +import org.matrix.android.sdk.internal.auth.version.Versions +import org.matrix.android.sdk.internal.auth.version.isLoginAndRegistrationSupportedBySdk +import org.matrix.android.sdk.internal.auth.version.isSupportedBySdk +import org.matrix.android.sdk.internal.di.Unauthenticated +import org.matrix.android.sdk.internal.network.RetrofitFactory +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.network.httpclient.addSocketFactory +import org.matrix.android.sdk.internal.network.ssl.UnrecognizedCertificateException +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import org.matrix.android.sdk.internal.task.launchToCallback +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import org.matrix.android.sdk.internal.util.exhaustive +import org.matrix.android.sdk.internal.util.toCancelable +import org.matrix.android.sdk.internal.wellknown.GetWellknownTask +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import javax.inject.Inject +import javax.net.ssl.HttpsURLConnection + +internal class DefaultAuthenticationService @Inject constructor( + @Unauthenticated + private val okHttpClient: Lazy, + private val retrofitFactory: RetrofitFactory, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val sessionParamsStore: SessionParamsStore, + private val sessionManager: SessionManager, + private val sessionCreator: SessionCreator, + private val pendingSessionStore: PendingSessionStore, + private val getWellknownTask: GetWellknownTask, + private val directLoginTask: DirectLoginTask, + private val taskExecutor: TaskExecutor +) : AuthenticationService { + + private var pendingSessionData: PendingSessionData? = pendingSessionStore.getPendingSessionData() + + private var currentLoginWizard: LoginWizard? = null + private var currentRegistrationWizard: RegistrationWizard? = null + + override fun hasAuthenticatedSessions(): Boolean { + return sessionParamsStore.getLast() != null + } + + override fun getLastAuthenticatedSession(): Session? { + val sessionParams = sessionParamsStore.getLast() + return sessionParams?.let { + sessionManager.getOrCreateSession(it) + } + } + + override fun getLoginFlowOfSession(sessionId: String, callback: MatrixCallback): Cancelable { + val homeServerConnectionConfig = sessionParamsStore.get(sessionId)?.homeServerConnectionConfig + + return if (homeServerConnectionConfig == null) { + callback.onFailure(IllegalStateException("Session not found")) + NoOpCancellable + } else { + getLoginFlow(homeServerConnectionConfig, callback) + } + } + + override fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback): Cancelable { + pendingSessionData = null + + return taskExecutor.executorScope.launch(coroutineDispatchers.main) { + pendingSessionStore.delete() + + val result = runCatching { + getLoginFlowInternal(homeServerConnectionConfig) + } + result.fold( + { + if (it is LoginFlowResult.Success) { + // The homeserver exists and up to date, keep the config + // Homeserver url may have been changed, if it was a Riot url + val alteredHomeServerConnectionConfig = homeServerConnectionConfig.copy( + homeServerUri = Uri.parse(it.homeServerUrl) + ) + + pendingSessionData = PendingSessionData(alteredHomeServerConnectionConfig) + .also { data -> pendingSessionStore.savePendingSessionData(data) } + } + callback.onSuccess(it) + }, + { + if (it is UnrecognizedCertificateException) { + callback.onFailure(Failure.UnrecognizedCertificateFailure(homeServerConnectionConfig.homeServerUri.toString(), it.fingerprint)) + } else { + callback.onFailure(it) + } + } + ) + } + .toCancelable() + } + + private suspend fun getLoginFlowInternal(homeServerConnectionConfig: HomeServerConnectionConfig): LoginFlowResult { + return withContext(coroutineDispatchers.io) { + val authAPI = buildAuthAPI(homeServerConnectionConfig) + + // First check the homeserver version + runCatching { + executeRequest(null) { + apiCall = authAPI.versions() + } + } + .map { versions -> + // Ok, it seems that the homeserver url is valid + getLoginFlowResult(authAPI, versions, homeServerConnectionConfig.homeServerUri.toString()) + } + .fold( + { + it + }, + { + if (it is Failure.OtherServerError + && it.httpCode == HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) { + // It's maybe a Riot url? + getRiotLoginFlowInternal(homeServerConnectionConfig) + } else { + throw it + } + } + ) + } + } + + private suspend fun getRiotLoginFlowInternal(homeServerConnectionConfig: HomeServerConnectionConfig): LoginFlowResult { + val authAPI = buildAuthAPI(homeServerConnectionConfig) + + // Ok, try to get the config.json file of a RiotWeb client + return runCatching { + executeRequest(null) { + apiCall = authAPI.getRiotConfig() + } + } + .map { riotConfig -> + if (riotConfig.defaultHomeServerUrl?.isNotBlank() == true) { + // Ok, good sign, we got a default hs url + val newHomeServerConnectionConfig = homeServerConnectionConfig.copy( + homeServerUri = Uri.parse(riotConfig.defaultHomeServerUrl) + ) + + val newAuthAPI = buildAuthAPI(newHomeServerConnectionConfig) + + val versions = executeRequest(null) { + apiCall = newAuthAPI.versions() + } + + getLoginFlowResult(newAuthAPI, versions, riotConfig.defaultHomeServerUrl) + } else { + // Config exists, but there is no default homeserver url (ex: https://riot.im/app) + throw Failure.OtherServerError("", HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) + } + } + .fold( + { + it + }, + { + if (it is Failure.OtherServerError + && it.httpCode == HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) { + // Try with wellknown + getWellknownLoginFlowInternal(homeServerConnectionConfig) + } else { + throw it + } + } + ) + } + + private suspend fun getWellknownLoginFlowInternal(homeServerConnectionConfig: HomeServerConnectionConfig): LoginFlowResult { + val domain = homeServerConnectionConfig.homeServerUri.host + ?: throw Failure.OtherServerError("", HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) + + // Create a fake userId, for the getWellknown task + val fakeUserId = "@alice:$domain" + val wellknownResult = getWellknownTask.execute(GetWellknownTask.Params(fakeUserId, homeServerConnectionConfig)) + + return when (wellknownResult) { + is WellknownResult.Prompt -> { + val newHomeServerConnectionConfig = homeServerConnectionConfig.copy( + homeServerUri = Uri.parse(wellknownResult.homeServerUrl), + identityServerUri = wellknownResult.identityServerUrl?.let { Uri.parse(it) } + ) + + val newAuthAPI = buildAuthAPI(newHomeServerConnectionConfig) + + val versions = executeRequest(null) { + apiCall = newAuthAPI.versions() + } + + getLoginFlowResult(newAuthAPI, versions, wellknownResult.homeServerUrl) + } + else -> throw Failure.OtherServerError("", HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) + }.exhaustive + } + + private suspend fun getLoginFlowResult(authAPI: AuthAPI, versions: Versions, homeServerUrl: String): LoginFlowResult { + return if (versions.isSupportedBySdk()) { + // Get the login flow + val loginFlowResponse = executeRequest(null) { + apiCall = authAPI.getLoginFlows() + } + LoginFlowResult.Success(loginFlowResponse.flows.orEmpty().mapNotNull { it.type }, versions.isLoginAndRegistrationSupportedBySdk(), homeServerUrl) + } else { + // Not supported + LoginFlowResult.OutdatedHomeserver + } + } + + override fun getRegistrationWizard(): RegistrationWizard { + return currentRegistrationWizard + ?: let { + pendingSessionData?.homeServerConnectionConfig?.let { + DefaultRegistrationWizard( + buildClient(it), + retrofitFactory, + coroutineDispatchers, + sessionCreator, + pendingSessionStore, + taskExecutor.executorScope + ).also { + currentRegistrationWizard = it + } + } ?: error("Please call getLoginFlow() with success first") + } + } + + override val isRegistrationStarted: Boolean + get() = currentRegistrationWizard?.isRegistrationStarted == true + + override fun getLoginWizard(): LoginWizard { + return currentLoginWizard + ?: let { + pendingSessionData?.homeServerConnectionConfig?.let { + DefaultLoginWizard( + buildClient(it), + retrofitFactory, + coroutineDispatchers, + sessionCreator, + pendingSessionStore, + taskExecutor.executorScope + ).also { + currentLoginWizard = it + } + } ?: error("Please call getLoginFlow() with success first") + } + } + + override fun cancelPendingLoginOrRegistration() { + currentLoginWizard = null + currentRegistrationWizard = null + + // Keep only the home sever config + // Update the local pendingSessionData synchronously + pendingSessionData = pendingSessionData?.homeServerConnectionConfig + ?.let { PendingSessionData(it) } + .also { + taskExecutor.executorScope.launch(coroutineDispatchers.main) { + if (it == null) { + // Should not happen + pendingSessionStore.delete() + } else { + pendingSessionStore.savePendingSessionData(it) + } + } + } + } + + override fun reset() { + currentLoginWizard = null + currentRegistrationWizard = null + + pendingSessionData = null + + taskExecutor.executorScope.launch(coroutineDispatchers.main) { + pendingSessionStore.delete() + } + } + + override fun createSessionFromSso(homeServerConnectionConfig: HomeServerConnectionConfig, + credentials: Credentials, + callback: MatrixCallback): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + createSessionFromSso(credentials, homeServerConnectionConfig) + } + } + + override fun getWellKnownData(matrixId: String, + homeServerConnectionConfig: HomeServerConnectionConfig?, + callback: MatrixCallback): Cancelable { + return getWellknownTask + .configureWith(GetWellknownTask.Params(matrixId, homeServerConnectionConfig)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun directAuthentication(homeServerConnectionConfig: HomeServerConnectionConfig, + matrixId: String, + password: String, + initialDeviceName: String, + callback: MatrixCallback): Cancelable { + return directLoginTask + .configureWith(DirectLoginTask.Params(homeServerConnectionConfig, matrixId, password, initialDeviceName)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + private suspend fun createSessionFromSso(credentials: Credentials, + homeServerConnectionConfig: HomeServerConnectionConfig): Session = withContext(coroutineDispatchers.computation) { + sessionCreator.createSession(credentials, homeServerConnectionConfig) + } + + private fun buildAuthAPI(homeServerConnectionConfig: HomeServerConnectionConfig): AuthAPI { + val retrofit = retrofitFactory.create(buildClient(homeServerConnectionConfig), homeServerConnectionConfig.homeServerUri.toString()) + return retrofit.create(AuthAPI::class.java) + } + + private fun buildClient(homeServerConnectionConfig: HomeServerConnectionConfig): OkHttpClient { + return okHttpClient.get() + .newBuilder() + .addSocketFactory(homeServerConnectionConfig) + .build() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/PendingSessionStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/PendingSessionStore.kt new file mode 100644 index 0000000000..3b1c61e272 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/PendingSessionStore.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.auth + +import org.matrix.android.sdk.internal.auth.db.PendingSessionData + +/** + * Store for elements when doing login or registration + */ +internal interface PendingSessionStore { + + suspend fun savePendingSessionData(pendingSessionData: PendingSessionData) + + fun getPendingSessionData(): PendingSessionData? + + suspend fun delete() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/SessionCreator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/SessionCreator.kt new file mode 100644 index 0000000000..a44cda5b57 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/SessionCreator.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.auth + +import android.net.Uri +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import org.matrix.android.sdk.api.auth.data.SessionParams +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.internal.SessionManager +import timber.log.Timber +import javax.inject.Inject + +internal interface SessionCreator { + suspend fun createSession(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig): Session +} + +internal class DefaultSessionCreator @Inject constructor( + private val sessionParamsStore: SessionParamsStore, + private val sessionManager: SessionManager, + private val pendingSessionStore: PendingSessionStore +) : SessionCreator { + + /** + * Credentials can affect the homeServerConnectionConfig, override home server url and/or + * identity server url if provided in the credentials + */ + override suspend fun createSession(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig): Session { + // We can cleanup the pending session params + pendingSessionStore.delete() + + val sessionParams = SessionParams( + credentials = credentials, + homeServerConnectionConfig = homeServerConnectionConfig.copy( + homeServerUri = credentials.discoveryInformation?.homeServer?.baseURL + // remove trailing "/" + ?.trim { it == '/' } + ?.takeIf { it.isNotBlank() } + ?.also { Timber.d("Overriding homeserver url to $it") } + ?.let { Uri.parse(it) } + ?: homeServerConnectionConfig.homeServerUri, + identityServerUri = credentials.discoveryInformation?.identityServer?.baseURL + // remove trailing "/" + ?.trim { it == '/' } + ?.takeIf { it.isNotBlank() } + ?.also { Timber.d("Overriding identity server url to $it") } + ?.let { Uri.parse(it) } + ?: homeServerConnectionConfig.identityServerUri + ), + isTokenValid = true) + + sessionParamsStore.save(sessionParams) + return sessionManager.getOrCreateSession(sessionParams) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/SessionParamsStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/SessionParamsStore.kt new file mode 100644 index 0000000000..eb038ecffb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/SessionParamsStore.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.auth + +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.auth.data.SessionParams + +internal interface SessionParamsStore { + + fun get(sessionId: String): SessionParams? + + fun getLast(): SessionParams? + + fun getAll(): List + + suspend fun save(sessionParams: SessionParams) + + suspend fun setTokenInvalid(sessionId: String) + + suspend fun updateCredentials(newCredentials: Credentials) + + suspend fun delete(sessionId: String) + + suspend fun deleteAll() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/InteractiveAuthenticationFlow.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/InteractiveAuthenticationFlow.kt new file mode 100644 index 0000000000..7a631a5677 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/InteractiveAuthenticationFlow.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.auth.data + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * An interactive authentication flow. + */ +@JsonClass(generateAdapter = true) +data class InteractiveAuthenticationFlow( + + @Json(name = "type") + val type: String? = null, + + @Json(name = "stages") + val stages: List? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt new file mode 100644 index 0000000000..9fb7eb5f3a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.auth.data + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class LoginFlowResponse( + /** + * The homeserver's supported login types + */ + @Json(name = "flows") + val flows: List? +) + +@JsonClass(generateAdapter = true) +internal data class LoginFlow( + /** + * The login type. This is supplied as the type when logging in. + */ + @Json(name = "type") + val type: String? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginParams.kt new file mode 100644 index 0000000000..fc7206779e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginParams.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.auth.data + +internal interface LoginParams { + val type: String +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/PasswordLoginParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/PasswordLoginParams.kt new file mode 100644 index 0000000000..60eebea57d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/PasswordLoginParams.kt @@ -0,0 +1,100 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.auth.data + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes + +/** + * Ref: + * - https://matrix.org/docs/spec/client_server/r0.5.0#password-based + * - https://matrix.org/docs/spec/client_server/r0.5.0#identifier-types + */ +@JsonClass(generateAdapter = true) +internal data class PasswordLoginParams( + @Json(name = "identifier") val identifier: Map, + @Json(name = "password") val password: String, + @Json(name = "type") override val type: String, + @Json(name = "initial_device_display_name") val deviceDisplayName: String?, + @Json(name = "device_id") val deviceId: String?) : LoginParams { + + companion object { + private const val IDENTIFIER_KEY_TYPE = "type" + + private const val IDENTIFIER_KEY_TYPE_USER = "m.id.user" + private const val IDENTIFIER_KEY_USER = "user" + + private const val IDENTIFIER_KEY_TYPE_THIRD_PARTY = "m.id.thirdparty" + private const val IDENTIFIER_KEY_MEDIUM = "medium" + private const val IDENTIFIER_KEY_ADDRESS = "address" + + private const val IDENTIFIER_KEY_TYPE_PHONE = "m.id.phone" + private const val IDENTIFIER_KEY_COUNTRY = "country" + private const val IDENTIFIER_KEY_PHONE = "phone" + + fun userIdentifier(user: String, + password: String, + deviceDisplayName: String? = null, + deviceId: String? = null): PasswordLoginParams { + return PasswordLoginParams( + mapOf( + IDENTIFIER_KEY_TYPE to IDENTIFIER_KEY_TYPE_USER, + IDENTIFIER_KEY_USER to user + ), + password, + LoginFlowTypes.PASSWORD, + deviceDisplayName, + deviceId) + } + + fun thirdPartyIdentifier(medium: String, + address: String, + password: String, + deviceDisplayName: String? = null, + deviceId: String? = null): PasswordLoginParams { + return PasswordLoginParams( + mapOf( + IDENTIFIER_KEY_TYPE to IDENTIFIER_KEY_TYPE_THIRD_PARTY, + IDENTIFIER_KEY_MEDIUM to medium, + IDENTIFIER_KEY_ADDRESS to address + ), + password, + LoginFlowTypes.PASSWORD, + deviceDisplayName, + deviceId) + } + + fun phoneIdentifier(country: String, + phone: String, + password: String, + deviceDisplayName: String? = null, + deviceId: String? = null): PasswordLoginParams { + return PasswordLoginParams( + mapOf( + IDENTIFIER_KEY_TYPE to IDENTIFIER_KEY_TYPE_PHONE, + IDENTIFIER_KEY_COUNTRY to country, + IDENTIFIER_KEY_PHONE to phone + ), + password, + LoginFlowTypes.PASSWORD, + deviceDisplayName, + deviceId) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/RiotConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/RiotConfig.kt new file mode 100644 index 0000000000..42db315262 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/RiotConfig.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.auth.data + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class RiotConfig( + // There are plenty of other elements in the file config.json of a RiotWeb client, but for the moment only one is interesting + // Ex: "brand", "branding", etc. + @Json(name = "default_hs_url") + val defaultHomeServerUrl: String? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/ThreePidMedium.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/ThreePidMedium.kt new file mode 100644 index 0000000000..d47eca8c9f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/ThreePidMedium.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.auth.data + +internal object ThreePidMedium { + const val EMAIL = "email" + const val MSISDN = "msisdn" +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/TokenLoginParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/TokenLoginParams.kt new file mode 100644 index 0000000000..3d9f58f048 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/TokenLoginParams.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.auth.data + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes + +@JsonClass(generateAdapter = true) +internal data class TokenLoginParams( + @Json(name = "type") override val type: String = LoginFlowTypes.TOKEN, + @Json(name = "token") val token: String +) : LoginParams diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/AuthRealmMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/AuthRealmMigration.kt new file mode 100644 index 0000000000..88e2804798 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/AuthRealmMigration.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.auth.db + +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.auth.data.sessionId +import org.matrix.android.sdk.internal.di.MoshiProvider +import io.realm.DynamicRealm +import io.realm.RealmMigration +import timber.log.Timber + +internal object AuthRealmMigration : RealmMigration { + + // Current schema version + const val SCHEMA_VERSION = 3L + + override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { + Timber.d("Migrating Auth Realm from $oldVersion to $newVersion") + + if (oldVersion <= 0) migrateTo1(realm) + if (oldVersion <= 1) migrateTo2(realm) + if (oldVersion <= 2) migrateTo3(realm) + } + + private fun migrateTo1(realm: DynamicRealm) { + Timber.d("Step 0 -> 1") + Timber.d("Create PendingSessionEntity") + + realm.schema.create("PendingSessionEntity") + .addField(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, String::class.java) + .setRequired(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, true) + .addField(PendingSessionEntityFields.CLIENT_SECRET, String::class.java) + .setRequired(PendingSessionEntityFields.CLIENT_SECRET, true) + .addField(PendingSessionEntityFields.SEND_ATTEMPT, Integer::class.java) + .setRequired(PendingSessionEntityFields.SEND_ATTEMPT, true) + .addField(PendingSessionEntityFields.RESET_PASSWORD_DATA_JSON, String::class.java) + .addField(PendingSessionEntityFields.CURRENT_SESSION, String::class.java) + .addField(PendingSessionEntityFields.IS_REGISTRATION_STARTED, Boolean::class.java) + .addField(PendingSessionEntityFields.CURRENT_THREE_PID_DATA_JSON, String::class.java) + } + + private fun migrateTo2(realm: DynamicRealm) { + Timber.d("Step 1 -> 2") + Timber.d("Add boolean isTokenValid in SessionParamsEntity, with value true") + + realm.schema.get("SessionParamsEntity") + ?.addField(SessionParamsEntityFields.IS_TOKEN_VALID, Boolean::class.java) + ?.transform { it.set(SessionParamsEntityFields.IS_TOKEN_VALID, true) } + } + + private fun migrateTo3(realm: DynamicRealm) { + Timber.d("Step 2 -> 3") + Timber.d("Update SessionParamsEntity primary key, to allow several sessions with the same userId") + + realm.schema.get("SessionParamsEntity") + ?.removePrimaryKey() + ?.addField(SessionParamsEntityFields.SESSION_ID, String::class.java) + ?.setRequired(SessionParamsEntityFields.SESSION_ID, true) + ?.transform { + val credentialsJson = it.getString(SessionParamsEntityFields.CREDENTIALS_JSON) + + val credentials = MoshiProvider.providesMoshi() + .adapter(Credentials::class.java) + .fromJson(credentialsJson) + + it.set(SessionParamsEntityFields.SESSION_ID, credentials!!.sessionId()) + } + ?.addPrimaryKey(SessionParamsEntityFields.SESSION_ID) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/AuthRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/AuthRealmModule.kt new file mode 100644 index 0000000000..282d0df75d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/AuthRealmModule.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.auth.db + +import io.realm.annotations.RealmModule + +/** + * Realm module for authentication classes + */ +@RealmModule(library = true, + classes = [ + SessionParamsEntity::class, + PendingSessionEntity::class + ]) +internal class AuthRealmModule diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/PendingSessionData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/PendingSessionData.kt new file mode 100644 index 0000000000..ad51f63ee8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/PendingSessionData.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.auth.db + +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import org.matrix.android.sdk.internal.auth.login.ResetPasswordData +import org.matrix.android.sdk.internal.auth.registration.ThreePidData +import java.util.UUID + +/** + * This class holds all pending data when creating a session, either by login or by register + */ +internal data class PendingSessionData( + val homeServerConnectionConfig: HomeServerConnectionConfig, + + /* ========================================================================================== + * Common + * ========================================================================================== */ + + val clientSecret: String = UUID.randomUUID().toString(), + val sendAttempt: Int = 0, + + /* ========================================================================================== + * For login + * ========================================================================================== */ + + val resetPasswordData: ResetPasswordData? = null, + + /* ========================================================================================== + * For register + * ========================================================================================== */ + + val currentSession: String? = null, + val isRegistrationStarted: Boolean = false, + val currentThreePidData: ThreePidData? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/PendingSessionEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/PendingSessionEntity.kt new file mode 100644 index 0000000000..2ee342d02c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/PendingSessionEntity.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.auth.db + +import io.realm.RealmObject + +internal open class PendingSessionEntity( + var homeServerConnectionConfigJson: String = "", + var clientSecret: String = "", + var sendAttempt: Int = 0, + var resetPasswordDataJson: String? = null, + var currentSession: String? = null, + var isRegistrationStarted: Boolean = false, + var currentThreePidDataJson: String? = null +) : RealmObject() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/PendingSessionMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/PendingSessionMapper.kt new file mode 100644 index 0000000000..d357221f82 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/PendingSessionMapper.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.auth.db + +import com.squareup.moshi.Moshi +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import org.matrix.android.sdk.internal.auth.login.ResetPasswordData +import org.matrix.android.sdk.internal.auth.registration.ThreePidData +import javax.inject.Inject + +internal class PendingSessionMapper @Inject constructor(moshi: Moshi) { + + private val homeServerConnectionConfigAdapter = moshi.adapter(HomeServerConnectionConfig::class.java) + private val resetPasswordDataAdapter = moshi.adapter(ResetPasswordData::class.java) + private val threePidDataAdapter = moshi.adapter(ThreePidData::class.java) + + fun map(entity: PendingSessionEntity?): PendingSessionData? { + if (entity == null) { + return null + } + + val homeServerConnectionConfig = homeServerConnectionConfigAdapter.fromJson(entity.homeServerConnectionConfigJson)!! + val resetPasswordData = entity.resetPasswordDataJson?.let { resetPasswordDataAdapter.fromJson(it) } + val threePidData = entity.currentThreePidDataJson?.let { threePidDataAdapter.fromJson(it) } + + return PendingSessionData( + homeServerConnectionConfig = homeServerConnectionConfig, + clientSecret = entity.clientSecret, + sendAttempt = entity.sendAttempt, + resetPasswordData = resetPasswordData, + currentSession = entity.currentSession, + isRegistrationStarted = entity.isRegistrationStarted, + currentThreePidData = threePidData) + } + + fun map(sessionData: PendingSessionData?): PendingSessionEntity? { + if (sessionData == null) { + return null + } + + val homeServerConnectionConfigJson = homeServerConnectionConfigAdapter.toJson(sessionData.homeServerConnectionConfig) + val resetPasswordDataJson = resetPasswordDataAdapter.toJson(sessionData.resetPasswordData) + val currentThreePidDataJson = threePidDataAdapter.toJson(sessionData.currentThreePidData) + + return PendingSessionEntity( + homeServerConnectionConfigJson = homeServerConnectionConfigJson, + clientSecret = sessionData.clientSecret, + sendAttempt = sessionData.sendAttempt, + resetPasswordDataJson = resetPasswordDataJson, + currentSession = sessionData.currentSession, + isRegistrationStarted = sessionData.isRegistrationStarted, + currentThreePidDataJson = currentThreePidDataJson + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/RealmPendingSessionStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/RealmPendingSessionStore.kt new file mode 100644 index 0000000000..41851fc2c6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/RealmPendingSessionStore.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.auth.db + +import org.matrix.android.sdk.internal.auth.PendingSessionStore +import org.matrix.android.sdk.internal.database.awaitTransaction +import org.matrix.android.sdk.internal.di.AuthDatabase +import io.realm.Realm +import io.realm.RealmConfiguration +import javax.inject.Inject + +internal class RealmPendingSessionStore @Inject constructor(private val mapper: PendingSessionMapper, + @AuthDatabase + private val realmConfiguration: RealmConfiguration +) : PendingSessionStore { + + override suspend fun savePendingSessionData(pendingSessionData: PendingSessionData) { + awaitTransaction(realmConfiguration) { realm -> + val entity = mapper.map(pendingSessionData) + if (entity != null) { + realm.where(PendingSessionEntity::class.java) + .findAll() + .deleteAllFromRealm() + + realm.insert(entity) + } + } + } + + override fun getPendingSessionData(): PendingSessionData? { + return Realm.getInstance(realmConfiguration).use { realm -> + realm + .where(PendingSessionEntity::class.java) + .findAll() + .map { mapper.map(it) } + .firstOrNull() + } + } + + override suspend fun delete() { + awaitTransaction(realmConfiguration) { + it.where(PendingSessionEntity::class.java) + .findAll() + .deleteAllFromRealm() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/RealmSessionParamsStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/RealmSessionParamsStore.kt new file mode 100644 index 0000000000..57f1c23e99 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/RealmSessionParamsStore.kt @@ -0,0 +1,144 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.auth.db + +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.auth.data.SessionParams +import org.matrix.android.sdk.api.auth.data.sessionId +import org.matrix.android.sdk.internal.auth.SessionParamsStore +import org.matrix.android.sdk.internal.database.awaitTransaction +import org.matrix.android.sdk.internal.di.AuthDatabase +import io.realm.Realm +import io.realm.RealmConfiguration +import io.realm.exceptions.RealmPrimaryKeyConstraintException +import timber.log.Timber +import javax.inject.Inject + +internal class RealmSessionParamsStore @Inject constructor(private val mapper: SessionParamsMapper, + @AuthDatabase + private val realmConfiguration: RealmConfiguration +) : SessionParamsStore { + + override fun getLast(): SessionParams? { + return Realm.getInstance(realmConfiguration).use { realm -> + realm + .where(SessionParamsEntity::class.java) + .findAll() + .map { mapper.map(it) } + .lastOrNull() + } + } + + override fun get(sessionId: String): SessionParams? { + return Realm.getInstance(realmConfiguration).use { realm -> + realm + .where(SessionParamsEntity::class.java) + .equalTo(SessionParamsEntityFields.SESSION_ID, sessionId) + .findAll() + .map { mapper.map(it) } + .firstOrNull() + } + } + + override fun getAll(): List { + return Realm.getInstance(realmConfiguration).use { realm -> + realm + .where(SessionParamsEntity::class.java) + .findAll() + .mapNotNull { mapper.map(it) } + } + } + + override suspend fun save(sessionParams: SessionParams) { + awaitTransaction(realmConfiguration) { + val entity = mapper.map(sessionParams) + if (entity != null) { + try { + it.insert(entity) + } catch (e: RealmPrimaryKeyConstraintException) { + Timber.e(e, "Something wrong happened during previous session creation. Override with new credentials") + it.insertOrUpdate(entity) + } + } + } + } + + override suspend fun setTokenInvalid(sessionId: String) { + awaitTransaction(realmConfiguration) { realm -> + val currentSessionParams = realm + .where(SessionParamsEntity::class.java) + .equalTo(SessionParamsEntityFields.SESSION_ID, sessionId) + .findAll() + .firstOrNull() + + if (currentSessionParams == null) { + // Should not happen + "Session param not found for id $sessionId" + .let { Timber.w(it) } + .also { error(it) } + } else { + currentSessionParams.isTokenValid = false + } + } + } + + override suspend fun updateCredentials(newCredentials: Credentials) { + awaitTransaction(realmConfiguration) { realm -> + val currentSessionParams = realm + .where(SessionParamsEntity::class.java) + .equalTo(SessionParamsEntityFields.SESSION_ID, newCredentials.sessionId()) + .findAll() + .map { mapper.map(it) } + .firstOrNull() + + if (currentSessionParams == null) { + // Should not happen + "Session param not found for id ${newCredentials.sessionId()}" + .let { Timber.w(it) } + .also { error(it) } + } else { + val newSessionParams = currentSessionParams.copy( + credentials = newCredentials, + isTokenValid = true + ) + + val entity = mapper.map(newSessionParams) + if (entity != null) { + realm.insertOrUpdate(entity) + } + } + } + } + + override suspend fun delete(sessionId: String) { + awaitTransaction(realmConfiguration) { + it.where(SessionParamsEntity::class.java) + .equalTo(SessionParamsEntityFields.SESSION_ID, sessionId) + .findAll() + .deleteAllFromRealm() + } + } + + override suspend fun deleteAll() { + awaitTransaction(realmConfiguration) { + it.where(SessionParamsEntity::class.java) + .findAll() + .deleteAllFromRealm() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/SessionParamsEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/SessionParamsEntity.kt new file mode 100644 index 0000000000..81202d2f52 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/SessionParamsEntity.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.auth.db + +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class SessionParamsEntity( + @PrimaryKey var sessionId: String = "", + var userId: String = "", + var credentialsJson: String = "", + var homeServerConnectionConfigJson: String = "", + // Set to false when the token is invalid and the user has been soft logged out + // In case of hard logout, this object is deleted from DB + var isTokenValid: Boolean = true +) : RealmObject() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/SessionParamsMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/SessionParamsMapper.kt new file mode 100644 index 0000000000..78324b6916 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/SessionParamsMapper.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.auth.db + +import com.squareup.moshi.Moshi +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import org.matrix.android.sdk.api.auth.data.SessionParams +import org.matrix.android.sdk.api.auth.data.sessionId +import javax.inject.Inject + +internal class SessionParamsMapper @Inject constructor(moshi: Moshi) { + + private val credentialsAdapter = moshi.adapter(Credentials::class.java) + private val homeServerConnectionConfigAdapter = moshi.adapter(HomeServerConnectionConfig::class.java) + + fun map(entity: SessionParamsEntity?): SessionParams? { + if (entity == null) { + return null + } + val credentials = credentialsAdapter.fromJson(entity.credentialsJson) + val homeServerConnectionConfig = homeServerConnectionConfigAdapter.fromJson(entity.homeServerConnectionConfigJson) + if (credentials == null || homeServerConnectionConfig == null) { + return null + } + return SessionParams(credentials, homeServerConnectionConfig, entity.isTokenValid) + } + + fun map(sessionParams: SessionParams?): SessionParamsEntity? { + if (sessionParams == null) { + return null + } + val credentialsJson = credentialsAdapter.toJson(sessionParams.credentials) + val homeServerConnectionConfigJson = homeServerConnectionConfigAdapter.toJson(sessionParams.homeServerConnectionConfig) + if (credentialsJson == null || homeServerConnectionConfigJson == null) { + return null + } + return SessionParamsEntity( + sessionParams.credentials.sessionId(), + sessionParams.userId, + credentialsJson, + homeServerConnectionConfigJson, + sessionParams.isTokenValid) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt new file mode 100644 index 0000000000..71b8f64069 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt @@ -0,0 +1,148 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.auth.login + +import android.util.Patterns +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.auth.login.LoginWizard +import org.matrix.android.sdk.api.auth.registration.RegisterThreePid +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.NoOpCancellable +import org.matrix.android.sdk.internal.auth.AuthAPI +import org.matrix.android.sdk.internal.auth.PendingSessionStore +import org.matrix.android.sdk.internal.auth.SessionCreator +import org.matrix.android.sdk.internal.auth.data.PasswordLoginParams +import org.matrix.android.sdk.internal.auth.data.ThreePidMedium +import org.matrix.android.sdk.internal.auth.data.TokenLoginParams +import org.matrix.android.sdk.internal.auth.db.PendingSessionData +import org.matrix.android.sdk.internal.auth.registration.AddThreePidRegistrationParams +import org.matrix.android.sdk.internal.auth.registration.AddThreePidRegistrationResponse +import org.matrix.android.sdk.internal.auth.registration.RegisterAddThreePidTask +import org.matrix.android.sdk.internal.network.RetrofitFactory +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.launchToCallback +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient + +internal class DefaultLoginWizard( + okHttpClient: OkHttpClient, + retrofitFactory: RetrofitFactory, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val sessionCreator: SessionCreator, + private val pendingSessionStore: PendingSessionStore, + private val coroutineScope: CoroutineScope +) : LoginWizard { + + private var pendingSessionData: PendingSessionData = pendingSessionStore.getPendingSessionData() ?: error("Pending session data should exist here") + + private val authAPI = retrofitFactory.create(okHttpClient, pendingSessionData.homeServerConnectionConfig.homeServerUri.toString()) + .create(AuthAPI::class.java) + + override fun login(login: String, + password: String, + deviceName: String, + callback: MatrixCallback): Cancelable { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { + loginInternal(login, password, deviceName) + } + } + + /** + * Ref: https://matrix.org/docs/spec/client_server/latest#handling-the-authentication-endpoint + */ + override fun loginWithToken(loginToken: String, callback: MatrixCallback): Cancelable { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { + val loginParams = TokenLoginParams( + token = loginToken + ) + val credentials = executeRequest(null) { + apiCall = authAPI.login(loginParams) + } + + sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig) + } + } + + private suspend fun loginInternal(login: String, + password: String, + deviceName: String) = withContext(coroutineDispatchers.computation) { + val loginParams = if (Patterns.EMAIL_ADDRESS.matcher(login).matches()) { + PasswordLoginParams.thirdPartyIdentifier(ThreePidMedium.EMAIL, login, password, deviceName) + } else { + PasswordLoginParams.userIdentifier(login, password, deviceName) + } + val credentials = executeRequest(null) { + apiCall = authAPI.login(loginParams) + } + + sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig) + } + + override fun resetPassword(email: String, newPassword: String, callback: MatrixCallback): Cancelable { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { + resetPasswordInternal(email, newPassword) + } + } + + private suspend fun resetPasswordInternal(email: String, newPassword: String) { + val param = RegisterAddThreePidTask.Params( + RegisterThreePid.Email(email), + pendingSessionData.clientSecret, + pendingSessionData.sendAttempt + ) + + pendingSessionData = pendingSessionData.copy(sendAttempt = pendingSessionData.sendAttempt + 1) + .also { pendingSessionStore.savePendingSessionData(it) } + + val result = executeRequest(null) { + apiCall = authAPI.resetPassword(AddThreePidRegistrationParams.from(param)) + } + + pendingSessionData = pendingSessionData.copy(resetPasswordData = ResetPasswordData(newPassword, result)) + .also { pendingSessionStore.savePendingSessionData(it) } + } + + override fun resetPasswordMailConfirmed(callback: MatrixCallback): Cancelable { + val safeResetPasswordData = pendingSessionData.resetPasswordData ?: run { + callback.onFailure(IllegalStateException("developer error, no reset password in progress")) + return NoOpCancellable + } + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { + resetPasswordMailConfirmedInternal(safeResetPasswordData) + } + } + + private suspend fun resetPasswordMailConfirmedInternal(resetPasswordData: ResetPasswordData) { + val param = ResetPasswordMailConfirmed.create( + pendingSessionData.clientSecret, + resetPasswordData.addThreePidRegistrationResponse.sid, + resetPasswordData.newPassword + ) + + executeRequest(null) { + apiCall = authAPI.resetPasswordMailConfirmed(param) + } + + // Set to null? + // resetPasswordData = null + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DirectLoginTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DirectLoginTask.kt new file mode 100644 index 0000000000..f759dc4235 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DirectLoginTask.kt @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.auth.login + +import dagger.Lazy +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.internal.auth.AuthAPI +import org.matrix.android.sdk.internal.auth.SessionCreator +import org.matrix.android.sdk.internal.auth.data.PasswordLoginParams +import org.matrix.android.sdk.internal.di.Unauthenticated +import org.matrix.android.sdk.internal.network.RetrofitFactory +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.network.httpclient.addSocketFactory +import org.matrix.android.sdk.internal.network.ssl.UnrecognizedCertificateException +import org.matrix.android.sdk.internal.task.Task +import okhttp3.OkHttpClient +import javax.inject.Inject + +internal interface DirectLoginTask : Task { + data class Params( + val homeServerConnectionConfig: HomeServerConnectionConfig, + val userId: String, + val password: String, + val deviceName: String + ) +} + +internal class DefaultDirectLoginTask @Inject constructor( + @Unauthenticated + private val okHttpClient: Lazy, + private val retrofitFactory: RetrofitFactory, + private val sessionCreator: SessionCreator +) : DirectLoginTask { + + override suspend fun execute(params: DirectLoginTask.Params): Session { + val client = buildClient(params.homeServerConnectionConfig) + val homeServerUrl = params.homeServerConnectionConfig.homeServerUri.toString() + + val authAPI = retrofitFactory.create(client, homeServerUrl) + .create(AuthAPI::class.java) + + val loginParams = PasswordLoginParams.userIdentifier(params.userId, params.password, params.deviceName) + + val credentials = try { + executeRequest(null) { + apiCall = authAPI.login(loginParams) + } + } catch (throwable: Throwable) { + when (throwable) { + is UnrecognizedCertificateException -> { + throw Failure.UnrecognizedCertificateFailure( + homeServerUrl, + throwable.fingerprint + ) + } + else -> + throw throwable + } + } + + return sessionCreator.createSession(credentials, params.homeServerConnectionConfig) + } + + private fun buildClient(homeServerConnectionConfig: HomeServerConnectionConfig): OkHttpClient { + return okHttpClient.get() + .newBuilder() + .addSocketFactory(homeServerConnectionConfig) + .build() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/ResetPasswordData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/ResetPasswordData.kt new file mode 100644 index 0000000000..a6f621c2db --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/ResetPasswordData.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.auth.login + +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.auth.registration.AddThreePidRegistrationResponse + +/** + * Container to store the data when a reset password is in the email validation step + */ +@JsonClass(generateAdapter = true) +internal data class ResetPasswordData( + val newPassword: String, + val addThreePidRegistrationResponse: AddThreePidRegistrationResponse +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/ResetPasswordMailConfirmed.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/ResetPasswordMailConfirmed.kt new file mode 100644 index 0000000000..c291c78882 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/ResetPasswordMailConfirmed.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2014 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.auth.login + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.auth.registration.AuthParams + +/** + * Class to pass parameters to reset the password once a email has been validated. + */ +@JsonClass(generateAdapter = true) +internal data class ResetPasswordMailConfirmed( + // authentication parameters + @Json(name = "auth") + val auth: AuthParams? = null, + + // the new password + @Json(name = "new_password") + val newPassword: String? = null +) { + companion object { + fun create(clientSecret: String, sid: String, newPassword: String): ResetPasswordMailConfirmed { + return ResetPasswordMailConfirmed( + auth = AuthParams.createForResetPassword(clientSecret, sid), + newPassword = newPassword + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/AddThreePidRegistrationParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/AddThreePidRegistrationParams.kt new file mode 100644 index 0000000000..7fbdaacb81 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/AddThreePidRegistrationParams.kt @@ -0,0 +1,102 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.auth.registration + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.auth.registration.RegisterThreePid + +/** + * Add a three Pid during authentication + */ +@JsonClass(generateAdapter = true) +internal data class AddThreePidRegistrationParams( + /** + * Required. A unique string generated by the client, and used to identify the validation attempt. + * It must be a string consisting of the characters [0-9a-zA-Z.=_-]. Its length must not exceed 255 characters and it must not be empty. + */ + @Json(name = "client_secret") + val clientSecret: String, + + /** + * Required. The server will only send an email if the send_attempt is a number greater than the most recent one which it has seen, + * scoped to that email + client_secret pair. This is to avoid repeatedly sending the same email in the case of request retries between + * the POSTing user and the identity server. The client should increment this value if they desire a new email (e.g. a reminder) to be sent. + * If they do not, the server should respond with success but not resend the email. + */ + @Json(name = "send_attempt") + val sendAttempt: Int, + + /** + * Optional. When the validation is completed, the identity server will redirect the user to this URL. This option is ignored when + * submitting 3PID validation information through a POST request. + */ + @Json(name = "next_link") + val nextLink: String? = null, + + /** + * Required. The hostname of the identity server to communicate with. May optionally include a port. + * This parameter is ignored when the homeserver handles 3PID verification. + */ + @Json(name = "id_server") + val id_server: String? = null, + + /* ========================================================================================== + * For emails + * ========================================================================================== */ + + /** + * Required. The email address to validate. + */ + @Json(name = "email") + val email: String? = null, + + /* ========================================================================================== + * For Msisdn + * ========================================================================================== */ + + /** + * Required. The two-letter uppercase ISO country code that the number in phone_number should be parsed as if it were dialled from. + */ + @Json(name = "country") + val countryCode: String? = null, + + /** + * Required. The phone number to validate. + */ + @Json(name = "phone_number") + val msisdn: String? = null +) { + companion object { + fun from(params: RegisterAddThreePidTask.Params): AddThreePidRegistrationParams { + return when (params.threePid) { + is RegisterThreePid.Email -> AddThreePidRegistrationParams( + email = params.threePid.email, + clientSecret = params.clientSecret, + sendAttempt = params.sendAttempt + ) + is RegisterThreePid.Msisdn -> AddThreePidRegistrationParams( + msisdn = params.threePid.msisdn, + countryCode = params.threePid.countryCode, + clientSecret = params.clientSecret, + sendAttempt = params.sendAttempt + ) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/AddThreePidRegistrationResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/AddThreePidRegistrationResponse.kt new file mode 100644 index 0000000000..2d60724e99 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/AddThreePidRegistrationResponse.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.auth.registration + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class AddThreePidRegistrationResponse( + /** + * Required. The session ID. Session IDs are opaque strings that must consist entirely of the characters [0-9a-zA-Z.=_-]. + * Their length must not exceed 255 characters and they must not be empty. + */ + @Json(name = "sid") + val sid: String, + + /** + * An optional field containing a URL where the client must submit the validation token to, with identical parameters to the Identity + * Service API's POST /validate/email/submitToken endpoint. The homeserver must send this token to the user (if applicable), + * who should then be prompted to provide it to the client. + * + * If this field is not present, the client can assume that verification will happen without the client's involvement provided + * the homeserver advertises this specification version in the /versions response (ie: r0.5.0). + */ + @Json(name = "submit_url") + val submitUrl: String? = null, + + /* ========================================================================================== + * It seems that the homeserver is sending more data, we may need it + * ========================================================================================== */ + + @Json(name = "msisdn") + val msisdn: String? = null, + + @Json(name = "intl_fmt") + val formattedMsisdn: String? = null, + + @Json(name = "success") + val success: Boolean? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/AuthParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/AuthParams.kt new file mode 100644 index 0000000000..f3136526da --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/AuthParams.kt @@ -0,0 +1,103 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.auth.registration + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes + +/** + * Open class, parent to all possible authentication parameters + */ +@JsonClass(generateAdapter = true) +internal data class AuthParams( + @Json(name = "type") + val type: String, + + /** + * Note: session can be null for reset password request + */ + @Json(name = "session") + val session: String?, + + /** + * parameter for "m.login.recaptcha" type + */ + @Json(name = "response") + val captchaResponse: String? = null, + + /** + * parameter for "m.login.email.identity" type + */ + @Json(name = "threepid_creds") + val threePidCredentials: ThreePidCredentials? = null +) { + + companion object { + fun createForCaptcha(session: String, captchaResponse: String): AuthParams { + return AuthParams( + type = LoginFlowTypes.RECAPTCHA, + session = session, + captchaResponse = captchaResponse + ) + } + + fun createForEmailIdentity(session: String, threePidCredentials: ThreePidCredentials): AuthParams { + return AuthParams( + type = LoginFlowTypes.EMAIL_IDENTITY, + session = session, + threePidCredentials = threePidCredentials + ) + } + + /** + * Note that there is a bug in Synapse (I have to investigate where), but if we pass LoginFlowTypes.MSISDN, + * the homeserver answer with the login flow with MatrixError fields and not with a simple MatrixError 401. + */ + fun createForMsisdnIdentity(session: String, threePidCredentials: ThreePidCredentials): AuthParams { + return AuthParams( + type = LoginFlowTypes.MSISDN, + session = session, + threePidCredentials = threePidCredentials + ) + } + + fun createForResetPassword(clientSecret: String, sid: String): AuthParams { + return AuthParams( + type = LoginFlowTypes.EMAIL_IDENTITY, + session = null, + threePidCredentials = ThreePidCredentials( + clientSecret = clientSecret, + sid = sid + ) + ) + } + } +} + +@JsonClass(generateAdapter = true) +data class ThreePidCredentials( + @Json(name = "client_secret") + val clientSecret: String? = null, + + @Json(name = "id_server") + val idServer: String? = null, + + @Json(name = "sid") + val sid: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt new file mode 100644 index 0000000000..79b71b208e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt @@ -0,0 +1,247 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.auth.registration + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes +import org.matrix.android.sdk.api.auth.registration.RegisterThreePid +import org.matrix.android.sdk.api.auth.registration.RegistrationResult +import org.matrix.android.sdk.api.auth.registration.RegistrationWizard +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.Failure.RegistrationFlowError +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.NoOpCancellable +import org.matrix.android.sdk.internal.auth.AuthAPI +import org.matrix.android.sdk.internal.auth.PendingSessionStore +import org.matrix.android.sdk.internal.auth.SessionCreator +import org.matrix.android.sdk.internal.auth.db.PendingSessionData +import org.matrix.android.sdk.internal.network.RetrofitFactory +import org.matrix.android.sdk.internal.task.launchToCallback +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import okhttp3.OkHttpClient + +/** + * This class execute the registration request and is responsible to keep the session of interactive authentication + */ +internal class DefaultRegistrationWizard( + private val okHttpClient: OkHttpClient, + private val retrofitFactory: RetrofitFactory, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val sessionCreator: SessionCreator, + private val pendingSessionStore: PendingSessionStore, + private val coroutineScope: CoroutineScope +) : RegistrationWizard { + + private var pendingSessionData: PendingSessionData = pendingSessionStore.getPendingSessionData() ?: error("Pending session data should exist here") + + private val authAPI = buildAuthAPI() + private val registerTask = DefaultRegisterTask(authAPI) + private val registerAddThreePidTask = DefaultRegisterAddThreePidTask(authAPI) + private val validateCodeTask = DefaultValidateCodeTask(authAPI) + + override val currentThreePid: String? + get() { + return when (val threePid = pendingSessionData.currentThreePidData?.threePid) { + is RegisterThreePid.Email -> threePid.email + is RegisterThreePid.Msisdn -> { + // Take formatted msisdn if provided by the server + pendingSessionData.currentThreePidData?.addThreePidRegistrationResponse?.formattedMsisdn?.takeIf { it.isNotBlank() } ?: threePid.msisdn + } + null -> null + } + } + + override val isRegistrationStarted: Boolean + get() = pendingSessionData.isRegistrationStarted + + override fun getRegistrationFlow(callback: MatrixCallback): Cancelable { + val params = RegistrationParams() + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { + performRegistrationRequest(params) + } + } + + override fun createAccount(userName: String, + password: String, + initialDeviceDisplayName: String?, + callback: MatrixCallback): Cancelable { + val params = RegistrationParams( + username = userName, + password = password, + initialDeviceDisplayName = initialDeviceDisplayName + ) + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { + performRegistrationRequest(params) + .also { + pendingSessionData = pendingSessionData.copy(isRegistrationStarted = true) + .also { pendingSessionStore.savePendingSessionData(it) } + } + } + } + + override fun performReCaptcha(response: String, callback: MatrixCallback): Cancelable { + val safeSession = pendingSessionData.currentSession ?: run { + callback.onFailure(IllegalStateException("developer error, call createAccount() method first")) + return NoOpCancellable + } + val params = RegistrationParams(auth = AuthParams.createForCaptcha(safeSession, response)) + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { + performRegistrationRequest(params) + } + } + + override fun acceptTerms(callback: MatrixCallback): Cancelable { + val safeSession = pendingSessionData.currentSession ?: run { + callback.onFailure(IllegalStateException("developer error, call createAccount() method first")) + return NoOpCancellable + } + val params = RegistrationParams(auth = AuthParams(type = LoginFlowTypes.TERMS, session = safeSession)) + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { + performRegistrationRequest(params) + } + } + + override fun addThreePid(threePid: RegisterThreePid, callback: MatrixCallback): Cancelable { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { + pendingSessionData = pendingSessionData.copy(currentThreePidData = null) + .also { pendingSessionStore.savePendingSessionData(it) } + + sendThreePid(threePid) + } + } + + override fun sendAgainThreePid(callback: MatrixCallback): Cancelable { + val safeCurrentThreePid = pendingSessionData.currentThreePidData?.threePid ?: run { + callback.onFailure(IllegalStateException("developer error, call createAccount() method first")) + return NoOpCancellable + } + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { + sendThreePid(safeCurrentThreePid) + } + } + + private suspend fun sendThreePid(threePid: RegisterThreePid): RegistrationResult { + val safeSession = pendingSessionData.currentSession ?: throw IllegalStateException("developer error, call createAccount() method first") + val response = registerAddThreePidTask.execute( + RegisterAddThreePidTask.Params( + threePid, + pendingSessionData.clientSecret, + pendingSessionData.sendAttempt)) + + pendingSessionData = pendingSessionData.copy(sendAttempt = pendingSessionData.sendAttempt + 1) + .also { pendingSessionStore.savePendingSessionData(it) } + + val params = RegistrationParams( + auth = if (threePid is RegisterThreePid.Email) { + AuthParams.createForEmailIdentity(safeSession, + ThreePidCredentials( + clientSecret = pendingSessionData.clientSecret, + sid = response.sid + ) + ) + } else { + AuthParams.createForMsisdnIdentity(safeSession, + ThreePidCredentials( + clientSecret = pendingSessionData.clientSecret, + sid = response.sid + ) + ) + } + ) + // Store data + pendingSessionData = pendingSessionData.copy(currentThreePidData = ThreePidData.from(threePid, response, params)) + .also { pendingSessionStore.savePendingSessionData(it) } + + // and send the sid a first time + return performRegistrationRequest(params) + } + + override fun checkIfEmailHasBeenValidated(delayMillis: Long, callback: MatrixCallback): Cancelable { + val safeParam = pendingSessionData.currentThreePidData?.registrationParams ?: run { + callback.onFailure(IllegalStateException("developer error, no pending three pid")) + return NoOpCancellable + } + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { + performRegistrationRequest(safeParam, delayMillis) + } + } + + override fun handleValidateThreePid(code: String, callback: MatrixCallback): Cancelable { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { + validateThreePid(code) + } + } + + private suspend fun validateThreePid(code: String): RegistrationResult { + val registrationParams = pendingSessionData.currentThreePidData?.registrationParams + ?: throw IllegalStateException("developer error, no pending three pid") + val safeCurrentData = pendingSessionData.currentThreePidData ?: throw IllegalStateException("developer error, call createAccount() method first") + val url = safeCurrentData.addThreePidRegistrationResponse.submitUrl ?: throw IllegalStateException("Missing url the send the code") + val validationBody = ValidationCodeBody( + clientSecret = pendingSessionData.clientSecret, + sid = safeCurrentData.addThreePidRegistrationResponse.sid, + code = code + ) + val validationResponse = validateCodeTask.execute(ValidateCodeTask.Params(url, validationBody)) + if (validationResponse.isSuccess()) { + // The entered code is correct + // Same than validate email + return performRegistrationRequest(registrationParams, 3_000) + } else { + // The code is not correct + throw Failure.SuccessError + } + } + + override fun dummy(callback: MatrixCallback): Cancelable { + val safeSession = pendingSessionData.currentSession ?: run { + callback.onFailure(IllegalStateException("developer error, call createAccount() method first")) + return NoOpCancellable + } + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { + val params = RegistrationParams(auth = AuthParams(type = LoginFlowTypes.DUMMY, session = safeSession)) + performRegistrationRequest(params) + } + } + + private suspend fun performRegistrationRequest(registrationParams: RegistrationParams, + delayMillis: Long = 0): RegistrationResult { + delay(delayMillis) + val credentials = try { + registerTask.execute(RegisterTask.Params(registrationParams)) + } catch (exception: Throwable) { + if (exception is RegistrationFlowError) { + pendingSessionData = pendingSessionData.copy(currentSession = exception.registrationFlowResponse.session) + .also { pendingSessionStore.savePendingSessionData(it) } + return RegistrationResult.FlowResponse(exception.registrationFlowResponse.toFlowResult()) + } else { + throw exception + } + } + + val session = sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig) + return RegistrationResult.Success(session) + } + + private fun buildAuthAPI(): AuthAPI { + val retrofit = retrofitFactory.create(okHttpClient, pendingSessionData.homeServerConnectionConfig.homeServerUri.toString()) + return retrofit.create(AuthAPI::class.java) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/LocalizedFlowDataLoginTerms.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/LocalizedFlowDataLoginTerms.kt new file mode 100644 index 0000000000..45e2f80fcc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/LocalizedFlowDataLoginTerms.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.auth.registration + +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +/** + * This class represent a localized privacy policy for registration Flow. + */ +@Parcelize +data class LocalizedFlowDataLoginTerms( + var policyName: String? = null, + var version: String? = null, + var localizedUrl: String? = null, + var localizedName: String? = null +) : Parcelable diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegisterAddThreePidTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegisterAddThreePidTask.kt new file mode 100644 index 0000000000..3ad15822ca --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegisterAddThreePidTask.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.auth.registration + +import org.matrix.android.sdk.api.auth.registration.RegisterThreePid +import org.matrix.android.sdk.internal.auth.AuthAPI +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task + +internal interface RegisterAddThreePidTask : Task { + data class Params( + val threePid: RegisterThreePid, + val clientSecret: String, + val sendAttempt: Int + ) +} + +internal class DefaultRegisterAddThreePidTask( + private val authAPI: AuthAPI +) : RegisterAddThreePidTask { + + override suspend fun execute(params: RegisterAddThreePidTask.Params): AddThreePidRegistrationResponse { + return executeRequest(null) { + apiCall = authAPI.add3Pid(params.threePid.toPath(), AddThreePidRegistrationParams.from(params)) + } + } + + private fun RegisterThreePid.toPath(): String { + return when (this) { + is RegisterThreePid.Email -> "email" + is RegisterThreePid.Msisdn -> "msisdn" + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegisterTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegisterTask.kt new file mode 100644 index 0000000000..2b3924138e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegisterTask.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.auth.registration + +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse +import org.matrix.android.sdk.internal.auth.AuthAPI +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task + +internal interface RegisterTask : Task { + data class Params( + val registrationParams: RegistrationParams + ) +} + +internal class DefaultRegisterTask( + private val authAPI: AuthAPI +) : RegisterTask { + + override suspend fun execute(params: RegisterTask.Params): Credentials { + try { + return executeRequest(null) { + apiCall = authAPI.register(params.registrationParams) + } + } catch (throwable: Throwable) { + throw throwable.toRegistrationFlowResponse() + ?.let { Failure.RegistrationFlowError(it) } + ?: throwable + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegistrationFlowResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegistrationFlowResponse.kt new file mode 100644 index 0000000000..267e50eeb9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegistrationFlowResponse.kt @@ -0,0 +1,100 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.auth.registration + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes +import org.matrix.android.sdk.api.auth.registration.FlowResult +import org.matrix.android.sdk.api.auth.registration.Stage +import org.matrix.android.sdk.api.auth.registration.TermPolicies +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.auth.data.InteractiveAuthenticationFlow + +@JsonClass(generateAdapter = true) +data class RegistrationFlowResponse( + + /** + * The list of flows. + */ + @Json(name = "flows") + val flows: List? = null, + + /** + * The list of stages the client has completed successfully. + */ + @Json(name = "completed") + val completedStages: List? = null, + + /** + * The session identifier that the client must pass back to the home server, if one is provided, + * in subsequent attempts to authenticate in the same API call. + */ + @Json(name = "session") + val session: String? = null, + + /** + * The information that the client will need to know in order to use a given type of authentication. + * For each login stage type presented, that type may be present as a key in this dictionary. + * For example, the public key of reCAPTCHA stage could be given here. + */ + @Json(name = "params") + val params: JsonDict? = null + + /** + * WARNING, + * The two MatrixError fields "errcode" and "error" can also be present here in case of error when validating a stage, + * But in this case Moshi will be able to parse the result as a MatrixError, see [RetrofitExtensions.toFailure] + * Ex: when polling for "m.login.msisdn" validation + */ +) + +/** + * Convert to something easier to handle on client side + */ +fun RegistrationFlowResponse.toFlowResult(): FlowResult { + // Get all the returned stages + val allFlowTypes = mutableSetOf() + + val missingStage = mutableListOf() + val completedStage = mutableListOf() + + this.flows?.forEach { it.stages?.mapTo(allFlowTypes) { type -> type } } + + allFlowTypes.forEach { type -> + val isMandatory = flows?.all { type in it.stages.orEmpty() } == true + + val stage = when (type) { + LoginFlowTypes.RECAPTCHA -> Stage.ReCaptcha(isMandatory, ((params?.get(type) as? Map<*, *>)?.get("public_key") as? String) + ?: "") + LoginFlowTypes.DUMMY -> Stage.Dummy(isMandatory) + LoginFlowTypes.TERMS -> Stage.Terms(isMandatory, params?.get(type) as? TermPolicies ?: emptyMap()) + LoginFlowTypes.EMAIL_IDENTITY -> Stage.Email(isMandatory) + LoginFlowTypes.MSISDN -> Stage.Msisdn(isMandatory) + else -> Stage.Other(isMandatory, type, (params?.get(type) as? Map<*, *>)) + } + + if (type in completedStages.orEmpty()) { + completedStage.add(stage) + } else { + missingStage.add(stage) + } + } + + return FlowResult(missingStage, completedStage) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegistrationParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegistrationParams.kt new file mode 100644 index 0000000000..4089e280d7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegistrationParams.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2014 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.auth.registration + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class to pass parameters to the different registration types for /register. + */ +@JsonClass(generateAdapter = true) +internal data class RegistrationParams( + // authentication parameters + @Json(name = "auth") + val auth: AuthParams? = null, + + // the account username + @Json(name = "username") + val username: String? = null, + + // the account password + @Json(name = "password") + val password: String? = null, + + // device name + @Json(name = "initial_device_display_name") + val initialDeviceDisplayName: String? = null, + + // Temporary flag to notify the server that we support msisdn flow. Used to prevent old app + // versions to end up in fallback because the HS returns the msisdn flow which they don't support + val x_show_msisdn: Boolean? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/SuccessResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/SuccessResult.kt new file mode 100644 index 0000000000..bfebc57884 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/SuccessResult.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.auth.registration + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.extensions.orFalse + +@JsonClass(generateAdapter = true) +data class SuccessResult( + @Json(name = "success") + val success: Boolean? +) { + fun isSuccess() = success.orFalse() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/ThreePidData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/ThreePidData.kt new file mode 100644 index 0000000000..25a7fa3ab2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/ThreePidData.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.auth.registration + +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.auth.registration.RegisterThreePid + +/** + * Container to store the data when a three pid is in validation step + */ +@JsonClass(generateAdapter = true) +internal data class ThreePidData( + val email: String, + val msisdn: String, + val country: String, + val addThreePidRegistrationResponse: AddThreePidRegistrationResponse, + val registrationParams: RegistrationParams +) { + val threePid: RegisterThreePid + get() { + return if (email.isNotBlank()) { + RegisterThreePid.Email(email) + } else { + RegisterThreePid.Msisdn(msisdn, country) + } + } + + companion object { + fun from(threePid: RegisterThreePid, + addThreePidRegistrationResponse: AddThreePidRegistrationResponse, + registrationParams: RegistrationParams): ThreePidData { + return when (threePid) { + is RegisterThreePid.Email -> + ThreePidData(threePid.email, "", "", addThreePidRegistrationResponse, registrationParams) + is RegisterThreePid.Msisdn -> + ThreePidData("", threePid.msisdn, threePid.countryCode, addThreePidRegistrationResponse, registrationParams) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/ValidateCodeTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/ValidateCodeTask.kt new file mode 100644 index 0000000000..470faae710 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/ValidateCodeTask.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.auth.registration + +import org.matrix.android.sdk.internal.auth.AuthAPI +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task + +internal interface ValidateCodeTask : Task { + data class Params( + val url: String, + val body: ValidationCodeBody + ) +} + +internal class DefaultValidateCodeTask( + private val authAPI: AuthAPI +) : ValidateCodeTask { + + override suspend fun execute(params: ValidateCodeTask.Params): SuccessResult { + return executeRequest(null) { + apiCall = authAPI.validate3Pid(params.url, params.body) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/ValidationCodeBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/ValidationCodeBody.kt new file mode 100644 index 0000000000..ad4a3d4609 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/ValidationCodeBody.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.auth.registration + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This object is used to send a code received by SMS to validate Msisdn ownership + */ +@JsonClass(generateAdapter = true) +data class ValidationCodeBody( + @Json(name = "client_secret") + val clientSecret: String, + + @Json(name = "sid") + val sid: String, + + @Json(name = "token") + val code: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/HomeServerVersion.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/HomeServerVersion.kt new file mode 100644 index 0000000000..9a02bc62e9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/HomeServerVersion.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.auth.version + +/** + * Values will take the form "rX.Y.Z". + * Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-versions + */ +internal data class HomeServerVersion( + val major: Int, + val minor: Int, + val patch: Int +) : Comparable { + override fun compareTo(other: HomeServerVersion): Int { + return when { + major > other.major -> 1 + major < other.major -> -1 + minor > other.minor -> 1 + minor < other.minor -> -1 + patch > other.patch -> 1 + patch < other.patch -> -1 + else -> 0 + } + } + + companion object { + internal val pattern = Regex("""r(\d+)\.(\d+)\.(\d+)""") + + internal fun parse(value: String): HomeServerVersion? { + val result = pattern.matchEntire(value) ?: return null + return HomeServerVersion( + major = result.groupValues[1].toInt(), + minor = result.groupValues[2].toInt(), + patch = result.groupValues[3].toInt() + ) + } + + val r0_0_0 = HomeServerVersion(major = 0, minor = 0, patch = 0) + val r0_1_0 = HomeServerVersion(major = 0, minor = 1, patch = 0) + val r0_2_0 = HomeServerVersion(major = 0, minor = 2, patch = 0) + val r0_3_0 = HomeServerVersion(major = 0, minor = 3, patch = 0) + val r0_4_0 = HomeServerVersion(major = 0, minor = 4, patch = 0) + val r0_5_0 = HomeServerVersion(major = 0, minor = 5, patch = 0) + val r0_6_0 = HomeServerVersion(major = 0, minor = 6, patch = 0) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt new file mode 100644 index 0000000000..483c43f502 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.auth.version + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Model for https://matrix.org/docs/spec/client_server/latest#get-matrix-client-versions + * + * Ex: + *

+ *   {
+ *     "unstable_features": {
+ *       "m.lazy_load_members": true
+ *     },
+ *     "versions": [
+ *       "r0.0.1",
+ *       "r0.1.0",
+ *       "r0.2.0",
+ *       "r0.3.0"
+ *     ]
+ *   }
+ * 
+ */ +@JsonClass(generateAdapter = true) +internal data class Versions( + @Json(name = "versions") + val supportedVersions: List? = null, + + @Json(name = "unstable_features") + val unstableFeatures: Map? = null +) + +// MatrixVersionsFeature +private const val FEATURE_LAZY_LOAD_MEMBERS = "m.lazy_load_members" +private const val FEATURE_REQUIRE_IDENTITY_SERVER = "m.require_identity_server" +private const val FEATURE_ID_ACCESS_TOKEN = "m.id_access_token" +private const val FEATURE_SEPARATE_ADD_AND_BIND = "m.separate_add_and_bind" + +/** + * Return true if the SDK supports this homeserver version + */ +internal fun Versions.isSupportedBySdk(): Boolean { + return supportLazyLoadMembers() +} + +/** + * Return true if the SDK supports this homeserver version for login and registration + */ +internal fun Versions.isLoginAndRegistrationSupportedBySdk(): Boolean { + return !doesServerRequireIdentityServerParam() + && doesServerAcceptIdentityAccessToken() + && doesServerSeparatesAddAndBind() +} + +/** + * Return true if the server support the lazy loading of room members + * + * @return true if the server support the lazy loading of room members + */ +private fun Versions.supportLazyLoadMembers(): Boolean { + return getMaxVersion() >= HomeServerVersion.r0_5_0 + || unstableFeatures?.get(FEATURE_LAZY_LOAD_MEMBERS) == true +} + +/** + * Indicate if the `id_server` parameter is required when registering with an 3pid, + * adding a 3pid or resetting password. + */ +private fun Versions.doesServerRequireIdentityServerParam(): Boolean { + if (getMaxVersion() >= HomeServerVersion.r0_6_0) return false + return unstableFeatures?.get(FEATURE_REQUIRE_IDENTITY_SERVER) ?: true +} + +/** + * Indicate if the `id_access_token` parameter can be safely passed to the homeserver. + * Some homeservers may trigger errors if they are not prepared for the new parameter. + */ +private fun Versions.doesServerAcceptIdentityAccessToken(): Boolean { + return getMaxVersion() >= HomeServerVersion.r0_6_0 + || unstableFeatures?.get(FEATURE_ID_ACCESS_TOKEN) ?: false +} + +private fun Versions.doesServerSeparatesAddAndBind(): Boolean { + return getMaxVersion() >= HomeServerVersion.r0_6_0 + || unstableFeatures?.get(FEATURE_SEPARATE_ADD_AND_BIND) ?: false +} + +private fun Versions.getMaxVersion(): HomeServerVersion { + return supportedVersions + ?.mapNotNull { HomeServerVersion.parse(it) } + ?.max() + ?: HomeServerVersion.r0_0_0 +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CancelGossipRequestWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CancelGossipRequestWorker.kt new file mode 100644 index 0000000000..c4f55d14bf --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CancelGossipRequestWorker.kt @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.Data +import androidx.work.WorkerParameters +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.failure.shouldBeRetried +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.LocalEcho +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.model.rest.ShareRequestCancellation +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import org.matrix.android.sdk.internal.worker.getSessionComponent +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import javax.inject.Inject + +internal class CancelGossipRequestWorker(context: Context, + params: WorkerParameters) + : CoroutineWorker(context, params) { + + @JsonClass(generateAdapter = true) + internal data class Params( + val sessionId: String, + val requestId: String, + val recipients: Map> + ) { + companion object { + fun fromRequest(sessionId: String, request: OutgoingGossipingRequest): Params { + return Params( + sessionId = sessionId, + requestId = request.requestId, + recipients = request.recipients + ) + } + } + } + + @Inject lateinit var sendToDeviceTask: SendToDeviceTask + @Inject lateinit var cryptoStore: IMXCryptoStore + @Inject lateinit var eventBus: EventBus + @Inject lateinit var credentials: Credentials + + override suspend fun doWork(): Result { + val errorOutputData = Data.Builder().putBoolean("failed", true).build() + val params = WorkerParamsFactory.fromData(inputData) + ?: return Result.success(errorOutputData) + + val sessionComponent = getSessionComponent(params.sessionId) + ?: return Result.success(errorOutputData).also { + // TODO, can this happen? should I update local echo? + Timber.e("Unknown Session, cannot send message, sessionId: ${params.sessionId}") + } + sessionComponent.inject(this) + + val localId = LocalEcho.createLocalEchoId() + val contentMap = MXUsersDevicesMap() + val toDeviceContent = ShareRequestCancellation( + requestingDeviceId = credentials.deviceId, + requestId = params.requestId + ) + cryptoStore.saveGossipingEvent(Event( + type = EventType.ROOM_KEY_REQUEST, + content = toDeviceContent.toContent(), + senderId = credentials.userId + ).also { + it.ageLocalTs = System.currentTimeMillis() + }) + + params.recipients.forEach { userToDeviceMap -> + userToDeviceMap.value.forEach { deviceId -> + contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent) + } + } + + try { + cryptoStore.updateOutgoingGossipingRequestState(params.requestId, OutgoingGossipingRequestState.CANCELLING) + sendToDeviceTask.execute( + SendToDeviceTask.Params( + eventType = EventType.ROOM_KEY_REQUEST, + contentMap = contentMap, + transactionId = localId + ) + ) + cryptoStore.updateOutgoingGossipingRequestState(params.requestId, OutgoingGossipingRequestState.CANCELLED) + return Result.success() + } catch (exception: Throwable) { + return if (exception.shouldBeRetried()) { + Result.retry() + } else { + cryptoStore.updateOutgoingGossipingRequestState(params.requestId, OutgoingGossipingRequestState.FAILED_TO_CANCEL) + Result.success(errorOutputData) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoConstants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoConstants.kt new file mode 100644 index 0000000000..3c8b525b96 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoConstants.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto + +/** + * Matrix algorithm value for olm. + */ +const val MXCRYPTO_ALGORITHM_OLM = "m.olm.v1.curve25519-aes-sha2" + +/** + * Matrix algorithm value for megolm. + */ +const val MXCRYPTO_ALGORITHM_MEGOLM = "m.megolm.v1.aes-sha2" + +/** + * Matrix algorithm value for megolm keys backup. + */ +const val MXCRYPTO_ALGORITHM_MEGOLM_BACKUP = "m.megolm_backup.v1.curve25519-aes-sha2" + +/** + * Secured Shared Storage algorithm constant + */ +const val SSSS_ALGORITHM_CURVE25519_AES_SHA2 = "m.secret_storage.v1.curve25519-aes-sha2" +/* Secrets are encrypted using AES-CTR-256 and MACed using HMAC-SHA-256. **/ +const val SSSS_ALGORITHM_AES_HMAC_SHA2 = "m.secret_storage.v1.aes-hmac-sha2" + +// TODO Refacto: use this constants everywhere +const val ed25519 = "ed25519" +const val curve25519 = "curve25519" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt new file mode 100644 index 0000000000..e5496a6fd1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt @@ -0,0 +1,260 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto + +import dagger.Binds +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService +import org.matrix.android.sdk.internal.crypto.api.CryptoApi +import org.matrix.android.sdk.internal.crypto.crosssigning.ComputeTrustTask +import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultComputeTrustTask +import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService +import org.matrix.android.sdk.internal.crypto.keysbackup.api.RoomKeysApi +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.CreateKeysBackupVersionTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultCreateKeysBackupVersionTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultDeleteBackupTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultDeleteRoomSessionDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultDeleteRoomSessionsDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultDeleteSessionsDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultGetKeysBackupLastVersionTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultGetKeysBackupVersionTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultGetRoomSessionDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultGetRoomSessionsDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultGetSessionsDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultStoreRoomSessionDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultStoreRoomSessionsDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultStoreSessionsDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultUpdateKeysBackupVersionTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteBackupTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteRoomSessionDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteRoomSessionsDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteSessionsDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetKeysBackupLastVersionTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetKeysBackupVersionTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetRoomSessionDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetRoomSessionsDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetSessionsDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreRoomSessionDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreRoomSessionsDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreSessionsDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.UpdateKeysBackupVersionTask +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStore +import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreMigration +import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreModule +import org.matrix.android.sdk.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask +import org.matrix.android.sdk.internal.crypto.tasks.DefaultClaimOneTimeKeysForUsersDevice +import org.matrix.android.sdk.internal.crypto.tasks.DefaultDeleteDeviceTask +import org.matrix.android.sdk.internal.crypto.tasks.DefaultDeleteDeviceWithUserPasswordTask +import org.matrix.android.sdk.internal.crypto.tasks.DefaultDownloadKeysForUsers +import org.matrix.android.sdk.internal.crypto.tasks.DefaultEncryptEventTask +import org.matrix.android.sdk.internal.crypto.tasks.DefaultGetDeviceInfoTask +import org.matrix.android.sdk.internal.crypto.tasks.DefaultGetDevicesTask +import org.matrix.android.sdk.internal.crypto.tasks.DefaultInitializeCrossSigningTask +import org.matrix.android.sdk.internal.crypto.tasks.DefaultSendEventTask +import org.matrix.android.sdk.internal.crypto.tasks.DefaultSendToDeviceTask +import org.matrix.android.sdk.internal.crypto.tasks.DefaultSendVerificationMessageTask +import org.matrix.android.sdk.internal.crypto.tasks.DefaultSetDeviceNameTask +import org.matrix.android.sdk.internal.crypto.tasks.DefaultUploadKeysTask +import org.matrix.android.sdk.internal.crypto.tasks.DefaultUploadSignaturesTask +import org.matrix.android.sdk.internal.crypto.tasks.DefaultUploadSigningKeysTask +import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceTask +import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceWithUserPasswordTask +import org.matrix.android.sdk.internal.crypto.tasks.DownloadKeysForUsersTask +import org.matrix.android.sdk.internal.crypto.tasks.EncryptEventTask +import org.matrix.android.sdk.internal.crypto.tasks.GetDeviceInfoTask +import org.matrix.android.sdk.internal.crypto.tasks.GetDevicesTask +import org.matrix.android.sdk.internal.crypto.tasks.InitializeCrossSigningTask +import org.matrix.android.sdk.internal.crypto.tasks.SendEventTask +import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask +import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask +import org.matrix.android.sdk.internal.crypto.tasks.SetDeviceNameTask +import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask +import org.matrix.android.sdk.internal.crypto.tasks.UploadSignaturesTask +import org.matrix.android.sdk.internal.crypto.tasks.UploadSigningKeysTask +import org.matrix.android.sdk.internal.database.RealmKeysUtils +import org.matrix.android.sdk.internal.di.CryptoDatabase +import org.matrix.android.sdk.internal.di.SessionFilesDirectory +import org.matrix.android.sdk.internal.di.UserMd5 +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.cache.ClearCacheTask +import org.matrix.android.sdk.internal.session.cache.RealmClearCacheTask +import io.realm.RealmConfiguration +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import retrofit2.Retrofit +import java.io.File + +@Module +internal abstract class CryptoModule { + + @Module + companion object { + internal fun getKeyAlias(userMd5: String) = "crypto_module_$userMd5" + + @JvmStatic + @Provides + @CryptoDatabase + @SessionScope + fun providesRealmConfiguration(@SessionFilesDirectory directory: File, + @UserMd5 userMd5: String, + realmCryptoStoreMigration: RealmCryptoStoreMigration, + realmKeysUtils: RealmKeysUtils): RealmConfiguration { + return RealmConfiguration.Builder() + .directory(directory) + .apply { + realmKeysUtils.configureEncryption(this, getKeyAlias(userMd5)) + } + .name("crypto_store.realm") + .modules(RealmCryptoStoreModule()) + .schemaVersion(RealmCryptoStoreMigration.CRYPTO_STORE_SCHEMA_VERSION) + .migration(realmCryptoStoreMigration) + .build() + } + + @JvmStatic + @Provides + @SessionScope + fun providesCryptoCoroutineScope(): CoroutineScope { + return CoroutineScope(SupervisorJob()) + } + + @JvmStatic + @Provides + @CryptoDatabase + fun providesClearCacheTask(@CryptoDatabase + realmConfiguration: RealmConfiguration): ClearCacheTask { + return RealmClearCacheTask(realmConfiguration) + } + + @JvmStatic + @Provides + @SessionScope + fun providesCryptoAPI(retrofit: Retrofit): CryptoApi { + return retrofit.create(CryptoApi::class.java) + } + + @JvmStatic + @Provides + @SessionScope + fun providesRoomKeysAPI(retrofit: Retrofit): RoomKeysApi { + return retrofit.create(RoomKeysApi::class.java) + } + } + + @Binds + abstract fun bindCryptoService(service: DefaultCryptoService): CryptoService + + @Binds + abstract fun bindDeleteDeviceTask(task: DefaultDeleteDeviceTask): DeleteDeviceTask + + @Binds + abstract fun bindGetDevicesTask(task: DefaultGetDevicesTask): GetDevicesTask + + @Binds + abstract fun bindGetDeviceInfoTask(task: DefaultGetDeviceInfoTask): GetDeviceInfoTask + + @Binds + abstract fun bindSetDeviceNameTask(task: DefaultSetDeviceNameTask): SetDeviceNameTask + + @Binds + abstract fun bindUploadKeysTask(task: DefaultUploadKeysTask): UploadKeysTask + + @Binds + abstract fun bindUploadSigningKeysTask(task: DefaultUploadSigningKeysTask): UploadSigningKeysTask + + @Binds + abstract fun bindUploadSignaturesTask(task: DefaultUploadSignaturesTask): UploadSignaturesTask + + @Binds + abstract fun bindDownloadKeysForUsersTask(task: DefaultDownloadKeysForUsers): DownloadKeysForUsersTask + + @Binds + abstract fun bindCreateKeysBackupVersionTask(task: DefaultCreateKeysBackupVersionTask): CreateKeysBackupVersionTask + + @Binds + abstract fun bindDeleteBackupTask(task: DefaultDeleteBackupTask): DeleteBackupTask + + @Binds + abstract fun bindDeleteRoomSessionDataTask(task: DefaultDeleteRoomSessionDataTask): DeleteRoomSessionDataTask + + @Binds + abstract fun bindDeleteRoomSessionsDataTask(task: DefaultDeleteRoomSessionsDataTask): DeleteRoomSessionsDataTask + + @Binds + abstract fun bindDeleteSessionsDataTask(task: DefaultDeleteSessionsDataTask): DeleteSessionsDataTask + + @Binds + abstract fun bindGetKeysBackupLastVersionTask(task: DefaultGetKeysBackupLastVersionTask): GetKeysBackupLastVersionTask + + @Binds + abstract fun bindGetKeysBackupVersionTask(task: DefaultGetKeysBackupVersionTask): GetKeysBackupVersionTask + + @Binds + abstract fun bindGetRoomSessionDataTask(task: DefaultGetRoomSessionDataTask): GetRoomSessionDataTask + + @Binds + abstract fun bindGetRoomSessionsDataTask(task: DefaultGetRoomSessionsDataTask): GetRoomSessionsDataTask + + @Binds + abstract fun bindGetSessionsDataTask(task: DefaultGetSessionsDataTask): GetSessionsDataTask + + @Binds + abstract fun bindStoreRoomSessionDataTask(task: DefaultStoreRoomSessionDataTask): StoreRoomSessionDataTask + + @Binds + abstract fun bindStoreRoomSessionsDataTask(task: DefaultStoreRoomSessionsDataTask): StoreRoomSessionsDataTask + + @Binds + abstract fun bindStoreSessionsDataTask(task: DefaultStoreSessionsDataTask): StoreSessionsDataTask + + @Binds + abstract fun bindUpdateKeysBackupVersionTask(task: DefaultUpdateKeysBackupVersionTask): UpdateKeysBackupVersionTask + + @Binds + abstract fun bindSendToDeviceTask(task: DefaultSendToDeviceTask): SendToDeviceTask + + @Binds + abstract fun bindEncryptEventTask(task: DefaultEncryptEventTask): EncryptEventTask + + @Binds + abstract fun bindSendVerificationMessageTask(task: DefaultSendVerificationMessageTask): SendVerificationMessageTask + + @Binds + abstract fun bindClaimOneTimeKeysForUsersDeviceTask(task: DefaultClaimOneTimeKeysForUsersDevice): ClaimOneTimeKeysForUsersDeviceTask + + @Binds + abstract fun bindDeleteDeviceWithUserPasswordTask(task: DefaultDeleteDeviceWithUserPasswordTask): DeleteDeviceWithUserPasswordTask + + @Binds + abstract fun bindCrossSigningService(service: DefaultCrossSigningService): CrossSigningService + + @Binds + abstract fun bindCryptoStore(store: RealmCryptoStore): IMXCryptoStore + + @Binds + abstract fun bindComputeShieldTrustTask(task: DefaultComputeTrustTask): ComputeTrustTask + + @Binds + abstract fun bindInitializeCrossSigningTask(task: DefaultInitializeCrossSigningTask): InitializeCrossSigningTask + + @Binds + abstract fun bindSendEventTask(task: DefaultSendEventTask): SendEventTask +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt new file mode 100755 index 0000000000..f8fb5a35d0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt @@ -0,0 +1,1360 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto + +import android.content.Context +import android.os.Handler +import android.os.Looper +import androidx.annotation.VisibleForTesting +import androidx.lifecycle.LiveData +import com.squareup.moshi.Types +import com.zhuinden.monarchy.Monarchy +import dagger.Lazy +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.NoOpMatrixCallback +import org.matrix.android.sdk.api.crypto.MXCryptoConfig +import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.listeners.ProgressListener +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility +import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent +import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary +import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction +import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter +import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter +import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction +import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting +import org.matrix.android.sdk.internal.crypto.algorithms.IMXWithHeldExtension +import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory +import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmEncryptionFactory +import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService +import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel +import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult +import org.matrix.android.sdk.internal.crypto.model.MXDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.MXEncryptEventContentResult +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent +import org.matrix.android.sdk.internal.crypto.model.event.OlmEventContent +import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyContent +import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent +import org.matrix.android.sdk.internal.crypto.model.event.SecretSendEventContent +import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo +import org.matrix.android.sdk.internal.crypto.model.rest.DevicesListResponse +import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody +import org.matrix.android.sdk.internal.crypto.model.toRest +import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceTask +import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceWithUserPasswordTask +import org.matrix.android.sdk.internal.crypto.tasks.GetDeviceInfoTask +import org.matrix.android.sdk.internal.crypto.tasks.GetDevicesTask +import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask +import org.matrix.android.sdk.internal.crypto.tasks.SetDeviceNameTask +import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask +import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.EventEntityFields +import org.matrix.android.sdk.internal.database.query.whereType +import org.matrix.android.sdk.internal.di.DeviceId +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.extensions.foldToCallback +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask +import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper +import org.matrix.android.sdk.internal.session.sync.model.SyncResponse +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.TaskThread +import org.matrix.android.sdk.internal.task.configureWith +import org.matrix.android.sdk.internal.task.launchToCallback +import org.matrix.android.sdk.internal.util.JsonCanonicalizer +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import org.matrix.android.sdk.internal.util.fetchCopied +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import org.matrix.olm.OlmManager +import timber.log.Timber +import java.util.concurrent.atomic.AtomicBoolean +import javax.inject.Inject +import kotlin.math.max + +/** + * A `CryptoService` class instance manages the end-to-end crypto for a session. + * + * + * Messages posted by the user are automatically redirected to CryptoService in order to be encrypted + * before sending. + * In the other hand, received events goes through CryptoService for decrypting. + * CryptoService maintains all necessary keys and their sharing with other devices required for the crypto. + * Specially, it tracks all room membership changes events in order to do keys updates. + */ +@SessionScope +internal class DefaultCryptoService @Inject constructor( + // Olm Manager + private val olmManager: OlmManager, + @UserId + private val userId: String, + @DeviceId + private val deviceId: String?, + private val myDeviceInfoHolder: Lazy, + // the crypto store + private val cryptoStore: IMXCryptoStore, + // Room encryptors store + private val roomEncryptorsStore: RoomEncryptorsStore, + // Olm device + private val olmDevice: MXOlmDevice, + // Set of parameters used to configure/customize the end-to-end crypto. + private val mxCryptoConfig: MXCryptoConfig, + // Device list manager + private val deviceListManager: DeviceListManager, + // The key backup service. + private val keysBackupService: DefaultKeysBackupService, + // + private val objectSigner: ObjectSigner, + // + private val oneTimeKeysUploader: OneTimeKeysUploader, + // + private val roomDecryptorProvider: RoomDecryptorProvider, + // The verification service. + private val verificationService: DefaultVerificationService, + + private val crossSigningService: DefaultCrossSigningService, + // + private val incomingGossipingRequestManager: IncomingGossipingRequestManager, + // + private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, + // Actions + private val setDeviceVerificationAction: SetDeviceVerificationAction, + private val megolmSessionDataImporter: MegolmSessionDataImporter, + private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository, + // Repository + private val megolmEncryptionFactory: MXMegolmEncryptionFactory, + private val olmEncryptionFactory: MXOlmEncryptionFactory, + private val deleteDeviceTask: DeleteDeviceTask, + private val deleteDeviceWithUserPasswordTask: DeleteDeviceWithUserPasswordTask, + // Tasks + private val getDevicesTask: GetDevicesTask, + private val getDeviceInfoTask: GetDeviceInfoTask, + private val setDeviceNameTask: SetDeviceNameTask, + private val uploadKeysTask: UploadKeysTask, + private val loadRoomMembersTask: LoadRoomMembersTask, + @SessionDatabase private val monarchy: Monarchy, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val taskExecutor: TaskExecutor, + private val cryptoCoroutineScope: CoroutineScope, + private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, + private val sendToDeviceTask: SendToDeviceTask, + private val messageEncrypter: MessageEncrypter +) : CryptoService { + + init { + verificationService.cryptoService = this + } + + private val uiHandler = Handler(Looper.getMainLooper()) + + private val isStarting = AtomicBoolean(false) + private val isStarted = AtomicBoolean(false) + + // The date of the last time we forced establishment + // of a new session for each user:device. + private val lastNewSessionForcedDates = MXUsersDevicesMap() + + fun onStateEvent(roomId: String, event: Event) { + when { + event.getClearType() == EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event) + event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) + event.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event) + } + } + + fun onLiveEvent(roomId: String, event: Event) { + when { + event.getClearType() == EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event) + event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) + event.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event) + } + } + + override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback) { + setDeviceNameTask + .configureWith(SetDeviceNameTask.Params(deviceId, deviceName)) { + this.executionThread = TaskThread.CRYPTO + this.callback = object : MatrixCallback { + override fun onSuccess(data: Unit) { + // bg refresh of crypto device + downloadKeys(listOf(userId), true, NoOpMatrixCallback()) + callback.onSuccess(data) + } + + override fun onFailure(failure: Throwable) { + callback.onFailure(failure) + } + } + } + .executeBy(taskExecutor) + } + + override fun deleteDevice(deviceId: String, callback: MatrixCallback) { + deleteDeviceTask + .configureWith(DeleteDeviceTask.Params(deviceId)) { + this.executionThread = TaskThread.CRYPTO + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun deleteDeviceWithUserPassword(deviceId: String, authSession: String?, password: String, callback: MatrixCallback) { + deleteDeviceWithUserPasswordTask + .configureWith(DeleteDeviceWithUserPasswordTask.Params(deviceId, authSession, password)) { + this.executionThread = TaskThread.CRYPTO + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun getCryptoVersion(context: Context, longFormat: Boolean): String { + return if (longFormat) olmManager.getDetailedVersion(context) else olmManager.version + } + + override fun getMyDevice(): CryptoDeviceInfo { + return myDeviceInfoHolder.get().myDevice + } + + override fun fetchDevicesList(callback: MatrixCallback) { + getDevicesTask + .configureWith { + // this.executionThread = TaskThread.CRYPTO + this.callback = object : MatrixCallback { + override fun onFailure(failure: Throwable) { + callback.onFailure(failure) + } + + override fun onSuccess(data: DevicesListResponse) { + // Save in local DB + cryptoStore.saveMyDevicesInfo(data.devices.orEmpty()) + callback.onSuccess(data) + } + } + } + .executeBy(taskExecutor) + } + + override fun getLiveMyDevicesInfo(): LiveData> { + return cryptoStore.getLiveMyDevicesInfo() + } + + override fun getMyDevicesInfo(): List { + return cryptoStore.getMyDevicesInfo() + } + + override fun getDeviceInfo(deviceId: String, callback: MatrixCallback) { + getDeviceInfoTask + .configureWith(GetDeviceInfoTask.Params(deviceId)) { + this.executionThread = TaskThread.CRYPTO + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int { + return cryptoStore.inboundGroupSessionsCount(onlyBackedUp) + } + + /** + * Provides the tracking status + * + * @param userId the user id + * @return the tracking status + */ + override fun getDeviceTrackingStatus(userId: String): Int { + return cryptoStore.getDeviceTrackingStatus(userId, DeviceListManager.TRACKING_STATUS_NOT_TRACKED) + } + + /** + * Tell if the MXCrypto is started + * + * @return true if the crypto is started + */ + fun isStarted(): Boolean { + return isStarted.get() + } + + /** + * Tells if the MXCrypto is starting. + * + * @return true if the crypto is starting + */ + fun isStarting(): Boolean { + return isStarting.get() + } + + /** + * Start the crypto module. + * Device keys will be uploaded, then one time keys if there are not enough on the homeserver + * and, then, if this is the first time, this new device will be announced to all other users + * devices. + * + */ + fun start() { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + internalStart() + } + // Just update + fetchDevicesList(NoOpMatrixCallback()) + } + + fun ensureDevice() { + cryptoCoroutineScope.launchToCallback(coroutineDispatchers.crypto, NoOpMatrixCallback()) { + // Open the store + cryptoStore.open() + // this can throw if no network + tryThis { + uploadDeviceKeys() + } + + oneTimeKeysUploader.maybeUploadOneTimeKeys() + // this can throw if no backup + tryThis { + keysBackupService.checkAndStartKeysBackup() + } + } + } + + fun onSyncWillProcess(isInitialSync: Boolean) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + if (isInitialSync) { + try { + // On initial sync, we start all our tracking from + // scratch, so mark everything as untracked. onCryptoEvent will + // be called for all e2e rooms during the processing of the sync, + // at which point we'll start tracking all the users of that room. + deviceListManager.invalidateAllDeviceLists() + // always track my devices? + deviceListManager.startTrackingDeviceList(listOf(userId)) + deviceListManager.refreshOutdatedDeviceLists() + } catch (failure: Throwable) { + Timber.e(failure, "## CRYPTO onSyncWillProcess ") + } + } + } + } + + private fun internalStart() { + if (isStarted.get() || isStarting.get()) { + return + } + isStarting.set(true) + + // Open the store + cryptoStore.open() + + runCatching { +// if (isInitialSync) { +// // refresh the devices list for each known room members +// deviceListManager.invalidateAllDeviceLists() +// deviceListManager.refreshOutdatedDeviceLists() +// } else { + + // Why would we do that? it will be called at end of syn + incomingGossipingRequestManager.processReceivedGossipingRequests() +// } + }.fold( + { + isStarting.set(false) + isStarted.set(true) + }, + { + isStarting.set(false) + isStarted.set(false) + Timber.e(it, "Start failed") + } + ) + } + + /** + * Close the crypto + */ + fun close() = runBlocking(coroutineDispatchers.crypto) { + cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module")) + + olmDevice.release() + cryptoStore.close() + } + + // Aways enabled on RiotX + override fun isCryptoEnabled() = true + + /** + * @return the Keys backup Service + */ + override fun keysBackupService() = keysBackupService + + /** + * @return the VerificationService + */ + override fun verificationService() = verificationService + + override fun crossSigningService() = crossSigningService + + /** + * A sync response has been received + * + * @param syncResponse the syncResponse + */ + fun onSyncCompleted(syncResponse: SyncResponse) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + runCatching { + if (syncResponse.deviceLists != null) { + deviceListManager.handleDeviceListsChanges(syncResponse.deviceLists.changed, syncResponse.deviceLists.left) + } + if (syncResponse.deviceOneTimeKeysCount != null) { + val currentCount = syncResponse.deviceOneTimeKeysCount.signedCurve25519 ?: 0 + oneTimeKeysUploader.updateOneTimeKeyCount(currentCount) + } + if (isStarted()) { + // Make sure we process to-device messages before generating new one-time-keys #2782 + deviceListManager.refreshOutdatedDeviceLists() + oneTimeKeysUploader.maybeUploadOneTimeKeys() + incomingGossipingRequestManager.processReceivedGossipingRequests() + } + } + } + } + + /** + * Find a device by curve25519 identity key + * + * @param senderKey the curve25519 key to match. + * @param algorithm the encryption algorithm. + * @return the device info, or null if not found / unsupported algorithm / crypto released + */ + override fun deviceWithIdentityKey(senderKey: String, algorithm: String): CryptoDeviceInfo? { + return if (algorithm != MXCRYPTO_ALGORITHM_MEGOLM && algorithm != MXCRYPTO_ALGORITHM_OLM) { + // We only deal in olm keys + null + } else cryptoStore.deviceWithIdentityKey(senderKey) + } + + /** + * Provides the device information for a user id and a device Id + * + * @param userId the user id + * @param deviceId the device id + */ + override fun getDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo? { + return if (userId.isNotEmpty() && !deviceId.isNullOrEmpty()) { + cryptoStore.getUserDevice(userId, deviceId) + } else { + null + } + } + + override fun getCryptoDeviceInfo(userId: String): List { + return cryptoStore.getUserDeviceList(userId).orEmpty() + } + + override fun getLiveCryptoDeviceInfo(): LiveData> { + return cryptoStore.getLiveDeviceList() + } + + override fun getLiveCryptoDeviceInfo(userId: String): LiveData> { + return cryptoStore.getLiveDeviceList(userId) + } + + override fun getLiveCryptoDeviceInfo(userIds: List): LiveData> { + return cryptoStore.getLiveDeviceList(userIds) + } + + /** + * Set the devices as known + * + * @param devices the devices. Note that the verified member of the devices in this list will not be updated by this method. + * @param callback the asynchronous callback + */ + override fun setDevicesKnown(devices: List, callback: MatrixCallback?) { + // build a devices map + val devicesIdListByUserId = devices.groupBy({ it.userId }, { it.deviceId }) + + for ((userId, deviceIds) in devicesIdListByUserId) { + val storedDeviceIDs = cryptoStore.getUserDevices(userId) + + // sanity checks + if (null != storedDeviceIDs) { + var isUpdated = false + + deviceIds.forEach { deviceId -> + val device = storedDeviceIDs[deviceId] + + // assume if the device is either verified or blocked + // it means that the device is known + if (device?.isUnknown == true) { + device.trustLevel = DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false) + isUpdated = true + } + } + + if (isUpdated) { + cryptoStore.storeUserDevices(userId, storedDeviceIDs) + } + } + } + + callback?.onSuccess(Unit) + } + + /** + * Update the blocked/verified state of the given device. + * + * @param trustLevel the new trust level + * @param userId the owner of the device + * @param deviceId the unique identifier for the device. + */ + override fun setDeviceVerification(trustLevel: DeviceTrustLevel, userId: String, deviceId: String) { + setDeviceVerificationAction.handle(trustLevel, userId, deviceId) + } + + /** + * Configure a room to use encryption. + * + * @param roomId the room id to enable encryption in. + * @param algorithm the encryption config for the room. + * @param inhibitDeviceQuery true to suppress device list query for users in the room (for now) + * @param membersId list of members to start tracking their devices + * @return true if the operation succeeds. + */ + private suspend fun setEncryptionInRoom(roomId: String, + algorithm: String?, + inhibitDeviceQuery: Boolean, + membersId: List): Boolean { + // If we already have encryption in this room, we should ignore this event + // (for now at least. Maybe we should alert the user somehow?) + val existingAlgorithm = cryptoStore.getRoomAlgorithm(roomId) + + if (!existingAlgorithm.isNullOrEmpty() && existingAlgorithm != algorithm) { + Timber.e("## CRYPTO | setEncryptionInRoom() : Ignoring m.room.encryption event which requests a change of config in $roomId") + return false + } + + val encryptingClass = MXCryptoAlgorithms.hasEncryptorClassForAlgorithm(algorithm) + + if (!encryptingClass) { + Timber.e("## CRYPTO | setEncryptionInRoom() : Unable to encrypt room $roomId with $algorithm") + return false + } + + cryptoStore.storeRoomAlgorithm(roomId, algorithm!!) + + val alg: IMXEncrypting = when (algorithm) { + MXCRYPTO_ALGORITHM_MEGOLM -> megolmEncryptionFactory.create(roomId) + else -> olmEncryptionFactory.create(roomId) + } + + roomEncryptorsStore.put(roomId, alg) + + // if encryption was not previously enabled in this room, we will have been + // ignoring new device events for these users so far. We may well have + // up-to-date lists for some users, for instance if we were sharing other + // e2e rooms with them, so there is room for optimisation here, but for now + // we just invalidate everyone in the room. + if (null == existingAlgorithm) { + Timber.v("Enabling encryption in $roomId for the first time; invalidating device lists for all users therein") + + val userIds = ArrayList(membersId) + + deviceListManager.startTrackingDeviceList(userIds) + + if (!inhibitDeviceQuery) { + deviceListManager.refreshOutdatedDeviceLists() + } + } + + return true + } + + /** + * Tells if a room is encrypted with MXCRYPTO_ALGORITHM_MEGOLM + * + * @param roomId the room id + * @return true if the room is encrypted with algorithm MXCRYPTO_ALGORITHM_MEGOLM + */ + override fun isRoomEncrypted(roomId: String): Boolean { + val encryptionEvent = monarchy.fetchCopied { realm -> + EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION) + .contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"") + .findFirst() + } + return encryptionEvent != null + } + + /** + * @return the stored device keys for a user. + */ + override fun getUserDevices(userId: String): MutableList { + return cryptoStore.getUserDevices(userId)?.values?.toMutableList() ?: ArrayList() + } + + private fun isEncryptionEnabledForInvitedUser(): Boolean { + return mxCryptoConfig.enableEncryptionForInvitedMembers + } + + override fun getEncryptionAlgorithm(roomId: String): String? { + return cryptoStore.getRoomAlgorithm(roomId) + } + + /** + * Determine whether we should encrypt messages for invited users in this room. + *

+ * Check here whether the invited members are allowed to read messages in the room history + * from the point they were invited onwards. + * + * @return true if we should encrypt messages for invited users. + */ + override fun shouldEncryptForInvitedMembers(roomId: String): Boolean { + return cryptoStore.shouldEncryptForInvitedMembers(roomId) + } + + /** + * Encrypt an event content according to the configuration of the room. + * + * @param eventContent the content of the event. + * @param eventType the type of the event. + * @param roomId the room identifier the event will be sent. + * @param callback the asynchronous callback + */ + override fun encryptEventContent(eventContent: Content, + eventType: String, + roomId: String, + callback: MatrixCallback) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { +// if (!isStarted()) { +// Timber.v("## CRYPTO | encryptEventContent() : wait after e2e init") +// internalStart(false) +// } + val userIds = getRoomUserIds(roomId) + var alg = roomEncryptorsStore.get(roomId) + if (alg == null) { + val algorithm = getEncryptionAlgorithm(roomId) + if (algorithm != null) { + if (setEncryptionInRoom(roomId, algorithm, false, userIds)) { + alg = roomEncryptorsStore.get(roomId) + } + } + } + val safeAlgorithm = alg + if (safeAlgorithm != null) { + val t0 = System.currentTimeMillis() + Timber.v("## CRYPTO | encryptEventContent() starts") + runCatching { + val content = safeAlgorithm.encryptEventContent(eventContent, eventType, userIds) + Timber.v("## CRYPTO | encryptEventContent() : succeeds after ${System.currentTimeMillis() - t0} ms") + MXEncryptEventContentResult(content, EventType.ENCRYPTED) + }.foldToCallback(callback) + } else { + val algorithm = getEncryptionAlgorithm(roomId) + val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON, + algorithm ?: MXCryptoError.NO_MORE_ALGORITHM_REASON) + Timber.e("## CRYPTO | encryptEventContent() : $reason") + callback.onFailure(Failure.CryptoError(MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_ENCRYPT, reason))) + } + } + } + + override fun discardOutboundSession(roomId: String) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + roomEncryptorsStore.get(roomId)?.discardSessionKey() + } + } + + /** + * Decrypt an event + * + * @param event the raw event. + * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. + * @return the MXEventDecryptionResult data, or throw in case of error + */ + @Throws(MXCryptoError::class) + override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { + return internalDecryptEvent(event, timeline) + } + + /** + * Decrypt an event asynchronously + * + * @param event the raw event. + * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. + * @param callback the callback to return data or null + */ + override fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback) { + cryptoCoroutineScope.launch { + val result = runCatching { + withContext(coroutineDispatchers.crypto) { + internalDecryptEvent(event, timeline) + } + } + result.foldToCallback(callback) + } + } + + /** + * Decrypt an event + * + * @param event the raw event. + * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. + * @return the MXEventDecryptionResult data, or null in case of error + */ + @Throws(MXCryptoError::class) + private fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult { + val eventContent = event.content + if (eventContent == null) { + Timber.e("## CRYPTO | decryptEvent : empty event content") + throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON) + } else { + val algorithm = eventContent["algorithm"]?.toString() + val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm) + if (alg == null) { + val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm) + Timber.e("## CRYPTO | decryptEvent() : $reason") + throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason) + } else { + try { + return alg.decryptEvent(event, timeline) + } catch (mxCryptoError: MXCryptoError) { + Timber.d("## CRYPTO | internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError") + if (algorithm == MXCRYPTO_ALGORITHM_OLM) { + if (mxCryptoError is MXCryptoError.Base + && mxCryptoError.errorType == MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE) { + // need to find sending device + val olmContent = event.content.toModel() + cryptoStore.getUserDevices(event.senderId ?: "") + ?.values + ?.firstOrNull { it.identityKey() == olmContent?.senderKey } + ?.let { + markOlmSessionForUnwedging(event.senderId ?: "", it) + } + ?: run { + Timber.v("## CRYPTO | markOlmSessionForUnwedging() : Failed to find sender crypto device") + } + } + } + throw mxCryptoError + } + } + } + } + + /** + * Reset replay attack data for the given timeline. + * + * @param timelineId the timeline id + */ + fun resetReplayAttackCheckInTimeline(timelineId: String) { + olmDevice.resetReplayAttackCheckInTimeline(timelineId) + } + + /** + * Handle the 'toDevice' event + * + * @param event the event + */ + fun onToDeviceEvent(event: Event) { + // event have already been decrypted + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + when (event.getClearType()) { + EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> { + cryptoStore.saveGossipingEvent(event) + // Keys are imported directly, not waiting for end of sync + onRoomKeyEvent(event) + } + EventType.REQUEST_SECRET, + EventType.ROOM_KEY_REQUEST -> { + // save audit trail + cryptoStore.saveGossipingEvent(event) + // Requests are stacked, and will be handled one by one at the end of the sync (onSyncComplete) + incomingGossipingRequestManager.onGossipingRequestEvent(event) + } + EventType.SEND_SECRET -> { + cryptoStore.saveGossipingEvent(event) + onSecretSendReceived(event) + } + EventType.ROOM_KEY_WITHHELD -> { + onKeyWithHeldReceived(event) + } + else -> { + // ignore + } + } + } + } + + /** + * Handle a key event. + * + * @param event the key event. + */ + private fun onRoomKeyEvent(event: Event) { + val roomKeyContent = event.getClearContent().toModel() ?: return + Timber.v("## CRYPTO | GOSSIP onRoomKeyEvent() : type<${event.type}> , sessionId<${roomKeyContent.sessionId}>") + if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) { + Timber.e("## CRYPTO | GOSSIP onRoomKeyEvent() : missing fields") + return + } + val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(roomKeyContent.roomId, roomKeyContent.algorithm) + if (alg == null) { + Timber.e("## CRYPTO | GOSSIP onRoomKeyEvent() : Unable to handle keys for ${roomKeyContent.algorithm}") + return + } + alg.onRoomKeyEvent(event, keysBackupService) + } + + private fun onKeyWithHeldReceived(event: Event) { + val withHeldContent = event.getClearContent().toModel() ?: return Unit.also { + Timber.e("## CRYPTO | Malformed onKeyWithHeldReceived() : missing fields") + } + Timber.d("## CRYPTO | onKeyWithHeldReceived() received : content <$withHeldContent>") + val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(withHeldContent.roomId, withHeldContent.algorithm) + if (alg is IMXWithHeldExtension) { + alg.onRoomKeyWithHeldEvent(withHeldContent) + } else { + Timber.e("## CRYPTO | onKeyWithHeldReceived() : Unable to handle WithHeldContent for ${withHeldContent.algorithm}") + return + } + } + + private fun onSecretSendReceived(event: Event) { + Timber.i("## CRYPTO | GOSSIP onSecretSend() : onSecretSendReceived ${event.content?.get("sender_key")}") + if (!event.isEncrypted()) { + // secret send messages must be encrypted + Timber.e("## CRYPTO | GOSSIP onSecretSend() :Received unencrypted secret send event") + return + } + + // Was that sent by us? + if (event.senderId != userId) { + Timber.e("## CRYPTO | GOSSIP onSecretSend() : Ignore secret from other user ${event.senderId}") + return + } + + val secretContent = event.getClearContent().toModel() ?: return + + val existingRequest = cryptoStore + .getOutgoingSecretKeyRequests().firstOrNull { it.requestId == secretContent.requestId } + + if (existingRequest == null) { + Timber.i("## CRYPTO | GOSSIP onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}") + return + } + + if (!handleSDKLevelGossip(existingRequest.secretName, secretContent.secretValue)) { + // TODO Ask to application layer? + Timber.v("## CRYPTO | onSecretSend() : secret not handled by SDK") + } + } + + /** + * Returns true if handled by SDK, otherwise should be sent to application layer + */ + private fun handleSDKLevelGossip(secretName: String?, secretValue: String): Boolean { + return when (secretName) { + MASTER_KEY_SSSS_NAME -> { + crossSigningService.onSecretMSKGossip(secretValue) + true + } + SELF_SIGNING_KEY_SSSS_NAME -> { + crossSigningService.onSecretSSKGossip(secretValue) + true + } + USER_SIGNING_KEY_SSSS_NAME -> { + crossSigningService.onSecretUSKGossip(secretValue) + true + } + KEYBACKUP_SECRET_SSSS_NAME -> { + keysBackupService.onSecretKeyGossip(secretValue) + true + } + else -> false + } + } + + /** + * Handle an m.room.encryption event. + * + * @param event the encryption event. + */ + private fun onRoomEncryptionEvent(roomId: String, event: Event) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + val params = LoadRoomMembersTask.Params(roomId) + try { + loadRoomMembersTask.execute(params) + } catch (throwable: Throwable) { + Timber.e(throwable, "## CRYPTO | onRoomEncryptionEvent ERROR FAILED TO SETUP CRYPTO ") + } finally { + val userIds = getRoomUserIds(roomId) + setEncryptionInRoom(roomId, event.content?.get("algorithm")?.toString(), true, userIds) + } + } + } + + private fun getRoomUserIds(roomId: String): List { + var userIds: List = emptyList() + monarchy.doWithRealm { realm -> + // Check whether the event content must be encrypted for the invited members. + val encryptForInvitedMembers = isEncryptionEnabledForInvitedUser() + && shouldEncryptForInvitedMembers(roomId) + + userIds = if (encryptForInvitedMembers) { + RoomMemberHelper(realm, roomId).getActiveRoomMemberIds() + } else { + RoomMemberHelper(realm, roomId).getJoinedRoomMemberIds() + } + } + return userIds + } + + /** + * Handle a change in the membership state of a member of a room. + * + * @param event the membership event causing the change + */ + private fun onRoomMembershipEvent(roomId: String, event: Event) { + roomEncryptorsStore.get(roomId) ?: /* No encrypting in this room */ return + + event.stateKey?.let { userId -> + val roomMember: RoomMemberSummary? = event.content.toModel() + val membership = roomMember?.membership + if (membership == Membership.JOIN) { + // make sure we are tracking the deviceList for this user. + deviceListManager.startTrackingDeviceList(listOf(userId)) + } else if (membership == Membership.INVITE + && shouldEncryptForInvitedMembers(roomId) + && isEncryptionEnabledForInvitedUser()) { + // track the deviceList for this invited user. + // Caution: there's a big edge case here in that federated servers do not + // know what other servers are in the room at the time they've been invited. + // They therefore will not send device updates if a user logs in whilst + // their state is invite. + deviceListManager.startTrackingDeviceList(listOf(userId)) + } + } + } + + private fun onRoomHistoryVisibilityEvent(roomId: String, event: Event) { + val eventContent = event.content.toModel() + eventContent?.historyVisibility?.let { + cryptoStore.setShouldEncryptForInvitedMembers(roomId, it != RoomHistoryVisibility.JOINED) + } + } + + /** + * Upload my user's device keys. + */ + private suspend fun uploadDeviceKeys() { + if (cryptoStore.getDeviceKeysUploaded()) { + Timber.d("Keys already uploaded, nothing to do") + return + } + // Prepare the device keys data to send + // Sign it + val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, getMyDevice().signalableJSONDictionary()) + var rest = getMyDevice().toRest() + + rest = rest.copy( + signatures = objectSigner.signObject(canonicalJson) + ) + + val uploadDeviceKeysParams = UploadKeysTask.Params(rest, null) + uploadKeysTask.execute(uploadDeviceKeysParams) + + cryptoStore.setDeviceKeysUploaded(true) + } + + /** + * Export the crypto keys + * + * @param password the password + * @param callback the exported keys + */ + override fun exportRoomKeys(password: String, callback: MatrixCallback) { + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + runCatching { + exportRoomKeys(password, MXMegolmExportEncryption.DEFAULT_ITERATION_COUNT) + }.foldToCallback(callback) + } + } + + /** + * Export the crypto keys + * + * @param password the password + * @param anIterationCount the encryption iteration count (0 means no encryption) + */ + private suspend fun exportRoomKeys(password: String, anIterationCount: Int): ByteArray { + return withContext(coroutineDispatchers.crypto) { + val iterationCount = max(0, anIterationCount) + + val exportedSessions = cryptoStore.getInboundGroupSessions().mapNotNull { it.exportKeys() } + + val adapter = MoshiProvider.providesMoshi() + .adapter(List::class.java) + + MXMegolmExportEncryption.encryptMegolmKeyFile(adapter.toJson(exportedSessions), password, iterationCount) + } + } + + /** + * Import the room keys + * + * @param roomKeysAsArray the room keys as array. + * @param password the password + * @param progressListener the progress listener + * @param callback the asynchronous callback. + */ + override fun importRoomKeys(roomKeysAsArray: ByteArray, + password: String, + progressListener: ProgressListener?, + callback: MatrixCallback) { + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + runCatching { + withContext(coroutineDispatchers.crypto) { + Timber.v("## CRYPTO | importRoomKeys starts") + + val t0 = System.currentTimeMillis() + val roomKeys = MXMegolmExportEncryption.decryptMegolmKeyFile(roomKeysAsArray, password) + val t1 = System.currentTimeMillis() + + Timber.v("## CRYPTO | importRoomKeys : decryptMegolmKeyFile done in ${t1 - t0} ms") + + val importedSessions = MoshiProvider.providesMoshi() + .adapter>(Types.newParameterizedType(List::class.java, MegolmSessionData::class.java)) + .fromJson(roomKeys) + + val t2 = System.currentTimeMillis() + + Timber.v("## CRYPTO | importRoomKeys : JSON parsing ${t2 - t1} ms") + + if (importedSessions == null) { + throw Exception("Error") + } + + megolmSessionDataImporter.handle(importedSessions, true, progressListener) + } + }.foldToCallback(callback) + } + } + + /** + * Update the warn status when some unknown devices are detected. + * + * @param warn true to warn when some unknown devices are detected. + */ + override fun setWarnOnUnknownDevices(warn: Boolean) { + warnOnUnknownDevicesRepository.setWarnOnUnknownDevices(warn) + } + + /** + * Check if the user ids list have some unknown devices. + * A success means there is no unknown devices. + * If there are some unknown devices, a MXCryptoError.UnknownDevice exception is triggered. + * + * @param userIds the user ids list + * @param callback the asynchronous callback. + */ + fun checkUnknownDevices(userIds: List, callback: MatrixCallback) { + // force the refresh to ensure that the devices list is up-to-date + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + runCatching { + val keys = deviceListManager.downloadKeys(userIds, true) + val unknownDevices = getUnknownDevices(keys) + if (unknownDevices.map.isNotEmpty()) { + // trigger an an unknown devices exception + throw Failure.CryptoError(MXCryptoError.UnknownDevice(unknownDevices)) + } + }.foldToCallback(callback) + } + } + + /** + * Set the global override for whether the client should ever send encrypted + * messages to unverified devices. + * If false, it can still be overridden per-room. + * If true, it overrides the per-room settings. + * + * @param block true to unilaterally blacklist all + */ + override fun setGlobalBlacklistUnverifiedDevices(block: Boolean) { + cryptoStore.setGlobalBlacklistUnverifiedDevices(block) + } + + /** + * Tells whether the client should ever send encrypted messages to unverified devices. + * The default value is false. + * This function must be called in the getEncryptingThreadHandler() thread. + * + * @return true to unilaterally blacklist all unverified devices. + */ + override fun getGlobalBlacklistUnverifiedDevices(): Boolean { + return cryptoStore.getGlobalBlacklistUnverifiedDevices() + } + + /** + * Tells whether the client should encrypt messages only for the verified devices + * in this room. + * The default value is false. + * + * @param roomId the room id + * @return true if the client should encrypt messages only for the verified devices. + */ +// TODO add this info in CryptoRoomEntity? + override fun isRoomBlacklistUnverifiedDevices(roomId: String?): Boolean { + return roomId?.let { cryptoStore.getRoomsListBlacklistUnverifiedDevices().contains(it) } + ?: false + } + + /** + * Manages the room black-listing for unverified devices. + * + * @param roomId the room id + * @param add true to add the room id to the list, false to remove it. + */ + private fun setRoomBlacklistUnverifiedDevices(roomId: String, add: Boolean) { + val roomIds = cryptoStore.getRoomsListBlacklistUnverifiedDevices().toMutableList() + + if (add) { + if (roomId !in roomIds) { + roomIds.add(roomId) + } + } else { + roomIds.remove(roomId) + } + + cryptoStore.setRoomsListBlacklistUnverifiedDevices(roomIds) + } + + /** + * Add this room to the ones which don't encrypt messages to unverified devices. + * + * @param roomId the room id + */ + override fun setRoomBlacklistUnverifiedDevices(roomId: String) { + setRoomBlacklistUnverifiedDevices(roomId, true) + } + + /** + * Remove this room to the ones which don't encrypt messages to unverified devices. + * + * @param roomId the room id + */ + override fun setRoomUnBlacklistUnverifiedDevices(roomId: String) { + setRoomBlacklistUnverifiedDevices(roomId, false) + } + +// TODO Check if this method is still necessary + /** + * Cancel any earlier room key request + * + * @param requestBody requestBody + */ + override fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) { + outgoingGossipingRequestManager.cancelRoomKeyRequest(requestBody) + } + + /** + * Re request the encryption keys required to decrypt an event. + * + * @param event the event to decrypt again. + */ + override fun reRequestRoomKeyForEvent(event: Event) { + val wireContent = event.content.toModel() ?: return Unit.also { + Timber.e("## CRYPTO | reRequestRoomKeyForEvent Failed to re-request key, null content") + } + + val requestBody = RoomKeyRequestBody( + algorithm = wireContent.algorithm, + roomId = event.roomId, + senderKey = wireContent.senderKey, + sessionId = wireContent.sessionId + ) + + outgoingGossipingRequestManager.resendRoomKeyRequest(requestBody) + } + + override fun requestRoomKeyForEvent(event: Event) { + val wireContent = event.content.toModel() ?: return Unit.also { + Timber.e("## CRYPTO | requestRoomKeyForEvent Failed to request key, null content eventId: ${event.eventId}") + } + + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { +// if (!isStarted()) { +// Timber.v("## CRYPTO | requestRoomKeyForEvent() : wait after e2e init") +// internalStart(false) +// } + roomDecryptorProvider + .getOrCreateRoomDecryptor(event.roomId, wireContent.algorithm) + ?.requestKeysForEvent(event, false) ?: run { + Timber.v("## CRYPTO | requestRoomKeyForEvent() : No room decryptor for roomId:${event.roomId} algorithm:${wireContent.algorithm}") + } + } + } + + /** + * Add a GossipingRequestListener listener. + * + * @param listener listener + */ + override fun addRoomKeysRequestListener(listener: GossipingRequestListener) { + incomingGossipingRequestManager.addRoomKeysRequestListener(listener) + } + + /** + * Add a GossipingRequestListener listener. + * + * @param listener listener + */ + override fun removeRoomKeysRequestListener(listener: GossipingRequestListener) { + incomingGossipingRequestManager.removeRoomKeysRequestListener(listener) + } + + private fun markOlmSessionForUnwedging(senderId: String, deviceInfo: CryptoDeviceInfo) { + val deviceKey = deviceInfo.identityKey() + + val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0 + val now = System.currentTimeMillis() + if (now - lastForcedDate < CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) { + Timber.d("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another") + return + } + + Timber.d("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}") + lastNewSessionForcedDates.setObject(senderId, deviceKey, now) + + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true) + + // Now send a blank message on that session so the other side knows about it. + // (The keyshare request is sent in the clear so that won't do) + // We send this first such that, as long as the toDevice messages arrive in the + // same order we sent them, the other end will get this first, set up the new session, + // then get the keyshare request and send the key over this new session (because it + // is the session it has most recently received a message on). + val payloadJson = mapOf("type" to EventType.DUMMY) + + val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) + val sendToDeviceMap = MXUsersDevicesMap() + sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload) + Timber.v("## CRYPTO | markOlmSessionForUnwedging() : sending to $senderId:${deviceInfo.deviceId}") + val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) + sendToDeviceTask.execute(sendToDeviceParams) + } + } + + /** + * Provides the list of unknown devices + * + * @param devicesInRoom the devices map + * @return the unknown devices map + */ + private fun getUnknownDevices(devicesInRoom: MXUsersDevicesMap): MXUsersDevicesMap { + val unknownDevices = MXUsersDevicesMap() + val userIds = devicesInRoom.userIds + for (userId in userIds) { + devicesInRoom.getUserDeviceIds(userId)?.forEach { deviceId -> + devicesInRoom.getObject(userId, deviceId) + ?.takeIf { it.isUnknown } + ?.let { + unknownDevices.setObject(userId, deviceId, it) + } + } + } + + return unknownDevices + } + + override fun downloadKeys(userIds: List, forceDownload: Boolean, callback: MatrixCallback>) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + runCatching { + deviceListManager.downloadKeys(userIds, forceDownload) + }.foldToCallback(callback) + } + } + + override fun addNewSessionListener(newSessionListener: NewSessionListener) { + roomDecryptorProvider.addNewSessionListener(newSessionListener) + } + + override fun removeSessionListener(listener: NewSessionListener) { + roomDecryptorProvider.removeSessionListener(listener) + } +/* ========================================================================================== + * DEBUG INFO + * ========================================================================================== */ + + override fun toString(): String { + return "DefaultCryptoService of $userId ($deviceId)" + } + + override fun getOutgoingRoomKeyRequests(): List { + return cryptoStore.getOutgoingRoomKeyRequests() + } + + override fun getIncomingRoomKeyRequests(): List { + return cryptoStore.getIncomingRoomKeyRequests() + } + + override fun getGossipingEventsTrail(): List { + return cryptoStore.getGossipingEventsTrail() + } + + override fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap { + return cryptoStore.getSharedWithInfo(roomId, sessionId) + } + + override fun getWithHeldMegolmSession(roomId: String, sessionId: String): RoomKeyWithHeldContent? { + return cryptoStore.getWithHeldMegolmSession(roomId, sessionId) + } + /* ========================================================================================== + * For test only + * ========================================================================================== */ + + @VisibleForTesting + val cryptoStoreForTesting = cryptoStore + + companion object { + const val CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS = 3_600_000 // one hour + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt new file mode 100755 index 0000000000..bb41edefe1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt @@ -0,0 +1,547 @@ +/* + * Copyright 2017 Vector Creations Ltd + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto + +import org.matrix.android.sdk.api.MatrixPatterns +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.CryptoInfoMapper +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.tasks.DownloadKeysForUsersTask +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.sync.SyncTokenStore +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +// Legacy name: MXDeviceList +@SessionScope +internal class DeviceListManager @Inject constructor(private val cryptoStore: IMXCryptoStore, + private val olmDevice: MXOlmDevice, + private val syncTokenStore: SyncTokenStore, + private val credentials: Credentials, + private val downloadKeysForUsersTask: DownloadKeysForUsersTask, + coroutineDispatchers: MatrixCoroutineDispatchers, + taskExecutor: TaskExecutor) { + + interface UserDevicesUpdateListener { + fun onUsersDeviceUpdate(userIds: List) + } + + private val deviceChangeListeners = mutableListOf() + + fun addListener(listener: UserDevicesUpdateListener) { + synchronized(deviceChangeListeners) { + deviceChangeListeners.add(listener) + } + } + + fun removeListener(listener: UserDevicesUpdateListener) { + synchronized(deviceChangeListeners) { + deviceChangeListeners.remove(listener) + } + } + + private fun dispatchDeviceChange(users: List) { + synchronized(deviceChangeListeners) { + deviceChangeListeners.forEach { + try { + it.onUsersDeviceUpdate(users) + } catch (failure: Throwable) { + Timber.e(failure, "Failed to dispatch device change") + } + } + } + } + + // HS not ready for retry + private val notReadyToRetryHS = mutableSetOf() + + init { + taskExecutor.executorScope.launch(coroutineDispatchers.crypto) { + var isUpdated = false + val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap() + for ((userId, status) in deviceTrackingStatuses) { + if (TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == status || TRACKING_STATUS_UNREACHABLE_SERVER == status) { + // if a download was in progress when we got shut down, it isn't any more. + deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD + isUpdated = true + } + } + if (isUpdated) { + cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses) + } + } + } + + /** + * Tells if the key downloads should be tried + * + * @param userId the userId + * @return true if the keys download can be retrieved + */ + private fun canRetryKeysDownload(userId: String): Boolean { + var res = false + + if (':' in userId) { + try { + synchronized(notReadyToRetryHS) { + res = !notReadyToRetryHS.contains(userId.substringAfterLast(':')) + } + } catch (e: Exception) { + Timber.e(e, "## CRYPTO | canRetryKeysDownload() failed") + } + } + + return res + } + + /** + * Clear the unavailable server lists + */ + private fun clearUnavailableServersList() { + synchronized(notReadyToRetryHS) { + notReadyToRetryHS.clear() + } + } + + /** + * Mark the cached device list for the given user outdated + * flag the given user for device-list tracking, if they are not already. + * + * @param userIds the user ids list + */ + fun startTrackingDeviceList(userIds: List?) { + if (null != userIds) { + var isUpdated = false + val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap() + + for (userId in userIds) { + if (!deviceTrackingStatuses.containsKey(userId) || TRACKING_STATUS_NOT_TRACKED == deviceTrackingStatuses[userId]) { + Timber.v("## CRYPTO | startTrackingDeviceList() : Now tracking device list for $userId") + deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD + isUpdated = true + } + } + + if (isUpdated) { + cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses) + } + } + } + + /** + * Update the devices list statuses + * + * @param changed the user ids list which have new devices + * @param left the user ids list which left a room + */ + fun handleDeviceListsChanges(changed: Collection, left: Collection) { + Timber.v("## CRYPTO: handleDeviceListsChanges changed:$changed / left:$left") + var isUpdated = false + val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap() + + for (userId in changed) { + if (deviceTrackingStatuses.containsKey(userId)) { + Timber.v("## CRYPTO | invalidateUserDeviceList() : Marking device list outdated for $userId") + deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD + isUpdated = true + } + } + + for (userId in left) { + if (deviceTrackingStatuses.containsKey(userId)) { + Timber.v("## CRYPTO | invalidateUserDeviceList() : No longer tracking device list for $userId") + deviceTrackingStatuses[userId] = TRACKING_STATUS_NOT_TRACKED + isUpdated = true + } + } + + if (isUpdated) { + cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses) + } + } + + /** + * This will flag each user whose devices we are tracking as in need of an + * + update + */ + fun invalidateAllDeviceLists() { + handleDeviceListsChanges(cryptoStore.getDeviceTrackingStatuses().keys, emptyList()) + } + + /** + * The keys download failed + * + * @param userIds the user ids list + */ + private fun onKeysDownloadFailed(userIds: List) { + val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap() + userIds.associateWithTo(deviceTrackingStatuses) { TRACKING_STATUS_PENDING_DOWNLOAD } + cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses) + } + + /** + * The keys download succeeded. + * + * @param userIds the userIds list + * @param failures the failure map. + */ + private fun onKeysDownloadSucceed(userIds: List, failures: Map>?): MXUsersDevicesMap { + if (failures != null) { + for ((k, value) in failures) { + val statusCode = when (val status = value["status"]) { + is Double -> status.toInt() + is Int -> status.toInt() + else -> 0 + } + if (statusCode == 503) { + synchronized(notReadyToRetryHS) { + notReadyToRetryHS.add(k) + } + } + } + } + val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap() + val usersDevicesInfoMap = MXUsersDevicesMap() + for (userId in userIds) { + val devices = cryptoStore.getUserDevices(userId) + if (null == devices) { + if (canRetryKeysDownload(userId)) { + deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD + Timber.e("failed to retry the devices of $userId : retry later") + } else { + if (deviceTrackingStatuses.containsKey(userId) && TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == deviceTrackingStatuses[userId]) { + deviceTrackingStatuses[userId] = TRACKING_STATUS_UNREACHABLE_SERVER + Timber.e("failed to retry the devices of $userId : the HS is not available") + } + } + } else { + if (deviceTrackingStatuses.containsKey(userId) && TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == deviceTrackingStatuses[userId]) { + // we didn't get any new invalidations since this download started: + // this user's device list is now up to date. + deviceTrackingStatuses[userId] = TRACKING_STATUS_UP_TO_DATE + Timber.v("Device list for $userId now up to date") + } + // And the response result + usersDevicesInfoMap.setObjects(userId, devices) + } + } + cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses) + + dispatchDeviceChange(userIds) + return usersDevicesInfoMap + } + + /** + * Download the device keys for a list of users and stores the keys in the MXStore. + * It must be called in getEncryptingThreadHandler() thread. + * + * @param userIds The users to fetch. + * @param forceDownload Always download the keys even if cached. + */ + suspend fun downloadKeys(userIds: List?, forceDownload: Boolean): MXUsersDevicesMap { + Timber.v("## CRYPTO | downloadKeys() : forceDownload $forceDownload : $userIds") + // Map from userId -> deviceId -> MXDeviceInfo + val stored = MXUsersDevicesMap() + + // List of user ids we need to download keys for + val downloadUsers = ArrayList() + if (null != userIds) { + if (forceDownload) { + downloadUsers.addAll(userIds) + } else { + for (userId in userIds) { + val status = cryptoStore.getDeviceTrackingStatus(userId, TRACKING_STATUS_NOT_TRACKED) + // downloading keys ->the keys download won't be triggered twice but the callback requires the dedicated keys + // not yet retrieved + if (TRACKING_STATUS_UP_TO_DATE != status && TRACKING_STATUS_UNREACHABLE_SERVER != status) { + downloadUsers.add(userId) + } else { + val devices = cryptoStore.getUserDevices(userId) + // should always be true + if (devices != null) { + stored.setObjects(userId, devices) + } else { + downloadUsers.add(userId) + } + } + } + } + } + return if (downloadUsers.isEmpty()) { + Timber.v("## CRYPTO | downloadKeys() : no new user device") + stored + } else { + Timber.v("## CRYPTO | downloadKeys() : starts") + val t0 = System.currentTimeMillis() + val result = doKeyDownloadForUsers(downloadUsers) + Timber.v("## CRYPTO | downloadKeys() : doKeyDownloadForUsers succeeds after ${System.currentTimeMillis() - t0} ms") + result.also { + it.addEntriesFromMap(stored) + } + } + } + + /** + * Download the devices keys for a set of users. + * + * @param downloadUsers the user ids list + */ + private suspend fun doKeyDownloadForUsers(downloadUsers: List): MXUsersDevicesMap { + Timber.v("## CRYPTO | doKeyDownloadForUsers() : doKeyDownloadForUsers $downloadUsers") + // get the user ids which did not already trigger a keys download + val filteredUsers = downloadUsers.filter { MatrixPatterns.isUserId(it) } + if (filteredUsers.isEmpty()) { + // trigger nothing + return MXUsersDevicesMap() + } + val params = DownloadKeysForUsersTask.Params(filteredUsers, syncTokenStore.getLastToken()) + val response = try { + downloadKeysForUsersTask.execute(params) + } catch (throwable: Throwable) { + Timber.e(throwable, "## CRYPTO | doKeyDownloadForUsers(): error") + onKeysDownloadFailed(filteredUsers) + throw throwable + } + Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users") + for (userId in filteredUsers) { + // al devices = + val models = response.deviceKeys?.get(userId)?.mapValues { entry -> CryptoInfoMapper.map(entry.value) } + + Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for $userId : $models") + if (!models.isNullOrEmpty()) { + val workingCopy = models.toMutableMap() + for ((deviceId, deviceInfo) in models) { + // Get the potential previously store device keys for this device + val previouslyStoredDeviceKeys = cryptoStore.getUserDevice(userId, deviceId) + + // in some race conditions (like unit tests) + // the self device must be seen as verified + if (deviceInfo.deviceId == credentials.deviceId && userId == credentials.userId) { + deviceInfo.trustLevel = DeviceTrustLevel(previouslyStoredDeviceKeys?.trustLevel?.crossSigningVerified ?: false, true) + } + // Validate received keys + if (!validateDeviceKeys(deviceInfo, userId, deviceId, previouslyStoredDeviceKeys)) { + // New device keys are not valid. Do not store them + workingCopy.remove(deviceId) + if (null != previouslyStoredDeviceKeys) { + // But keep old validated ones if any + workingCopy[deviceId] = previouslyStoredDeviceKeys + } + } else if (null != previouslyStoredDeviceKeys) { + // The verified status is not sync'ed with hs. + // This is a client side information, valid only for this client. + // So, transfer its previous value + workingCopy[deviceId]!!.trustLevel = previouslyStoredDeviceKeys.trustLevel + } + } + // Update the store + // Note that devices which aren't in the response will be removed from the stores + cryptoStore.storeUserDevices(userId, workingCopy) + } + + // Handle cross signing keys update + val masterKey = response.masterKeys?.get(userId)?.toCryptoModel().also { + Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : MSK ${it?.unpaddedBase64PublicKey}") + } + val selfSigningKey = response.selfSigningKeys?.get(userId)?.toCryptoModel()?.also { + Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : SSK ${it.unpaddedBase64PublicKey}") + } + val userSigningKey = response.userSigningKeys?.get(userId)?.toCryptoModel()?.also { + Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : USK ${it.unpaddedBase64PublicKey}") + } + cryptoStore.storeUserCrossSigningKeys( + userId, + masterKey, + selfSigningKey, + userSigningKey + ) + } + + // Update devices trust for these users + dispatchDeviceChange(downloadUsers) + + return onKeysDownloadSucceed(filteredUsers, response.failures) + } + + /** + * Validate device keys. + * This method must called on getEncryptingThreadHandler() thread. + * + * @param deviceKeys the device keys to validate. + * @param userId the id of the user of the device. + * @param deviceId the id of the device. + * @param previouslyStoredDeviceKeys the device keys we received before for this device + * @return true if succeeds + */ + private fun validateDeviceKeys(deviceKeys: CryptoDeviceInfo?, userId: String, deviceId: String, previouslyStoredDeviceKeys: CryptoDeviceInfo?): Boolean { + if (null == deviceKeys) { + Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys is null from $userId:$deviceId") + return false + } + + if (null == deviceKeys.keys) { + Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys.keys is null from $userId:$deviceId") + return false + } + + if (null == deviceKeys.signatures) { + Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys.signatures is null from $userId:$deviceId") + return false + } + + // Check that the user_id and device_id in the received deviceKeys are correct + if (deviceKeys.userId != userId) { + Timber.e("## CRYPTO | validateDeviceKeys() : Mismatched user_id ${deviceKeys.userId} from $userId:$deviceId") + return false + } + + if (deviceKeys.deviceId != deviceId) { + Timber.e("## CRYPTO | validateDeviceKeys() : Mismatched device_id ${deviceKeys.deviceId} from $userId:$deviceId") + return false + } + + val signKeyId = "ed25519:" + deviceKeys.deviceId + val signKey = deviceKeys.keys[signKeyId] + + if (null == signKey) { + Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no ed25519 key") + return false + } + + val signatureMap = deviceKeys.signatures[userId] + + if (null == signatureMap) { + Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no map for $userId") + return false + } + + val signature = signatureMap[signKeyId] + + if (null == signature) { + Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} is not signed") + return false + } + + var isVerified = false + var errorMessage: String? = null + + try { + olmDevice.verifySignature(signKey, deviceKeys.signalableJSONDictionary(), signature) + isVerified = true + } catch (e: Exception) { + errorMessage = e.message + } + + if (!isVerified) { + Timber.e("## CRYPTO | validateDeviceKeys() : Unable to verify signature on device " + userId + ":" + + deviceKeys.deviceId + " with error " + errorMessage) + return false + } + + if (null != previouslyStoredDeviceKeys) { + if (previouslyStoredDeviceKeys.fingerprint() != signKey) { + // This should only happen if the list has been MITMed; we are + // best off sticking with the original keys. + // + // Should we warn the user about it somehow? + Timber.e("## CRYPTO | validateDeviceKeys() : WARNING:Ed25519 key for device " + userId + ":" + + deviceKeys.deviceId + " has changed : " + + previouslyStoredDeviceKeys.fingerprint() + " -> " + signKey) + + Timber.e("## CRYPTO | validateDeviceKeys() : $previouslyStoredDeviceKeys -> $deviceKeys") + Timber.e("## CRYPTO | validateDeviceKeys() : ${previouslyStoredDeviceKeys.keys} -> ${deviceKeys.keys}") + + return false + } + } + + return true + } + + /** + * Start device queries for any users who sent us an m.new_device recently + * This method must be called on getEncryptingThreadHandler() thread. + */ + suspend fun refreshOutdatedDeviceLists() { + Timber.v("## CRYPTO | refreshOutdatedDeviceLists()") + val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap() + + val users = deviceTrackingStatuses.keys.filterTo(mutableListOf()) { userId -> + TRACKING_STATUS_PENDING_DOWNLOAD == deviceTrackingStatuses[userId] + } + + if (users.isEmpty()) { + return + } + + // update the statuses + users.associateWithTo(deviceTrackingStatuses) { TRACKING_STATUS_DOWNLOAD_IN_PROGRESS } + + cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses) + runCatching { + doKeyDownloadForUsers(users) + }.fold( + { + Timber.v("## CRYPTO | refreshOutdatedDeviceLists() : done") + }, + { + Timber.e(it, "## CRYPTO | refreshOutdatedDeviceLists() : ERROR updating device keys for users $users") + } + ) + } + + companion object { + + /** + * State transition diagram for DeviceList.deviceTrackingStatus + *

+         *
+         *                                   |
+         *        stopTrackingDeviceList     V
+         *      +---------------------> NOT_TRACKED
+         *      |                            |
+         *      +<--------------------+      | startTrackingDeviceList
+         *      |                     |      V
+         *      |   +-------------> PENDING_DOWNLOAD <--------------------+-+
+         *      |   |                      ^ |                            | |
+         *      |   | restart     download | |  start download            | | invalidateUserDeviceList
+         *      |   | client        failed | |                            | |
+         *      |   |                      | V                            | |
+         *      |   +------------ DOWNLOAD_IN_PROGRESS -------------------+ |
+         *      |                    |       |                              |
+         *      +<-------------------+       |  download successful         |
+         *      ^                            V                              |
+         *      +----------------------- UP_TO_DATE ------------------------+
+         *
+         * 
+ */ + + const val TRACKING_STATUS_NOT_TRACKED = -1 + const val TRACKING_STATUS_PENDING_DOWNLOAD = 1 + const val TRACKING_STATUS_DOWNLOAD_IN_PROGRESS = 2 + const val TRACKING_STATUS_UP_TO_DATE = 3 + const val TRACKING_STATUS_UNREACHABLE_SERVER = 4 + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GossipingRequestState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GossipingRequestState.kt new file mode 100644 index 0000000000..9c2e498863 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GossipingRequestState.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto + +enum class GossipRequestType { + KEY, + SECRET +} + +enum class GossipingRequestState { + NONE, + PENDING, + REJECTED, + ACCEPTING, + ACCEPTED, + FAILED_TO_ACCEPTED, + // USER_REJECTED, + UNABLE_TO_PROCESS, + CANCELLED_BY_REQUESTER, + RE_REQUESTED +} + +enum class OutgoingGossipingRequestState { + UNSENT, + SENDING, + SENT, + CANCELLING, + CANCELLED, + FAILED_TO_SEND, + FAILED_TO_CANCEL +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GossipingWorkManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GossipingWorkManager.kt new file mode 100644 index 0000000000..50e11e40b8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GossipingWorkManager.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto + +import androidx.work.BackoffPolicy +import androidx.work.Data +import androidx.work.ExistingWorkPolicy +import androidx.work.ListenableWorker +import androidx.work.OneTimeWorkRequest +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.di.WorkManagerProvider +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.util.CancelableWork +import org.matrix.android.sdk.internal.worker.startChain +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +@SessionScope +internal class GossipingWorkManager @Inject constructor( + private val workManagerProvider: WorkManagerProvider +) { + + inline fun createWork(data: Data, startChain: Boolean): OneTimeWorkRequest { + return workManagerProvider.matrixOneTimeWorkRequestBuilder() + .setConstraints(WorkManagerProvider.workConstraints) + .startChain(startChain) + .setInputData(data) + .setBackoffCriteria(BackoffPolicy.LINEAR, 10_000L, TimeUnit.MILLISECONDS) + .build() + } + + // Prevent sending queue to stay broken after app restart + // The unique queue id will stay the same as long as this object is instanciated + val queueSuffixApp = System.currentTimeMillis() + + fun postWork(workRequest: OneTimeWorkRequest, policy: ExistingWorkPolicy = ExistingWorkPolicy.APPEND): Cancelable { + workManagerProvider.workManager + .beginUniqueWork(this::class.java.name + "_$queueSuffixApp", policy, workRequest) + .enqueue() + + return CancelableWork(workManagerProvider.workManager, workRequest.id) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt new file mode 100644 index 0000000000..da72186136 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt @@ -0,0 +1,434 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto + +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.crypto.MXCryptoConfig +import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding +import org.matrix.android.sdk.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey +import org.matrix.android.sdk.internal.crypto.model.rest.GossipingDefaultContent +import org.matrix.android.sdk.internal.crypto.model.rest.GossipingToDeviceObject +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.di.SessionId +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +@SessionScope +internal class IncomingGossipingRequestManager @Inject constructor( + @SessionId private val sessionId: String, + private val credentials: Credentials, + private val cryptoStore: IMXCryptoStore, + private val cryptoConfig: MXCryptoConfig, + private val gossipingWorkManager: GossipingWorkManager, + private val roomEncryptorsStore: RoomEncryptorsStore, + private val roomDecryptorProvider: RoomDecryptorProvider, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val cryptoCoroutineScope: CoroutineScope) { + + // list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations + // we received in the current sync. + private val receivedGossipingRequests = ArrayList() + private val receivedRequestCancellations = ArrayList() + + // the listeners + private val gossipingRequestListeners: MutableSet = HashSet() + + init { + receivedGossipingRequests.addAll(cryptoStore.getPendingIncomingGossipingRequests()) + } + + // Recently verified devices (map of deviceId and timestamp) + private val recentlyVerifiedDevices = HashMap() + + /** + * Called when a session has been verified. + * This information can be used by the manager to decide whether or not to fullfil gossiping requests + */ + fun onVerificationCompleteForDevice(deviceId: String) { + // For now we just keep an in memory cache + synchronized(recentlyVerifiedDevices) { + recentlyVerifiedDevices[deviceId] = System.currentTimeMillis() + } + } + + private fun hasBeenVerifiedLessThanFiveMinutesFromNow(deviceId: String): Boolean { + val verifTimestamp: Long? + synchronized(recentlyVerifiedDevices) { + verifTimestamp = recentlyVerifiedDevices[deviceId] + } + if (verifTimestamp == null) return false + + val age = System.currentTimeMillis() - verifTimestamp + + return age < FIVE_MINUTES_IN_MILLIS + } + + /** + * Called when we get an m.room_key_request event + * It must be called on CryptoThread + * + * @param event the announcement event. + */ + fun onGossipingRequestEvent(event: Event) { + Timber.v("## CRYPTO | GOSSIP onGossipingRequestEvent type ${event.type} from user ${event.senderId}") + val roomKeyShare = event.getClearContent().toModel() + val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it } + when (roomKeyShare?.action) { + GossipingToDeviceObject.ACTION_SHARE_REQUEST -> { + if (event.getClearType() == EventType.REQUEST_SECRET) { + IncomingSecretShareRequest.fromEvent(event)?.let { + if (event.senderId == credentials.userId && it.deviceId == credentials.deviceId) { + // ignore, it was sent by me as * + Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} ignore remote echo") + } else { + // save in DB + cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs) + receivedGossipingRequests.add(it) + } + } + } else if (event.getClearType() == EventType.ROOM_KEY_REQUEST) { + IncomingRoomKeyRequest.fromEvent(event)?.let { + if (event.senderId == credentials.userId && it.deviceId == credentials.deviceId) { + // ignore, it was sent by me as * + Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} ignore remote echo") + } else { + cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs) + receivedGossipingRequests.add(it) + } + } + } + } + GossipingToDeviceObject.ACTION_SHARE_CANCELLATION -> { + IncomingRequestCancellation.fromEvent(event)?.let { + receivedRequestCancellations.add(it) + } + } + else -> { + Timber.e("## GOSSIP onGossipingRequestEvent() : unsupported action ${roomKeyShare?.action}") + } + } + } + + /** + * Process any m.room_key_request or m.secret.request events which were queued up during the + * current sync. + * It must be called on CryptoThread + */ + fun processReceivedGossipingRequests() { + val roomKeyRequestsToProcess = receivedGossipingRequests.toList() + receivedGossipingRequests.clear() + for (request in roomKeyRequestsToProcess) { + if (request is IncomingRoomKeyRequest) { + processIncomingRoomKeyRequest(request) + } else if (request is IncomingSecretShareRequest) { + processIncomingSecretShareRequest(request) + } + } + + var receivedRequestCancellations: List? = null + + synchronized(this.receivedRequestCancellations) { + if (this.receivedRequestCancellations.isNotEmpty()) { + receivedRequestCancellations = this.receivedRequestCancellations.toList() + this.receivedRequestCancellations.clear() + } + } + + receivedRequestCancellations?.forEach { request -> + Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : m.room_key_request cancellation $request") + // we should probably only notify the app of cancellations we told it + // about, but we don't currently have a record of that, so we just pass + // everything through. + if (request.userId == credentials.userId && request.deviceId == credentials.deviceId) { + // ignore remote echo + return@forEach + } + val matchingIncoming = cryptoStore.getIncomingRoomKeyRequest(request.userId ?: "", request.deviceId ?: "", request.requestId ?: "") + if (matchingIncoming == null) { + // ignore that? + return@forEach + } else { + // If it was accepted from this device, keep the information, do not mark as cancelled + if (matchingIncoming.state != GossipingRequestState.ACCEPTED) { + onRoomKeyRequestCancellation(request) + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.CANCELLED_BY_REQUESTER) + } + } + } + } + + private fun processIncomingRoomKeyRequest(request: IncomingRoomKeyRequest) { + val userId = request.userId ?: return + val deviceId = request.deviceId ?: return + val body = request.requestBody ?: return + val roomId = body.roomId ?: return + val alg = body.algorithm ?: return + + Timber.v("## CRYPTO | GOSSIP processIncomingRoomKeyRequest from $userId:$deviceId for $roomId / ${body.sessionId} id ${request.requestId}") + if (credentials.userId != userId) { + Timber.w("## CRYPTO | GOSSIP processReceivedGossipingRequests() : room key request from other user") + val senderKey = body.senderKey ?: return Unit + .also { Timber.w("missing senderKey") } + .also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) } + val sessionId = body.sessionId ?: return Unit + .also { Timber.w("missing sessionId") } + .also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) } + + if (alg != MXCRYPTO_ALGORITHM_MEGOLM) { + return Unit + .also { Timber.w("Only megolm is accepted here") } + .also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) } + } + + val roomEncryptor = roomEncryptorsStore.get(roomId) ?: return Unit + .also { Timber.w("no room Encryptor") } + .also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) } + + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + val isSuccess = roomEncryptor.reshareKey(sessionId, userId, deviceId, senderKey) + + if (isSuccess) { + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED) + } else { + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.UNABLE_TO_PROCESS) + } + } + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.RE_REQUESTED) + return + } + // TODO: should we queue up requests we don't yet have keys for, in case they turn up later? + // if we don't have a decryptor for this room/alg, we don't have + // the keys for the requested events, and can drop the requests. + val decryptor = roomDecryptorProvider.getRoomDecryptor(roomId, alg) + if (null == decryptor) { + Timber.w("## CRYPTO | GOSSIP processReceivedGossipingRequests() : room key request for unknown $alg in room $roomId") + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + return + } + if (!decryptor.hasKeysForKeyRequest(request)) { + Timber.w("## CRYPTO | GOSSIP processReceivedGossipingRequests() : room key request for unknown session ${body.sessionId!!}") + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + return + } + + if (credentials.deviceId == deviceId && credentials.userId == userId) { + Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : oneself device - ignored") + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + return + } + request.share = Runnable { + decryptor.shareKeysWithDevice(request) + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED) + } + request.ignore = Runnable { + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + } + // if the device is verified already, share the keys + val device = cryptoStore.getUserDevice(userId, deviceId) + if (device != null) { + if (device.isVerified) { + Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : device is already verified: sharing keys") + request.share?.run() + return + } + + if (device.isBlocked) { + Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : device is blocked -> ignored") + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + return + } + } + + // As per config we automatically discard untrusted devices request + if (cryptoConfig.discardRoomKeyRequestsFromUntrustedDevices) { + Timber.v("## CRYPTO | processReceivedGossipingRequests() : discardRoomKeyRequestsFromUntrustedDevices") + // At this point the device is unknown, we don't want to bother user with that + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + return + } + + // Pass to application layer to decide what to do + onRoomKeyRequest(request) + } + + private fun processIncomingSecretShareRequest(request: IncomingSecretShareRequest) { + val secretName = request.secretName ?: return Unit.also { + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + Timber.v("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Missing secret name") + } + + val userId = request.userId + if (userId == null || credentials.userId != userId) { + Timber.e("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Ignoring secret share request from other users") + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + return + } + + val deviceId = request.deviceId + ?: return Unit.also { + Timber.e("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Malformed request, no ") + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + } + + val device = cryptoStore.getUserDevice(userId, deviceId) + ?: return Unit.also { + Timber.e("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Received secret share request from unknown device ${request.deviceId}") + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + } + + if (!device.isVerified || device.isBlocked) { + Timber.v("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Ignoring secret share request from untrusted/blocked session $device") + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + return + } + + val isDeviceLocallyVerified = cryptoStore.getUserDevice(userId, deviceId)?.trustLevel?.isLocallyVerified() + + when (secretName) { + MASTER_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.master + SELF_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.selfSigned + USER_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.user + KEYBACKUP_SECRET_SSSS_NAME -> cryptoStore.getKeyBackupRecoveryKeyInfo()?.recoveryKey + ?.let { + extractCurveKeyFromRecoveryKey(it)?.toBase64NoPadding() + } + else -> null + }?.let { secretValue -> + Timber.i("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Sharing secret $secretName with $device locally trusted") + if (isDeviceLocallyVerified == true && hasBeenVerifiedLessThanFiveMinutesFromNow(deviceId)) { + val params = SendGossipWorker.Params( + sessionId = sessionId, + secretValue = secretValue, + request = request + ) + + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTING) + val workRequest = gossipingWorkManager.createWork(WorkerParamsFactory.toData(params), true) + gossipingWorkManager.postWork(workRequest) + } else { + Timber.v("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Can't share secret $secretName with $device, verification too old") + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + } + return + } + + Timber.v("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : $secretName unknown at SDK level, asking to app layer") + + request.ignore = Runnable { + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + } + + request.share = { secretValue -> + + val params = SendGossipWorker.Params( + sessionId = userId, + secretValue = secretValue, + request = request + ) + + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTING) + val workRequest = gossipingWorkManager.createWork(WorkerParamsFactory.toData(params), true) + gossipingWorkManager.postWork(workRequest) + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED) + } + + onShareRequest(request) + } + + /** + * Dispatch onRoomKeyRequest + * + * @param request the request + */ + private fun onRoomKeyRequest(request: IncomingRoomKeyRequest) { + synchronized(gossipingRequestListeners) { + for (listener in gossipingRequestListeners) { + try { + listener.onRoomKeyRequest(request) + } catch (e: Exception) { + Timber.e(e, "## CRYPTO | onRoomKeyRequest() failed") + } + } + } + } + + /** + * Ask for a value to the listeners, and take the first one + */ + private fun onShareRequest(request: IncomingSecretShareRequest) { + synchronized(gossipingRequestListeners) { + for (listener in gossipingRequestListeners) { + try { + if (listener.onSecretShareRequest(request)) { + return + } + } catch (e: Exception) { + Timber.e(e, "## CRYPTO | GOSSIP onRoomKeyRequest() failed") + } + } + } + // Not handled, ignore + request.ignore?.run() + } + + /** + * A room key request cancellation has been received. + * + * @param request the cancellation request + */ + private fun onRoomKeyRequestCancellation(request: IncomingRequestCancellation) { + synchronized(gossipingRequestListeners) { + for (listener in gossipingRequestListeners) { + try { + listener.onRoomKeyRequestCancellation(request) + } catch (e: Exception) { + Timber.e(e, "## CRYPTO | GOSSIP onRoomKeyRequestCancellation() failed") + } + } + } + } + + fun addRoomKeysRequestListener(listener: GossipingRequestListener) { + synchronized(gossipingRequestListeners) { + gossipingRequestListeners.add(listener) + } + } + + fun removeRoomKeysRequestListener(listener: GossipingRequestListener) { + synchronized(gossipingRequestListeners) { + gossipingRequestListeners.remove(listener) + } + } + + companion object { + private const val FIVE_MINUTES_IN_MILLIS = 5 * 60 * 1000 + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingRequestCancellation.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingRequestCancellation.kt new file mode 100755 index 0000000000..04b78fc89f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingRequestCancellation.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.internal.crypto.model.rest.ShareRequestCancellation + +/** + * IncomingRequestCancellation describes the incoming room key cancellation. + */ +data class IncomingRequestCancellation( + /** + * The user id + */ + override val userId: String? = null, + + /** + * The device id + */ + override val deviceId: String? = null, + + /** + * The request id + */ + override val requestId: String? = null, + override val localCreationTimestamp: Long? +) : IncomingShareRequestCommon { + companion object { + /** + * Factory + * + * @param event the event + */ + fun fromEvent(event: Event): IncomingRequestCancellation? { + return event.getClearContent() + .toModel() + ?.let { + IncomingRequestCancellation( + userId = event.senderId, + deviceId = it.requestingDeviceId, + requestId = it.requestId, + localCreationTimestamp = event.ageLocalTs ?: System.currentTimeMillis() + ) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingRoomKeyRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingRoomKeyRequest.kt new file mode 100755 index 0000000000..04e18bf7f9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingRoomKeyRequest.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody +import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyShareRequest + +/** + * IncomingRoomKeyRequest class defines the incoming room keys request. + */ +data class IncomingRoomKeyRequest( + /** + * The user id + */ + override val userId: String? = null, + + /** + * The device id + */ + override val deviceId: String? = null, + + /** + * The request id + */ + override val requestId: String? = null, + + /** + * The request body + */ + val requestBody: RoomKeyRequestBody? = null, + + val state: GossipingRequestState = GossipingRequestState.NONE, + + /** + * The runnable to call to accept to share the keys + */ + @Transient + var share: Runnable? = null, + + /** + * The runnable to call to ignore the key share request. + */ + @Transient + var ignore: Runnable? = null, + override val localCreationTimestamp: Long? +) : IncomingShareRequestCommon { + companion object { + /** + * Factory + * + * @param event the event + */ + fun fromEvent(event: Event): IncomingRoomKeyRequest? { + return event.getClearContent() + .toModel() + ?.let { + IncomingRoomKeyRequest( + userId = event.senderId, + deviceId = it.requestingDeviceId, + requestId = it.requestId, + requestBody = it.body ?: RoomKeyRequestBody(), + localCreationTimestamp = event.ageLocalTs ?: System.currentTimeMillis() + ) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingSecretShareRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingSecretShareRequest.kt new file mode 100755 index 0000000000..4b91ed5d76 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingSecretShareRequest.kt @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.internal.crypto.model.rest.SecretShareRequest + +/** + * IncomingRoomKeyRequest class defines the incoming room keys request. + */ +data class IncomingSecretShareRequest( + /** + * The user id + */ + override val userId: String? = null, + + /** + * The device id + */ + override val deviceId: String? = null, + + /** + * The request id + */ + override val requestId: String? = null, + + /** + * The request body + */ + val secretName: String? = null, + + /** + * The runnable to call to accept to share the keys + */ + @Transient + var share: ((String) -> Unit)? = null, + + /** + * The runnable to call to ignore the key share request. + */ + @Transient + var ignore: Runnable? = null, + + override val localCreationTimestamp: Long? + +) : IncomingShareRequestCommon { + companion object { + /** + * Factory + * + * @param event the event + */ + fun fromEvent(event: Event): IncomingSecretShareRequest? { + return event.getClearContent() + .toModel() + ?.let { + IncomingSecretShareRequest( + userId = event.senderId, + deviceId = it.requestingDeviceId, + requestId = it.requestId, + secretName = it.secretName, + localCreationTimestamp = event.ageLocalTs ?: System.currentTimeMillis() + ) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingShareRequestCommon.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingShareRequestCommon.kt new file mode 100644 index 0000000000..d57584f49f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingShareRequestCommon.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto + +interface IncomingShareRequestCommon { + /** + * The user id + */ + val userId: String? + + /** + * The device id + */ + val deviceId: String? + + /** + * The request id + */ + val requestId: String? + + val localCreationTimestamp: Long? +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXCryptoAlgorithms.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXCryptoAlgorithms.kt new file mode 100755 index 0000000000..90b0b318b9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXCryptoAlgorithms.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto + +// TODO Update comment +internal object MXCryptoAlgorithms { + + /** + * Get the class implementing encryption for the provided algorithm. + * + * @param algorithm the algorithm tag. + * @return A class implementing 'IMXEncrypting'. + */ + fun hasEncryptorClassForAlgorithm(algorithm: String?): Boolean { + return when (algorithm) { + MXCRYPTO_ALGORITHM_MEGOLM, + MXCRYPTO_ALGORITHM_OLM -> true + else -> false + } + } + + /** + * Get the class implementing decryption for the provided algorithm. + * + * @param algorithm the algorithm tag. + * @return A class implementing 'IMXDecrypting'. + */ + + fun hasDecryptorClassForAlgorithm(algorithm: String?): Boolean { + return when (algorithm) { + MXCRYPTO_ALGORITHM_MEGOLM, + MXCRYPTO_ALGORITHM_OLM -> true + else -> false + } + } + + /** + * @return The list of registered algorithms. + */ + fun supportedAlgorithms(): List { + return listOf(MXCRYPTO_ALGORITHM_MEGOLM, MXCRYPTO_ALGORITHM_OLM) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXEventDecryptionResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXEventDecryptionResult.kt new file mode 100755 index 0000000000..f094b5c656 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXEventDecryptionResult.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto + +import org.matrix.android.sdk.api.util.JsonDict + +/** + * The result of a (successful) call to decryptEvent. + */ +data class MXEventDecryptionResult( + + /** + * The plaintext payload for the event (typically containing "type" and "content" fields). + */ + val clearEvent: JsonDict, + + /** + * Key owned by the sender of this event. + * See MXEvent.senderKey. + */ + val senderCurve25519Key: String? = null, + + /** + * Ed25519 key claimed by the sender of this event. + * See MXEvent.claimedEd25519Key. + */ + val claimedEd25519Key: String? = null, + + /** + * List of curve25519 keys involved in telling us about the senderCurve25519Key and + * claimedEd25519Key. See MXEvent.forwardingCurve25519KeyChain. + */ + val forwardingCurve25519KeyChain: List = emptyList() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXMegolmExportEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXMegolmExportEncryption.kt new file mode 100755 index 0000000000..4526ba8a51 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXMegolmExportEncryption.kt @@ -0,0 +1,350 @@ +/* + * Copyright 2017 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto + +import android.util.Base64 +import org.matrix.android.sdk.internal.extensions.toUnsignedInt +import timber.log.Timber +import java.io.ByteArrayOutputStream +import java.nio.charset.Charset +import java.security.SecureRandom +import javax.crypto.Cipher +import javax.crypto.Mac +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec +import kotlin.experimental.and +import kotlin.experimental.xor +import kotlin.math.min + +/** + * Utility class to import/export the crypto data + */ +object MXMegolmExportEncryption { + private const val HEADER_LINE = "-----BEGIN MEGOLM SESSION DATA-----" + private const val TRAILER_LINE = "-----END MEGOLM SESSION DATA-----" + // we split into lines before base64ing, because encodeBase64 doesn't deal + // terribly well with large arrays. + private const val LINE_LENGTH = 72 * 4 / 3 + + // default iteration count to export the e2e keys + const val DEFAULT_ITERATION_COUNT = 500000 + + /** + * Extract the AES key from the deriveKeys result. + * + * @param keyBits the deriveKeys result. + * @return the AES key + */ + private fun getAesKey(keyBits: ByteArray): ByteArray { + return keyBits.copyOfRange(0, 32) + } + + /** + * Extract the Hmac key from the deriveKeys result. + * + * @param keyBits the deriveKeys result. + * @return the Hmac key. + */ + private fun getHmacKey(keyBits: ByteArray): ByteArray { + return keyBits.copyOfRange(32, keyBits.size) + } + + /** + * Decrypt a megolm key file + * + * @param data the data to decrypt + * @param password the password. + * @return the decrypted output. + * @throws Exception the failure reason + */ + @Throws(Exception::class) + fun decryptMegolmKeyFile(data: ByteArray, password: String): String { + val body = unpackMegolmKeyFile(data) + + // check we have a version byte + if (null == body || body.isEmpty()) { + Timber.e("## decryptMegolmKeyFile() : Invalid file: too short") + throw Exception("Invalid file: too short") + } + + val version = body[0] + if (version.toInt() != 1) { + Timber.e("## decryptMegolmKeyFile() : Invalid file: too short") + throw Exception("Unsupported version") + } + + val ciphertextLength = body.size - (1 + 16 + 16 + 4 + 32) + if (ciphertextLength < 0) { + throw Exception("Invalid file: too short") + } + + if (password.isEmpty()) { + throw Exception("Empty password is not supported") + } + + val salt = body.copyOfRange(1, 1 + 16) + val iv = body.copyOfRange(17, 17 + 16) + val iterations = + (body[33].toUnsignedInt() shl 24) or (body[34].toUnsignedInt() shl 16) or (body[35].toUnsignedInt() shl 8) or body[36].toUnsignedInt() + val ciphertext = body.copyOfRange(37, 37 + ciphertextLength) + val hmac = body.copyOfRange(body.size - 32, body.size) + + val deriveKey = deriveKeys(salt, iterations, password) + + val toVerify = body.copyOfRange(0, body.size - 32) + + val macKey = SecretKeySpec(getHmacKey(deriveKey), "HmacSHA256") + val mac = Mac.getInstance("HmacSHA256") + mac.init(macKey) + val digest = mac.doFinal(toVerify) + + if (!hmac.contentEquals(digest)) { + Timber.e("## decryptMegolmKeyFile() : Authentication check failed: incorrect password?") + throw Exception("Authentication check failed: incorrect password?") + } + + val decryptCipher = Cipher.getInstance("AES/CTR/NoPadding") + + val secretKeySpec = SecretKeySpec(getAesKey(deriveKey), "AES") + val ivParameterSpec = IvParameterSpec(iv) + decryptCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec) + + val outStream = ByteArrayOutputStream() + outStream.write(decryptCipher.update(ciphertext)) + outStream.write(decryptCipher.doFinal()) + + val decodedString = String(outStream.toByteArray(), Charset.defaultCharset()) + outStream.close() + + return decodedString + } + + /** + * Encrypt a string into the megolm export format. + * + * @param data the data to encrypt. + * @param password the password + * @param kdf_rounds the iteration count + * @return the encrypted data + * @throws Exception the failure reason + */ + @Throws(Exception::class) + @JvmOverloads + fun encryptMegolmKeyFile(data: String, password: String, kdf_rounds: Int = DEFAULT_ITERATION_COUNT): ByteArray { + if (password.isEmpty()) { + throw Exception("Empty password is not supported") + } + + val secureRandom = SecureRandom() + + val salt = ByteArray(16) + secureRandom.nextBytes(salt) + + val iv = ByteArray(16) + secureRandom.nextBytes(iv) + + // clear bit 63 of the salt to stop us hitting the 64-bit counter boundary + // (which would mean we wouldn't be able to decrypt on Android). The loss + // of a single bit of salt is a price we have to pay. + iv[9] = iv[9] and 0x7f + + val deriveKey = deriveKeys(salt, kdf_rounds, password) + + val decryptCipher = Cipher.getInstance("AES/CTR/NoPadding") + + val secretKeySpec = SecretKeySpec(getAesKey(deriveKey), "AES") + val ivParameterSpec = IvParameterSpec(iv) + decryptCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec) + + val outStream = ByteArrayOutputStream() + outStream.write(decryptCipher.update(data.toByteArray(charset("UTF-8")))) + outStream.write(decryptCipher.doFinal()) + + val cipherArray = outStream.toByteArray() + val bodyLength = 1 + salt.size + iv.size + 4 + cipherArray.size + 32 + + val resultBuffer = ByteArray(bodyLength) + var idx = 0 + resultBuffer[idx++] = 1 // version + + System.arraycopy(salt, 0, resultBuffer, idx, salt.size) + idx += salt.size + + System.arraycopy(iv, 0, resultBuffer, idx, iv.size) + idx += iv.size + + resultBuffer[idx++] = (kdf_rounds shr 24 and 0xff).toByte() + resultBuffer[idx++] = (kdf_rounds shr 16 and 0xff).toByte() + resultBuffer[idx++] = (kdf_rounds shr 8 and 0xff).toByte() + resultBuffer[idx++] = (kdf_rounds and 0xff).toByte() + + System.arraycopy(cipherArray, 0, resultBuffer, idx, cipherArray.size) + idx += cipherArray.size + + val toSign = resultBuffer.copyOfRange(0, idx) + + val macKey = SecretKeySpec(getHmacKey(deriveKey), "HmacSHA256") + val mac = Mac.getInstance("HmacSHA256") + mac.init(macKey) + val digest = mac.doFinal(toSign) + System.arraycopy(digest, 0, resultBuffer, idx, digest.size) + + return packMegolmKeyFile(resultBuffer) + } + + /** + * Unbase64 an ascii-armoured megolm key file + * Strips the header and trailer lines, and unbase64s the content + * + * @param data the input data + * @return unbase64ed content + */ + @Throws(Exception::class) + private fun unpackMegolmKeyFile(data: ByteArray): ByteArray? { + val fileStr = String(data, Charset.defaultCharset()) + + // look for the start line + var lineStart = 0 + + while (true) { + val lineEnd = fileStr.indexOf('\n', lineStart) + + if (lineEnd < 0) { + Timber.e("## unpackMegolmKeyFile() : Header line not found") + throw Exception("Header line not found") + } + + val line = fileStr.substring(lineStart, lineEnd).trim() + + // start the next line after the newline + lineStart = lineEnd + 1 + + if (line == HEADER_LINE) { + break + } + } + + val dataStart = lineStart + + // look for the end line + while (true) { + val lineEnd = fileStr.indexOf('\n', lineStart) + val line = if (lineEnd < 0) { + fileStr.substring(lineStart) + } else { + fileStr.substring(lineStart, lineEnd) + }.trim() + + if (line == TRAILER_LINE) { + break + } + + if (lineEnd < 0) { + Timber.e("## unpackMegolmKeyFile() : Trailer line not found") + throw Exception("Trailer line not found") + } + + // start the next line after the newline + lineStart = lineEnd + 1 + } + + val dataEnd = lineStart + + // Receiving side + return Base64.decode(fileStr.substring(dataStart, dataEnd), Base64.DEFAULT) + } + + /** + * Pack the megolm data. + * + * @param data the data to pack. + * @return the packed data + * @throws Exception the failure reason. + */ + @Throws(Exception::class) + private fun packMegolmKeyFile(data: ByteArray): ByteArray { + val nLines = (data.size + LINE_LENGTH - 1) / LINE_LENGTH + + val outStream = ByteArrayOutputStream() + outStream.write(HEADER_LINE.toByteArray()) + + var o = 0 + + for (i in 1..nLines) { + outStream.write("\n".toByteArray()) + + val len = min(LINE_LENGTH, data.size - o) + outStream.write(Base64.encode(data, o, len, Base64.DEFAULT)) + o += LINE_LENGTH + } + + outStream.write("\n".toByteArray()) + outStream.write(TRAILER_LINE.toByteArray()) + outStream.write("\n".toByteArray()) + + return outStream.toByteArray() + } + + /** + * Derive the AES and HMAC-SHA-256 keys for the file + * + * @param salt salt for pbkdf + * @param iterations number of pbkdf iterations + * @param password password + * @return the derived keys + */ + @Throws(Exception::class) + private fun deriveKeys(salt: ByteArray, iterations: Int, password: String): ByteArray { + val t0 = System.currentTimeMillis() + + // based on https://en.wikipedia.org/wiki/PBKDF2 algorithm + // it is simpler than the generic algorithm because the expected key length is equal to the mac key length. + // noticed as dklen/hlen + val prf = Mac.getInstance("HmacSHA512") + prf.init(SecretKeySpec(password.toByteArray(Charsets.UTF_8), "HmacSHA512")) + + // 512 bits key length + val key = ByteArray(64) + val Uc = ByteArray(64) + + // U1 = PRF(Password, Salt || INT_32_BE(i)) + prf.update(salt) + val int32BE = ByteArray(4) { 0.toByte() } + int32BE[3] = 1.toByte() + prf.update(int32BE) + prf.doFinal(Uc, 0) + + // copy to the key + System.arraycopy(Uc, 0, key, 0, Uc.size) + + for (index in 2..iterations) { + // Uc = PRF(Password, Uc-1) + prf.update(Uc) + prf.doFinal(Uc, 0) + + // F(Password, Salt, c, i) = U1 ^ U2 ^ ... ^ Uc + for (byteIndex in Uc.indices) { + key[byteIndex] = key[byteIndex] xor Uc[byteIndex] + } + } + + Timber.v("## deriveKeys() : $iterations in ${System.currentTimeMillis() - t0} ms") + + return key + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt new file mode 100755 index 0000000000..cfdd050801 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt @@ -0,0 +1,779 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto + +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult +import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 +import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.util.JsonCanonicalizer +import org.matrix.android.sdk.internal.util.convertFromUTF8 +import org.matrix.android.sdk.internal.util.convertToUTF8 +import org.matrix.olm.OlmAccount +import org.matrix.olm.OlmException +import org.matrix.olm.OlmMessage +import org.matrix.olm.OlmOutboundGroupSession +import org.matrix.olm.OlmSession +import org.matrix.olm.OlmUtility +import timber.log.Timber +import java.net.URLEncoder +import javax.inject.Inject + +// The libolm wrapper. +@SessionScope +internal class MXOlmDevice @Inject constructor( + /** + * The store where crypto data is saved. + */ + private val store: IMXCryptoStore) { + + /** + * @return the Curve25519 key for the account. + */ + var deviceCurve25519Key: String? = null + private set + + /** + * @return the Ed25519 key for the account. + */ + var deviceEd25519Key: String? = null + private set + + // The OLM lib utility instance. + private var olmUtility: OlmUtility? = null + + // The outbound group session. + // They are not stored in 'store' to avoid to remember to which devices we sent the session key. + // Plus, in cryptography, it is good to refresh sessions from time to time. + // The key is the session id, the value the outbound group session. + private val outboundGroupSessionStore: MutableMap = HashMap() + + // Store a set of decrypted message indexes for each group session. + // This partially mitigates a replay attack where a MITM resends a group + // message into the room. + // + // The Matrix SDK exposes events through MXEventTimelines. A developer can open several + // timelines from a same room so that a message can be decrypted several times but from + // a different timeline. + // So, store these message indexes per timeline id. + // + // The first level keys are timeline ids. + // The second level keys are strings of form "||" + private val inboundGroupSessionMessageIndexes: MutableMap> = HashMap() + + init { + // Retrieve the account from the store + try { + store.getOrCreateOlmAccount() + } catch (e: Exception) { + Timber.e(e, "MXOlmDevice : cannot initialize olmAccount") + } + + try { + olmUtility = OlmUtility() + } catch (e: Exception) { + Timber.e(e, "## MXOlmDevice : OlmUtility failed with error") + olmUtility = null + } + + try { + deviceCurve25519Key = store.getOlmAccount().identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY] + } catch (e: Exception) { + Timber.e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_IDENTITY_KEY} with error") + } + + try { + deviceEd25519Key = store.getOlmAccount().identityKeys()[OlmAccount.JSON_KEY_FINGER_PRINT_KEY] + } catch (e: Exception) { + Timber.e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_FINGER_PRINT_KEY} with error") + } + } + + /** + * @return The current (unused, unpublished) one-time keys for this account. + */ + fun getOneTimeKeys(): Map>? { + try { + return store.getOlmAccount().oneTimeKeys() + } catch (e: Exception) { + Timber.e(e, "## getOneTimeKeys() : failed") + } + + return null + } + + /** + * @return The maximum number of one-time keys the olm account can store. + */ + fun getMaxNumberOfOneTimeKeys(): Long { + return store.getOlmAccount().maxOneTimeKeys() + } + + /** + * Release the instance + */ + fun release() { + olmUtility?.releaseUtility() + } + + /** + * Signs a message with the ed25519 key for this account. + * + * @param message the message to be signed. + * @return the base64-encoded signature. + */ + fun signMessage(message: String): String? { + try { + return store.getOlmAccount().signMessage(message) + } catch (e: Exception) { + Timber.e(e, "## signMessage() : failed") + } + + return null + } + + /** + * Marks all of the one-time keys as published. + */ + fun markKeysAsPublished() { + try { + store.getOlmAccount().markOneTimeKeysAsPublished() + store.saveOlmAccount() + } catch (e: Exception) { + Timber.e(e, "## markKeysAsPublished() : failed") + } + } + + /** + * Generate some new one-time keys + * + * @param numKeys number of keys to generate + */ + fun generateOneTimeKeys(numKeys: Int) { + try { + store.getOlmAccount().generateOneTimeKeys(numKeys) + store.saveOlmAccount() + } catch (e: Exception) { + Timber.e(e, "## generateOneTimeKeys() : failed") + } + } + + /** + * Generate a new outbound session. + * The new session will be stored in the MXStore. + * + * @param theirIdentityKey the remote user's Curve25519 identity key + * @param theirOneTimeKey the remote user's one-time Curve25519 key + * @return the session id for the outbound session. + */ + fun createOutboundSession(theirIdentityKey: String, theirOneTimeKey: String): String? { + Timber.v("## createOutboundSession() ; theirIdentityKey $theirIdentityKey theirOneTimeKey $theirOneTimeKey") + var olmSession: OlmSession? = null + + try { + olmSession = OlmSession() + olmSession.initOutboundSession(store.getOlmAccount(), theirIdentityKey, theirOneTimeKey) + + val olmSessionWrapper = OlmSessionWrapper(olmSession, 0) + + // Pretend we've received a message at this point, otherwise + // if we try to send a message to the device, it won't use + // this session + olmSessionWrapper.onMessageReceived() + + store.storeSession(olmSessionWrapper, theirIdentityKey) + + val sessionIdentifier = olmSession.sessionIdentifier() + + Timber.v("## createOutboundSession() ; olmSession.sessionIdentifier: $sessionIdentifier") + return sessionIdentifier + } catch (e: Exception) { + Timber.e(e, "## createOutboundSession() failed") + + olmSession?.releaseSession() + } + + return null + } + + /** + * Generate a new inbound session, given an incoming message. + * + * @param theirDeviceIdentityKey the remote user's Curve25519 identity key. + * @param messageType the message_type field from the received message (must be 0). + * @param ciphertext base64-encoded body from the received message. + * @return {{payload: string, session_id: string}} decrypted payload, and session id of new session. + */ + fun createInboundSession(theirDeviceIdentityKey: String, messageType: Int, ciphertext: String): Map? { + Timber.v("## createInboundSession() : theirIdentityKey: $theirDeviceIdentityKey") + + var olmSession: OlmSession? = null + + try { + try { + olmSession = OlmSession() + olmSession.initInboundSessionFrom(store.getOlmAccount(), theirDeviceIdentityKey, ciphertext) + } catch (e: Exception) { + Timber.e(e, "## createInboundSession() : the session creation failed") + return null + } + + Timber.v("## createInboundSession() : sessionId: ${olmSession.sessionIdentifier()}") + + try { + store.getOlmAccount().removeOneTimeKeys(olmSession) + store.saveOlmAccount() + } catch (e: Exception) { + Timber.e(e, "## createInboundSession() : removeOneTimeKeys failed") + } + + Timber.v("## createInboundSession() : ciphertext: $ciphertext") + try { + val sha256 = olmUtility!!.sha256(URLEncoder.encode(ciphertext, "utf-8")) + Timber.v("## createInboundSession() :ciphertext: SHA256: $sha256") + } catch (e: Exception) { + Timber.e(e, "## createInboundSession() :ciphertext: cannot encode ciphertext") + } + + val olmMessage = OlmMessage() + olmMessage.mCipherText = ciphertext + olmMessage.mType = messageType.toLong() + + var payloadString: String? = null + + try { + payloadString = olmSession.decryptMessage(olmMessage) + + val olmSessionWrapper = OlmSessionWrapper(olmSession, 0) + // This counts as a received message: set last received message time to now + olmSessionWrapper.onMessageReceived() + + store.storeSession(olmSessionWrapper, theirDeviceIdentityKey) + } catch (e: Exception) { + Timber.e(e, "## createInboundSession() : decryptMessage failed") + } + + val res = HashMap() + + if (!payloadString.isNullOrEmpty()) { + res["payload"] = payloadString + } + + val sessionIdentifier = olmSession.sessionIdentifier() + + if (!sessionIdentifier.isNullOrEmpty()) { + res["session_id"] = sessionIdentifier + } + + return res + } catch (e: Exception) { + Timber.e(e, "## createInboundSession() : OlmSession creation failed") + + olmSession?.releaseSession() + } + + return null + } + + /** + * Get a list of known session IDs for the given device. + * + * @param theirDeviceIdentityKey the Curve25519 identity key for the remote device. + * @return a list of known session ids for the device. + */ + fun getSessionIds(theirDeviceIdentityKey: String): Set? { + return store.getDeviceSessionIds(theirDeviceIdentityKey) + } + + /** + * Get the right olm session id for encrypting messages to the given identity key. + * + * @param theirDeviceIdentityKey the Curve25519 identity key for the remote device. + * @return the session id, or null if no established session. + */ + fun getSessionId(theirDeviceIdentityKey: String): String? { + return store.getLastUsedSessionId(theirDeviceIdentityKey) + } + + /** + * Encrypt an outgoing message using an existing session. + * + * @param theirDeviceIdentityKey the Curve25519 identity key for the remote device. + * @param sessionId the id of the active session + * @param payloadString the payload to be encrypted and sent + * @return the cipher text + */ + fun encryptMessage(theirDeviceIdentityKey: String, sessionId: String, payloadString: String): Map? { + var res: MutableMap? = null + val olmMessage: OlmMessage + val olmSessionWrapper = getSessionForDevice(theirDeviceIdentityKey, sessionId) + + if (olmSessionWrapper != null) { + try { + Timber.v("## encryptMessage() : olmSession.sessionIdentifier: $sessionId") + // Timber.v("## encryptMessage() : payloadString: " + payloadString); + + olmMessage = olmSessionWrapper.olmSession.encryptMessage(payloadString) + store.storeSession(olmSessionWrapper, theirDeviceIdentityKey) + res = HashMap() + + res["body"] = olmMessage.mCipherText + res["type"] = olmMessage.mType + } catch (e: Exception) { + Timber.e(e, "## encryptMessage() : failed") + } + } else { + Timber.e("## encryptMessage() : Failed to encrypt unknown session $sessionId") + } + + return res + } + + /** + * Decrypt an incoming message using an existing session. + * + * @param ciphertext the base64-encoded body from the received message. + * @param messageType message_type field from the received message. + * @param theirDeviceIdentityKey the Curve25519 identity key for the remote device. + * @param sessionId the id of the active session. + * @return the decrypted payload. + */ + fun decryptMessage(ciphertext: String, messageType: Int, sessionId: String, theirDeviceIdentityKey: String): String? { + var payloadString: String? = null + + val olmSessionWrapper = getSessionForDevice(theirDeviceIdentityKey, sessionId) + + if (null != olmSessionWrapper) { + val olmMessage = OlmMessage() + olmMessage.mCipherText = ciphertext + olmMessage.mType = messageType.toLong() + + try { + payloadString = olmSessionWrapper.olmSession.decryptMessage(olmMessage) + olmSessionWrapper.onMessageReceived() + store.storeSession(olmSessionWrapper, theirDeviceIdentityKey) + } catch (e: Exception) { + Timber.e(e, "## decryptMessage() : decryptMessage failed") + } + } + + return payloadString + } + + /** + * Determine if an incoming messages is a prekey message matching an existing session. + * + * @param theirDeviceIdentityKey the Curve25519 identity key for the remote device. + * @param sessionId the id of the active session. + * @param messageType message_type field from the received message. + * @param ciphertext the base64-encoded body from the received message. + * @return YES if the received message is a prekey message which matchesthe given session. + */ + fun matchesSession(theirDeviceIdentityKey: String, sessionId: String, messageType: Int, ciphertext: String): Boolean { + if (messageType != 0) { + return false + } + + val olmSessionWrapper = getSessionForDevice(theirDeviceIdentityKey, sessionId) + return null != olmSessionWrapper && olmSessionWrapper.olmSession.matchesInboundSession(ciphertext) + } + + // Outbound group session + + /** + * Generate a new outbound group session. + * + * @return the session id for the outbound session. + */ + fun createOutboundGroupSession(): String? { + var session: OlmOutboundGroupSession? = null + try { + session = OlmOutboundGroupSession() + outboundGroupSessionStore[session.sessionIdentifier()] = session + return session.sessionIdentifier() + } catch (e: Exception) { + Timber.e(e, "createOutboundGroupSession") + + session?.releaseSession() + } + + return null + } + + /** + * Get the current session key of an outbound group session. + * + * @param sessionId the id of the outbound group session. + * @return the base64-encoded secret key. + */ + fun getSessionKey(sessionId: String): String? { + if (sessionId.isNotEmpty()) { + try { + return outboundGroupSessionStore[sessionId]!!.sessionKey() + } catch (e: Exception) { + Timber.e(e, "## getSessionKey() : failed") + } + } + return null + } + + /** + * Get the current message index of an outbound group session. + * + * @param sessionId the id of the outbound group session. + * @return the current chain index. + */ + fun getMessageIndex(sessionId: String): Int { + return if (sessionId.isNotEmpty()) { + outboundGroupSessionStore[sessionId]!!.messageIndex() + } else 0 + } + + /** + * Encrypt an outgoing message with an outbound group session. + * + * @param sessionId the id of the outbound group session. + * @param payloadString the payload to be encrypted and sent. + * @return ciphertext + */ + fun encryptGroupMessage(sessionId: String, payloadString: String): String? { + if (sessionId.isNotEmpty() && payloadString.isNotEmpty()) { + try { + return outboundGroupSessionStore[sessionId]!!.encryptMessage(payloadString) + } catch (e: Exception) { + Timber.e(e, "## encryptGroupMessage() : failed") + } + } + return null + } + + // Inbound group session + + /** + * Add an inbound group session to the session store. + * + * @param sessionId the session identifier. + * @param sessionKey base64-encoded secret key. + * @param roomId the id of the room in which this session will be used. + * @param senderKey the base64-encoded curve25519 key of the sender. + * @param forwardingCurve25519KeyChain Devices involved in forwarding this session to us. + * @param keysClaimed Other keys the sender claims. + * @param exportFormat true if the megolm keys are in export format + * @return true if the operation succeeds. + */ + fun addInboundGroupSession(sessionId: String, + sessionKey: String, + roomId: String, + senderKey: String, + forwardingCurve25519KeyChain: List, + keysClaimed: Map, + exportFormat: Boolean): Boolean { + val session = OlmInboundGroupSessionWrapper2(sessionKey, exportFormat) + runCatching { getInboundGroupSession(sessionId, senderKey, roomId) } + .fold( + { + // If we already have this session, consider updating it + Timber.e("## addInboundGroupSession() : Update for megolm session $senderKey/$sessionId") + + val existingFirstKnown = it.firstKnownIndex!! + val newKnownFirstIndex = session.firstKnownIndex + + // If our existing session is better we keep it + if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) { + session.olmInboundGroupSession?.releaseSession() + return false + } + }, + { + // Nothing to do in case of error + } + ) + + // sanity check + if (null == session.olmInboundGroupSession) { + Timber.e("## addInboundGroupSession : invalid session") + return false + } + + try { + if (session.olmInboundGroupSession!!.sessionIdentifier() != sessionId) { + Timber.e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey") + session.olmInboundGroupSession!!.releaseSession() + return false + } + } catch (e: Exception) { + session.olmInboundGroupSession?.releaseSession() + Timber.e(e, "## addInboundGroupSession : sessionIdentifier() failed") + return false + } + + session.senderKey = senderKey + session.roomId = roomId + session.keysClaimed = keysClaimed + session.forwardingCurve25519KeyChain = forwardingCurve25519KeyChain + + store.storeInboundGroupSessions(listOf(session)) + + return true + } + + /** + * Import an inbound group sessions to the session store. + * + * @param megolmSessionsData the megolm sessions data + * @return the successfully imported sessions. + */ + fun importInboundGroupSessions(megolmSessionsData: List): List { + val sessions = ArrayList(megolmSessionsData.size) + + for (megolmSessionData in megolmSessionsData) { + val sessionId = megolmSessionData.sessionId + val senderKey = megolmSessionData.senderKey + val roomId = megolmSessionData.roomId + + var session: OlmInboundGroupSessionWrapper2? = null + + try { + session = OlmInboundGroupSessionWrapper2(megolmSessionData) + } catch (e: Exception) { + Timber.e(e, "## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId") + } + + // sanity check + if (session?.olmInboundGroupSession == null) { + Timber.e("## importInboundGroupSession : invalid session") + continue + } + + try { + if (session.olmInboundGroupSession?.sessionIdentifier() != sessionId) { + Timber.e("## importInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey") + if (session.olmInboundGroupSession != null) session.olmInboundGroupSession!!.releaseSession() + continue + } + } catch (e: Exception) { + Timber.e(e, "## importInboundGroupSession : sessionIdentifier() failed") + session.olmInboundGroupSession!!.releaseSession() + continue + } + + runCatching { getInboundGroupSession(sessionId, senderKey, roomId) } + .fold( + { + // If we already have this session, consider updating it + Timber.e("## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId") + + // For now we just ignore updates. TODO: implement something here + if (it.firstKnownIndex!! <= session.firstKnownIndex!!) { + // Ignore this, keep existing + session.olmInboundGroupSession!!.releaseSession() + } else { + sessions.add(session) + } + Unit + }, + { + // Session does not already exist, add it + sessions.add(session) + } + + ) + } + + store.storeInboundGroupSessions(sessions) + + return sessions + } + + /** + * Remove an inbound group session + * + * @param sessionId the session identifier. + * @param sessionKey base64-encoded secret key. + */ + fun removeInboundGroupSession(sessionId: String?, sessionKey: String?) { + if (null != sessionId && null != sessionKey) { + store.removeInboundGroupSession(sessionId, sessionKey) + } + } + + /** + * Decrypt a received message with an inbound group session. + * + * @param body the base64-encoded body of the encrypted message. + * @param roomId the room in which the message was received. + * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. + * @param sessionId the session identifier. + * @param senderKey the base64-encoded curve25519 key of the sender. + * @return the decrypting result. Nil if the sessionId is unknown. + */ + @Throws(MXCryptoError::class) + fun decryptGroupMessage(body: String, + roomId: String, + timeline: String?, + sessionId: String, + senderKey: String): OlmDecryptionResult { + val session = getInboundGroupSession(sessionId, senderKey, roomId) + // Check that the room id matches the original one for the session. This stops + // the HS pretending a message was targeting a different room. + if (roomId == session.roomId) { + val decryptResult = try { + session.olmInboundGroupSession!!.decryptMessage(body) + } catch (e: OlmException) { + Timber.e(e, "## decryptGroupMessage () : decryptMessage failed") + throw MXCryptoError.OlmError(e) + } + + if (timeline?.isNotBlank() == true) { + val timelineSet = inboundGroupSessionMessageIndexes.getOrPut(timeline) { mutableSetOf() } + + val messageIndexKey = senderKey + "|" + sessionId + "|" + decryptResult.mIndex + + if (timelineSet.contains(messageIndexKey)) { + val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex) + Timber.e("## decryptGroupMessage() : $reason") + throw MXCryptoError.Base(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, reason) + } + + timelineSet.add(messageIndexKey) + } + + store.storeInboundGroupSessions(listOf(session)) + val payload = try { + val adapter = MoshiProvider.providesMoshi().adapter(JSON_DICT_PARAMETERIZED_TYPE) + val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage) + adapter.fromJson(payloadString) + } catch (e: Exception) { + Timber.e("## decryptGroupMessage() : fails to parse the payload") + throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON) + } + + return OlmDecryptionResult( + payload, + session.keysClaimed, + senderKey, + session.forwardingCurve25519KeyChain + ) + } else { + val reason = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.roomId) + Timber.e("## decryptGroupMessage() : $reason") + throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, reason) + } + } + + /** + * Reset replay attack data for the given timeline. + * + * @param timeline the id of the timeline. + */ + fun resetReplayAttackCheckInTimeline(timeline: String?) { + if (null != timeline) { + inboundGroupSessionMessageIndexes.remove(timeline) + } + } + +// Utilities + + /** + * Verify an ed25519 signature on a JSON object. + * + * @param key the ed25519 key. + * @param jsonDictionary the JSON object which was signed. + * @param signature the base64-encoded signature to be checked. + * @throws Exception the exception + */ + @Throws(Exception::class) + fun verifySignature(key: String, jsonDictionary: Map, signature: String) { + // Check signature on the canonical version of the JSON + olmUtility!!.verifyEd25519Signature(signature, key, JsonCanonicalizer.getCanonicalJson(Map::class.java, jsonDictionary)) + } + + /** + * Calculate the SHA-256 hash of the input and encodes it as base64. + * + * @param message the message to hash. + * @return the base64-encoded hash value. + */ + fun sha256(message: String): String { + return olmUtility!!.sha256(convertToUTF8(message)) + } + + /** + * Search an OlmSession + * + * @param theirDeviceIdentityKey the device key + * @param sessionId the session Id + * @return the olm session + */ + private fun getSessionForDevice(theirDeviceIdentityKey: String, sessionId: String): OlmSessionWrapper? { + // sanity check + return if (theirDeviceIdentityKey.isEmpty() || sessionId.isEmpty()) null else { + store.getDeviceSession(sessionId, theirDeviceIdentityKey) + } + } + + /** + * Extract an InboundGroupSession from the session store and do some check. + * inboundGroupSessionWithIdError describes the failure reason. + * + * @param roomId the room where the session is used. + * @param sessionId the session identifier. + * @param senderKey the base64-encoded curve25519 key of the sender. + * @return the inbound group session. + */ + fun getInboundGroupSession(sessionId: String?, senderKey: String?, roomId: String?): OlmInboundGroupSessionWrapper2 { + if (sessionId.isNullOrBlank() || senderKey.isNullOrBlank()) { + throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY, MXCryptoError.ERROR_MISSING_PROPERTY_REASON) + } + + val session = store.getInboundGroupSession(sessionId, senderKey) + + if (session != null) { + // Check that the room id matches the original one for the session. This stops + // the HS pretending a message was targeting a different room. + if (roomId != session.roomId) { + val errorDescription = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.roomId) + Timber.e("## getInboundGroupSession() : $errorDescription") + throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, errorDescription) + } else { + return session + } + } else { + Timber.v("## getInboundGroupSession() : Cannot retrieve inbound group session $sessionId") + throw MXCryptoError.Base(MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID, MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_REASON) + } + } + + /** + * Determine if we have the keys for a given megolm session. + * + * @param roomId room in which the message was received + * @param senderKey base64-encoded curve25519 key of the sender + * @param sessionId session identifier + * @return true if the unbound session keys are known. + */ + fun hasInboundSessionKeys(roomId: String, senderKey: String, sessionId: String): Boolean { + return runCatching { getInboundGroupSession(sessionId, senderKey, roomId) }.isSuccess + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MegolmSessionData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MegolmSessionData.kt new file mode 100644 index 0000000000..9991115f28 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MegolmSessionData.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * The type of object we use for importing and exporting megolm session data. + */ +@JsonClass(generateAdapter = true) +data class MegolmSessionData( + /** + * The algorithm used. + */ + @Json(name = "algorithm") + val algorithm: String? = null, + + /** + * Unique id for the session. + */ + @Json(name = "session_id") + val sessionId: String? = null, + + /** + * Sender's Curve25519 device key. + */ + @Json(name = "sender_key") + val senderKey: String? = null, + + /** + * Room this session is used in. + */ + @Json(name = "room_id") + val roomId: String? = null, + + /** + * Base64'ed key data. + */ + @Json(name = "session_key") + val sessionKey: String? = null, + + /** + * Other keys the sender claims. + */ + @Json(name = "sender_claimed_keys") + val senderClaimedKeys: Map? = null, + + // This is a shortcut for sender_claimed_keys.get("ed25519") + // Keep it for compatibility reason. + @Json(name = "sender_claimed_ed25519_key") + val senderClaimedEd25519Key: String? = null, + + /** + * Devices which forwarded this session to us (normally empty). + */ + @Json(name = "forwarding_curve25519_key_chain") + val forwardingCurve25519KeyChain: List? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MyDeviceInfoHolder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MyDeviceInfoHolder.kt new file mode 100644 index 0000000000..092ab672a6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MyDeviceInfoHolder.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto + +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.session.SessionScope +import javax.inject.Inject + +@SessionScope +internal class MyDeviceInfoHolder @Inject constructor( + // The credentials, + credentials: Credentials, + // the crypto store + cryptoStore: IMXCryptoStore, + // Olm device + olmDevice: MXOlmDevice +) { + // Our device keys + /** + * my device info + */ + val myDevice: CryptoDeviceInfo + + init { + + val keys = HashMap() + +// TODO it's a bit strange, why not load from DB? + if (!olmDevice.deviceEd25519Key.isNullOrEmpty()) { + keys["ed25519:" + credentials.deviceId] = olmDevice.deviceEd25519Key!! + } + + if (!olmDevice.deviceCurve25519Key.isNullOrEmpty()) { + keys["curve25519:" + credentials.deviceId] = olmDevice.deviceCurve25519Key!! + } + +// myDevice.keys = keys +// +// myDevice.algorithms = MXCryptoAlgorithms.supportedAlgorithms() + + // TODO hwo to really check cross signed status? + // + val crossSigned = cryptoStore.getMyCrossSigningInfo()?.masterKey()?.trustLevel?.locallyVerified ?: false +// myDevice.trustLevel = DeviceTrustLevel(crossSigned, true) + + myDevice = CryptoDeviceInfo( + credentials.deviceId!!, + credentials.userId, + keys = keys, + algorithms = MXCryptoAlgorithms.supportedAlgorithms(), + trustLevel = DeviceTrustLevel(crossSigned, true) + ) + + // Add our own deviceinfo to the store + val endToEndDevicesForUser = cryptoStore.getUserDevices(credentials.userId) + + val myDevices = endToEndDevicesForUser.orEmpty().toMutableMap() + + myDevices[myDevice.deviceId] = myDevice + + cryptoStore.storeUserDevices(credentials.userId, myDevices) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/NewSessionListener.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/NewSessionListener.kt new file mode 100644 index 0000000000..19a8468e9c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/NewSessionListener.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto + +interface NewSessionListener { + fun onNewSession(roomId: String?, senderKey: String, sessionId: String) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/ObjectSigner.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/ObjectSigner.kt new file mode 100644 index 0000000000..e59fe10c82 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/ObjectSigner.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto + +import org.matrix.android.sdk.api.auth.data.Credentials +import javax.inject.Inject + +internal class ObjectSigner @Inject constructor(private val credentials: Credentials, + private val olmDevice: MXOlmDevice) { + + /** + * Sign Object + * + * Example: + *
+     *     {
+     *         "[MY_USER_ID]": {
+     *             "ed25519:[MY_DEVICE_ID]": "sign(str)"
+     *         }
+     *     }
+     * 
+ * + * @param strToSign the String to sign and to include in the Map + * @return a Map (see example) + */ + fun signObject(strToSign: String): Map> { + val result = HashMap>() + + val content = HashMap() + + content["ed25519:" + credentials.deviceId] = olmDevice.signMessage(strToSign) + ?: "" // null reported by rageshake if happens during logout + + result[credentials.userId] = content + + return result + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt new file mode 100644 index 0000000000..e37c2df69e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt @@ -0,0 +1,169 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto + +import org.matrix.android.sdk.internal.crypto.model.MXKey +import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadResponse +import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.util.JsonCanonicalizer +import org.matrix.olm.OlmAccount +import timber.log.Timber +import javax.inject.Inject +import kotlin.math.floor +import kotlin.math.min + +@SessionScope +internal class OneTimeKeysUploader @Inject constructor( + private val olmDevice: MXOlmDevice, + private val objectSigner: ObjectSigner, + private val uploadKeysTask: UploadKeysTask +) { + // tell if there is a OTK check in progress + private var oneTimeKeyCheckInProgress = false + + // last OTK check timestamp + private var lastOneTimeKeyCheck: Long = 0 + private var oneTimeKeyCount: Int? = null + + /** + * Stores the current one_time_key count which will be handled later (in a call of + * _onSyncCompleted). The count is e.g. coming from a /sync response. + * + * @param currentCount the new count + */ + fun updateOneTimeKeyCount(currentCount: Int) { + oneTimeKeyCount = currentCount + } + + /** + * Check if the OTK must be uploaded. + */ + suspend fun maybeUploadOneTimeKeys() { + if (oneTimeKeyCheckInProgress) { + Timber.v("maybeUploadOneTimeKeys: already in progress") + return + } + if (System.currentTimeMillis() - lastOneTimeKeyCheck < ONE_TIME_KEY_UPLOAD_PERIOD) { + // we've done a key upload recently. + Timber.v("maybeUploadOneTimeKeys: executed too recently") + return + } + + lastOneTimeKeyCheck = System.currentTimeMillis() + oneTimeKeyCheckInProgress = true + + // We then check how many keys we can store in the Account object. + val maxOneTimeKeys = olmDevice.getMaxNumberOfOneTimeKeys() + + // Try to keep at most half that number on the server. This leaves the + // rest of the slots free to hold keys that have been claimed from the + // server but we haven't received a message for. + // If we run out of slots when generating new keys then olm will + // discard the oldest private keys first. This will eventually clean + // out stale private keys that won't receive a message. + val keyLimit = floor(maxOneTimeKeys / 2.0).toInt() + val oneTimeKeyCountFromSync = oneTimeKeyCount + if (oneTimeKeyCountFromSync != null) { + // We need to keep a pool of one time public keys on the server so that + // other devices can start conversations with us. But we can only store + // a finite number of private keys in the olm Account object. + // To complicate things further then can be a delay between a device + // claiming a public one time key from the server and it sending us a + // message. We need to keep the corresponding private key locally until + // we receive the message. + // But that message might never arrive leaving us stuck with duff + // private keys clogging up our local storage. + // So we need some kind of engineering compromise to balance all of + // these factors. + try { + val uploadedKeys = uploadOTK(oneTimeKeyCountFromSync, keyLimit) + Timber.v("## uploadKeys() : success, $uploadedKeys key(s) sent") + } finally { + oneTimeKeyCheckInProgress = false + } + } else { + Timber.w("maybeUploadOneTimeKeys: waiting to know the number of OTK from the sync") + oneTimeKeyCheckInProgress = false + lastOneTimeKeyCheck = 0 + } + } + + /** + * Upload some the OTKs. + * + * @param keyCount the key count + * @param keyLimit the limit + * @return the number of uploaded keys + */ + private suspend fun uploadOTK(keyCount: Int, keyLimit: Int): Int { + if (keyLimit <= keyCount) { + // If we don't need to generate any more keys then we are done. + return 0 + } + val keysThisLoop = min(keyLimit - keyCount, ONE_TIME_KEY_GENERATION_MAX_NUMBER) + olmDevice.generateOneTimeKeys(keysThisLoop) + val response = uploadOneTimeKeys(olmDevice.getOneTimeKeys()) + olmDevice.markKeysAsPublished() + + if (response.hasOneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)) { + // Maybe upload other keys + return keysThisLoop + uploadOTK(response.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE), keyLimit) + } else { + Timber.e("## uploadOTK() : response for uploading keys does not contain one_time_key_counts.signed_curve25519") + throw Exception("response for uploading keys does not contain one_time_key_counts.signed_curve25519") + } + } + + /** + * Upload curve25519 one time keys. + */ + private suspend fun uploadOneTimeKeys(oneTimeKeys: Map>?): KeysUploadResponse { + val oneTimeJson = mutableMapOf() + + val curve25519Map = oneTimeKeys?.get(OlmAccount.JSON_KEY_ONE_TIME_KEY).orEmpty() + + curve25519Map.forEach { (key_id, value) -> + val k = mutableMapOf() + k["key"] = value + + // the key is also signed + val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, k) + + k["signatures"] = objectSigner.signObject(canonicalJson) + + oneTimeJson["signed_curve25519:$key_id"] = k + } + + // For now, we set the device id explicitly, as we may not be using the + // same one as used in login. + val uploadParams = UploadKeysTask.Params(null, oneTimeJson) + return uploadKeysTask.execute(uploadParams) + } + + companion object { + // max number of keys to upload at once + // Creating keys can be an expensive operation so we limit the + // number we generate in one go to avoid blocking the application + // for too long. + private const val ONE_TIME_KEY_GENERATION_MAX_NUMBER = 5 + + // frequency with which to check & upload one-time keys + private const val ONE_TIME_KEY_UPLOAD_PERIOD = (60 * 1000).toLong() // one minute + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequest.kt new file mode 100644 index 0000000000..34661fcc21 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequest.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto + +interface OutgoingGossipingRequest { + var recipients: Map> + var requestId: String + var state: OutgoingGossipingRequestState + // transaction id for the cancellation, if any + // var cancellationTxnId: String? +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequestManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequestManager.kt new file mode 100755 index 0000000000..030560b77f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequestManager.kt @@ -0,0 +1,163 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto + +import org.matrix.android.sdk.api.session.events.model.LocalEcho +import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.di.SessionId +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +@SessionScope +internal class OutgoingGossipingRequestManager @Inject constructor( + @SessionId private val sessionId: String, + private val cryptoStore: IMXCryptoStore, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val cryptoCoroutineScope: CoroutineScope, + private val gossipingWorkManager: GossipingWorkManager) { + + /** + * Send off a room key request, if we haven't already done so. + * + * + * The `requestBody` is compared (with a deep-equality check) against + * previous queued or sent requests and if it matches, no change is made. + * Otherwise, a request is added to the pending list, and a job is started + * in the background to send it. + * + * @param requestBody requestBody + * @param recipients recipients + */ + fun sendRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map>) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + cryptoStore.getOrAddOutgoingRoomKeyRequest(requestBody, recipients)?.let { + // Don't resend if it's already done, you need to cancel first (reRequest) + if (it.state == OutgoingGossipingRequestState.SENDING || it.state == OutgoingGossipingRequestState.SENT) { + Timber.v("## CRYPTO - GOSSIP sendOutgoingRoomKeyRequest() : we already request for that session: $it") + return@launch + } + + sendOutgoingGossipingRequest(it) + } + } + } + + fun sendSecretShareRequest(secretName: String, recipients: Map>) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + // A bit dirty, but for better stability give other party some time to mark + // devices trusted :/ + delay(1500) + cryptoStore.getOrAddOutgoingSecretShareRequest(secretName, recipients)?.let { + // TODO check if there is already one that is being sent? + if (it.state == OutgoingGossipingRequestState.SENDING /**|| it.state == OutgoingGossipingRequestState.SENT*/) { + Timber.v("## CRYPTO - GOSSIP sendSecretShareRequest() : we are already sending for that session: $it") + return@launch + } + + sendOutgoingGossipingRequest(it) + } + } + } + + /** + * Cancel room key requests, if any match the given details + * + * @param requestBody requestBody + */ + fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + cancelRoomKeyRequest(requestBody, false) + } + } + + /** + * Cancel room key requests, if any match the given details, and resend + * + * @param requestBody requestBody + */ + fun resendRoomKeyRequest(requestBody: RoomKeyRequestBody) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + cancelRoomKeyRequest(requestBody, true) + } + } + + /** + * Cancel room key requests, if any match the given details, and resend + * + * @param requestBody requestBody + * @param andResend true to resend the key request + */ + private fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody, andResend: Boolean) { + val req = cryptoStore.getOutgoingRoomKeyRequest(requestBody) + ?: // no request was made for this key + return Unit.also { + Timber.v("## CRYPTO - GOSSIP cancelRoomKeyRequest() Unknown request $requestBody") + } + + sendOutgoingRoomKeyRequestCancellation(req, andResend) + } + + /** + * Send the outgoing key request. + * + * @param request the request + */ + private fun sendOutgoingGossipingRequest(request: OutgoingGossipingRequest) { + Timber.v("## CRYPTO - GOSSIP sendOutgoingRoomKeyRequest() : Requesting keys $request") + + val params = SendGossipRequestWorker.Params( + sessionId = sessionId, + keyShareRequest = request as? OutgoingRoomKeyRequest, + secretShareRequest = request as? OutgoingSecretRequest + ) + cryptoStore.updateOutgoingGossipingRequestState(request.requestId, OutgoingGossipingRequestState.SENDING) + val workRequest = gossipingWorkManager.createWork(WorkerParamsFactory.toData(params), true) + gossipingWorkManager.postWork(workRequest) + } + + /** + * Given a OutgoingRoomKeyRequest, cancel it and delete the request record + * + * @param request the request + */ + private fun sendOutgoingRoomKeyRequestCancellation(request: OutgoingRoomKeyRequest, resend: Boolean = false) { + Timber.v("## CRYPTO - sendOutgoingRoomKeyRequestCancellation $request") + val params = CancelGossipRequestWorker.Params.fromRequest(sessionId, request) + cryptoStore.updateOutgoingGossipingRequestState(request.requestId, OutgoingGossipingRequestState.CANCELLING) + + val workRequest = gossipingWorkManager.createWork(WorkerParamsFactory.toData(params), true) + gossipingWorkManager.postWork(workRequest) + + if (resend) { + val reSendParams = SendGossipRequestWorker.Params( + sessionId = sessionId, + keyShareRequest = request.copy(requestId = LocalEcho.createLocalEchoId()) + ) + val reSendWorkRequest = gossipingWorkManager.createWork(WorkerParamsFactory.toData(reSendParams), true) + gossipingWorkManager.postWork(reSendWorkRequest) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingRoomKeyRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingRoomKeyRequest.kt new file mode 100755 index 0000000000..f27338b712 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingRoomKeyRequest.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto + +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody + +/** + * Represents an outgoing room key request + */ +@JsonClass(generateAdapter = true) +data class OutgoingRoomKeyRequest( + // RequestBody + var requestBody: RoomKeyRequestBody?, + // list of recipients for the request + override var recipients: Map>, + // Unique id for this request. Used for both + // an id within the request for later pairing with a cancellation, and for + // the transaction id when sending the to_device messages to our local + override var requestId: String, // current state of this request + override var state: OutgoingGossipingRequestState + // transaction id for the cancellation, if any + // override var cancellationTxnId: String? = null +) : OutgoingGossipingRequest { + + /** + * Used only for log. + * + * @return the room id. + */ + val roomId: String? + get() = if (null != requestBody) { + requestBody!!.roomId + } else null + + /** + * Used only for log. + * + * @return the session id + */ + val sessionId: String? + get() = if (null != requestBody) { + requestBody!!.sessionId + } else null +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingSecretRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingSecretRequest.kt new file mode 100755 index 0000000000..6b51b42b53 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingSecretRequest.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto + +import com.squareup.moshi.JsonClass + +/** + * Represents an outgoing room key request + */ +@JsonClass(generateAdapter = true) +class OutgoingSecretRequest( + // Secret Name + val secretName: String?, + // list of recipients for the request + override var recipients: Map>, + // Unique id for this request. Used for both + // an id within the request for later pairing with a cancellation, and for + // the transaction id when sending the to_device messages to our local + override var requestId: String, + // current state of this request + override var state: OutgoingGossipingRequestState) : OutgoingGossipingRequest { + + // transaction id for the cancellation, if any +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomDecryptorProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomDecryptorProvider.kt new file mode 100644 index 0000000000..e574627d39 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomDecryptorProvider.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto + +import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting +import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmDecryptionFactory +import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmDecryptionFactory +import org.matrix.android.sdk.internal.session.SessionScope +import timber.log.Timber +import javax.inject.Inject + +@SessionScope +internal class RoomDecryptorProvider @Inject constructor( + private val olmDecryptionFactory: MXOlmDecryptionFactory, + private val megolmDecryptionFactory: MXMegolmDecryptionFactory +) { + + // A map from algorithm to MXDecrypting instance, for each room + private val roomDecryptors: MutableMap> = HashMap() + + private val newSessionListeners = ArrayList() + + fun addNewSessionListener(listener: NewSessionListener) { + if (!newSessionListeners.contains(listener)) newSessionListeners.add(listener) + } + + fun removeSessionListener(listener: NewSessionListener) { + newSessionListeners.remove(listener) + } + + /** + * Get a decryptor for a given room and algorithm. + * If we already have a decryptor for the given room and algorithm, return + * it. Otherwise try to instantiate it. + * + * @param roomId the room id + * @param algorithm the crypto algorithm + * @return the decryptor + * // TODO Create another method for the case of roomId is null + */ + fun getOrCreateRoomDecryptor(roomId: String?, algorithm: String?): IMXDecrypting? { + // sanity check + if (algorithm.isNullOrEmpty()) { + Timber.e("## getRoomDecryptor() : null algorithm") + return null + } + if (roomId != null && roomId.isNotEmpty()) { + synchronized(roomDecryptors) { + val decryptors = roomDecryptors.getOrPut(roomId) { mutableMapOf() } + val alg = decryptors[algorithm] + if (alg != null) { + return alg + } + } + } + val decryptingClass = MXCryptoAlgorithms.hasDecryptorClassForAlgorithm(algorithm) + if (decryptingClass) { + val alg = when (algorithm) { + MXCRYPTO_ALGORITHM_MEGOLM -> megolmDecryptionFactory.create().apply { + this.newSessionListener = object : NewSessionListener { + override fun onNewSession(roomId: String?, senderKey: String, sessionId: String) { + // PR reviewer: the parameter has been renamed so is now in conflict with the parameter of getOrCreateRoomDecryptor + newSessionListeners.forEach { + try { + it.onNewSession(roomId, senderKey, sessionId) + } catch (e: Throwable) { + } + } + } + } + } + else -> olmDecryptionFactory.create() + } + if (!roomId.isNullOrEmpty()) { + synchronized(roomDecryptors) { + roomDecryptors[roomId]?.put(algorithm, alg) + } + } + return alg + } + return null + } + + fun getRoomDecryptor(roomId: String?, algorithm: String?): IMXDecrypting? { + if (roomId == null || algorithm == null) { + return null + } + return roomDecryptors[roomId]?.get(algorithm) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomEncryptorsStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomEncryptorsStore.kt new file mode 100644 index 0000000000..aabe2aedec --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomEncryptorsStore.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto + +import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting +import org.matrix.android.sdk.internal.session.SessionScope +import javax.inject.Inject + +@SessionScope +internal class RoomEncryptorsStore @Inject constructor() { + + // MXEncrypting instance for each room. + private val roomEncryptors = mutableMapOf() + + fun put(roomId: String, alg: IMXEncrypting) { + synchronized(roomEncryptors) { + roomEncryptors.put(roomId, alg) + } + } + + fun get(roomId: String): IMXEncrypting? { + return synchronized(roomEncryptors) { + roomEncryptors[roomId] + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipRequestWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipRequestWorker.kt new file mode 100644 index 0000000000..db85f2c246 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipRequestWorker.kt @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.Data +import androidx.work.WorkerParameters +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.failure.shouldBeRetried +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.LocalEcho +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.model.rest.GossipingToDeviceObject +import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyShareRequest +import org.matrix.android.sdk.internal.crypto.model.rest.SecretShareRequest +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import org.matrix.android.sdk.internal.worker.getSessionComponent +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import javax.inject.Inject + +internal class SendGossipRequestWorker(context: Context, + params: WorkerParameters) + : CoroutineWorker(context, params) { + + @JsonClass(generateAdapter = true) + internal data class Params( + val sessionId: String, + val keyShareRequest: OutgoingRoomKeyRequest? = null, + val secretShareRequest: OutgoingSecretRequest? = null + ) + + @Inject lateinit var sendToDeviceTask: SendToDeviceTask + @Inject lateinit var cryptoStore: IMXCryptoStore + @Inject lateinit var eventBus: EventBus + @Inject lateinit var credentials: Credentials + + override suspend fun doWork(): Result { + val errorOutputData = Data.Builder().putBoolean("failed", true).build() + val params = WorkerParamsFactory.fromData(inputData) + ?: return Result.success(errorOutputData) + + val sessionComponent = getSessionComponent(params.sessionId) + ?: return Result.success(errorOutputData).also { + // TODO, can this happen? should I update local echo? + Timber.e("Unknown Session, cannot send message, sessionId: ${params.sessionId}") + } + sessionComponent.inject(this) + + val localId = LocalEcho.createLocalEchoId() + val contentMap = MXUsersDevicesMap() + val eventType: String + val requestId: String + when { + params.keyShareRequest != null -> { + eventType = EventType.ROOM_KEY_REQUEST + requestId = params.keyShareRequest.requestId + val toDeviceContent = RoomKeyShareRequest( + requestingDeviceId = credentials.deviceId, + requestId = params.keyShareRequest.requestId, + action = GossipingToDeviceObject.ACTION_SHARE_REQUEST, + body = params.keyShareRequest.requestBody + ) + cryptoStore.saveGossipingEvent(Event( + type = eventType, + content = toDeviceContent.toContent(), + senderId = credentials.userId + ).also { + it.ageLocalTs = System.currentTimeMillis() + }) + + params.keyShareRequest.recipients.forEach { userToDeviceMap -> + userToDeviceMap.value.forEach { deviceId -> + contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent) + } + } + } + params.secretShareRequest != null -> { + eventType = EventType.REQUEST_SECRET + requestId = params.secretShareRequest.requestId + val toDeviceContent = SecretShareRequest( + requestingDeviceId = credentials.deviceId, + requestId = params.secretShareRequest.requestId, + action = GossipingToDeviceObject.ACTION_SHARE_REQUEST, + secretName = params.secretShareRequest.secretName + ) + + cryptoStore.saveGossipingEvent(Event( + type = eventType, + content = toDeviceContent.toContent(), + senderId = credentials.userId + ).also { + it.ageLocalTs = System.currentTimeMillis() + }) + + params.secretShareRequest.recipients.forEach { userToDeviceMap -> + userToDeviceMap.value.forEach { deviceId -> + contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent) + } + } + } + else -> { + return Result.success(errorOutputData).also { + Timber.e("Unknown empty gossiping request: $params") + } + } + } + try { + cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.SENDING) + sendToDeviceTask.execute( + SendToDeviceTask.Params( + eventType = eventType, + contentMap = contentMap, + transactionId = localId + ) + ) + cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.SENT) + return Result.success() + } catch (exception: Throwable) { + return if (exception.shouldBeRetried()) { + Result.retry() + } else { + cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.FAILED_TO_SEND) + Result.success(errorOutputData) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipWorker.kt new file mode 100644 index 0000000000..a3eb476b51 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipWorker.kt @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.Data +import androidx.work.WorkerParameters +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.failure.shouldBeRetried +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.LocalEcho +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction +import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.model.event.SecretSendEventContent +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import org.matrix.android.sdk.internal.worker.getSessionComponent +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import javax.inject.Inject + +internal class SendGossipWorker(context: Context, + params: WorkerParameters) + : CoroutineWorker(context, params) { + + @JsonClass(generateAdapter = true) + internal data class Params( + val sessionId: String, + val secretValue: String, + val request: IncomingSecretShareRequest + ) + + @Inject lateinit var sendToDeviceTask: SendToDeviceTask + @Inject lateinit var cryptoStore: IMXCryptoStore + @Inject lateinit var eventBus: EventBus + @Inject lateinit var credentials: Credentials + @Inject lateinit var messageEncrypter: MessageEncrypter + @Inject lateinit var ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction + + override suspend fun doWork(): Result { + val errorOutputData = Data.Builder().putBoolean("failed", true).build() + val params = WorkerParamsFactory.fromData(inputData) + ?: return Result.success(errorOutputData) + + val sessionComponent = getSessionComponent(params.sessionId) + ?: return Result.success(errorOutputData).also { + // TODO, can this happen? should I update local echo? + Timber.e("Unknown Session, cannot send message, sessionId: ${params.sessionId}") + } + sessionComponent.inject(this) + + val localId = LocalEcho.createLocalEchoId() + val eventType: String = EventType.SEND_SECRET + + val toDeviceContent = SecretSendEventContent( + requestId = params.request.requestId ?: "", + secretValue = params.secretValue + ) + + val requestingUserId = params.request.userId ?: "" + val requestingDeviceId = params.request.deviceId ?: "" + val deviceInfo = cryptoStore.getUserDevice(requestingUserId, requestingDeviceId) + ?: return Result.success(errorOutputData).also { + cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.FAILED_TO_ACCEPTED) + Timber.e("Unknown deviceInfo, cannot send message, sessionId: ${params.request.deviceId}") + } + + val sendToDeviceMap = MXUsersDevicesMap() + + val devicesByUser = mapOf(requestingUserId to listOf(deviceInfo)) + val usersDeviceMap = ensureOlmSessionsForDevicesAction.handle(devicesByUser) + val olmSessionResult = usersDeviceMap.getObject(requestingUserId, requestingDeviceId) + if (olmSessionResult?.sessionId == null) { + // no session with this device, probably because there + // were no one-time keys. + return Result.success(errorOutputData).also { + cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.FAILED_TO_ACCEPTED) + Timber.e("no session with this device, probably because there were no one-time keys.") + } + } + + val payloadJson = mapOf( + "type" to EventType.SEND_SECRET, + "content" to toDeviceContent.toContent() + ) + + try { + val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) + sendToDeviceMap.setObject(requestingUserId, requestingDeviceId, encodedPayload) + } catch (failure: Throwable) { + Timber.e("## Fail to encrypt gossip + ${failure.localizedMessage}") + } + + cryptoStore.saveGossipingEvent(Event( + type = eventType, + content = toDeviceContent.toContent(), + senderId = credentials.userId + ).also { + it.ageLocalTs = System.currentTimeMillis() + }) + + try { + sendToDeviceTask.execute( + SendToDeviceTask.Params( + eventType = EventType.ENCRYPTED, + contentMap = sendToDeviceMap, + transactionId = localId + ) + ) + cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.ACCEPTED) + return Result.success() + } catch (exception: Throwable) { + return if (exception.shouldBeRetried()) { + Result.retry() + } else { + cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.FAILED_TO_ACCEPTED) + Result.success(errorOutputData) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt new file mode 100644 index 0000000000..e69cac5a5e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt @@ -0,0 +1,144 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.actions + +import org.matrix.android.sdk.internal.crypto.MXOlmDevice +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.MXKey +import org.matrix.android.sdk.internal.crypto.model.MXOlmSessionResult +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask +import timber.log.Timber +import javax.inject.Inject + +internal class EnsureOlmSessionsForDevicesAction @Inject constructor( + private val olmDevice: MXOlmDevice, + private val oneTimeKeysForUsersDeviceTask: ClaimOneTimeKeysForUsersDeviceTask) { + + suspend fun handle(devicesByUser: Map>, force: Boolean = false): MXUsersDevicesMap { + val devicesWithoutSession = ArrayList() + + val results = MXUsersDevicesMap() + + for ((userId, deviceInfos) in devicesByUser) { + for (deviceInfo in deviceInfos) { + val deviceId = deviceInfo.deviceId + val key = deviceInfo.identityKey() + + val sessionId = olmDevice.getSessionId(key!!) + + if (sessionId.isNullOrEmpty() || force) { + devicesWithoutSession.add(deviceInfo) + } + + val olmSessionResult = MXOlmSessionResult(deviceInfo, sessionId) + results.setObject(userId, deviceId, olmSessionResult) + } + } + + if (devicesWithoutSession.size == 0) { + return results + } + + // Prepare the request for claiming one-time keys + val usersDevicesToClaim = MXUsersDevicesMap() + + val oneTimeKeyAlgorithm = MXKey.KEY_SIGNED_CURVE_25519_TYPE + + for (device in devicesWithoutSession) { + usersDevicesToClaim.setObject(device.userId, device.deviceId, oneTimeKeyAlgorithm) + } + + // TODO: this has a race condition - if we try to send another message + // while we are claiming a key, we will end up claiming two and setting up + // two sessions. + // + // That should eventually resolve itself, but it's poor form. + + Timber.v("## CRYPTO | claimOneTimeKeysForUsersDevices() : $usersDevicesToClaim") + + val claimParams = ClaimOneTimeKeysForUsersDeviceTask.Params(usersDevicesToClaim) + val oneTimeKeys = oneTimeKeysForUsersDeviceTask.execute(claimParams) + Timber.v("## CRYPTO | claimOneTimeKeysForUsersDevices() : keysClaimResponse.oneTimeKeys: $oneTimeKeys") + for ((userId, deviceInfos) in devicesByUser) { + for (deviceInfo in deviceInfos) { + var oneTimeKey: MXKey? = null + val deviceIds = oneTimeKeys.getUserDeviceIds(userId) + if (null != deviceIds) { + for (deviceId in deviceIds) { + val olmSessionResult = results.getObject(userId, deviceId) + if (olmSessionResult!!.sessionId != null && !force) { + // We already have a result for this device + continue + } + val key = oneTimeKeys.getObject(userId, deviceId) + if (key?.type == oneTimeKeyAlgorithm) { + oneTimeKey = key + } + if (oneTimeKey == null) { + Timber.v("## CRYPTO | ensureOlmSessionsForDevices() : No one-time keys " + oneTimeKeyAlgorithm + + " for device " + userId + " : " + deviceId) + continue + } + // Update the result for this device in results + olmSessionResult.sessionId = verifyKeyAndStartSession(oneTimeKey, userId, deviceInfo) + } + } + } + } + return results + } + + private fun verifyKeyAndStartSession(oneTimeKey: MXKey, userId: String, deviceInfo: CryptoDeviceInfo): String? { + var sessionId: String? = null + + val deviceId = deviceInfo.deviceId + val signKeyId = "ed25519:$deviceId" + val signature = oneTimeKey.signatureForUserId(userId, signKeyId) + + if (!signature.isNullOrEmpty() && !deviceInfo.fingerprint().isNullOrEmpty()) { + var isVerified = false + var errorMessage: String? = null + + try { + olmDevice.verifySignature(deviceInfo.fingerprint()!!, oneTimeKey.signalableJSONDictionary(), signature) + isVerified = true + } catch (e: Exception) { + errorMessage = e.message + } + + // Check one-time key signature + if (isVerified) { + sessionId = olmDevice.createOutboundSession(deviceInfo.identityKey()!!, oneTimeKey.value) + + if (!sessionId.isNullOrEmpty()) { + Timber.v("## CRYPTO | verifyKeyAndStartSession() : Started new sessionid " + sessionId + + " for device " + deviceInfo + "(theirOneTimeKey: " + oneTimeKey.value + ")") + } else { + // Possibly a bad key + Timber.e("## CRYPTO | verifyKeyAndStartSession() : Error starting session with device $userId:$deviceId") + } + } else { + Timber.e("## CRYPTO | verifyKeyAndStartSession() : Unable to verify signature on one-time key for device " + userId + + ":" + deviceId + " Error " + errorMessage) + } + } + + return sessionId + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt new file mode 100644 index 0000000000..270240f912 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.actions + +import org.matrix.android.sdk.internal.crypto.MXOlmDevice +import org.matrix.android.sdk.internal.crypto.model.MXOlmSessionResult +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import timber.log.Timber +import javax.inject.Inject + +internal class EnsureOlmSessionsForUsersAction @Inject constructor(private val olmDevice: MXOlmDevice, + private val cryptoStore: IMXCryptoStore, + private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction) { + + /** + * Try to make sure we have established olm sessions for the given users. + * @param users a list of user ids. + */ + suspend fun handle(users: List): MXUsersDevicesMap { + Timber.v("## ensureOlmSessionsForUsers() : ensureOlmSessionsForUsers $users") + val devicesByUser = users.associateWith { userId -> + val devices = cryptoStore.getUserDevices(userId)?.values.orEmpty() + + devices.filter { + // Don't bother setting up session to ourself + it.identityKey() != olmDevice.deviceCurve25519Key + // Don't bother setting up sessions with blocked users + && !(it.trustLevel?.isVerified() ?: false) + } + } + return ensureOlmSessionsForDevicesAction.handle(devicesByUser) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt new file mode 100644 index 0000000000..d9da459d7d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt @@ -0,0 +1,108 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.actions + +import androidx.annotation.WorkerThread +import org.matrix.android.sdk.api.listeners.ProgressListener +import org.matrix.android.sdk.internal.crypto.MXOlmDevice +import org.matrix.android.sdk.internal.crypto.MegolmSessionData +import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.RoomDecryptorProvider +import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult +import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import timber.log.Timber +import javax.inject.Inject + +internal class MegolmSessionDataImporter @Inject constructor(private val olmDevice: MXOlmDevice, + private val roomDecryptorProvider: RoomDecryptorProvider, + private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, + private val cryptoStore: IMXCryptoStore) { + + /** + * Import a list of megolm session keys. + * Must be call on the crypto coroutine thread + * + * @param megolmSessionsData megolm sessions. + * @param backUpKeys true to back up them to the homeserver. + * @param progressListener the progress listener + * @return import room keys result + */ + @WorkerThread + fun handle(megolmSessionsData: List, + fromBackup: Boolean, + progressListener: ProgressListener?): ImportRoomKeysResult { + val t0 = System.currentTimeMillis() + + val totalNumbersOfKeys = megolmSessionsData.size + var lastProgress = 0 + var totalNumbersOfImportedKeys = 0 + + progressListener?.onProgress(0, 100) + val olmInboundGroupSessionWrappers = olmDevice.importInboundGroupSessions(megolmSessionsData) + + megolmSessionsData.forEachIndexed { cpt, megolmSessionData -> + val decrypting = roomDecryptorProvider.getOrCreateRoomDecryptor(megolmSessionData.roomId, megolmSessionData.algorithm) + + if (null != decrypting) { + try { + val sessionId = megolmSessionData.sessionId + Timber.v("## importRoomKeys retrieve senderKey " + megolmSessionData.senderKey + " sessionId " + sessionId) + + totalNumbersOfImportedKeys++ + + // cancel any outstanding room key requests for this session + val roomKeyRequestBody = RoomKeyRequestBody( + algorithm = megolmSessionData.algorithm, + roomId = megolmSessionData.roomId, + senderKey = megolmSessionData.senderKey, + sessionId = megolmSessionData.sessionId + ) + + outgoingGossipingRequestManager.cancelRoomKeyRequest(roomKeyRequestBody) + + // Have another go at decrypting events sent with this session + decrypting.onNewSession(megolmSessionData.senderKey!!, sessionId!!) + } catch (e: Exception) { + Timber.e(e, "## importRoomKeys() : onNewSession failed") + } + } + + if (progressListener != null) { + val progress = 100 * (cpt + 1) / totalNumbersOfKeys + + if (lastProgress != progress) { + lastProgress = progress + + progressListener.onProgress(progress, 100) + } + } + } + + // Do not back up the key if it comes from a backup recovery + if (fromBackup) { + cryptoStore.markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers) + } + + val t1 = System.currentTimeMillis() + + Timber.v("## importMegolmSessionsData : sessions import " + (t1 - t0) + " ms (" + megolmSessionsData.size + " sessions)") + + return ImportRoomKeysResult(totalNumbersOfKeys, totalNumbersOfImportedKeys) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MessageEncrypter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MessageEncrypter.kt new file mode 100644 index 0000000000..c654622ffb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MessageEncrypter.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.actions + +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_OLM +import org.matrix.android.sdk.internal.crypto.MXOlmDevice +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedMessage +import org.matrix.android.sdk.internal.di.DeviceId +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.util.JsonCanonicalizer +import org.matrix.android.sdk.internal.util.convertToUTF8 +import timber.log.Timber +import javax.inject.Inject + +internal class MessageEncrypter @Inject constructor( + @UserId + private val userId: String, + @DeviceId + private val deviceId: String?, + private val olmDevice: MXOlmDevice) { + /** + * Encrypt an event payload for a list of devices. + * This method must be called from the getCryptoHandler() thread. + * + * @param payloadFields fields to include in the encrypted payload. + * @param deviceInfos list of device infos to encrypt for. + * @return the content for an m.room.encrypted event. + */ + fun encryptMessage(payloadFields: Content, deviceInfos: List): EncryptedMessage { + val deviceInfoParticipantKey = deviceInfos.associateBy { it.identityKey()!! } + + val payloadJson = payloadFields.toMutableMap() + + payloadJson["sender"] = userId + payloadJson["sender_device"] = deviceId!! + + // Include the Ed25519 key so that the recipient knows what + // device this message came from. + // We don't need to include the curve25519 key since the + // recipient will already know this from the olm headers. + // When combined with the device keys retrieved from the + // homeserver signed by the ed25519 key this proves that + // the curve25519 key and the ed25519 key are owned by + // the same device. + payloadJson["keys"] = mapOf("ed25519" to olmDevice.deviceEd25519Key!!) + + val ciphertext = mutableMapOf() + + for ((deviceKey, deviceInfo) in deviceInfoParticipantKey) { + val sessionId = olmDevice.getSessionId(deviceKey) + + if (!sessionId.isNullOrEmpty()) { + Timber.v("Using sessionid $sessionId for device $deviceKey") + + payloadJson["recipient"] = deviceInfo.userId + payloadJson["recipient_keys"] = mapOf("ed25519" to deviceInfo.fingerprint()!!) + + val payloadString = convertToUTF8(JsonCanonicalizer.getCanonicalJson(Map::class.java, payloadJson)) + ciphertext[deviceKey] = olmDevice.encryptMessage(deviceKey, sessionId, payloadString)!! + } + } + + return EncryptedMessage( + algorithm = MXCRYPTO_ALGORITHM_OLM, + senderKey = olmDevice.deviceCurve25519Key, + cipherText = ciphertext + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/SetDeviceVerificationAction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/SetDeviceVerificationAction.kt new file mode 100644 index 0000000000..a5c00c3632 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/SetDeviceVerificationAction.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.actions + +import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel +import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.di.UserId +import timber.log.Timber +import javax.inject.Inject + +internal class SetDeviceVerificationAction @Inject constructor( + private val cryptoStore: IMXCryptoStore, + @UserId private val userId: String, + private val defaultKeysBackupService: DefaultKeysBackupService) { + + fun handle(trustLevel: DeviceTrustLevel, userId: String, deviceId: String) { + val device = cryptoStore.getUserDevice(userId, deviceId) + + // Sanity check + if (null == device) { + Timber.w("## setDeviceVerification() : Unknown device $userId:$deviceId") + return + } + + if (device.isVerified != trustLevel.isVerified()) { + if (userId == this.userId) { + // If one of the user's own devices is being marked as verified / unverified, + // check the key backup status, since whether or not we use this depends on + // whether it has a signature from a verified device + defaultKeysBackupService.checkAndStartKeysBackup() + } + } + + if (device.trustLevel != trustLevel) { + device.trustLevel = trustLevel + cryptoStore.setDeviceTrust(userId, deviceId, trustLevel.crossSigningVerified, trustLevel.locallyVerified) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt new file mode 100644 index 0000000000..76efc4d77f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2015 OpenMarket Ltd + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.algorithms + +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.internal.crypto.IncomingRoomKeyRequest +import org.matrix.android.sdk.internal.crypto.IncomingSecretShareRequest +import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult +import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService + +/** + * An interface for decrypting data + */ +internal interface IMXDecrypting { + + /** + * Decrypt an event + * + * @param event the raw event. + * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. + * @return the decryption information, or an error + */ + @Throws(MXCryptoError::class) + fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult + + /** + * Handle a key event. + * + * @param event the key event. + */ + fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) {} + + /** + * Check if the some messages can be decrypted with a new session + * + * @param senderKey the session sender key + * @param sessionId the session id + */ + fun onNewSession(senderKey: String, sessionId: String) {} + + /** + * Determine if we have the keys necessary to respond to a room key request + * + * @param request keyRequest + * @return true if we have the keys and could (theoretically) share + */ + fun hasKeysForKeyRequest(request: IncomingRoomKeyRequest): Boolean = false + + /** + * Send the response to a room key request. + * + * @param request keyRequest + */ + fun shareKeysWithDevice(request: IncomingRoomKeyRequest) {} + + fun shareSecretWithDevice(request: IncomingSecretShareRequest, secretValue : String) {} + + fun requestKeysForEvent(event: Event, withHeld: Boolean) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXEncrypting.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXEncrypting.kt new file mode 100644 index 0000000000..60a5d7be7a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXEncrypting.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2015 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.algorithms + +import org.matrix.android.sdk.api.session.events.model.Content + +/** + * An interface for encrypting data + */ +internal interface IMXEncrypting { + + /** + * Encrypt an event content according to the configuration of the room. + * + * @param eventContent the content of the event. + * @param eventType the type of the event. + * @param userIds the room members the event will be sent to. + * @return the encrypted content + */ + suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List): Content + + /** + * In Megolm, each recipient maintains a record of the ratchet value which allows + * them to decrypt any messages sent in the session after the corresponding point + * in the conversation. If this value is compromised, an attacker can similarly + * decrypt past messages which were encrypted by a key derived from the + * compromised or subsequent ratchet values. This gives 'partial' forward + * secrecy. + * + * To mitigate this issue, the application should offer the user the option to + * discard historical conversations, by winding forward any stored ratchet values, + * or discarding sessions altogether. + */ + fun discardSessionKey() + + /** + * Re-shares a session key with devices if the key has already been + * sent to them. + * + * @param sessionId The id of the outbound session to share. + * @param userId The id of the user who owns the target device. + * @param deviceId The id of the target device. + * @param senderKey The key of the originating device for the session. + * + * @return true in case of success + */ + suspend fun reshareKey(sessionId: String, + userId: String, + deviceId: String, + senderKey: String): Boolean +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXWithHeldExtension.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXWithHeldExtension.kt new file mode 100644 index 0000000000..844cb38858 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXWithHeldExtension.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.algorithms + +import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent + +internal interface IMXWithHeldExtension { + fun onRoomKeyWithHeldEvent(withHeldInfo: RoomKeyWithHeldContent) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt new file mode 100644 index 0000000000..423c883927 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt @@ -0,0 +1,388 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.algorithms.megolm + +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.internal.crypto.DeviceListManager +import org.matrix.android.sdk.internal.crypto.IncomingRoomKeyRequest +import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult +import org.matrix.android.sdk.internal.crypto.MXOlmDevice +import org.matrix.android.sdk.internal.crypto.NewSessionListener +import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction +import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter +import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting +import org.matrix.android.sdk.internal.crypto.algorithms.IMXWithHeldExtension +import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent +import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyContent +import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent +import org.matrix.android.sdk.internal.crypto.model.rest.ForwardedRoomKeyContent +import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import timber.log.Timber + +internal class MXMegolmDecryption(private val userId: String, + private val olmDevice: MXOlmDevice, + private val deviceListManager: DeviceListManager, + private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, + private val messageEncrypter: MessageEncrypter, + private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, + private val cryptoStore: IMXCryptoStore, + private val sendToDeviceTask: SendToDeviceTask, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val cryptoCoroutineScope: CoroutineScope +) : IMXDecrypting, IMXWithHeldExtension { + + var newSessionListener: NewSessionListener? = null + + /** + * Events which we couldn't decrypt due to unknown sessions / indexes: map from + * senderKey|sessionId to timelines to list of MatrixEvents. + */ +// private var pendingEvents: MutableMap>> = HashMap() + + @Throws(MXCryptoError::class) + override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { + // If cross signing is enabled, we don't send request until the keys are trusted + // There could be a race effect here when xsigning is enabled, we should ensure that keys was downloaded once + val requestOnFail = cryptoStore.getMyCrossSigningInfo()?.isTrusted() == true + return decryptEvent(event, timeline, requestOnFail) + } + + @Throws(MXCryptoError::class) + private fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): MXEventDecryptionResult { + Timber.v("## CRYPTO | decryptEvent ${event.eventId} , requestKeysOnFail:$requestKeysOnFail") + if (event.roomId.isNullOrBlank()) { + throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) + } + + val encryptedEventContent = event.content.toModel() + ?: throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) + + if (encryptedEventContent.senderKey.isNullOrBlank() + || encryptedEventContent.sessionId.isNullOrBlank() + || encryptedEventContent.ciphertext.isNullOrBlank()) { + throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) + } + + return runCatching { + olmDevice.decryptGroupMessage(encryptedEventContent.ciphertext, + event.roomId, + timeline, + encryptedEventContent.sessionId, + encryptedEventContent.senderKey) + } + .fold( + { olmDecryptionResult -> + // the decryption succeeds + if (olmDecryptionResult.payload != null) { + MXEventDecryptionResult( + clearEvent = olmDecryptionResult.payload, + senderCurve25519Key = olmDecryptionResult.senderKey, + claimedEd25519Key = olmDecryptionResult.keysClaimed?.get("ed25519"), + forwardingCurve25519KeyChain = olmDecryptionResult.forwardingCurve25519KeyChain + .orEmpty() + ) + } else { + throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) + } + }, + { throwable -> + if (throwable is MXCryptoError.OlmError) { + // TODO Check the value of .message + if (throwable.olmException.message == "UNKNOWN_MESSAGE_INDEX") { + // addEventToPendingList(event, timeline) + // The session might has been partially withheld (and only pass ratcheted) + val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId) + if (withHeldInfo != null) { + if (requestKeysOnFail) { + requestKeysForEvent(event, true) + } + // Encapsulate as withHeld exception + throw MXCryptoError.Base(MXCryptoError.ErrorType.KEYS_WITHHELD, + withHeldInfo.code?.value ?: "", + withHeldInfo.reason) + } + + if (requestKeysOnFail) { + requestKeysForEvent(event, false) + } + } + + val reason = String.format(MXCryptoError.OLM_REASON, throwable.olmException.message) + val detailedReason = String.format(MXCryptoError.DETAILED_OLM_REASON, encryptedEventContent.ciphertext, reason) + + throw MXCryptoError.Base( + MXCryptoError.ErrorType.OLM, + reason, + detailedReason) + } + if (throwable is MXCryptoError.Base) { + if ( + /** if the session is unknown*/ + throwable.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID + ) { + val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId) + if (withHeldInfo != null) { + if (requestKeysOnFail) { + requestKeysForEvent(event, true) + } + // Encapsulate as withHeld exception + throw MXCryptoError.Base(MXCryptoError.ErrorType.KEYS_WITHHELD, + withHeldInfo.code?.value ?: "", + withHeldInfo.reason) + } else { + // This is un-used in riotX SDK, not sure if needed + // addEventToPendingList(event, timeline) + if (requestKeysOnFail) { + requestKeysForEvent(event, false) + } + } + } + } + throw throwable + } + ) + } + + /** + * Helper for the real decryptEvent and for _retryDecryption. If + * requestKeysOnFail is true, we'll send an m.room_key_request when we fail + * to decrypt the event due to missing megolm keys. + * + * @param event the event + */ + override fun requestKeysForEvent(event: Event, withHeld: Boolean) { + val sender = event.senderId ?: return + val encryptedEventContent = event.content.toModel() + val senderDevice = encryptedEventContent?.deviceId ?: return + + val recipients = if (event.senderId == userId || withHeld) { + mapOf( + userId to listOf("*") + ) + } else { + // for the case where you share the key with a device that has a broken olm session + // The other user might Re-shares a megolm session key with devices if the key has already been + // sent to them. + mapOf( + userId to listOf("*"), + sender to listOf(senderDevice) + ) + } + + val requestBody = RoomKeyRequestBody( + roomId = event.roomId, + algorithm = encryptedEventContent.algorithm, + senderKey = encryptedEventContent.senderKey, + sessionId = encryptedEventContent.sessionId + ) + + outgoingGossipingRequestManager.sendRoomKeyRequest(requestBody, recipients) + } + +// /** +// * Add an event to the list of those we couldn't decrypt the first time we +// * saw them. +// * +// * @param event the event to try to decrypt later +// * @param timelineId the timeline identifier +// */ +// private fun addEventToPendingList(event: Event, timelineId: String) { +// val encryptedEventContent = event.content.toModel() ?: return +// val pendingEventsKey = "${encryptedEventContent.senderKey}|${encryptedEventContent.sessionId}" +// +// val timeline = pendingEvents.getOrPut(pendingEventsKey) { HashMap() } +// val events = timeline.getOrPut(timelineId) { ArrayList() } +// +// if (event !in events) { +// Timber.v("## CRYPTO | addEventToPendingList() : add Event ${event.eventId} in room id ${event.roomId}") +// events.add(event) +// } +// } + + /** + * Handle a key event. + * + * @param event the key event. + */ + override fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) { + Timber.v("## CRYPTO | onRoomKeyEvent()") + var exportFormat = false + val roomKeyContent = event.getClearContent().toModel() ?: return + + var senderKey: String? = event.getSenderKey() + var keysClaimed: MutableMap = HashMap() + val forwardingCurve25519KeyChain: MutableList = ArrayList() + + if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.sessionId.isNullOrEmpty() || roomKeyContent.sessionKey.isNullOrEmpty()) { + Timber.e("## CRYPTO | onRoomKeyEvent() : Key event is missing fields") + return + } + if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) { + Timber.v("## CRYPTO | onRoomKeyEvent(), forward adding key : roomId ${roomKeyContent.roomId}" + + " sessionId ${roomKeyContent.sessionId} sessionKey ${roomKeyContent.sessionKey}") + val forwardedRoomKeyContent = event.getClearContent().toModel() + ?: return + + forwardedRoomKeyContent.forwardingCurve25519KeyChain?.let { + forwardingCurve25519KeyChain.addAll(it) + } + + if (senderKey == null) { + Timber.e("## CRYPTO | onRoomKeyEvent() : event is missing sender_key field") + return + } + + forwardingCurve25519KeyChain.add(senderKey) + + exportFormat = true + senderKey = forwardedRoomKeyContent.senderKey + if (null == senderKey) { + Timber.e("## CRYPTO | onRoomKeyEvent() : forwarded_room_key event is missing sender_key field") + return + } + + if (null == forwardedRoomKeyContent.senderClaimedEd25519Key) { + Timber.e("## CRYPTO | forwarded_room_key_event is missing sender_claimed_ed25519_key field") + return + } + + keysClaimed["ed25519"] = forwardedRoomKeyContent.senderClaimedEd25519Key + } else { + Timber.v("## CRYPTO | onRoomKeyEvent(), Adding key : roomId " + roomKeyContent.roomId + " sessionId " + roomKeyContent.sessionId + + " sessionKey " + roomKeyContent.sessionKey) // from " + event); + + if (null == senderKey) { + Timber.e("## onRoomKeyEvent() : key event has no sender key (not encrypted?)") + return + } + + // inherit the claimed ed25519 key from the setup message + keysClaimed = event.getKeysClaimed().toMutableMap() + } + + Timber.e("## CRYPTO | onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId}") + val added = olmDevice.addInboundGroupSession(roomKeyContent.sessionId, + roomKeyContent.sessionKey, + roomKeyContent.roomId, + senderKey, + forwardingCurve25519KeyChain, + keysClaimed, + exportFormat) + + if (added) { + defaultKeysBackupService.maybeBackupKeys() + + val content = RoomKeyRequestBody( + algorithm = roomKeyContent.algorithm, + roomId = roomKeyContent.roomId, + sessionId = roomKeyContent.sessionId, + senderKey = senderKey + ) + + outgoingGossipingRequestManager.cancelRoomKeyRequest(content) + + onNewSession(senderKey, roomKeyContent.sessionId) + } + } + + /** + * Check if the some messages can be decrypted with a new session + * + * @param senderKey the session sender key + * @param sessionId the session id + */ + override fun onNewSession(senderKey: String, sessionId: String) { + Timber.v(" CRYPTO | ON NEW SESSION $sessionId - $senderKey") + newSessionListener?.onNewSession(null, senderKey, sessionId) + } + + override fun hasKeysForKeyRequest(request: IncomingRoomKeyRequest): Boolean { + val roomId = request.requestBody?.roomId ?: return false + val senderKey = request.requestBody.senderKey ?: return false + val sessionId = request.requestBody.sessionId ?: return false + return olmDevice.hasInboundSessionKeys(roomId, senderKey, sessionId) + } + + override fun shareKeysWithDevice(request: IncomingRoomKeyRequest) { + // sanity checks + if (request.requestBody == null) { + return + } + val userId = request.userId ?: return + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + runCatching { deviceListManager.downloadKeys(listOf(userId), false) } + .mapCatching { + val deviceId = request.deviceId + val deviceInfo = cryptoStore.getUserDevice(userId, deviceId ?: "") + if (deviceInfo == null) { + throw RuntimeException() + } else { + val devicesByUser = mapOf(userId to listOf(deviceInfo)) + val usersDeviceMap = ensureOlmSessionsForDevicesAction.handle(devicesByUser) + val body = request.requestBody + val olmSessionResult = usersDeviceMap.getObject(userId, deviceId) + if (olmSessionResult?.sessionId == null) { + // no session with this device, probably because there + // were no one-time keys. + return@mapCatching + } + Timber.v("## CRYPTO | shareKeysWithDevice() : sharing keys for session" + + " ${body.senderKey}|${body.sessionId} with device $userId:$deviceId") + + val payloadJson = mutableMapOf("type" to EventType.FORWARDED_ROOM_KEY) + runCatching { olmDevice.getInboundGroupSession(body.sessionId, body.senderKey, body.roomId) } + .fold( + { + // TODO + payloadJson["content"] = it.exportKeys() ?: "" + }, + { + // TODO + } + + ) + + val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) + val sendToDeviceMap = MXUsersDevicesMap() + sendToDeviceMap.setObject(userId, deviceId, encodedPayload) + Timber.v("## CRYPTO | shareKeysWithDevice() : sending to $userId:$deviceId") + val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) + sendToDeviceTask.execute(sendToDeviceParams) + } + } + } + } + + override fun onRoomKeyWithHeldEvent(withHeldInfo: RoomKeyWithHeldContent) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + cryptoStore.addWithHeldMegolmSession(withHeldInfo) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt new file mode 100644 index 0000000000..b7b2919dbe --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.algorithms.megolm + +import org.matrix.android.sdk.internal.crypto.DeviceListManager +import org.matrix.android.sdk.internal.crypto.MXOlmDevice +import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction +import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import kotlinx.coroutines.CoroutineScope +import javax.inject.Inject + +internal class MXMegolmDecryptionFactory @Inject constructor( + @UserId private val userId: String, + private val olmDevice: MXOlmDevice, + private val deviceListManager: DeviceListManager, + private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, + private val messageEncrypter: MessageEncrypter, + private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, + private val cryptoStore: IMXCryptoStore, + private val sendToDeviceTask: SendToDeviceTask, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val cryptoCoroutineScope: CoroutineScope +) { + + fun create(): MXMegolmDecryption { + return MXMegolmDecryption( + userId, + olmDevice, + deviceListManager, + outgoingGossipingRequestManager, + messageEncrypter, + ensureOlmSessionsForDevicesAction, + cryptoStore, + sendToDeviceTask, + coroutineDispatchers, + cryptoCoroutineScope) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt new file mode 100644 index 0000000000..8c2dfc9e5d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt @@ -0,0 +1,440 @@ +/* + * Copyright 2015 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.algorithms.megolm + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.internal.crypto.DeviceListManager +import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.internal.crypto.MXOlmDevice +import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction +import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter +import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting +import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent +import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode +import org.matrix.android.sdk.internal.crypto.model.forEach +import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import org.matrix.android.sdk.internal.util.JsonCanonicalizer +import org.matrix.android.sdk.internal.util.convertToUTF8 +import timber.log.Timber + +internal class MXMegolmEncryption( + // The id of the room we will be sending to. + private val roomId: String, + private val olmDevice: MXOlmDevice, + private val defaultKeysBackupService: DefaultKeysBackupService, + private val cryptoStore: IMXCryptoStore, + private val deviceListManager: DeviceListManager, + private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, + private val credentials: Credentials, + private val sendToDeviceTask: SendToDeviceTask, + private val messageEncrypter: MessageEncrypter, + private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository, + private val taskExecutor: TaskExecutor +) : IMXEncrypting { + + // OutboundSessionInfo. Null if we haven't yet started setting one up. Note + // that even if this is non-null, it may not be ready for use (in which + // case outboundSession.shareOperation will be non-null.) + private var outboundSession: MXOutboundSessionInfo? = null + + // Default rotation periods + // TODO: Make it configurable via parameters + // Session rotation periods + private var sessionRotationPeriodMsgs: Int = 100 + private var sessionRotationPeriodMs: Int = 7 * 24 * 3600 * 1000 + + override suspend fun encryptEventContent(eventContent: Content, + eventType: String, + userIds: List): Content { + val ts = System.currentTimeMillis() + Timber.v("## CRYPTO | encryptEventContent : getDevicesInRoom") + val devices = getDevicesInRoom(userIds) + Timber.v("## CRYPTO | encryptEventContent ${System.currentTimeMillis() - ts}: getDevicesInRoom ${devices.allowedDevices.map}") + val outboundSession = ensureOutboundSession(devices.allowedDevices) + + return encryptContent(outboundSession, eventType, eventContent) + .also { + notifyWithheldForSession(devices.withHeldDevices, outboundSession) + } + } + + private fun notifyWithheldForSession(devices: MXUsersDevicesMap, outboundSession: MXOutboundSessionInfo) { + mutableListOf>().apply { + devices.forEach { userId, deviceId, withheldCode -> + this.add(UserDevice(userId, deviceId) to withheldCode) + } + }.groupBy( + { it.second }, + { it.first } + ).forEach { (code, targets) -> + notifyKeyWithHeld(targets, outboundSession.sessionId, olmDevice.deviceCurve25519Key, code) + } + } + + override fun discardSessionKey() { + outboundSession = null + } + + /** + * Prepare a new session. + * + * @return the session description + */ + private fun prepareNewSessionInRoom(): MXOutboundSessionInfo { + Timber.v("## CRYPTO | prepareNewSessionInRoom() ") + val sessionId = olmDevice.createOutboundGroupSession() + + val keysClaimedMap = HashMap() + keysClaimedMap["ed25519"] = olmDevice.deviceEd25519Key!! + + olmDevice.addInboundGroupSession(sessionId!!, olmDevice.getSessionKey(sessionId)!!, roomId, olmDevice.deviceCurve25519Key!!, + emptyList(), keysClaimedMap, false) + + defaultKeysBackupService.maybeBackupKeys() + + return MXOutboundSessionInfo(sessionId, SharedWithHelper(roomId, sessionId, cryptoStore)) + } + + /** + * Ensure the outbound session + * + * @param devicesInRoom the devices list + */ + private suspend fun ensureOutboundSession(devicesInRoom: MXUsersDevicesMap): MXOutboundSessionInfo { + Timber.v("## CRYPTO | ensureOutboundSession start") + var session = outboundSession + if (session == null + // Need to make a brand new session? + || session.needsRotation(sessionRotationPeriodMsgs, sessionRotationPeriodMs) + // Determine if we have shared with anyone we shouldn't have + || session.sharedWithTooManyDevices(devicesInRoom)) { + session = prepareNewSessionInRoom() + outboundSession = session + } + val safeSession = session + val shareMap = HashMap>()/* userId */ + val userIds = devicesInRoom.userIds + for (userId in userIds) { + val deviceIds = devicesInRoom.getUserDeviceIds(userId) + for (deviceId in deviceIds!!) { + val deviceInfo = devicesInRoom.getObject(userId, deviceId) + if (deviceInfo != null && !cryptoStore.wasSessionSharedWithUser(roomId, safeSession.sessionId, userId, deviceId).found) { + val devices = shareMap.getOrPut(userId) { ArrayList() } + devices.add(deviceInfo) + } + } + } + shareKey(safeSession, shareMap) + return safeSession + } + + /** + * Share the device key to a list of users + * + * @param session the session info + * @param devicesByUsers the devices map + */ + private suspend fun shareKey(session: MXOutboundSessionInfo, + devicesByUsers: Map>) { + // nothing to send, the task is done + if (devicesByUsers.isEmpty()) { + Timber.v("## CRYPTO | shareKey() : nothing more to do") + return + } + // reduce the map size to avoid request timeout when there are too many devices (Users size * devices per user) + val subMap = HashMap>() + var devicesCount = 0 + for ((userId, devices) in devicesByUsers) { + subMap[userId] = devices + devicesCount += devices.size + if (devicesCount > 100) { + break + } + } + Timber.v("## CRYPTO | shareKey() ; sessionId<${session.sessionId}> userId ${subMap.keys}") + shareUserDevicesKey(session, subMap) + val remainingDevices = devicesByUsers - subMap.keys + shareKey(session, remainingDevices) + } + + /** + * Share the device keys of a an user + * + * @param session the session info + * @param devicesByUser the devices map + */ + private suspend fun shareUserDevicesKey(session: MXOutboundSessionInfo, + devicesByUser: Map>) { + val sessionKey = olmDevice.getSessionKey(session.sessionId) + val chainIndex = olmDevice.getMessageIndex(session.sessionId) + + val submap = HashMap() + submap["algorithm"] = MXCRYPTO_ALGORITHM_MEGOLM + submap["room_id"] = roomId + submap["session_id"] = session.sessionId + submap["session_key"] = sessionKey!! + submap["chain_index"] = chainIndex + + val payload = HashMap() + payload["type"] = EventType.ROOM_KEY + payload["content"] = submap + + var t0 = System.currentTimeMillis() + Timber.v("## CRYPTO | shareUserDevicesKey() : starts") + + val results = ensureOlmSessionsForDevicesAction.handle(devicesByUser) + Timber.v("## CRYPTO | shareUserDevicesKey() : ensureOlmSessionsForDevices succeeds after " + + (System.currentTimeMillis() - t0) + " ms") + val contentMap = MXUsersDevicesMap() + var haveTargets = false + val userIds = results.userIds + for (userId in userIds) { + val devicesToShareWith = devicesByUser[userId] + for ((deviceID) in devicesToShareWith!!) { + val sessionResult = results.getObject(userId, deviceID) + if (sessionResult?.sessionId == null) { + // no session with this device, probably because there + // were no one-time keys. + + // MSC 2399 + // send withheld m.no_olm: an olm session could not be established. + // This may happen, for example, if the sender was unable to obtain a one-time key from the recipient. + notifyKeyWithHeld( + listOf(UserDevice(userId, deviceID)), + session.sessionId, + olmDevice.deviceCurve25519Key, + WithHeldCode.NO_OLM + ) + + continue + } + Timber.v("## CRYPTO | shareUserDevicesKey() : Sharing keys with device $userId:$deviceID") + contentMap.setObject(userId, deviceID, messageEncrypter.encryptMessage(payload, listOf(sessionResult.deviceInfo))) + haveTargets = true + } + } + + // Add the devices we have shared with to session.sharedWithDevices. + // we deliberately iterate over devicesByUser (ie, the devices we + // attempted to share with) rather than the contentMap (those we did + // share with), because we don't want to try to claim a one-time-key + // for dead devices on every message. + for ((userId, devicesToShareWith) in devicesByUser) { + for ((deviceId) in devicesToShareWith) { + session.sharedWithHelper.markedSessionAsShared(userId, deviceId, chainIndex) + } + } + + if (haveTargets) { + t0 = System.currentTimeMillis() + Timber.v("## CRYPTO | shareUserDevicesKey() : has target") + val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap) + try { + sendToDeviceTask.execute(sendToDeviceParams) + Timber.v("## CRYPTO | shareUserDevicesKey() : sendToDevice succeeds after ${System.currentTimeMillis() - t0} ms") + } catch (failure: Throwable) { + // What to do here... + Timber.e("## CRYPTO | shareUserDevicesKey() : Failed to share session <${session.sessionId}> with $devicesByUser ") + } + } else { + Timber.v("## CRYPTO | shareUserDevicesKey() : no need to sharekey") + } + } + + private fun notifyKeyWithHeld(targets: List, sessionId: String, senderKey: String?, code: WithHeldCode) { + val withHeldContent = RoomKeyWithHeldContent( + roomId = roomId, + senderKey = senderKey, + algorithm = MXCRYPTO_ALGORITHM_MEGOLM, + sessionId = sessionId, + codeString = code.value + ) + val params = SendToDeviceTask.Params( + EventType.ROOM_KEY_WITHHELD, + MXUsersDevicesMap().apply { + targets.forEach { + setObject(it.userId, it.deviceId, withHeldContent) + } + } + ) + sendToDeviceTask.configureWith(params) { + callback = object : MatrixCallback { + override fun onFailure(failure: Throwable) { + Timber.e("## CRYPTO | notifyKeyWithHeld() : Failed to notify withheld key for $targets session: $sessionId ") + } + } + }.executeBy(taskExecutor) + } + + /** + * process the pending encryptions + */ + private fun encryptContent(session: MXOutboundSessionInfo, eventType: String, eventContent: Content): Content { + // Everything is in place, encrypt all pending events + val payloadJson = HashMap() + payloadJson["room_id"] = roomId + payloadJson["type"] = eventType + payloadJson["content"] = eventContent + + // Get canonical Json from + + val payloadString = convertToUTF8(JsonCanonicalizer.getCanonicalJson(Map::class.java, payloadJson)) + val ciphertext = olmDevice.encryptGroupMessage(session.sessionId, payloadString) + + val map = HashMap() + map["algorithm"] = MXCRYPTO_ALGORITHM_MEGOLM + map["sender_key"] = olmDevice.deviceCurve25519Key!! + map["ciphertext"] = ciphertext!! + map["session_id"] = session.sessionId + + // Include our device ID so that recipients can send us a + // m.new_device message if they don't have our session key. + map["device_id"] = credentials.deviceId!! + session.useCount++ + return map + } + + /** + * Get the list of devices which can encrypt data to. + * This method must be called in getDecryptingThreadHandler() thread. + * + * @param userIds the user ids whose devices must be checked. + */ + private suspend fun getDevicesInRoom(userIds: List): DeviceInRoomInfo { + // We are happy to use a cached version here: we assume that if we already + // have a list of the user's devices, then we already share an e2e room + // with them, which means that they will have announced any new devices via + // an m.new_device. + val keys = deviceListManager.downloadKeys(userIds, false) + val encryptToVerifiedDevicesOnly = cryptoStore.getGlobalBlacklistUnverifiedDevices() + || cryptoStore.getRoomsListBlacklistUnverifiedDevices().contains(roomId) + + val devicesInRoom = DeviceInRoomInfo() + val unknownDevices = MXUsersDevicesMap() + + for (userId in keys.userIds) { + val deviceIds = keys.getUserDeviceIds(userId) ?: continue + for (deviceId in deviceIds) { + val deviceInfo = keys.getObject(userId, deviceId) ?: continue + if (warnOnUnknownDevicesRepository.warnOnUnknownDevices() && deviceInfo.isUnknown) { + // The device is not yet known by the user + unknownDevices.setObject(userId, deviceId, deviceInfo) + continue + } + if (deviceInfo.isBlocked) { + // Remove any blocked devices + devicesInRoom.withHeldDevices.setObject(userId, deviceId, WithHeldCode.BLACKLISTED) + continue + } + + if (!deviceInfo.isVerified && encryptToVerifiedDevicesOnly) { + devicesInRoom.withHeldDevices.setObject(userId, deviceId, WithHeldCode.UNVERIFIED) + continue + } + + if (deviceInfo.identityKey() == olmDevice.deviceCurve25519Key) { + // Don't bother sending to ourself + continue + } + devicesInRoom.allowedDevices.setObject(userId, deviceId, deviceInfo) + } + } + if (unknownDevices.isEmpty) { + return devicesInRoom + } else { + throw MXCryptoError.UnknownDevice(unknownDevices) + } + } + + override suspend fun reshareKey(sessionId: String, + userId: String, + deviceId: String, + senderKey: String): Boolean { + Timber.d("[MXMegolmEncryption] reshareKey: $sessionId to $userId:$deviceId") + val deviceInfo = cryptoStore.getUserDevice(userId, deviceId) ?: return false + .also { Timber.w("Device not found") } + + // Get the chain index of the key we previously sent this device + val chainIndex = outboundSession?.sharedWithHelper?.wasSharedWith(userId, deviceId) ?: return false + .also { + // Send a room key with held + notifyKeyWithHeld(listOf(UserDevice(userId, deviceId)), sessionId, senderKey, WithHeldCode.UNAUTHORISED) + Timber.w("[MXMegolmEncryption] reshareKey : ERROR : Never share megolm with this device") + } + + val devicesByUser = mapOf(userId to listOf(deviceInfo)) + val usersDeviceMap = ensureOlmSessionsForDevicesAction.handle(devicesByUser) + val olmSessionResult = usersDeviceMap.getObject(userId, deviceId) + olmSessionResult?.sessionId + ?: // no session with this device, probably because there were no one-time keys. + // ensureOlmSessionsForDevicesAction has already done the logging, so just skip it. + return false + + Timber.d("[MXMegolmEncryption] reshareKey: sharing keys for session $senderKey|$sessionId:$chainIndex with device $userId:$deviceId") + + val payloadJson = mutableMapOf("type" to EventType.FORWARDED_ROOM_KEY) + + runCatching { olmDevice.getInboundGroupSession(sessionId, senderKey, roomId) } + .fold( + { + // TODO + payloadJson["content"] = it.exportKeys(chainIndex.toLong()) ?: "" + }, + { + // TODO + } + + ) + + val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) + val sendToDeviceMap = MXUsersDevicesMap() + sendToDeviceMap.setObject(userId, deviceId, encodedPayload) + Timber.v("## CRYPTO | CRYPTO | reshareKey() : sending to $userId:$deviceId") + val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) + return try { + sendToDeviceTask.execute(sendToDeviceParams) + true + } catch (failure: Throwable) { + Timber.v("## CRYPTO | CRYPTO | reshareKey() : fail to send <$sessionId> to $userId:$deviceId") + false + } + } + + data class DeviceInRoomInfo( + val allowedDevices: MXUsersDevicesMap = MXUsersDevicesMap(), + val withHeldDevices: MXUsersDevicesMap = MXUsersDevicesMap() + ) + + data class UserDevice( + val userId: String, + val deviceId: String + ) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt new file mode 100644 index 0000000000..ca7b9657ae --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.algorithms.megolm + +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.internal.crypto.DeviceListManager +import org.matrix.android.sdk.internal.crypto.MXOlmDevice +import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction +import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter +import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService +import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask +import org.matrix.android.sdk.internal.task.TaskExecutor +import javax.inject.Inject + +internal class MXMegolmEncryptionFactory @Inject constructor( + private val olmDevice: MXOlmDevice, + private val defaultKeysBackupService: DefaultKeysBackupService, + private val cryptoStore: IMXCryptoStore, + private val deviceListManager: DeviceListManager, + private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, + private val credentials: Credentials, + private val sendToDeviceTask: SendToDeviceTask, + private val messageEncrypter: MessageEncrypter, + private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository, + private val taskExecutor: TaskExecutor) { + + fun create(roomId: String): MXMegolmEncryption { + return MXMegolmEncryption( + roomId, + olmDevice, + defaultKeysBackupService, + cryptoStore, + deviceListManager, + ensureOlmSessionsForDevicesAction, + credentials, + sendToDeviceTask, + messageEncrypter, + warnOnUnknownDevicesRepository, + taskExecutor + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt new file mode 100644 index 0000000000..9bdebdf24f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2015 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.algorithms.megolm + +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import timber.log.Timber + +internal class MXOutboundSessionInfo( + // The id of the session + val sessionId: String, + val sharedWithHelper: SharedWithHelper) { + // When the session was created + private val creationTime = System.currentTimeMillis() + + // Number of times this session has been used + var useCount: Int = 0 + + fun needsRotation(rotationPeriodMsgs: Int, rotationPeriodMs: Int): Boolean { + var needsRotation = false + val sessionLifetime = System.currentTimeMillis() - creationTime + + if (useCount >= rotationPeriodMsgs || sessionLifetime >= rotationPeriodMs) { + Timber.v("## needsRotation() : Rotating megolm session after " + useCount + ", " + sessionLifetime + "ms") + needsRotation = true + } + + return needsRotation + } + + /** + * Determine if this session has been shared with devices which it shouldn't have been. + * + * @param devicesInRoom the devices map + * @return true if we have shared the session with devices which aren't in devicesInRoom. + */ + fun sharedWithTooManyDevices(devicesInRoom: MXUsersDevicesMap): Boolean { + val sharedWithDevices = sharedWithHelper.sharedWithDevices() + val userIds = sharedWithDevices.userIds + + for (userId in userIds) { + if (null == devicesInRoom.getUserDeviceIds(userId)) { + Timber.v("## sharedWithTooManyDevices() : Starting new session because we shared with $userId") + return true + } + + val deviceIds = sharedWithDevices.getUserDeviceIds(userId) + + for (deviceId in deviceIds!!) { + if (null == devicesInRoom.getObject(userId, deviceId)) { + Timber.v("## sharedWithTooManyDevices() : Starting new session because we shared with $userId:$deviceId") + return true + } + } + } + + return false + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/SharedWithHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/SharedWithHelper.kt new file mode 100644 index 0000000000..c018f6e275 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/SharedWithHelper.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.algorithms.megolm + +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore + +internal class SharedWithHelper( + private val roomId: String, + private val sessionId: String, + private val cryptoStore: IMXCryptoStore) { + + fun sharedWithDevices(): MXUsersDevicesMap { + return cryptoStore.getSharedWithInfo(roomId, sessionId) + } + + fun wasSharedWith(userId: String, deviceId: String): Int? { + return cryptoStore.wasSessionSharedWithUser(roomId, sessionId, userId, deviceId).chainIndex + } + + fun markedSessionAsShared(userId: String, deviceId: String, chainIndex: Int) { + cryptoStore.markedSessionAsShared(roomId, sessionId, userId, deviceId, chainIndex) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt new file mode 100644 index 0000000000..d4295e2cec --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt @@ -0,0 +1,219 @@ +/* + * Copyright 2015 OpenMarket Ltd + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.algorithms.olm + +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult +import org.matrix.android.sdk.internal.crypto.MXOlmDevice +import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting +import org.matrix.android.sdk.internal.crypto.model.event.OlmEventContent +import org.matrix.android.sdk.internal.crypto.model.event.OlmPayloadContent +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.android.sdk.internal.util.convertFromUTF8 +import timber.log.Timber + +internal class MXOlmDecryption( + // The olm device interface + private val olmDevice: MXOlmDevice, + // the matrix userId + private val userId: String) + : IMXDecrypting { + + @Throws(MXCryptoError::class) + override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { + val olmEventContent = event.content.toModel() ?: run { + Timber.e("## decryptEvent() : bad event format") + throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_EVENT_FORMAT, + MXCryptoError.BAD_EVENT_FORMAT_TEXT_REASON) + } + + val cipherText = olmEventContent.ciphertext ?: run { + Timber.e("## decryptEvent() : missing cipher text") + throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_CIPHER_TEXT, + MXCryptoError.MISSING_CIPHER_TEXT_REASON) + } + + val senderKey = olmEventContent.senderKey ?: run { + Timber.e("## decryptEvent() : missing sender key") + throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY, + MXCryptoError.MISSING_SENDER_KEY_TEXT_REASON) + } + + val messageAny = cipherText[olmDevice.deviceCurve25519Key] ?: run { + Timber.e("## decryptEvent() : our device ${olmDevice.deviceCurve25519Key} is not included in recipients") + throw MXCryptoError.Base(MXCryptoError.ErrorType.NOT_INCLUDE_IN_RECIPIENTS, MXCryptoError.NOT_INCLUDED_IN_RECIPIENT_REASON) + } + + // The message for myUser + @Suppress("UNCHECKED_CAST") + val message = messageAny as JsonDict + + val decryptedPayload = decryptMessage(message, senderKey) + + if (decryptedPayload == null) { + Timber.e("## decryptEvent() Failed to decrypt Olm event (id= ${event.eventId} from $senderKey") + throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON) + } + val payloadString = convertFromUTF8(decryptedPayload) + + val adapter = MoshiProvider.providesMoshi().adapter(JSON_DICT_PARAMETERIZED_TYPE) + val payload = adapter.fromJson(payloadString) + + if (payload == null) { + Timber.e("## decryptEvent failed : null payload") + throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, MXCryptoError.MISSING_CIPHER_TEXT_REASON) + } + + val olmPayloadContent = OlmPayloadContent.fromJsonString(payloadString) ?: run { + Timber.e("## decryptEvent() : bad olmPayloadContent format") + throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON) + } + + if (olmPayloadContent.recipient.isNullOrBlank()) { + val reason = String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient") + Timber.e("## decryptEvent() : $reason") + throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY, reason) + } + + if (olmPayloadContent.recipient != userId) { + Timber.e("## decryptEvent() : Event ${event.eventId}:" + + " Intended recipient ${olmPayloadContent.recipient} does not match our id $userId") + throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_RECIPIENT, + String.format(MXCryptoError.BAD_RECIPIENT_REASON, olmPayloadContent.recipient)) + } + + val recipientKeys = olmPayloadContent.recipient_keys ?: run { + Timber.e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'recipient_keys'" + + " property; cannot prevent unknown-key attack") + throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY, + String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient_keys")) + } + + val ed25519 = recipientKeys["ed25519"] + + if (ed25519 != olmDevice.deviceEd25519Key) { + Timber.e("## decryptEvent() : Event ${event.eventId}: Intended recipient ed25519 key $ed25519 did not match ours") + throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_RECIPIENT_KEY, + MXCryptoError.BAD_RECIPIENT_KEY_REASON) + } + + if (olmPayloadContent.sender.isNullOrBlank()) { + Timber.e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'sender' property; cannot prevent unknown-key attack") + throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY, + String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "sender")) + } + + if (olmPayloadContent.sender != event.senderId) { + Timber.e("Event ${event.eventId}: original sender ${olmPayloadContent.sender} does not match reported sender ${event.senderId}") + throw MXCryptoError.Base(MXCryptoError.ErrorType.FORWARDED_MESSAGE, + String.format(MXCryptoError.FORWARDED_MESSAGE_REASON, olmPayloadContent.sender)) + } + + if (olmPayloadContent.room_id != event.roomId) { + Timber.e("## decryptEvent() : Event ${event.eventId}: original room ${olmPayloadContent.room_id} does not match reported room ${event.roomId}") + throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ROOM, + String.format(MXCryptoError.BAD_ROOM_REASON, olmPayloadContent.room_id)) + } + + val keys = olmPayloadContent.keys ?: run { + Timber.e("## decryptEvent failed : null keys") + throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, + MXCryptoError.MISSING_CIPHER_TEXT_REASON) + } + + return MXEventDecryptionResult( + clearEvent = payload, + senderCurve25519Key = senderKey, + claimedEd25519Key = keys["ed25519"] + ) + } + + /** + * Attempt to decrypt an Olm message. + * + * @param theirDeviceIdentityKey the Curve25519 identity key of the sender. + * @param message message object, with 'type' and 'body' fields. + * @return payload, if decrypted successfully. + */ + private fun decryptMessage(message: JsonDict, theirDeviceIdentityKey: String): String? { + val sessionIds = olmDevice.getSessionIds(theirDeviceIdentityKey) ?: emptySet() + + val messageBody = message["body"] as? String ?: return null + val messageType = when (val typeAsVoid = message["type"]) { + is Double -> typeAsVoid.toInt() + is Int -> typeAsVoid + is Long -> typeAsVoid.toInt() + else -> return null + } + + // Try each session in turn + // decryptionErrors = {}; + for (sessionId in sessionIds) { + val payload = olmDevice.decryptMessage(messageBody, messageType, sessionId, theirDeviceIdentityKey) + + if (null != payload) { + Timber.v("## decryptMessage() : Decrypted Olm message from $theirDeviceIdentityKey with session $sessionId") + return payload + } else { + val foundSession = olmDevice.matchesSession(theirDeviceIdentityKey, sessionId, messageType, messageBody) + + if (foundSession) { + // Decryption failed, but it was a prekey message matching this + // session, so it should have worked. + Timber.e("## decryptMessage() : Error decrypting prekey message with existing session id $sessionId:TODO") + return null + } + } + } + + if (messageType != 0) { + // not a prekey message, so it should have matched an existing session, but it + // didn't work. + + if (sessionIds.isEmpty()) { + Timber.e("## decryptMessage() : No existing sessions") + } else { + Timber.e("## decryptMessage() : Error decrypting non-prekey message with existing sessions") + } + + return null + } + + // prekey message which doesn't match any existing sessions: make a new + // session. + val res = olmDevice.createInboundSession(theirDeviceIdentityKey, messageType, messageBody) + + if (null == res) { + Timber.e("## decryptMessage() : Error decrypting non-prekey message with existing sessions") + return null + } + + Timber.v("## decryptMessage() : Created new inbound Olm session get id ${res["session_id"]} with $theirDeviceIdentityKey") + + return res["payload"] + } + + override fun requestKeysForEvent(event: Event, withHeld: Boolean) { + // nop + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryptionFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryptionFactory.kt new file mode 100644 index 0000000000..17c743fc08 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryptionFactory.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.algorithms.olm + +import org.matrix.android.sdk.internal.crypto.MXOlmDevice +import org.matrix.android.sdk.internal.di.UserId +import javax.inject.Inject + +internal class MXOlmDecryptionFactory @Inject constructor(private val olmDevice: MXOlmDevice, + @UserId private val userId: String) { + + fun create(): MXOlmDecryption { + return MXOlmDecryption( + olmDevice, + userId) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryption.kt new file mode 100644 index 0000000000..f253ce005a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryption.kt @@ -0,0 +1,91 @@ +/* + * Copyright 2015 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.algorithms.olm + +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.internal.crypto.DeviceListManager +import org.matrix.android.sdk.internal.crypto.MXOlmDevice +import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForUsersAction +import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter +import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore + +internal class MXOlmEncryption( + private val roomId: String, + private val olmDevice: MXOlmDevice, + private val cryptoStore: IMXCryptoStore, + private val messageEncrypter: MessageEncrypter, + private val deviceListManager: DeviceListManager, + private val ensureOlmSessionsForUsersAction: EnsureOlmSessionsForUsersAction) + : IMXEncrypting { + + override suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List): Content { + // pick the list of recipients based on the membership list. + // + // TODO: there is a race condition here! What if a new user turns up + ensureSession(userIds) + val deviceInfos = ArrayList() + for (userId in userIds) { + val devices = cryptoStore.getUserDevices(userId)?.values.orEmpty() + for (device in devices) { + val key = device.identityKey() + if (key == olmDevice.deviceCurve25519Key) { + // Don't bother setting up session to ourself + continue + } + if (device.isBlocked) { + // Don't bother setting up sessions with blocked users + continue + } + deviceInfos.add(device) + } + } + + val messageMap = mapOf( + "room_id" to roomId, + "type" to eventType, + "content" to eventContent + ) + + messageEncrypter.encryptMessage(messageMap, deviceInfos) + return messageMap.toContent() + } + + /** + * Ensure that the session + * + * @param users the user ids list + */ + private suspend fun ensureSession(users: List) { + deviceListManager.downloadKeys(users, false) + ensureOlmSessionsForUsersAction.handle(users) + } + + override fun discardSessionKey() { + // No need for olm + } + + override suspend fun reshareKey(sessionId: String, userId: String, deviceId: String, senderKey: String): Boolean { + // No need for olm + return false + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryptionFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryptionFactory.kt new file mode 100644 index 0000000000..d80c344854 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryptionFactory.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.algorithms.olm + +import org.matrix.android.sdk.internal.crypto.DeviceListManager +import org.matrix.android.sdk.internal.crypto.MXOlmDevice +import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForUsersAction +import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import javax.inject.Inject + +internal class MXOlmEncryptionFactory @Inject constructor(private val olmDevice: MXOlmDevice, + private val cryptoStore: IMXCryptoStore, + private val messageEncrypter: MessageEncrypter, + private val deviceListManager: DeviceListManager, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val ensureOlmSessionsForUsersAction: EnsureOlmSessionsForUsersAction) { + + fun create(roomId: String): MXOlmEncryption { + return MXOlmEncryption( + roomId, + olmDevice, + cryptoStore, + messageEncrypter, + deviceListManager, + ensureOlmSessionsForUsersAction) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/OlmDecryptionResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/OlmDecryptionResult.kt new file mode 100755 index 0000000000..ba627d4c30 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/OlmDecryptionResult.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.algorithms.olm + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.util.JsonDict + +/** + * This class represents the decryption result. + */ +@JsonClass(generateAdapter = true) +data class OlmDecryptionResult( + /** + * The decrypted payload (with properties 'type', 'content') + */ + @Json(name = "payload") val payload: JsonDict? = null, + + /** + * keys that the sender of the event claims ownership of: + * map from key type to base64-encoded key. + */ + @Json(name = "keysClaimed") val keysClaimed: Map? = null, + + /** + * The curve25519 key that the sender of the event is known to have ownership of. + */ + @Json(name = "senderKey") val senderKey: String? = null, + + /** + * Devices which forwarded this session to us (normally empty). + */ + @Json(name = "forwardingCurve25519KeyChain") val forwardingCurve25519KeyChain: List? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/api/CryptoApi.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/api/CryptoApi.kt new file mode 100644 index 0000000000..a12b725efd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/api/CryptoApi.kt @@ -0,0 +1,159 @@ +/* + * Copyright 2014 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.api + +import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams +import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo +import org.matrix.android.sdk.internal.crypto.model.rest.DevicesListResponse +import org.matrix.android.sdk.internal.crypto.model.rest.KeyChangesResponse +import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimBody +import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimResponse +import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryBody +import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse +import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadBody +import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadResponse +import org.matrix.android.sdk.internal.crypto.model.rest.SendToDeviceBody +import org.matrix.android.sdk.internal.crypto.model.rest.SignatureUploadResponse +import org.matrix.android.sdk.internal.crypto.model.rest.UpdateDeviceInfoBody +import org.matrix.android.sdk.internal.crypto.model.rest.UploadSigningKeysBody +import org.matrix.android.sdk.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.HTTP +import retrofit2.http.POST +import retrofit2.http.PUT +import retrofit2.http.Path +import retrofit2.http.Query + +internal interface CryptoApi { + + /** + * Get the devices list + * Doc: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-devices + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices") + fun getDevices(): Call + + /** + * Get the device info by id + * Doc: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-devices-deviceid + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices/{deviceId}") + fun getDeviceInfo(@Path("deviceId") deviceId: String): Call + + /** + * Upload device and/or one-time keys. + * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-keys-upload + * + * @param body the keys to be sent. + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "keys/upload") + fun uploadKeys(@Body body: KeysUploadBody): Call + + /** + * Download device keys. + * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-keys-query + * + * @param params the params. + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "keys/query") + fun downloadKeysForUsers(@Body params: KeysQueryBody): Call + + /** + * CrossSigning - Uploading signing keys + * Public keys for the cross-signing keys are uploaded to the servers using /keys/device_signing/upload. + * This endpoint requires UI Auth. + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "keys/device_signing/upload") + fun uploadSigningKeys(@Body params: UploadSigningKeysBody): Call + + /** + * CrossSigning - Uploading signatures + * Signatures of device keys can be up + * loaded using /keys/signatures/upload. + * For example, Alice signs one of her devices (HIJKLMN) (using her self-signing key), + * her own master key (using her HIJKLMN device), Bob's master key (using her user-signing key). + * + * The response contains a failures property, which is a map of user ID to device ID to failure reason, if any of the uploaded keys failed. + * The homeserver should verify that the signatures on the uploaded keys are valid. + * If a signature is not valid, the homeserver should set the corresponding entry in failures to a JSON object + * with the errcode property set to M_INVALID_SIGNATURE. + * + * After Alice uploads a signature for her own devices or master key, + * her signature will be included in the results of the /keys/query request when anyone requests her keys. + * However, signatures made for other users' keys, made by her user-signing key, will not be included. + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "keys/signatures/upload") + fun uploadSignatures(@Body params: Map?): Call + + /** + * Claim one-time keys. + * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-keys-claim + * + * @param params the params. + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "keys/claim") + fun claimOneTimeKeysForUsersDevices(@Body body: KeysClaimBody): Call + + /** + * Send an event to a specific list of devices + * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#put-matrix-client-r0-sendtodevice-eventtype-txnid + * + * @param eventType the type of event to send + * @param transactionId the transaction ID for this event + * @param body the body + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "sendToDevice/{eventType}/{txnId}") + fun sendToDevice(@Path("eventType") eventType: String, + @Path("txnId") transactionId: String, + @Body body: SendToDeviceBody): Call + + /** + * Delete a device. + * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#delete-matrix-client-r0-devices-deviceid + * + * @param deviceId the device id + * @param params the deletion parameters + */ + @HTTP(path = NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices/{device_id}", method = "DELETE", hasBody = true) + fun deleteDevice(@Path("device_id") deviceId: String, + @Body params: DeleteDeviceParams): Call + + /** + * Update the device information. + * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#put-matrix-client-r0-devices-deviceid + * + * @param deviceId the device id + * @param params the params + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices/{device_id}") + fun updateDeviceInfo(@Path("device_id") deviceId: String, + @Body params: UpdateDeviceInfoBody): Call + + /** + * Get the update devices list from two sync token. + * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-keys-changes + * + * @param oldToken the start token. + * @param newToken the up-to token. + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "keys/changes") + fun getKeyChanges(@Query("from") oldToken: String, + @Query("to") newToken: String): Call +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/ElementToDecrypt.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/ElementToDecrypt.kt new file mode 100644 index 0000000000..21f7c209ef --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/ElementToDecrypt.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.attachments + +import android.os.Parcelable +import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo +import kotlinx.android.parcel.Parcelize + +fun EncryptedFileInfo.toElementToDecrypt(): ElementToDecrypt? { + // Check the validity of some fields + if (isValid()) { + // It's valid so the data are here + return ElementToDecrypt( + iv = this.iv ?: "", + k = this.key?.k ?: "", + sha256 = this.hashes?.get("sha256") ?: "" + ) + } + + return null +} + +/** + * Represent data to decode an attachment + */ +@Parcelize +data class ElementToDecrypt( + val iv: String, + val k: String, + val sha256: String +) : Parcelable diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/EncryptionResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/EncryptionResult.kt new file mode 100644 index 0000000000..721ee0639d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/EncryptionResult.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.attachments + +import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo + +/** + * Define the result of an encryption file + */ +internal data class EncryptionResult( + var encryptedFileInfo: EncryptedFileInfo, + var encryptedByteArray: ByteArray +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt new file mode 100755 index 0000000000..cec1480d7b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt @@ -0,0 +1,214 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.attachments + +import android.util.Base64 +import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo +import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileKey +import timber.log.Timber +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.security.MessageDigest +import java.security.SecureRandom +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec + +internal object MXEncryptedAttachments { + private const val CRYPTO_BUFFER_SIZE = 32 * 1024 + private const val CIPHER_ALGORITHM = "AES/CTR/NoPadding" + private const val SECRET_KEY_SPEC_ALGORITHM = "AES" + private const val MESSAGE_DIGEST_ALGORITHM = "SHA-256" + + /*** + * Encrypt an attachment stream. + * @param attachmentStream the attachment stream. Will be closed after this method call. + * @param mimetype the mime type + * @return the encryption file info + */ + fun encryptAttachment(attachmentStream: InputStream, mimetype: String?): EncryptionResult { + val t0 = System.currentTimeMillis() + val secureRandom = SecureRandom() + + // generate a random iv key + // Half of the IV is random, the lower order bits are zeroed + // such that the counter never wraps. + // See https://github.com/matrix-org/matrix-ios-kit/blob/3dc0d8e46b4deb6669ed44f72ad79be56471354c/MatrixKit/Models/Room/MXEncryptedAttachments.m#L75 + val initVectorBytes = ByteArray(16) { 0.toByte() } + + val ivRandomPart = ByteArray(8) + secureRandom.nextBytes(ivRandomPart) + + System.arraycopy(ivRandomPart, 0, initVectorBytes, 0, ivRandomPart.size) + + val key = ByteArray(32) + secureRandom.nextBytes(key) + + ByteArrayOutputStream().use { outputStream -> + val encryptCipher = Cipher.getInstance(CIPHER_ALGORITHM) + val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM) + val ivParameterSpec = IvParameterSpec(initVectorBytes) + encryptCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec) + + val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM) + + val data = ByteArray(CRYPTO_BUFFER_SIZE) + var read: Int + var encodedBytes: ByteArray + + attachmentStream.use { inputStream -> + read = inputStream.read(data) + while (read != -1) { + encodedBytes = encryptCipher.update(data, 0, read) + messageDigest.update(encodedBytes, 0, encodedBytes.size) + outputStream.write(encodedBytes) + read = inputStream.read(data) + } + } + + // encrypt the latest chunk + encodedBytes = encryptCipher.doFinal() + messageDigest.update(encodedBytes, 0, encodedBytes.size) + outputStream.write(encodedBytes) + + return EncryptionResult( + encryptedFileInfo = EncryptedFileInfo( + url = null, + mimetype = mimetype, + key = EncryptedFileKey( + alg = "A256CTR", + ext = true, + key_ops = listOf("encrypt", "decrypt"), + kty = "oct", + k = base64ToBase64Url(Base64.encodeToString(key, Base64.DEFAULT)) + ), + iv = Base64.encodeToString(initVectorBytes, Base64.DEFAULT).replace("\n", "").replace("=", ""), + hashes = mapOf("sha256" to base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))), + v = "v2" + ), + encryptedByteArray = outputStream.toByteArray() + ) + .also { Timber.v("Encrypt in ${System.currentTimeMillis() - t0}ms") } + } + } + + /** + * Decrypt an attachment + * + * @param attachmentStream the attachment stream. Will be closed after this method call. + * @param encryptedFileInfo the encryption file info + * @return the decrypted attachment stream + */ + fun decryptAttachment(attachmentStream: InputStream?, encryptedFileInfo: EncryptedFileInfo?): InputStream? { + if (encryptedFileInfo?.isValid() != true) { + Timber.e("## decryptAttachment() : some fields are not defined, or invalid key fields") + return null + } + + val elementToDecrypt = encryptedFileInfo.toElementToDecrypt() + + return decryptAttachment(attachmentStream, elementToDecrypt) + } + + /** + * Decrypt an attachment + * + * @param attachmentStream the attachment stream. Will be closed after this method call. + * @param elementToDecrypt the elementToDecrypt info + * @return the decrypted attachment stream + */ + fun decryptAttachment(attachmentStream: InputStream?, elementToDecrypt: ElementToDecrypt?): InputStream? { + // sanity checks + if (null == attachmentStream || elementToDecrypt == null) { + Timber.e("## decryptAttachment() : null stream") + return null + } + + val t0 = System.currentTimeMillis() + + ByteArrayOutputStream().use { outputStream -> + try { + val key = Base64.decode(base64UrlToBase64(elementToDecrypt.k), Base64.DEFAULT) + val initVectorBytes = Base64.decode(elementToDecrypt.iv, Base64.DEFAULT) + + val decryptCipher = Cipher.getInstance(CIPHER_ALGORITHM) + val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM) + val ivParameterSpec = IvParameterSpec(initVectorBytes) + decryptCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec) + + val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM) + + var read: Int + val data = ByteArray(CRYPTO_BUFFER_SIZE) + var decodedBytes: ByteArray + + attachmentStream.use { inputStream -> + read = inputStream.read(data) + while (read != -1) { + messageDigest.update(data, 0, read) + decodedBytes = decryptCipher.update(data, 0, read) + outputStream.write(decodedBytes) + read = inputStream.read(data) + } + } + + // decrypt the last chunk + decodedBytes = decryptCipher.doFinal() + outputStream.write(decodedBytes) + + val currentDigestValue = base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT)) + + if (elementToDecrypt.sha256 != currentDigestValue) { + Timber.e("## decryptAttachment() : Digest value mismatch") + return null + } + + return ByteArrayInputStream(outputStream.toByteArray()) + .also { Timber.v("Decrypt in ${System.currentTimeMillis() - t0}ms") } + } catch (oom: OutOfMemoryError) { + Timber.e(oom, "## decryptAttachment() failed: OOM") + } catch (e: Exception) { + Timber.e(e, "## decryptAttachment() failed") + } + } + + return null + } + + /** + * Base64 URL conversion methods + */ + + private fun base64UrlToBase64(base64Url: String): String { + return base64Url.replace('-', '+') + .replace('_', '/') + } + + internal fun base64ToBase64Url(base64: String): String { + return base64.replace("\n".toRegex(), "") + .replace("\\+".toRegex(), "-") + .replace('/', '_') + .replace("=", "") + } + + private fun base64ToUnpaddedBase64(base64: String): String { + return base64.replace("\n".toRegex(), "") + .replace("=", "") + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/ComputeTrustTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/ComputeTrustTask.kt new file mode 100644 index 0000000000..3bcbeefa91 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/ComputeTrustTask.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.crosssigning + +import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import kotlinx.coroutines.withContext +import javax.inject.Inject + +internal interface ComputeTrustTask : Task { + data class Params( + val activeMemberUserIds: List, + val isDirectRoom: Boolean + ) +} + +internal class DefaultComputeTrustTask @Inject constructor( + private val cryptoStore: IMXCryptoStore, + @UserId private val userId: String, + private val coroutineDispatchers: MatrixCoroutineDispatchers +) : ComputeTrustTask { + + override suspend fun execute(params: ComputeTrustTask.Params): RoomEncryptionTrustLevel = withContext(coroutineDispatchers.crypto) { + // The set of “all users” depends on the type of room: + // For regular / topic rooms, all users including yourself, are considered when decorating a room + // For 1:1 and group DM rooms, all other users (i.e. excluding yourself) are considered when decorating a room + val listToCheck = if (params.isDirectRoom) { + params.activeMemberUserIds.filter { it != userId } + } else { + params.activeMemberUserIds + } + + val allTrustedUserIds = listToCheck + .filter { userId -> getUserCrossSigningKeys(userId)?.isTrusted() == true } + + if (allTrustedUserIds.isEmpty()) { + RoomEncryptionTrustLevel.Default + } else { + // If one of the verified user as an untrusted device -> warning + // If all devices of all verified users are trusted -> green + // else -> black + allTrustedUserIds + .mapNotNull { cryptoStore.getUserDeviceList(it) } + .flatten() + .let { allDevices -> + if (getMyCrossSigningKeys() != null) { + allDevices.any { !it.trustLevel?.crossSigningVerified.orFalse() } + } else { + // Legacy method + allDevices.any { !it.isVerified } + } + } + .let { hasWarning -> + if (hasWarning) { + RoomEncryptionTrustLevel.Warning + } else { + if (listToCheck.size == allTrustedUserIds.size) { + // all users are trusted and all devices are verified + RoomEncryptionTrustLevel.Trusted + } else { + RoomEncryptionTrustLevel.Default + } + } + } + } + } + + private fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo? { + return cryptoStore.getCrossSigningInfo(otherUserId) + } + + private fun getMyCrossSigningKeys(): MXCrossSigningInfo? { + return cryptoStore.getMyCrossSigningInfo() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt new file mode 100644 index 0000000000..8cd4a6b8e8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt @@ -0,0 +1,747 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.crosssigning + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService +import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.internal.crypto.DeviceListManager +import org.matrix.android.sdk.internal.crypto.model.rest.UploadSignatureQueryBuilder +import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo +import org.matrix.android.sdk.internal.crypto.tasks.InitializeCrossSigningTask +import org.matrix.android.sdk.internal.crypto.tasks.UploadSignaturesTask +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.TaskThread +import org.matrix.android.sdk.internal.task.configureWith +import org.matrix.android.sdk.internal.util.JsonCanonicalizer +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import org.matrix.android.sdk.internal.util.withoutPrefix +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.greenrobot.eventbus.EventBus +import org.matrix.olm.OlmPkSigning +import org.matrix.olm.OlmUtility +import timber.log.Timber +import javax.inject.Inject + +@SessionScope +internal class DefaultCrossSigningService @Inject constructor( + @UserId private val userId: String, + private val cryptoStore: IMXCryptoStore, + private val deviceListManager: DeviceListManager, + private val initializeCrossSigningTask: InitializeCrossSigningTask, + private val uploadSignaturesTask: UploadSignaturesTask, + private val taskExecutor: TaskExecutor, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val cryptoCoroutineScope: CoroutineScope, + private val eventBus: EventBus) : CrossSigningService, DeviceListManager.UserDevicesUpdateListener { + + private var olmUtility: OlmUtility? = null + + private var masterPkSigning: OlmPkSigning? = null + private var userPkSigning: OlmPkSigning? = null + private var selfSigningPkSigning: OlmPkSigning? = null + + init { + try { + olmUtility = OlmUtility() + + // Try to get stored keys if they exist + cryptoStore.getMyCrossSigningInfo()?.let { mxCrossSigningInfo -> + Timber.i("## CrossSigning - Found Existing self signed keys") + Timber.i("## CrossSigning - Checking if private keys are known") + + cryptoStore.getCrossSigningPrivateKeys()?.let { privateKeysInfo -> + privateKeysInfo.master + ?.fromBase64() + ?.let { privateKeySeed -> + val pkSigning = OlmPkSigning() + if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) { + masterPkSigning = pkSigning + Timber.i("## CrossSigning - Loading master key success") + } else { + Timber.w("## CrossSigning - Public master key does not match the private key") + pkSigning.releaseSigning() + // TODO untrust? + } + } + privateKeysInfo.user + ?.fromBase64() + ?.let { privateKeySeed -> + val pkSigning = OlmPkSigning() + if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) { + userPkSigning = pkSigning + Timber.i("## CrossSigning - Loading User Signing key success") + } else { + Timber.w("## CrossSigning - Public User key does not match the private key") + pkSigning.releaseSigning() + // TODO untrust? + } + } + privateKeysInfo.selfSigned + ?.fromBase64() + ?.let { privateKeySeed -> + val pkSigning = OlmPkSigning() + if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) { + selfSigningPkSigning = pkSigning + Timber.i("## CrossSigning - Loading Self Signing key success") + } else { + Timber.w("## CrossSigning - Public Self Signing key does not match the private key") + pkSigning.releaseSigning() + // TODO untrust? + } + } + } + + // Recover local trust in case private key are there? + setUserKeysAsTrusted(userId, checkUserTrust(userId).isVerified()) + } + } catch (e: Throwable) { + // Mmm this kind of a big issue + Timber.e(e, "Failed to initialize Cross Signing") + } + + deviceListManager.addListener(this) + } + + fun release() { + olmUtility?.releaseUtility() + listOf(masterPkSigning, userPkSigning, selfSigningPkSigning).forEach { it?.releaseSigning() } + deviceListManager.removeListener(this) + } + + protected fun finalize() { + release() + } + + /** + * - Make 3 key pairs (MSK, USK, SSK) + * - Save the private keys with proper security + * - Sign the keys and upload them + * - Sign the current device with SSK and sign MSK with device key (migration) and upload signatures + */ + override fun initializeCrossSigning(authParams: UserPasswordAuth?, callback: MatrixCallback) { + Timber.d("## CrossSigning initializeCrossSigning") + + val params = InitializeCrossSigningTask.Params( + authParams = authParams + ) + initializeCrossSigningTask.configureWith(params) { + this.callbackThread = TaskThread.CRYPTO + this.callback = object : MatrixCallback { + override fun onFailure(failure: Throwable) { + Timber.e(failure, "Error in initializeCrossSigning()") + callback.onFailure(failure) + } + + override fun onSuccess(data: InitializeCrossSigningTask.Result) { + val crossSigningInfo = MXCrossSigningInfo(userId, listOf(data.masterKeyInfo, data.userKeyInfo, data.selfSignedKeyInfo)) + cryptoStore.setMyCrossSigningInfo(crossSigningInfo) + setUserKeysAsTrusted(userId, true) + cryptoStore.storePrivateKeysInfo(data.masterKeyPK, data.userKeyPK, data.selfSigningKeyPK) + masterPkSigning = OlmPkSigning().apply { initWithSeed(data.masterKeyPK.fromBase64()) } + userPkSigning = OlmPkSigning().apply { initWithSeed(data.userKeyPK.fromBase64()) } + selfSigningPkSigning = OlmPkSigning().apply { initWithSeed(data.selfSigningKeyPK.fromBase64()) } + + callback.onSuccess(Unit) + } + } + }.executeBy(taskExecutor) + } + + override fun onSecretMSKGossip(mskPrivateKey: String) { + Timber.i("## CrossSigning - onSecretSSKGossip") + val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return Unit.also { + Timber.e("## CrossSigning - onSecretMSKGossip() received secret but public key is not known") + } + + mskPrivateKey.fromBase64() + .let { privateKeySeed -> + val pkSigning = OlmPkSigning() + try { + if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) { + masterPkSigning?.releaseSigning() + masterPkSigning = pkSigning + Timber.i("## CrossSigning - Loading MSK success") + cryptoStore.storeMSKPrivateKey(mskPrivateKey) + return + } else { + Timber.e("## CrossSigning - onSecretMSKGossip() private key do not match public key") + pkSigning.releaseSigning() + } + } catch (failure: Throwable) { + Timber.e("## CrossSigning - onSecretMSKGossip() ${failure.localizedMessage}") + pkSigning.releaseSigning() + } + } + } + + override fun onSecretSSKGossip(sskPrivateKey: String) { + Timber.i("## CrossSigning - onSecretSSKGossip") + val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return Unit.also { + Timber.e("## CrossSigning - onSecretSSKGossip() received secret but public key is not known") + } + + sskPrivateKey.fromBase64() + .let { privateKeySeed -> + val pkSigning = OlmPkSigning() + try { + if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) { + selfSigningPkSigning?.releaseSigning() + selfSigningPkSigning = pkSigning + Timber.i("## CrossSigning - Loading SSK success") + cryptoStore.storeSSKPrivateKey(sskPrivateKey) + return + } else { + Timber.e("## CrossSigning - onSecretSSKGossip() private key do not match public key") + pkSigning.releaseSigning() + } + } catch (failure: Throwable) { + Timber.e("## CrossSigning - onSecretSSKGossip() ${failure.localizedMessage}") + pkSigning.releaseSigning() + } + } + } + + override fun onSecretUSKGossip(uskPrivateKey: String) { + Timber.i("## CrossSigning - onSecretUSKGossip") + val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return Unit.also { + Timber.e("## CrossSigning - onSecretUSKGossip() received secret but public key is not knwow ") + } + + uskPrivateKey.fromBase64() + .let { privateKeySeed -> + val pkSigning = OlmPkSigning() + try { + if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) { + userPkSigning?.releaseSigning() + userPkSigning = pkSigning + Timber.i("## CrossSigning - Loading USK success") + cryptoStore.storeUSKPrivateKey(uskPrivateKey) + return + } else { + Timber.e("## CrossSigning - onSecretUSKGossip() private key do not match public key") + pkSigning.releaseSigning() + } + } catch (failure: Throwable) { + pkSigning.releaseSigning() + } + } + } + + override fun checkTrustFromPrivateKeys(masterKeyPrivateKey: String?, + uskKeyPrivateKey: String?, + sskPrivateKey: String? + ): UserTrustResult { + val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return UserTrustResult.CrossSigningNotConfigured(userId) + + var masterKeyIsTrusted = false + var userKeyIsTrusted = false + var selfSignedKeyIsTrusted = false + + masterKeyPrivateKey?.fromBase64() + ?.let { privateKeySeed -> + val pkSigning = OlmPkSigning() + try { + if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) { + masterPkSigning?.releaseSigning() + masterPkSigning = pkSigning + masterKeyIsTrusted = true + Timber.i("## CrossSigning - Loading master key success") + } else { + pkSigning.releaseSigning() + } + } catch (failure: Throwable) { + pkSigning.releaseSigning() + } + } + + uskKeyPrivateKey?.fromBase64() + ?.let { privateKeySeed -> + val pkSigning = OlmPkSigning() + try { + if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) { + userPkSigning?.releaseSigning() + userPkSigning = pkSigning + userKeyIsTrusted = true + Timber.i("## CrossSigning - Loading master key success") + } else { + pkSigning.releaseSigning() + } + } catch (failure: Throwable) { + pkSigning.releaseSigning() + } + } + + sskPrivateKey?.fromBase64() + ?.let { privateKeySeed -> + val pkSigning = OlmPkSigning() + try { + if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) { + selfSigningPkSigning?.releaseSigning() + selfSigningPkSigning = pkSigning + selfSignedKeyIsTrusted = true + Timber.i("## CrossSigning - Loading master key success") + } else { + pkSigning.releaseSigning() + } + } catch (failure: Throwable) { + pkSigning.releaseSigning() + } + } + + if (!masterKeyIsTrusted || !userKeyIsTrusted || !selfSignedKeyIsTrusted) { + return UserTrustResult.KeysNotTrusted(mxCrossSigningInfo) + } else { + cryptoStore.markMyMasterKeyAsLocallyTrusted(true) + val checkSelfTrust = checkSelfTrust() + if (checkSelfTrust.isVerified()) { + cryptoStore.storePrivateKeysInfo(masterKeyPrivateKey, uskKeyPrivateKey, sskPrivateKey) + setUserKeysAsTrusted(userId, true) + } + return checkSelfTrust + } + } + + /** + * + * ┏━━━━━━━━┓ ┏━━━━━━━━┓ + * ┃ ALICE ┃ ┃ BOB ┃ + * ┗━━━━━━━━┛ ┗━━━━━━━━┛ + * MSK ┌────────────▶ MSK + * │ + * │ │ + * │ SSK │ + * │ │ + * │ │ + * └──▶ USK ────────────┘ + */ + override fun isUserTrusted(otherUserId: String): Boolean { + return cryptoStore.getCrossSigningInfo(userId)?.isTrusted() == true + } + + override fun isCrossSigningVerified(): Boolean { + return checkSelfTrust().isVerified() + } + + /** + * Will not force a download of the key, but will verify signatures trust chain + */ + override fun checkUserTrust(otherUserId: String): UserTrustResult { + Timber.v("## CrossSigning checkUserTrust for $otherUserId") + if (otherUserId == userId) { + return checkSelfTrust() + } + // I trust a user if I trust his master key + // I can trust the master key if it is signed by my user key + // TODO what if the master key is signed by a device key that i have verified + + // First let's get my user key + val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId) + + val myUserKey = myCrossSigningInfo?.userKey() + ?: return UserTrustResult.CrossSigningNotConfigured(userId) + + if (!myCrossSigningInfo.isTrusted()) { + return UserTrustResult.KeysNotTrusted(myCrossSigningInfo) + } + + // Let's get the other user master key + val otherMasterKey = cryptoStore.getCrossSigningInfo(otherUserId)?.masterKey() + ?: return UserTrustResult.UnknownCrossSignatureInfo(otherUserId) + + val masterKeySignaturesMadeByMyUserKey = otherMasterKey.signatures + ?.get(userId) // Signatures made by me + ?.get("ed25519:${myUserKey.unpaddedBase64PublicKey}") + + if (masterKeySignaturesMadeByMyUserKey.isNullOrBlank()) { + Timber.d("## CrossSigning checkUserTrust false for $otherUserId, not signed by my UserSigningKey") + return UserTrustResult.KeyNotSigned(otherMasterKey) + } + + // Check that Alice USK signature of Bob MSK is valid + try { + olmUtility!!.verifyEd25519Signature(masterKeySignaturesMadeByMyUserKey, myUserKey.unpaddedBase64PublicKey, otherMasterKey.canonicalSignable()) + } catch (failure: Throwable) { + return UserTrustResult.InvalidSignature(myUserKey, masterKeySignaturesMadeByMyUserKey) + } + + return UserTrustResult.Success + } + + private fun checkSelfTrust(): UserTrustResult { + // Special case when it's me, + // I have to check that MSK -> USK -> SSK + // and that MSK is trusted (i know the private key, or is signed by a trusted device) + val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId) + + val myMasterKey = myCrossSigningInfo?.masterKey() + ?: return UserTrustResult.CrossSigningNotConfigured(userId) + + // Is the master key trusted + // 1) check if I know the private key + val masterPrivateKey = cryptoStore.getCrossSigningPrivateKeys() + ?.master + ?.fromBase64() + + var isMaterKeyTrusted = false + if (myMasterKey.trustLevel?.locallyVerified == true) { + isMaterKeyTrusted = true + } else if (masterPrivateKey != null) { + // Check if private match public + var olmPkSigning: OlmPkSigning? = null + try { + olmPkSigning = OlmPkSigning() + val expectedPK = olmPkSigning.initWithSeed(masterPrivateKey) + isMaterKeyTrusted = myMasterKey.unpaddedBase64PublicKey == expectedPK + } catch (failure: Throwable) { + Timber.e(failure) + } + olmPkSigning?.releaseSigning() + } else { + // Maybe it's signed by a locally trusted device? + myMasterKey.signatures?.get(userId)?.forEach { (key, value) -> + val potentialDeviceId = key.withoutPrefix("ed25519:") + val potentialDevice = cryptoStore.getUserDevice(userId, potentialDeviceId) + if (potentialDevice != null && potentialDevice.isVerified) { + // Check signature validity? + try { + olmUtility?.verifyEd25519Signature(value, potentialDevice.fingerprint(), myMasterKey.canonicalSignable()) + isMaterKeyTrusted = true + return@forEach + } catch (failure: Throwable) { + // log + Timber.w(failure, "Signature not valid?") + } + } + } + } + + if (!isMaterKeyTrusted) { + return UserTrustResult.KeysNotTrusted(myCrossSigningInfo) + } + + val myUserKey = myCrossSigningInfo.userKey() + ?: return UserTrustResult.CrossSigningNotConfigured(userId) + + val userKeySignaturesMadeByMyMasterKey = myUserKey.signatures + ?.get(userId) // Signatures made by me + ?.get("ed25519:${myMasterKey.unpaddedBase64PublicKey}") + + if (userKeySignaturesMadeByMyMasterKey.isNullOrBlank()) { + Timber.d("## CrossSigning checkUserTrust false for $userId, USK not signed by MSK") + return UserTrustResult.KeyNotSigned(myUserKey) + } + + // Check that Alice USK signature of Alice MSK is valid + try { + olmUtility!!.verifyEd25519Signature(userKeySignaturesMadeByMyMasterKey, myMasterKey.unpaddedBase64PublicKey, myUserKey.canonicalSignable()) + } catch (failure: Throwable) { + return UserTrustResult.InvalidSignature(myUserKey, userKeySignaturesMadeByMyMasterKey) + } + + val mySSKey = myCrossSigningInfo.selfSigningKey() + ?: return UserTrustResult.CrossSigningNotConfigured(userId) + + val ssKeySignaturesMadeByMyMasterKey = mySSKey.signatures + ?.get(userId) // Signatures made by me + ?.get("ed25519:${myMasterKey.unpaddedBase64PublicKey}") + + if (ssKeySignaturesMadeByMyMasterKey.isNullOrBlank()) { + Timber.d("## CrossSigning checkUserTrust false for $userId, SSK not signed by MSK") + return UserTrustResult.KeyNotSigned(mySSKey) + } + + // Check that Alice USK signature of Alice MSK is valid + try { + olmUtility!!.verifyEd25519Signature(ssKeySignaturesMadeByMyMasterKey, myMasterKey.unpaddedBase64PublicKey, mySSKey.canonicalSignable()) + } catch (failure: Throwable) { + return UserTrustResult.InvalidSignature(mySSKey, ssKeySignaturesMadeByMyMasterKey) + } + + return UserTrustResult.Success + } + + override fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo? { + return cryptoStore.getCrossSigningInfo(otherUserId) + } + + override fun getLiveCrossSigningKeys(userId: String): LiveData> { + return cryptoStore.getLiveCrossSigningInfo(userId) + } + + override fun getMyCrossSigningKeys(): MXCrossSigningInfo? { + return cryptoStore.getMyCrossSigningInfo() + } + + override fun getCrossSigningPrivateKeys(): PrivateKeysInfo? { + return cryptoStore.getCrossSigningPrivateKeys() + } + + override fun getLiveCrossSigningPrivateKeys(): LiveData> { + return cryptoStore.getLiveCrossSigningPrivateKeys() + } + + override fun canCrossSign(): Boolean { + return checkSelfTrust().isVerified() && cryptoStore.getCrossSigningPrivateKeys()?.selfSigned != null + && cryptoStore.getCrossSigningPrivateKeys()?.user != null + } + + override fun allPrivateKeysKnown(): Boolean { + return checkSelfTrust().isVerified() + && cryptoStore.getCrossSigningPrivateKeys()?.allKnown().orFalse() + } + + override fun trustUser(otherUserId: String, callback: MatrixCallback) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + Timber.d("## CrossSigning - Mark user $userId as trusted ") + // We should have this user keys + val otherMasterKeys = getUserCrossSigningKeys(otherUserId)?.masterKey() + if (otherMasterKeys == null) { + callback.onFailure(Throwable("## CrossSigning - Other master signing key is not known")) + return@launch + } + val myKeys = getUserCrossSigningKeys(userId) + if (myKeys == null) { + callback.onFailure(Throwable("## CrossSigning - CrossSigning is not setup for this account")) + return@launch + } + val userPubKey = myKeys.userKey()?.unpaddedBase64PublicKey + if (userPubKey == null || userPkSigning == null) { + callback.onFailure(Throwable("## CrossSigning - Cannot sign from this account, privateKeyUnknown $userPubKey")) + return@launch + } + + // Sign the other MasterKey with our UserSigning key + val newSignature = JsonCanonicalizer.getCanonicalJson(Map::class.java, + otherMasterKeys.signalableJSONDictionary()).let { userPkSigning?.sign(it) } + + if (newSignature == null) { + // race?? + callback.onFailure(Throwable("## CrossSigning - Failed to sign")) + return@launch + } + + cryptoStore.setUserKeysAsTrusted(otherUserId, true) + // TODO update local copy with new signature directly here? kind of local echo of trust? + + Timber.d("## CrossSigning - Upload signature of $userId MSK signed by USK") + val uploadQuery = UploadSignatureQueryBuilder() + .withSigningKeyInfo(otherMasterKeys.copyForSignature(userId, userPubKey, newSignature)) + .build() + uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) { + this.executionThread = TaskThread.CRYPTO + this.callback = callback + }.executeBy(taskExecutor) + } + } + + override fun markMyMasterKeyAsTrusted() { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + cryptoStore.markMyMasterKeyAsLocallyTrusted(true) + checkSelfTrust() + } + } + + override fun trustDevice(deviceId: String, callback: MatrixCallback) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + // This device should be yours + val device = cryptoStore.getUserDevice(userId, deviceId) + if (device == null) { + callback.onFailure(IllegalArgumentException("This device [$deviceId] is not known, or not yours")) + return@launch + } + + val myKeys = getUserCrossSigningKeys(userId) + if (myKeys == null) { + callback.onFailure(Throwable("CrossSigning is not setup for this account")) + return@launch + } + + val ssPubKey = myKeys.selfSigningKey()?.unpaddedBase64PublicKey + if (ssPubKey == null || selfSigningPkSigning == null) { + callback.onFailure(Throwable("Cannot sign from this account, public and/or privateKey Unknown $ssPubKey")) + return@launch + } + + // Sign with self signing + val newSignature = selfSigningPkSigning?.sign(device.canonicalSignable()) + + if (newSignature == null) { + // race?? + callback.onFailure(Throwable("Failed to sign")) + return@launch + } + val toUpload = device.copy( + signatures = mapOf( + userId + to + mapOf( + "ed25519:$ssPubKey" to newSignature + ) + ) + ) + + val uploadQuery = UploadSignatureQueryBuilder() + .withDeviceInfo(toUpload) + .build() + uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) { + this.executionThread = TaskThread.CRYPTO + this.callback = callback + }.executeBy(taskExecutor) + } + } + + override fun checkDeviceTrust(otherUserId: String, otherDeviceId: String, locallyTrusted: Boolean?): DeviceTrustResult { + val otherDevice = cryptoStore.getUserDevice(otherUserId, otherDeviceId) + ?: return DeviceTrustResult.UnknownDevice(otherDeviceId) + + val myKeys = getUserCrossSigningKeys(userId) + ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(userId)) + + if (!myKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(myKeys)) + + val otherKeys = getUserCrossSigningKeys(otherUserId) + ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(otherUserId)) + + // TODO should we force verification ? + if (!otherKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(otherKeys)) + + // Check if the trust chain is valid + /* + * ┏━━━━━━━━┓ ┏━━━━━━━━┓ + * ┃ ALICE ┃ ┃ BOB ┃ + * ┗━━━━━━━━┛ ┗━━━━━━━━┛ + * MSK ┌────────────▶MSK + * │ + * │ │ │ + * │ SSK │ └──▶ SSK ──────────────────┐ + * │ │ │ + * │ │ USK │ + * └──▶ USK ────────────┘ (not visible by │ + * Alice) │ + * ▼ + * ┌──────────────┐ + * │ BOB's Device │ + * └──────────────┘ + */ + + val otherSSKSignature = otherDevice.signatures?.get(otherUserId)?.get("ed25519:${otherKeys.selfSigningKey()?.unpaddedBase64PublicKey}") + ?: return legacyFallbackTrust( + locallyTrusted, + DeviceTrustResult.MissingDeviceSignature(otherDeviceId, otherKeys.selfSigningKey() + ?.unpaddedBase64PublicKey + ?: "" + ) + ) + + // Check bob's device is signed by bob's SSK + try { + olmUtility!!.verifyEd25519Signature(otherSSKSignature, otherKeys.selfSigningKey()?.unpaddedBase64PublicKey, otherDevice.canonicalSignable()) + } catch (e: Throwable) { + return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.InvalidDeviceSignature(otherDeviceId, otherSSKSignature, e)) + } + + return DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = true, locallyVerified = locallyTrusted)) + } + + private fun legacyFallbackTrust(locallyTrusted: Boolean?, crossSignTrustFail: DeviceTrustResult): DeviceTrustResult { + return if (locallyTrusted == true) { + DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true)) + } else { + crossSignTrustFail + } + } + + override fun onUsersDeviceUpdate(userIds: List) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + Timber.d("## CrossSigning - onUsersDeviceUpdate for ${userIds.size} users") + userIds.forEach { otherUserId -> + checkUserTrust(otherUserId).let { + Timber.v("## CrossSigning - update trust for $otherUserId , verified=${it.isVerified()}") + setUserKeysAsTrusted(otherUserId, it.isVerified()) + } + } + } + + // now check device trust + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + userIds.forEach { otherUserId -> + // TODO if my keys have changes, i should recheck all devices of all users? + val devices = cryptoStore.getUserDeviceList(otherUserId) + devices?.forEach { device -> + val updatedTrust = checkDeviceTrust(otherUserId, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false) + Timber.v("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust") + cryptoStore.setDeviceTrust(otherUserId, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified()) + } + + if (otherUserId == userId) { + // It's me, i should check if a newly trusted device is signing my master key + // In this case it will change my MSK trust, and should then re-trigger a check of all other user trust + setUserKeysAsTrusted(otherUserId, checkSelfTrust().isVerified()) + } + } + + eventBus.post(CryptoToSessionUserTrustChange(userIds)) + } + } + + private fun setUserKeysAsTrusted(otherUserId: String, trusted: Boolean) { + val currentTrust = cryptoStore.getCrossSigningInfo(otherUserId)?.isTrusted() + cryptoStore.setUserKeysAsTrusted(otherUserId, trusted) + // If it's me, recheck trust of all users and devices? + val users = ArrayList() + if (otherUserId == userId && currentTrust != trusted) { +// reRequestAllPendingRoomKeyRequest() + cryptoStore.updateUsersTrust { + users.add(it) + checkUserTrust(it).isVerified() + } + + users.forEach { + cryptoStore.getUserDeviceList(it)?.forEach { device -> + val updatedTrust = checkDeviceTrust(it, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false) + Timber.v("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust") + cryptoStore.setDeviceTrust(it, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified()) + } + } + } + } + +// private fun reRequestAllPendingRoomKeyRequest() { +// cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { +// Timber.d("## CrossSigning - reRequest pending outgoing room key requests") +// cryptoStore.getOutgoingRoomKeyRequests().forEach { +// it.requestBody?.let { requestBody -> +// if (cryptoStore.getInboundGroupSession(requestBody.sessionId ?: "", requestBody.senderKey ?: "") == null) { +// outgoingRoomKeyRequestManager.resendRoomKeyRequest(requestBody) +// } else { +// outgoingRoomKeyRequestManager.cancelRoomKeyRequest(requestBody) +// } +// } +// } +// } +// } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DeviceTrustLevel.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DeviceTrustLevel.kt new file mode 100644 index 0000000000..c371c84ade --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DeviceTrustLevel.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.crosssigning + +data class DeviceTrustLevel( + val crossSigningVerified: Boolean, + val locallyVerified: Boolean? +) { + fun isVerified() = crossSigningVerified || locallyVerified == true + fun isCrossSigningVerified() = crossSigningVerified + fun isLocallyVerified() = locallyVerified +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DeviceTrustResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DeviceTrustResult.kt new file mode 100644 index 0000000000..cabfae1748 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DeviceTrustResult.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.crosssigning + +import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo + +sealed class DeviceTrustResult { + data class Success(val level: DeviceTrustLevel) : DeviceTrustResult() + data class UnknownDevice(val deviceID: String) : DeviceTrustResult() + data class CrossSigningNotConfigured(val userID: String) : DeviceTrustResult() + data class KeysNotTrusted(val key: MXCrossSigningInfo) : DeviceTrustResult() + data class MissingDeviceSignature(val deviceId: String, val signingKey: String) : DeviceTrustResult() + data class InvalidDeviceSignature(val deviceId: String, val signingKey: String, val throwable: Throwable?) : DeviceTrustResult() +} + +fun DeviceTrustResult.isSuccess() = this is DeviceTrustResult.Success +fun DeviceTrustResult.isCrossSignedVerified() = this is DeviceTrustResult.Success && level.isCrossSigningVerified() +fun DeviceTrustResult.isLocallyVerified() = this is DeviceTrustResult.Success && level.isLocallyVerified() == true diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/Extensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/Extensions.kt new file mode 100644 index 0000000000..8178a8810d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/Extensions.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.crosssigning + +import android.util.Base64 +import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.util.JsonCanonicalizer +import timber.log.Timber + +fun CryptoDeviceInfo.canonicalSignable(): String { + return JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableJSONDictionary()) +} + +fun CryptoCrossSigningKey.canonicalSignable(): String { + return JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableJSONDictionary()) +} + +fun ByteArray.toBase64NoPadding(): String { + return Base64.encodeToString(this, Base64.NO_PADDING or Base64.NO_WRAP) +} + +fun String.fromBase64(): ByteArray { + return Base64.decode(this, Base64.DEFAULT) +} + +/** + * Decode the base 64. Return null in case of bad format. Should be used when parsing received data from external source + */ +fun String.fromBase64Safe(): ByteArray? { + return try { + Base64.decode(this, Base64.DEFAULT) + } catch (throwable: Throwable) { + Timber.e(throwable, "Unable to decode base64 string") + null + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/SessionToCryptoRoomMembersUpdate.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/SessionToCryptoRoomMembersUpdate.kt new file mode 100644 index 0000000000..9b06d79693 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/SessionToCryptoRoomMembersUpdate.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.crosssigning + +data class SessionToCryptoRoomMembersUpdate( + val roomId: String, + val isDirect: Boolean, + val userIds: List +) + +data class CryptoToSessionUserTrustChange( + val userIds: List +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/ShieldTrustUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/ShieldTrustUpdater.kt new file mode 100644 index 0000000000..e8c1317604 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/ShieldTrustUpdater.kt @@ -0,0 +1,127 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.crosssigning + +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.SessionLifecycleObserver +import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater +import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.util.createBackgroundHandler +import io.realm.Realm +import io.realm.RealmConfiguration +import kotlinx.coroutines.android.asCoroutineDispatcher +import kotlinx.coroutines.launch +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import timber.log.Timber +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference +import javax.inject.Inject + +internal class ShieldTrustUpdater @Inject constructor( + private val eventBus: EventBus, + private val computeTrustTask: ComputeTrustTask, + private val taskExecutor: TaskExecutor, + @SessionDatabase private val sessionRealmConfiguration: RealmConfiguration, + private val roomSummaryUpdater: RoomSummaryUpdater +): SessionLifecycleObserver { + + companion object { + private val BACKGROUND_HANDLER = createBackgroundHandler("SHIELD_CRYPTO_DB_THREAD") + private val BACKGROUND_HANDLER_DISPATCHER = BACKGROUND_HANDLER.asCoroutineDispatcher() + } + + private val backgroundSessionRealm = AtomicReference() + + private val isStarted = AtomicBoolean() + + override fun onStart() { + if (isStarted.compareAndSet(false, true)) { + eventBus.register(this) + BACKGROUND_HANDLER.post { + backgroundSessionRealm.set(Realm.getInstance(sessionRealmConfiguration)) + } + } + } + + override fun onStop() { + if (isStarted.compareAndSet(true, false)) { + eventBus.unregister(this) + BACKGROUND_HANDLER.post { + backgroundSessionRealm.getAndSet(null).also { + it?.close() + } + } + } + } + + @Subscribe + fun onRoomMemberChange(update: SessionToCryptoRoomMembersUpdate) { + if (!isStarted.get()) { + return + } + taskExecutor.executorScope.launch(BACKGROUND_HANDLER_DISPATCHER) { + val updatedTrust = computeTrustTask.execute(ComputeTrustTask.Params(update.userIds, update.isDirect)) + // We need to send that back to session base + backgroundSessionRealm.get()?.executeTransaction { realm -> + roomSummaryUpdater.updateShieldTrust(realm, update.roomId, updatedTrust) + } + } + } + + @Subscribe + fun onTrustUpdate(update: CryptoToSessionUserTrustChange) { + if (!isStarted.get()) { + return + } + onCryptoDevicesChange(update.userIds) + } + + private fun onCryptoDevicesChange(users: List) { + taskExecutor.executorScope.launch(BACKGROUND_HANDLER_DISPATCHER) { + val realm = backgroundSessionRealm.get() ?: return@launch + val distinctRoomIds = realm.where(RoomMemberSummaryEntity::class.java) + .`in`(RoomMemberSummaryEntityFields.USER_ID, users.toTypedArray()) + .distinct(RoomMemberSummaryEntityFields.ROOM_ID) + .findAll() + .map { it.roomId } + + distinctRoomIds.forEach { roomId -> + val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() + if (roomSummary?.isEncrypted.orFalse()) { + val allActiveRoomMembers = RoomMemberHelper(realm, roomId).getActiveRoomMemberIds() + try { + val updatedTrust = computeTrustTask.execute( + ComputeTrustTask.Params(allActiveRoomMembers, roomSummary?.isDirect == true) + ) + realm.executeTransaction { + roomSummaryUpdater.updateShieldTrust(it, roomId, updatedTrust) + } + } catch (failure: Throwable) { + Timber.e(failure) + } + } + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UserTrustResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UserTrustResult.kt new file mode 100644 index 0000000000..878cbd0b32 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UserTrustResult.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.crosssigning + +import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo +import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey + +sealed class UserTrustResult { + object Success : UserTrustResult() + + // data class Success(val deviceID: String, val crossSigned: Boolean) : UserTrustResult() + // data class UnknownDevice(val deviceID: String) : UserTrustResult() + data class CrossSigningNotConfigured(val userID: String) : UserTrustResult() + + data class UnknownCrossSignatureInfo(val userID: String) : UserTrustResult() + data class KeysNotTrusted(val key: MXCrossSigningInfo) : UserTrustResult() + data class KeyNotSigned(val key: CryptoCrossSigningKey) : UserTrustResult() + data class InvalidSignature(val key: CryptoCrossSigningKey, val signature: String) : UserTrustResult() +} + +fun UserTrustResult.isVerified() = this is UserTrustResult.Success diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt new file mode 100644 index 0000000000..949677182c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt @@ -0,0 +1,1454 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup + +import android.os.Handler +import android.os.Looper +import androidx.annotation.UiThread +import androidx.annotation.VisibleForTesting +import androidx.annotation.WorkerThread +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.MatrixError +import org.matrix.android.sdk.api.listeners.ProgressListener +import org.matrix.android.sdk.api.listeners.StepProgressListener +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP +import org.matrix.android.sdk.internal.crypto.MXOlmDevice +import org.matrix.android.sdk.internal.crypto.MegolmSessionData +import org.matrix.android.sdk.internal.crypto.ObjectSigner +import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter +import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64 +import org.matrix.android.sdk.internal.crypto.keysbackup.model.KeysBackupVersionTrust +import org.matrix.android.sdk.internal.crypto.keysbackup.model.KeysBackupVersionTrustSignature +import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupAuthData +import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.BackupKeysResult +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.CreateKeysBackupVersionBody +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeyBackupData +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysBackupData +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.RoomKeysBackupData +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.UpdateKeysBackupVersionBody +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.CreateKeysBackupVersionTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteBackupTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteRoomSessionDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteRoomSessionsDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteSessionsDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetKeysBackupLastVersionTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetKeysBackupVersionTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetRoomSessionDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetRoomSessionsDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetSessionsDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreRoomSessionDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreRoomSessionsDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreSessionsDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.UpdateKeysBackupVersionTask +import org.matrix.android.sdk.internal.crypto.keysbackup.util.computeRecoveryKey +import org.matrix.android.sdk.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey +import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult +import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.store.SavedKeyBackupKeyInfo +import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.extensions.foldToCallback +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.TaskThread +import org.matrix.android.sdk.internal.task.configureWith +import org.matrix.android.sdk.internal.util.JsonCanonicalizer +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import org.matrix.android.sdk.internal.util.awaitCallback +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.matrix.olm.OlmException +import org.matrix.olm.OlmPkDecryption +import org.matrix.olm.OlmPkEncryption +import org.matrix.olm.OlmPkMessage +import timber.log.Timber +import java.security.InvalidParameterException +import javax.inject.Inject +import kotlin.random.Random + +/** + * A DefaultKeysBackupService class instance manage incremental backup of e2e keys (megolm keys) + * to the user's homeserver. + */ +@SessionScope +internal class DefaultKeysBackupService @Inject constructor( + @UserId private val userId: String, + private val credentials: Credentials, + private val cryptoStore: IMXCryptoStore, + private val olmDevice: MXOlmDevice, + private val objectSigner: ObjectSigner, + // Actions + private val megolmSessionDataImporter: MegolmSessionDataImporter, + // Tasks + private val createKeysBackupVersionTask: CreateKeysBackupVersionTask, + private val deleteBackupTask: DeleteBackupTask, + private val deleteRoomSessionDataTask: DeleteRoomSessionDataTask, + private val deleteRoomSessionsDataTask: DeleteRoomSessionsDataTask, + private val deleteSessionDataTask: DeleteSessionsDataTask, + private val getKeysBackupLastVersionTask: GetKeysBackupLastVersionTask, + private val getKeysBackupVersionTask: GetKeysBackupVersionTask, + private val getRoomSessionDataTask: GetRoomSessionDataTask, + private val getRoomSessionsDataTask: GetRoomSessionsDataTask, + private val getSessionsDataTask: GetSessionsDataTask, + private val storeRoomSessionDataTask: StoreRoomSessionDataTask, + private val storeSessionsDataTask: StoreRoomSessionsDataTask, + private val storeSessionDataTask: StoreSessionsDataTask, + private val updateKeysBackupVersionTask: UpdateKeysBackupVersionTask, + // Task executor + private val taskExecutor: TaskExecutor, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val cryptoCoroutineScope: CoroutineScope +) : KeysBackupService { + + private val uiHandler = Handler(Looper.getMainLooper()) + + private val keysBackupStateManager = KeysBackupStateManager(uiHandler) + + // The backup version + override var keysBackupVersion: KeysVersionResult? = null + private set + + // The backup key being used. + private var backupOlmPkEncryption: OlmPkEncryption? = null + + private var backupAllGroupSessionsCallback: MatrixCallback? = null + + private var keysBackupStateListener: KeysBackupStateListener? = null + + override val isEnabled: Boolean + get() = keysBackupStateManager.isEnabled + + override val isStucked: Boolean + get() = keysBackupStateManager.isStucked + + override val state: KeysBackupState + get() = keysBackupStateManager.state + + override val currentBackupVersion: String? + get() = keysBackupVersion?.version + + override fun addListener(listener: KeysBackupStateListener) { + keysBackupStateManager.addListener(listener) + } + + override fun removeListener(listener: KeysBackupStateListener) { + keysBackupStateManager.removeListener(listener) + } + + override fun prepareKeysBackupVersion(password: String?, + progressListener: ProgressListener?, + callback: MatrixCallback) { + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + runCatching { + withContext(coroutineDispatchers.crypto) { + val olmPkDecryption = OlmPkDecryption() + val megolmBackupAuthData = if (password != null) { + // Generate a private key from the password + val backgroundProgressListener = if (progressListener == null) { + null + } else { + object : ProgressListener { + override fun onProgress(progress: Int, total: Int) { + uiHandler.post { + try { + progressListener.onProgress(progress, total) + } catch (e: Exception) { + Timber.e(e, "prepareKeysBackupVersion: onProgress failure") + } + } + } + } + } + + val generatePrivateKeyResult = generatePrivateKeyWithPassword(password, backgroundProgressListener) + MegolmBackupAuthData( + publicKey = olmPkDecryption.setPrivateKey(generatePrivateKeyResult.privateKey), + privateKeySalt = generatePrivateKeyResult.salt, + privateKeyIterations = generatePrivateKeyResult.iterations + ) + } else { + val publicKey = olmPkDecryption.generateKey() + + MegolmBackupAuthData( + publicKey = publicKey + ) + } + + val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, megolmBackupAuthData.signalableJSONDictionary()) + + val signedMegolmBackupAuthData = megolmBackupAuthData.copy( + signatures = objectSigner.signObject(canonicalJson) + ) + + MegolmBackupCreationInfo( + algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP, + authData = signedMegolmBackupAuthData, + recoveryKey = computeRecoveryKey(olmPkDecryption.privateKey()) + ) + } + }.foldToCallback(callback) + } + } + + override fun createKeysBackupVersion(keysBackupCreationInfo: MegolmBackupCreationInfo, + callback: MatrixCallback) { + @Suppress("UNCHECKED_CAST") + val createKeysBackupVersionBody = CreateKeysBackupVersionBody( + algorithm = keysBackupCreationInfo.algorithm, + authData = MoshiProvider.providesMoshi().adapter(Map::class.java) + .fromJson(keysBackupCreationInfo.authData?.toJsonString() ?: "") as JsonDict? + ) + + keysBackupStateManager.state = KeysBackupState.Enabling + + createKeysBackupVersionTask + .configureWith(createKeysBackupVersionBody) { + this.callback = object : MatrixCallback { + override fun onSuccess(data: KeysVersion) { + // Reset backup markers. + cryptoStore.resetBackupMarkers() + + val keyBackupVersion = KeysVersionResult( + algorithm = createKeysBackupVersionBody.algorithm, + authData = createKeysBackupVersionBody.authData, + version = data.version, + // We can consider that the server does not have keys yet + count = 0, + hash = null + ) + + enableKeysBackup(keyBackupVersion) + + callback.onSuccess(data) + } + + override fun onFailure(failure: Throwable) { + keysBackupStateManager.state = KeysBackupState.Disabled + callback.onFailure(failure) + } + } + } + .executeBy(taskExecutor) + } + + override fun deleteBackup(version: String, callback: MatrixCallback?) { + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + withContext(coroutineDispatchers.crypto) { + // If we're currently backing up to this backup... stop. + // (We start using it automatically in createKeysBackupVersion so this is symmetrical). + if (keysBackupVersion != null && version == keysBackupVersion!!.version) { + resetKeysBackupData() + keysBackupVersion = null + keysBackupStateManager.state = KeysBackupState.Unknown + } + + deleteBackupTask + .configureWith(DeleteBackupTask.Params(version)) { + this.callback = object : MatrixCallback { + private fun eventuallyRestartBackup() { + // Do not stay in KeysBackupState.Unknown but check what is available on the homeserver + if (state == KeysBackupState.Unknown) { + checkAndStartKeysBackup() + } + } + + override fun onSuccess(data: Unit) { + eventuallyRestartBackup() + + uiHandler.post { callback?.onSuccess(Unit) } + } + + override fun onFailure(failure: Throwable) { + eventuallyRestartBackup() + + uiHandler.post { callback?.onFailure(failure) } + } + } + } + .executeBy(taskExecutor) + } + } + } + + override fun canRestoreKeys(): Boolean { + // Server contains more keys than locally + val totalNumberOfKeysLocally = getTotalNumbersOfKeys() + + val keysBackupData = cryptoStore.getKeysBackupData() + + val totalNumberOfKeysServer = keysBackupData?.backupLastServerNumberOfKeys ?: -1 + // Not used for the moment + // val hashServer = keysBackupData?.backupLastServerHash + + return when { + totalNumberOfKeysLocally < totalNumberOfKeysServer -> { + // Server contains more keys than this device + true + } + totalNumberOfKeysLocally == totalNumberOfKeysServer -> { + // Same number, compare hash? + // TODO We have not found any algorithm to determine if a restore is recommended here. Return false for the moment + false + } + else -> false + } + } + + override fun getTotalNumbersOfKeys(): Int { + return cryptoStore.inboundGroupSessionsCount(false) + } + + override fun getTotalNumbersOfBackedUpKeys(): Int { + return cryptoStore.inboundGroupSessionsCount(true) + } + + override fun backupAllGroupSessions(progressListener: ProgressListener?, + callback: MatrixCallback?) { + // Get a status right now + getBackupProgress(object : ProgressListener { + override fun onProgress(progress: Int, total: Int) { + // Reset previous listeners if any + resetBackupAllGroupSessionsListeners() + Timber.v("backupAllGroupSessions: backupProgress: $progress/$total") + try { + progressListener?.onProgress(progress, total) + } catch (e: Exception) { + Timber.e(e, "backupAllGroupSessions: onProgress failure") + } + + if (progress == total) { + Timber.v("backupAllGroupSessions: complete") + callback?.onSuccess(Unit) + return + } + + backupAllGroupSessionsCallback = callback + + // Listen to `state` change to determine when to call onBackupProgress and onComplete + keysBackupStateListener = object : KeysBackupStateListener { + override fun onStateChange(newState: KeysBackupState) { + getBackupProgress(object : ProgressListener { + override fun onProgress(progress: Int, total: Int) { + try { + progressListener?.onProgress(progress, total) + } catch (e: Exception) { + Timber.e(e, "backupAllGroupSessions: onProgress failure 2") + } + + // If backup is finished, notify the main listener + if (state === KeysBackupState.ReadyToBackUp) { + backupAllGroupSessionsCallback?.onSuccess(Unit) + resetBackupAllGroupSessionsListeners() + } + } + }) + } + }.also { keysBackupStateManager.addListener(it) } + + backupKeys() + } + }) + } + + override fun getKeysBackupTrust(keysBackupVersion: KeysVersionResult, + callback: MatrixCallback) { + // TODO Validate with François that this is correct + object : Task { + override suspend fun execute(params: KeysVersionResult): KeysBackupVersionTrust { + return getKeysBackupTrustBg(params) + } + } + .configureWith(keysBackupVersion) { + this.callback = callback + this.executionThread = TaskThread.COMPUTATION + } + .executeBy(taskExecutor) + } + + /** + * Check trust on a key backup version. + * This has to be called on background thread. + * + * @param keysBackupVersion the backup version to check. + * @return a KeysBackupVersionTrust object + */ + @WorkerThread + private fun getKeysBackupTrustBg(keysBackupVersion: KeysVersionResult): KeysBackupVersionTrust { + val keysBackupVersionTrust = KeysBackupVersionTrust() + val authData = keysBackupVersion.getAuthDataAsMegolmBackupAuthData() + + if (keysBackupVersion.algorithm == null + || authData == null + || authData.publicKey.isEmpty() + || authData.signatures.isNullOrEmpty()) { + Timber.v("getKeysBackupTrust: Key backup is absent or missing required data") + return keysBackupVersionTrust + } + + val mySigs = authData.signatures[userId] + if (mySigs.isNullOrEmpty()) { + Timber.v("getKeysBackupTrust: Ignoring key backup because it lacks any signatures from this user") + return keysBackupVersionTrust + } + + for ((keyId, mySignature) in mySigs) { + // XXX: is this how we're supposed to get the device id? + var deviceId: String? = null + val components = keyId.split(":") + if (components.size == 2) { + deviceId = components[1] + } + + if (deviceId != null) { + val device = cryptoStore.getUserDevice(userId, deviceId) + var isSignatureValid = false + + if (device == null) { + Timber.v("getKeysBackupTrust: Signature from unknown device $deviceId") + } else { + val fingerprint = device.fingerprint() + if (fingerprint != null) { + try { + olmDevice.verifySignature(fingerprint, authData.signalableJSONDictionary(), mySignature) + isSignatureValid = true + } catch (e: OlmException) { + Timber.w(e, "getKeysBackupTrust: Bad signature from device ${device.deviceId}") + } + } + + if (isSignatureValid && device.isVerified) { + keysBackupVersionTrust.usable = true + } + } + + val signature = KeysBackupVersionTrustSignature() + signature.device = device + signature.valid = isSignatureValid + signature.deviceId = deviceId + keysBackupVersionTrust.signatures.add(signature) + } + } + + return keysBackupVersionTrust + } + + override fun trustKeysBackupVersion(keysBackupVersion: KeysVersionResult, + trust: Boolean, + callback: MatrixCallback) { + Timber.v("trustKeyBackupVersion: $trust, version ${keysBackupVersion.version}") + + // Get auth data to update it + val authData = getMegolmBackupAuthData(keysBackupVersion) + + if (authData == null) { + Timber.w("trustKeyBackupVersion:trust: Key backup is missing required data") + + callback.onFailure(IllegalArgumentException("Missing element")) + } else { + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + val updateKeysBackupVersionBody = withContext(coroutineDispatchers.crypto) { + // Get current signatures, or create an empty set + val myUserSignatures = authData.signatures?.get(userId)?.toMutableMap() ?: HashMap() + + if (trust) { + // Add current device signature + val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, authData.signalableJSONDictionary()) + + val deviceSignatures = objectSigner.signObject(canonicalJson) + + deviceSignatures[userId]?.forEach { entry -> + myUserSignatures[entry.key] = entry.value + } + } else { + // Remove current device signature + myUserSignatures.remove("ed25519:${credentials.deviceId}") + } + + // Create an updated version of KeysVersionResult + val newMegolmBackupAuthData = authData.copy() + + val newSignatures = newMegolmBackupAuthData.signatures!!.toMutableMap() + newSignatures[userId] = myUserSignatures + + val newMegolmBackupAuthDataWithNewSignature = newMegolmBackupAuthData.copy( + signatures = newSignatures + ) + + val moshi = MoshiProvider.providesMoshi() + val adapter = moshi.adapter(Map::class.java) + + @Suppress("UNCHECKED_CAST") + UpdateKeysBackupVersionBody( + algorithm = keysBackupVersion.algorithm, + authData = adapter.fromJson(newMegolmBackupAuthDataWithNewSignature.toJsonString()) as Map?, + version = keysBackupVersion.version!!) + } + + // And send it to the homeserver + updateKeysBackupVersionTask + .configureWith(UpdateKeysBackupVersionTask.Params(keysBackupVersion.version!!, updateKeysBackupVersionBody)) { + this.callback = object : MatrixCallback { + override fun onSuccess(data: Unit) { + // Relaunch the state machine on this updated backup version + val newKeysBackupVersion = KeysVersionResult( + algorithm = keysBackupVersion.algorithm, + authData = updateKeysBackupVersionBody.authData, + version = keysBackupVersion.version, + hash = keysBackupVersion.hash, + count = keysBackupVersion.count + ) + + checkAndStartWithKeysBackupVersion(newKeysBackupVersion) + + callback.onSuccess(data) + } + + override fun onFailure(failure: Throwable) { + callback.onFailure(failure) + } + } + } + .executeBy(taskExecutor) + } + } + } + + override fun trustKeysBackupVersionWithRecoveryKey(keysBackupVersion: KeysVersionResult, + recoveryKey: String, + callback: MatrixCallback) { + Timber.v("trustKeysBackupVersionWithRecoveryKey: version ${keysBackupVersion.version}") + + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + val isValid = withContext(coroutineDispatchers.crypto) { + isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion) + } + + if (!isValid) { + Timber.w("trustKeyBackupVersionWithRecoveryKey: Invalid recovery key.") + + callback.onFailure(IllegalArgumentException("Invalid recovery key or password")) + } else { + trustKeysBackupVersion(keysBackupVersion, true, callback) + } + } + } + + override fun trustKeysBackupVersionWithPassphrase(keysBackupVersion: KeysVersionResult, + password: String, + callback: MatrixCallback) { + Timber.v("trustKeysBackupVersionWithPassphrase: version ${keysBackupVersion.version}") + + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + val recoveryKey = withContext(coroutineDispatchers.crypto) { + recoveryKeyFromPassword(password, keysBackupVersion, null) + } + + if (recoveryKey == null) { + Timber.w("trustKeysBackupVersionWithPassphrase: Key backup is missing required data") + + callback.onFailure(IllegalArgumentException("Missing element")) + } else { + // Check trust using the recovery key + trustKeysBackupVersionWithRecoveryKey(keysBackupVersion, recoveryKey, callback) + } + } + } + + override fun onSecretKeyGossip(secret: String) { + Timber.i("## CrossSigning - onSecretKeyGossip") + + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + try { + val keysBackupVersion = getKeysBackupLastVersionTask.execute(Unit) + val recoveryKey = computeRecoveryKey(secret.fromBase64()) + if (isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)) { + awaitCallback { + trustKeysBackupVersion(keysBackupVersion, true, it) + } + val importResult = awaitCallback { + restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, null, null, null, it) + } + cryptoStore.saveBackupRecoveryKey(recoveryKey, keysBackupVersion.version) + Timber.i("onSecretKeyGossip: Recovered keys ${importResult.successfullyNumberOfImportedKeys} out of ${importResult.totalNumberOfKeys}") + } else { + Timber.e("onSecretKeyGossip: Recovery key is not valid ${keysBackupVersion.version}") + } + } catch (failure: Throwable) { + Timber.e("onSecretKeyGossip: failed to trust key backup version ${keysBackupVersion?.version}") + } + } + } + + /** + * Get public key from a Recovery key + * + * @param recoveryKey the recovery key + * @return the corresponding public key, from Olm + */ + @WorkerThread + private fun pkPublicKeyFromRecoveryKey(recoveryKey: String): String? { + // Extract the primary key + val privateKey = extractCurveKeyFromRecoveryKey(recoveryKey) + + if (privateKey == null) { + Timber.w("pkPublicKeyFromRecoveryKey: private key is null") + + return null + } + + // Built the PK decryption with it + val pkPublicKey: String + + try { + val decryption = OlmPkDecryption() + pkPublicKey = decryption.setPrivateKey(privateKey) + } catch (e: OlmException) { + return null + } + + return pkPublicKey + } + + private fun resetBackupAllGroupSessionsListeners() { + backupAllGroupSessionsCallback = null + + keysBackupStateListener?.let { + keysBackupStateManager.removeListener(it) + } + + keysBackupStateListener = null + } + + override fun getBackupProgress(progressListener: ProgressListener) { + val backedUpKeys = cryptoStore.inboundGroupSessionsCount(true) + val total = cryptoStore.inboundGroupSessionsCount(false) + + progressListener.onProgress(backedUpKeys, total) + } + + override fun restoreKeysWithRecoveryKey(keysVersionResult: KeysVersionResult, + recoveryKey: String, + roomId: String?, + sessionId: String?, + stepProgressListener: StepProgressListener?, + callback: MatrixCallback) { + Timber.v("restoreKeysWithRecoveryKey: From backup version: ${keysVersionResult.version}") + + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + runCatching { + val decryption = withContext(coroutineDispatchers.crypto) { + // Check if the recovery is valid before going any further + if (!isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysVersionResult)) { + Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key for this keys version") + throw InvalidParameterException("Invalid recovery key") + } + + // Get a PK decryption instance + pkDecryptionFromRecoveryKey(recoveryKey) + } + if (decryption == null) { + // This should not happen anymore + Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key. Error") + throw InvalidParameterException("Invalid recovery key") + } + + stepProgressListener?.onStepProgress(StepProgressListener.Step.DownloadingKey) + + // Get backed up keys from the homeserver + val data = getKeys(sessionId, roomId, keysVersionResult.version!!) + + withContext(coroutineDispatchers.crypto) { + val sessionsData = ArrayList() + // Restore that data + var sessionsFromHsCount = 0 + for ((roomIdLoop, backupData) in data.roomIdToRoomKeysBackupData) { + for ((sessionIdLoop, keyBackupData) in backupData.sessionIdToKeyBackupData) { + sessionsFromHsCount++ + + val sessionData = decryptKeyBackupData(keyBackupData, sessionIdLoop, roomIdLoop, decryption) + + sessionData?.let { + sessionsData.add(it) + } + } + } + Timber.v("restoreKeysWithRecoveryKey: Decrypted ${sessionsData.size} keys out" + + " of $sessionsFromHsCount from the backup store on the homeserver") + + // Do not trigger a backup for them if they come from the backup version we are using + val backUp = keysVersionResult.version != keysBackupVersion?.version + if (backUp) { + Timber.v("restoreKeysWithRecoveryKey: Those keys will be backed up" + + " to backup version: ${keysBackupVersion?.version}") + } + + // Import them into the crypto store + val progressListener = if (stepProgressListener != null) { + object : ProgressListener { + override fun onProgress(progress: Int, total: Int) { + // Note: no need to post to UI thread, importMegolmSessionsData() will do it + stepProgressListener.onStepProgress(StepProgressListener.Step.ImportingKey(progress, total)) + } + } + } else { + null + } + + val result = megolmSessionDataImporter.handle(sessionsData, !backUp, progressListener) + + // Do not back up the key if it comes from a backup recovery + if (backUp) { + maybeBackupKeys() + } + // Save for next time and for gossiping + saveBackupRecoveryKey(recoveryKey, keysVersionResult.version) + result + } + }.foldToCallback(callback) + } + } + + override fun restoreKeyBackupWithPassword(keysBackupVersion: KeysVersionResult, + password: String, + roomId: String?, + sessionId: String?, + stepProgressListener: StepProgressListener?, + callback: MatrixCallback) { + Timber.v("[MXKeyBackup] restoreKeyBackup with password: From backup version: ${keysBackupVersion.version}") + + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + runCatching { + val progressListener = if (stepProgressListener != null) { + object : ProgressListener { + override fun onProgress(progress: Int, total: Int) { + uiHandler.post { + stepProgressListener.onStepProgress(StepProgressListener.Step.ComputingKey(progress, total)) + } + } + } + } else { + null + } + + val recoveryKey = withContext(coroutineDispatchers.crypto) { + recoveryKeyFromPassword(password, keysBackupVersion, progressListener) + } + if (recoveryKey == null) { + Timber.v("backupKeys: Invalid configuration") + throw IllegalStateException("Invalid configuration") + } else { + awaitCallback { + restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, roomId, sessionId, stepProgressListener, it) + } + } + }.foldToCallback(callback) + } + } + + /** + * Same method as [RoomKeysRestClient.getRoomKey] except that it accepts nullable + * parameters and always returns a KeysBackupData object through the Callback + */ + private suspend fun getKeys(sessionId: String?, + roomId: String?, + version: String): KeysBackupData { + return if (roomId != null && sessionId != null) { + // Get key for the room and for the session + val data = getRoomSessionDataTask.execute(GetRoomSessionDataTask.Params(roomId, sessionId, version)) + // Convert to KeysBackupData + KeysBackupData(mutableMapOf( + roomId to RoomKeysBackupData(mutableMapOf( + sessionId to data + )) + )) + } else if (roomId != null) { + // Get all keys for the room + val data = getRoomSessionsDataTask.execute(GetRoomSessionsDataTask.Params(roomId, version)) + // Convert to KeysBackupData + KeysBackupData(mutableMapOf(roomId to data)) + } else { + // Get all keys + getSessionsDataTask.execute(GetSessionsDataTask.Params(version)) + } + } + + @VisibleForTesting + @WorkerThread + fun pkDecryptionFromRecoveryKey(recoveryKey: String): OlmPkDecryption? { + // Extract the primary key + val privateKey = extractCurveKeyFromRecoveryKey(recoveryKey) + + // Built the PK decryption with it + var decryption: OlmPkDecryption? = null + if (privateKey != null) { + try { + decryption = OlmPkDecryption() + decryption.setPrivateKey(privateKey) + } catch (e: OlmException) { + Timber.e(e, "OlmException") + } + } + + return decryption + } + + /** + * Do a backup if there are new keys, with a delay + */ + fun maybeBackupKeys() { + when { + isStucked -> { + // If not already done, or in error case, check for a valid backup version on the homeserver. + // If there is one, maybeBackupKeys will be called again. + checkAndStartKeysBackup() + } + state == KeysBackupState.ReadyToBackUp -> { + keysBackupStateManager.state = KeysBackupState.WillBackUp + + // Wait between 0 and 10 seconds, to avoid backup requests from + // different clients hitting the server all at the same time when a + // new key is sent + val delayInMs = Random.nextLong(KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS) + + cryptoCoroutineScope.launch { + delay(delayInMs) + uiHandler.post { backupKeys() } + } + } + else -> { + Timber.v("maybeBackupKeys: Skip it because state: $state") + } + } + } + + override fun getVersion(version: String, + callback: MatrixCallback) { + getKeysBackupVersionTask + .configureWith(version) { + this.callback = object : MatrixCallback { + override fun onSuccess(data: KeysVersionResult) { + callback.onSuccess(data) + } + + override fun onFailure(failure: Throwable) { + if (failure is Failure.ServerError + && failure.error.code == MatrixError.M_NOT_FOUND) { + // Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup + callback.onSuccess(null) + } else { + // Transmit the error + callback.onFailure(failure) + } + } + } + } + .executeBy(taskExecutor) + } + + override fun getCurrentVersion(callback: MatrixCallback) { + getKeysBackupLastVersionTask + .configureWith { + this.callback = object : MatrixCallback { + override fun onSuccess(data: KeysVersionResult) { + callback.onSuccess(data) + } + + override fun onFailure(failure: Throwable) { + if (failure is Failure.ServerError + && failure.error.code == MatrixError.M_NOT_FOUND) { + // Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup + callback.onSuccess(null) + } else { + // Transmit the error + callback.onFailure(failure) + } + } + } + } + .executeBy(taskExecutor) + } + + override fun forceUsingLastVersion(callback: MatrixCallback) { + getCurrentVersion(object : MatrixCallback { + override fun onSuccess(data: KeysVersionResult?) { + val localBackupVersion = keysBackupVersion?.version + val serverBackupVersion = data?.version + + if (serverBackupVersion == null) { + if (localBackupVersion == null) { + // No backup on the server, and backup is not active + callback.onSuccess(true) + } else { + // No backup on the server, and we are currently backing up, so stop backing up + callback.onSuccess(false) + resetKeysBackupData() + keysBackupVersion = null + keysBackupStateManager.state = KeysBackupState.Disabled + } + } else { + if (localBackupVersion == null) { + // backup on the server, and backup is not active + callback.onSuccess(false) + // Do a check + checkAndStartWithKeysBackupVersion(data) + } else { + // Backup on the server, and we are currently backing up, compare version + if (localBackupVersion == serverBackupVersion) { + // We are already using the last version of the backup + callback.onSuccess(true) + } else { + // We are not using the last version, so delete the current version we are using on the server + callback.onSuccess(false) + + // This will automatically check for the last version then + deleteBackup(localBackupVersion, null) + } + } + } + } + + override fun onFailure(failure: Throwable) { + callback.onFailure(failure) + } + }) + } + + override fun checkAndStartKeysBackup() { + if (!isStucked) { + // Try to start or restart the backup only if it is in unknown or bad state + Timber.w("checkAndStartKeysBackup: invalid state: $state") + + return + } + + keysBackupVersion = null + keysBackupStateManager.state = KeysBackupState.CheckingBackUpOnHomeserver + + getCurrentVersion(object : MatrixCallback { + override fun onSuccess(data: KeysVersionResult?) { + checkAndStartWithKeysBackupVersion(data) + } + + override fun onFailure(failure: Throwable) { + Timber.e(failure, "checkAndStartKeysBackup: Failed to get current version") + keysBackupStateManager.state = KeysBackupState.Unknown + } + }) + } + + private fun checkAndStartWithKeysBackupVersion(keyBackupVersion: KeysVersionResult?) { + Timber.v("checkAndStartWithKeyBackupVersion: ${keyBackupVersion?.version}") + + keysBackupVersion = keyBackupVersion + + if (keyBackupVersion == null) { + Timber.v("checkAndStartWithKeysBackupVersion: Found no key backup version on the homeserver") + resetKeysBackupData() + keysBackupStateManager.state = KeysBackupState.Disabled + } else { + getKeysBackupTrust(keyBackupVersion, object : MatrixCallback { + override fun onSuccess(data: KeysBackupVersionTrust) { + val versionInStore = cryptoStore.getKeyBackupVersion() + + if (data.usable) { + Timber.v("checkAndStartWithKeysBackupVersion: Found usable key backup. version: ${keyBackupVersion.version}") + // Check the version we used at the previous app run + if (versionInStore != null && versionInStore != keyBackupVersion.version) { + Timber.v(" -> clean the previously used version $versionInStore") + resetKeysBackupData() + } + + Timber.v(" -> enabling key backups") + enableKeysBackup(keyBackupVersion) + } else { + Timber.v("checkAndStartWithKeysBackupVersion: No usable key backup. version: ${keyBackupVersion.version}") + if (versionInStore != null) { + Timber.v(" -> disabling key backup") + resetKeysBackupData() + } + + keysBackupStateManager.state = KeysBackupState.NotTrusted + } + } + + override fun onFailure(failure: Throwable) { + // Cannot happen + } + }) + } + } + +/* ========================================================================================== + * Private + * ========================================================================================== */ + + /** + * Extract MegolmBackupAuthData data from a backup version. + * + * @param keysBackupData the key backup data + * + * @return the authentication if found and valid, null in other case + */ + private fun getMegolmBackupAuthData(keysBackupData: KeysVersionResult): MegolmBackupAuthData? { + if (keysBackupData.version.isNullOrBlank() + || keysBackupData.algorithm != MXCRYPTO_ALGORITHM_MEGOLM_BACKUP + || keysBackupData.authData == null) { + return null + } + + val authData = keysBackupData.getAuthDataAsMegolmBackupAuthData() + + if (authData?.signatures == null || authData.publicKey.isBlank()) { + return null + } + + return authData + } + + /** + * Compute the recovery key from a password and key backup version. + * + * @param password the password. + * @param keysBackupData the backup and its auth data. + * + * @return the recovery key if successful, null in other cases + */ + @WorkerThread + private fun recoveryKeyFromPassword(password: String, keysBackupData: KeysVersionResult, progressListener: ProgressListener?): String? { + val authData = getMegolmBackupAuthData(keysBackupData) + + if (authData == null) { + Timber.w("recoveryKeyFromPassword: invalid parameter") + return null + } + + if (authData.privateKeySalt.isNullOrBlank() + || authData.privateKeyIterations == null) { + Timber.w("recoveryKeyFromPassword: Salt and/or iterations not found in key backup auth data") + + return null + } + + // Extract the recovery key from the passphrase + val data = retrievePrivateKeyWithPassword(password, authData.privateKeySalt, authData.privateKeyIterations, progressListener) + + return computeRecoveryKey(data) + } + + /** + * Check if a recovery key matches key backup authentication data. + * + * @param recoveryKey the recovery key to challenge. + * @param keysBackupData the backup and its auth data. + * + * @return true if successful. + */ + @WorkerThread + private fun isValidRecoveryKeyForKeysBackupVersion(recoveryKey: String, keysBackupData: KeysVersionResult): Boolean { + // Build PK decryption instance with the recovery key + val publicKey = pkPublicKeyFromRecoveryKey(recoveryKey) + + if (publicKey == null) { + Timber.w("isValidRecoveryKeyForKeysBackupVersion: public key is null") + + return false + } + + val authData = getMegolmBackupAuthData(keysBackupData) + + if (authData == null) { + Timber.w("isValidRecoveryKeyForKeysBackupVersion: Key backup is missing required data") + + return false + } + + // Compare both + if (publicKey != authData.publicKey) { + Timber.w("isValidRecoveryKeyForKeysBackupVersion: Public keys mismatch") + + return false + } + + // Public keys match! + return true + } + + override fun isValidRecoveryKeyForCurrentVersion(recoveryKey: String, callback: MatrixCallback) { + val safeKeysBackupVersion = keysBackupVersion ?: return Unit.also { callback.onSuccess(false) } + + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + isValidRecoveryKeyForKeysBackupVersion(recoveryKey, safeKeysBackupVersion).let { + callback.onSuccess(it) + } + } + } + + /** + * Enable backing up of keys. + * This method will update the state and will start sending keys in nominal case + * + * @param keysVersionResult backup information object as returned by [getCurrentVersion]. + */ + private fun enableKeysBackup(keysVersionResult: KeysVersionResult) { + if (keysVersionResult.authData != null) { + val retrievedMegolmBackupAuthData = keysVersionResult.getAuthDataAsMegolmBackupAuthData() + + if (retrievedMegolmBackupAuthData != null) { + keysBackupVersion = keysVersionResult + cryptoStore.setKeyBackupVersion(keysVersionResult.version) + + onServerDataRetrieved(keysVersionResult.count, keysVersionResult.hash) + + try { + backupOlmPkEncryption = OlmPkEncryption().apply { + setRecipientKey(retrievedMegolmBackupAuthData.publicKey) + } + } catch (e: OlmException) { + Timber.e(e, "OlmException") + keysBackupStateManager.state = KeysBackupState.Disabled + return + } + + keysBackupStateManager.state = KeysBackupState.ReadyToBackUp + + maybeBackupKeys() + } else { + Timber.e("Invalid authentication data") + keysBackupStateManager.state = KeysBackupState.Disabled + } + } else { + Timber.e("Invalid authentication data") + keysBackupStateManager.state = KeysBackupState.Disabled + } + } + + /** + * Update the DB with data fetch from the server + */ + private fun onServerDataRetrieved(count: Int?, hash: String?) { + cryptoStore.setKeysBackupData(KeysBackupDataEntity() + .apply { + backupLastServerNumberOfKeys = count + backupLastServerHash = hash + } + ) + } + + /** + * Reset all local key backup data. + * + * Note: This method does not update the state + */ + private fun resetKeysBackupData() { + resetBackupAllGroupSessionsListeners() + + cryptoStore.setKeyBackupVersion(null) + cryptoStore.setKeysBackupData(null) + backupOlmPkEncryption = null + + // Reset backup markers + cryptoStore.resetBackupMarkers() + } + + /** + * Send a chunk of keys to backup + */ + @UiThread + private fun backupKeys() { + Timber.v("backupKeys") + + // Sanity check, as this method can be called after a delay, the state may have change during the delay + if (!isEnabled || backupOlmPkEncryption == null || keysBackupVersion == null) { + Timber.v("backupKeys: Invalid configuration") + backupAllGroupSessionsCallback?.onFailure(IllegalStateException("Invalid configuration")) + resetBackupAllGroupSessionsListeners() + + return + } + + if (state === KeysBackupState.BackingUp) { + // Do nothing if we are already backing up + Timber.v("backupKeys: Invalid state: $state") + return + } + + // Get a chunk of keys to backup + val olmInboundGroupSessionWrappers = cryptoStore.inboundGroupSessionsToBackup(KEY_BACKUP_SEND_KEYS_MAX_COUNT) + + Timber.v("backupKeys: 1 - ${olmInboundGroupSessionWrappers.size} sessions to back up") + + if (olmInboundGroupSessionWrappers.isEmpty()) { + // Backup is up to date + keysBackupStateManager.state = KeysBackupState.ReadyToBackUp + + backupAllGroupSessionsCallback?.onSuccess(Unit) + resetBackupAllGroupSessionsListeners() + return + } + + keysBackupStateManager.state = KeysBackupState.BackingUp + + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + withContext(coroutineDispatchers.crypto) { + Timber.v("backupKeys: 2 - Encrypting keys") + + // Gather data to send to the homeserver + // roomId -> sessionId -> MXKeyBackupData + val keysBackupData = KeysBackupData( + roomIdToRoomKeysBackupData = HashMap() + ) + + for (olmInboundGroupSessionWrapper in olmInboundGroupSessionWrappers) { + val keyBackupData = encryptGroupSession(olmInboundGroupSessionWrapper) + if (keysBackupData.roomIdToRoomKeysBackupData[olmInboundGroupSessionWrapper.roomId] == null) { + val roomKeysBackupData = RoomKeysBackupData( + sessionIdToKeyBackupData = HashMap() + ) + keysBackupData.roomIdToRoomKeysBackupData[olmInboundGroupSessionWrapper.roomId!!] = roomKeysBackupData + } + + try { + keysBackupData.roomIdToRoomKeysBackupData[olmInboundGroupSessionWrapper.roomId]!! + .sessionIdToKeyBackupData[olmInboundGroupSessionWrapper.olmInboundGroupSession!!.sessionIdentifier()] = keyBackupData + } catch (e: OlmException) { + Timber.e(e, "OlmException") + } + } + + Timber.v("backupKeys: 4 - Sending request") + + val sendingRequestCallback = object : MatrixCallback { + override fun onSuccess(data: BackupKeysResult) { + uiHandler.post { + Timber.v("backupKeys: 5a - Request complete") + + // Mark keys as backed up + cryptoStore.markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers) + + if (olmInboundGroupSessionWrappers.size < KEY_BACKUP_SEND_KEYS_MAX_COUNT) { + Timber.v("backupKeys: All keys have been backed up") + onServerDataRetrieved(data.count, data.hash) + + // Note: Changing state will trigger the call to backupAllGroupSessionsCallback.onSuccess() + keysBackupStateManager.state = KeysBackupState.ReadyToBackUp + } else { + Timber.v("backupKeys: Continue to back up keys") + keysBackupStateManager.state = KeysBackupState.WillBackUp + + backupKeys() + } + } + } + + override fun onFailure(failure: Throwable) { + if (failure is Failure.ServerError) { + uiHandler.post { + Timber.e(failure, "backupKeys: backupKeys failed.") + + when (failure.error.code) { + MatrixError.M_NOT_FOUND, + MatrixError.M_WRONG_ROOM_KEYS_VERSION -> { + // Backup has been deleted on the server, or we are not using the last backup version + keysBackupStateManager.state = KeysBackupState.WrongBackUpVersion + backupAllGroupSessionsCallback?.onFailure(failure) + resetBackupAllGroupSessionsListeners() + resetKeysBackupData() + keysBackupVersion = null + + // Do not stay in KeysBackupState.WrongBackUpVersion but check what is available on the homeserver + checkAndStartKeysBackup() + } + else -> + // Come back to the ready state so that we will retry on the next received key + keysBackupStateManager.state = KeysBackupState.ReadyToBackUp + } + } + } else { + uiHandler.post { + backupAllGroupSessionsCallback?.onFailure(failure) + resetBackupAllGroupSessionsListeners() + + Timber.e("backupKeys: backupKeys failed.") + + // Retry a bit later + keysBackupStateManager.state = KeysBackupState.ReadyToBackUp + maybeBackupKeys() + } + } + } + } + + // Make the request + storeSessionDataTask + .configureWith(StoreSessionsDataTask.Params(keysBackupVersion!!.version!!, keysBackupData)) { + this.callback = sendingRequestCallback + } + .executeBy(taskExecutor) + } + } + } + + @VisibleForTesting + @WorkerThread + fun encryptGroupSession(olmInboundGroupSessionWrapper: OlmInboundGroupSessionWrapper2): KeyBackupData { + // Gather information for each key + val device = cryptoStore.deviceWithIdentityKey(olmInboundGroupSessionWrapper.senderKey!!) + + // Build the m.megolm_backup.v1.curve25519-aes-sha2 data as defined at + // https://github.com/uhoreg/matrix-doc/blob/e2e_backup/proposals/1219-storing-megolm-keys-serverside.md#mmegolm_backupv1curve25519-aes-sha2-key-format + val sessionData = olmInboundGroupSessionWrapper.exportKeys() + val sessionBackupData = mapOf( + "algorithm" to sessionData!!.algorithm, + "sender_key" to sessionData.senderKey, + "sender_claimed_keys" to sessionData.senderClaimedKeys, + "forwarding_curve25519_key_chain" to (sessionData.forwardingCurve25519KeyChain + ?: ArrayList()), + "session_key" to sessionData.sessionKey) + + var encryptedSessionBackupData: OlmPkMessage? = null + + val moshi = MoshiProvider.providesMoshi() + val adapter = moshi.adapter(Map::class.java) + + try { + val json = adapter.toJson(sessionBackupData) + + encryptedSessionBackupData = backupOlmPkEncryption?.encrypt(json) + } catch (e: OlmException) { + Timber.e(e, "OlmException") + } + + // Build backup data for that key + return KeyBackupData( + firstMessageIndex = try { + olmInboundGroupSessionWrapper.olmInboundGroupSession!!.firstKnownIndex + } catch (e: OlmException) { + Timber.e(e, "OlmException") + 0L + }, + forwardedCount = olmInboundGroupSessionWrapper.forwardingCurve25519KeyChain!!.size, + isVerified = device?.isVerified == true, + + sessionData = mapOf( + "ciphertext" to encryptedSessionBackupData!!.mCipherText, + "mac" to encryptedSessionBackupData.mMac, + "ephemeral" to encryptedSessionBackupData.mEphemeralKey) + ) + } + + @VisibleForTesting + @WorkerThread + fun decryptKeyBackupData(keyBackupData: KeyBackupData, sessionId: String, roomId: String, decryption: OlmPkDecryption): MegolmSessionData? { + var sessionBackupData: MegolmSessionData? = null + + val jsonObject = keyBackupData.sessionData + + val ciphertext = jsonObject?.get("ciphertext")?.toString() + val mac = jsonObject?.get("mac")?.toString() + val ephemeralKey = jsonObject?.get("ephemeral")?.toString() + + if (ciphertext != null && mac != null && ephemeralKey != null) { + val encrypted = OlmPkMessage() + encrypted.mCipherText = ciphertext + encrypted.mMac = mac + encrypted.mEphemeralKey = ephemeralKey + + try { + val decrypted = decryption.decrypt(encrypted) + + val moshi = MoshiProvider.providesMoshi() + val adapter = moshi.adapter(MegolmSessionData::class.java) + + sessionBackupData = adapter.fromJson(decrypted) + } catch (e: OlmException) { + Timber.e(e, "OlmException") + } + + if (sessionBackupData != null) { + sessionBackupData = sessionBackupData.copy( + sessionId = sessionId, + roomId = roomId + ) + } + } + + return sessionBackupData + } + + /* ========================================================================================== + * For test only + * ========================================================================================== */ + + // Direct access for test only + @VisibleForTesting + val store + get() = cryptoStore + + @VisibleForTesting + fun createFakeKeysBackupVersion(keysBackupCreationInfo: MegolmBackupCreationInfo, + callback: MatrixCallback) { + @Suppress("UNCHECKED_CAST") + val createKeysBackupVersionBody = CreateKeysBackupVersionBody( + algorithm = keysBackupCreationInfo.algorithm, + authData = MoshiProvider.providesMoshi().adapter(Map::class.java) + .fromJson(keysBackupCreationInfo.authData?.toJsonString() ?: "") as JsonDict? + ) + + createKeysBackupVersionTask + .configureWith(createKeysBackupVersionBody) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo? { + return cryptoStore.getKeyBackupRecoveryKeyInfo() + } + + override fun saveBackupRecoveryKey(recoveryKey: String?, version: String?) { + cryptoStore.saveBackupRecoveryKey(recoveryKey, version) + } + + companion object { + // Maximum delay in ms in {@link maybeBackupKeys} + private const val KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS = 10_000L + + // Maximum number of keys to send at a time to the homeserver. + private const val KEY_BACKUP_SEND_KEYS_MAX_COUNT = 100 + } + +/* ========================================================================================== + * DEBUG INFO + * ========================================================================================== */ + + override fun toString() = "KeysBackup for $userId" +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupPassword.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupPassword.kt new file mode 100644 index 0000000000..e796514cf4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupPassword.kt @@ -0,0 +1,152 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +/** + * Utility to compute a backup private key from a password and vice-versa. + */ +package org.matrix.android.sdk.internal.crypto.keysbackup + +import androidx.annotation.WorkerThread +import org.matrix.android.sdk.api.listeners.ProgressListener +import timber.log.Timber +import java.util.UUID +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec +import kotlin.experimental.xor + +private const val SALT_LENGTH = 32 +private const val DEFAULT_ITERATION = 500_000 + +data class GeneratePrivateKeyResult( + // The private key + val privateKey: ByteArray, + // the salt used to generate the private key + val salt: String, + // number of key derivations done on the generated private key. + val iterations: Int) + +/** + * Compute a private key from a password. + * + * @param password the password to use. + * + * @return a {privateKey, salt, iterations} tuple. + */ +@WorkerThread +fun generatePrivateKeyWithPassword(password: String, progressListener: ProgressListener?): GeneratePrivateKeyResult { + val salt = generateSalt() + val iterations = DEFAULT_ITERATION + val privateKey = deriveKey(password, salt, iterations, progressListener) + + return GeneratePrivateKeyResult(privateKey, salt, iterations) +} + +/** + * Retrieve a private key from {password, salt, iterations} + * + * @param password the password used to generated the private key. + * @param salt the salt. + * @param iterations number of key derivations. + * @param progressListener the progress listener + * + * @return a private key. + */ +@WorkerThread +fun retrievePrivateKeyWithPassword(password: String, + salt: String, + iterations: Int, + progressListener: ProgressListener? = null): ByteArray { + return deriveKey(password, salt, iterations, progressListener) +} + +/** + * Compute a private key by deriving a password and a salt strings. + * + * @param password the password. + * @param salt the salt. + * @param iterations number of derivations. + * @param progressListener a listener to follow progress. + * + * @return a private key. + */ +@WorkerThread +fun deriveKey(password: String, + salt: String, + iterations: Int, + progressListener: ProgressListener?): ByteArray { + // Note: copied and adapted from MXMegolmExportEncryption + val t0 = System.currentTimeMillis() + + // based on https://en.wikipedia.org/wiki/PBKDF2 algorithm + // it is simpler than the generic algorithm because the expected key length is equal to the mac key length. + // noticed as dklen/hlen + + // dklen = 256 + // hlen = 512 + val prf = Mac.getInstance("HmacSHA512") + + prf.init(SecretKeySpec(password.toByteArray(), "HmacSHA512")) + + // 256 bits key length + val dk = ByteArray(32) + val uc = ByteArray(64) + + // U1 = PRF(Password, Salt || INT_32_BE(i)) with i goes from 1 to dklen/hlen + prf.update(salt.toByteArray()) + val int32BE = byteArrayOf(0, 0, 0, 1) + prf.update(int32BE) + prf.doFinal(uc, 0) + + // copy to the key + System.arraycopy(uc, 0, dk, 0, dk.size) + + var lastProgress = -1 + + for (index in 2..iterations) { + // Uc = PRF(Password, Uc-1) + prf.update(uc) + prf.doFinal(uc, 0) + + // F(Password, Salt, c, i) = U1 ^ U2 ^ ... ^ Uc + for (byteIndex in dk.indices) { + dk[byteIndex] = dk[byteIndex] xor uc[byteIndex] + } + + val progress = (index + 1) * 100 / iterations + if (progress != lastProgress) { + lastProgress = progress + progressListener?.onProgress(lastProgress, 100) + } + } + + Timber.v("KeysBackupPassword: deriveKeys() : " + iterations + " in " + (System.currentTimeMillis() - t0) + " ms") + + return dk +} + +/** + * Generate a 32 chars salt + */ +private fun generateSalt(): String { + val salt = buildString { + do { + append(UUID.randomUUID().toString()) + } while (length < SALT_LENGTH) + } + + return salt.substring(0, SALT_LENGTH) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupStateManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupStateManager.kt new file mode 100644 index 0000000000..19a1f08177 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupStateManager.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup + +import android.os.Handler +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener +import timber.log.Timber + +internal class KeysBackupStateManager(private val uiHandler: Handler) { + + private val listeners = ArrayList() + + // Backup state + var state = KeysBackupState.Unknown + set(newState) { + Timber.v("KeysBackup: setState: $field -> $newState") + + field = newState + + // Notify listeners about the state change, on the ui thread + uiHandler.post { + synchronized(listeners) { + listeners.forEach { + // Use newState because state may have already changed again + it.onStateChange(newState) + } + } + } + } + + val isEnabled: Boolean + get() = state == KeysBackupState.ReadyToBackUp + || state == KeysBackupState.WillBackUp + || state == KeysBackupState.BackingUp + + // True if unknown or bad state + val isStucked: Boolean + get() = state == KeysBackupState.Unknown + || state == KeysBackupState.Disabled + || state == KeysBackupState.WrongBackUpVersion + || state == KeysBackupState.NotTrusted + + fun addListener(listener: KeysBackupStateListener) { + synchronized(listeners) { + listeners.add(listener) + } + } + + fun removeListener(listener: KeysBackupStateListener) { + synchronized(listeners) { + listeners.remove(listener) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/api/RoomKeysApi.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/api/RoomKeysApi.kt new file mode 100644 index 0000000000..de59aa8ae7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/api/RoomKeysApi.kt @@ -0,0 +1,193 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.api + +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.BackupKeysResult +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.CreateKeysBackupVersionBody +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeyBackupData +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysBackupData +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.RoomKeysBackupData +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.UpdateKeysBackupVersionBody +import org.matrix.android.sdk.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.DELETE +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.PUT +import retrofit2.http.Path +import retrofit2.http.Query + +/** + * Ref: https://github.com/uhoreg/matrix-doc/blob/e2e_backup/proposals/1219-storing-megolm-keys-serverside.md + */ +internal interface RoomKeysApi { + + /* ========================================================================================== + * Backup versions management + * ========================================================================================== */ + + /** + * Create a new keys backup version. + * @param createKeysBackupVersionBody the body + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/version") + fun createKeysBackupVersion(@Body createKeysBackupVersionBody: CreateKeysBackupVersionBody): Call + + /** + * Get the key backup last version + * If not supported by the server, an error is returned: {"errcode":"M_NOT_FOUND","error":"No backup found"} + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/version") + fun getKeysBackupLastVersion(): Call + + /** + * Get information about the given version. + * If not supported by the server, an error is returned: {"errcode":"M_NOT_FOUND","error":"No backup found"} + * + * @param version version + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/version/{version}") + fun getKeysBackupVersion(@Path("version") version: String): Call + + /** + * Update information about the given version. + * @param version version + * @param updateKeysBackupVersionBody the body + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/version/{version}") + fun updateKeysBackupVersion(@Path("version") version: String, + @Body keysBackupVersionBody: UpdateKeysBackupVersionBody): Call + + /* ========================================================================================== + * Storing keys + * ========================================================================================== */ + + /** + * Store the key for the given session in the given room, using the given backup version. + * + * + * If the server already has a backup in the backup version for the given session and room, then it will + * keep the "better" one. To determine which one is "better", key backups are compared first by the is_verified + * flag (true is better than false), then by the first_message_index (a lower number is better), and finally by + * forwarded_count (a lower number is better). + * + * @param roomId the room id + * @param sessionId the session id + * @param version the version of the backup + * @param keyBackupData the data to send + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys/{roomId}/{sessionId}") + fun storeRoomSessionData(@Path("roomId") roomId: String, + @Path("sessionId") sessionId: String, + @Query("version") version: String, + @Body keyBackupData: KeyBackupData): Call + + /** + * Store several keys for the given room, using the given backup version. + * + * @param roomId the room id + * @param version the version of the backup + * @param roomKeysBackupData the data to send + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys/{roomId}") + fun storeRoomSessionsData(@Path("roomId") roomId: String, + @Query("version") version: String, + @Body roomKeysBackupData: RoomKeysBackupData): Call + + /** + * Store several keys, using the given backup version. + * + * @param version the version of the backup + * @param keysBackupData the data to send + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys") + fun storeSessionsData(@Query("version") version: String, + @Body keysBackupData: KeysBackupData): Call + + /* ========================================================================================== + * Retrieving keys + * ========================================================================================== */ + + /** + * Retrieve the key for the given session in the given room from the backup. + * + * @param roomId the room id + * @param sessionId the session id + * @param version the version of the backup, or empty String to retrieve the last version + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys/{roomId}/{sessionId}") + fun getRoomSessionData(@Path("roomId") roomId: String, + @Path("sessionId") sessionId: String, + @Query("version") version: String): Call + + /** + * Retrieve all the keys for the given room from the backup. + * + * @param roomId the room id + * @param version the version of the backup, or empty String to retrieve the last version + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys/{roomId}") + fun getRoomSessionsData(@Path("roomId") roomId: String, + @Query("version") version: String): Call + + /** + * Retrieve all the keys from the backup. + * + * @param version the version of the backup, or empty String to retrieve the last version + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys") + fun getSessionsData(@Query("version") version: String): Call + + /* ========================================================================================== + * Deleting keys + * ========================================================================================== */ + + /** + * Deletes keys from the backup. + */ + @DELETE(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys/{roomId}/{sessionId}") + fun deleteRoomSessionData(@Path("roomId") roomId: String, + @Path("sessionId") sessionId: String, + @Query("version") version: String): Call + + /** + * Deletes keys from the backup. + */ + @DELETE(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys/{roomId}") + fun deleteRoomSessionsData(@Path("roomId") roomId: String, + @Query("version") version: String): Call + + /** + * Deletes keys from the backup. + */ + @DELETE(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/keys") + fun deleteSessionsData(@Query("version") version: String): Call + + /* ========================================================================================== + * Deleting backup + * ========================================================================================== */ + + /** + * Deletes a backup. + */ + @DELETE(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "room_keys/version/{version}") + fun deleteBackup(@Path("version") version: String): Call +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/KeyBackupVersionTrust.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/KeyBackupVersionTrust.kt new file mode 100644 index 0000000000..871874bc9a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/KeyBackupVersionTrust.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.model + +import com.squareup.moshi.JsonClass + +/** + * Data model for response to [KeysBackup.isKeyBackupTrusted()]. + */ +@JsonClass(generateAdapter = true) +data class KeyBackupVersionTrust( + /** + * Flag to indicate if the backup is trusted. + * true if there is a signature that is valid & from a trusted device. + */ + var usable: Boolean = false, + + /** + * Signatures found in the backup version. + */ + var signatures: MutableList = ArrayList() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/KeyBackupVersionTrustSignature.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/KeyBackupVersionTrustSignature.kt new file mode 100644 index 0000000000..955bd5e531 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/KeyBackupVersionTrustSignature.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.model + +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo + +/** + * A signature in a the `KeyBackupVersionTrust` object. + */ +class KeyBackupVersionTrustSignature { + + /** + * The device that signed the backup version. + */ + var device: CryptoDeviceInfo? = null + + /** + *Flag to indicate the signature from this device is valid. + */ + var valid = false +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/KeysBackupVersionTrust.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/KeysBackupVersionTrust.kt new file mode 100644 index 0000000000..a7d23c42dd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/KeysBackupVersionTrust.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.model + +/** + * Data model for response to [KeysBackup.getKeysBackupTrust()]. + */ +data class KeysBackupVersionTrust( + /** + * Flag to indicate if the backup is trusted. + * true if there is a signature that is valid & from a trusted device. + */ + var usable: Boolean = false, + + /** + * Signatures found in the backup version. + */ + var signatures: MutableList = ArrayList() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/KeysBackupVersionTrustSignature.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/KeysBackupVersionTrustSignature.kt new file mode 100644 index 0000000000..8382fff6f2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/KeysBackupVersionTrustSignature.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.model + +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo + +/** + * A signature in a `KeysBackupVersionTrust` object. + */ +class KeysBackupVersionTrustSignature { + + /** + * The id of the device that signed the backup version. + */ + var deviceId: String? = null + + /** + * The device that signed the backup version. + * Can be null if the device is not known. + */ + var device: CryptoDeviceInfo? = null + + /** + * Flag to indicate the signature from this device is valid. + */ + var valid = false +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/MegolmBackupAuthData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/MegolmBackupAuthData.kt new file mode 100644 index 0000000000..aa5629e6d9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/MegolmBackupAuthData.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.di.MoshiProvider + +/** + * Data model for [org.matrix.androidsdk.rest.model.keys.KeysAlgorithmAndData.authData] in case + * of [org.matrix.androidsdk.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP]. + */ +@JsonClass(generateAdapter = true) +data class MegolmBackupAuthData( + /** + * The curve25519 public key used to encrypt the backups. + */ + @Json(name = "public_key") + val publicKey: String = "", + + /** + * In case of a backup created from a password, the salt associated with the backup + * private key. + */ + @Json(name = "private_key_salt") + val privateKeySalt: String? = null, + + /** + * In case of a backup created from a password, the number of key derivations. + */ + @Json(name = "private_key_iterations") + val privateKeyIterations: Int? = null, + + /** + * Signatures of the public key. + * userId -> (deviceSignKeyId -> signature) + */ + @Json(name = "signatures") + val signatures: Map>? = null +) { + + fun toJsonString(): String { + return MoshiProvider.providesMoshi() + .adapter(MegolmBackupAuthData::class.java) + .toJson(this) + } + + /** + * Same as the parent [MXJSONModel JSONDictionary] but return only + * data that must be signed. + */ + fun signalableJSONDictionary(): Map = HashMap().apply { + put("public_key", publicKey) + + privateKeySalt?.let { + put("private_key_salt", it) + } + privateKeyIterations?.let { + put("private_key_iterations", it) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/MegolmBackupCreationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/MegolmBackupCreationInfo.kt new file mode 100644 index 0000000000..25b191e5bd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/MegolmBackupCreationInfo.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.model + +/** + * Data retrieved from Olm library. algorithm and authData will be send to the homeserver, and recoveryKey will be displayed to the user + */ +data class MegolmBackupCreationInfo( + /** + * The algorithm used for storing backups [org.matrix.androidsdk.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP]. + */ + val algorithm: String = "", + + /** + * Authentication data. + */ + val authData: MegolmBackupAuthData? = null, + + /** + * The Base58 recovery key. + */ + val recoveryKey: String = "" +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/BackupKeysResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/BackupKeysResult.kt new file mode 100644 index 0000000000..4903372abd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/BackupKeysResult.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.model.rest + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class BackupKeysResult( + + // The hash value which is an opaque string representing stored keys in the backup + var hash: String? = null, + + // The number of keys stored in the backup. + var count: Int? = null + +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/CreateKeysBackupVersionBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/CreateKeysBackupVersionBody.kt new file mode 100644 index 0000000000..1f493571d3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/CreateKeysBackupVersionBody.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.util.JsonDict + +@JsonClass(generateAdapter = true) +data class CreateKeysBackupVersionBody( + /** + * The algorithm used for storing backups. Currently, only "m.megolm_backup.v1.curve25519-aes-sha2" is defined + */ + @Json(name = "algorithm") + override val algorithm: String? = null, + + /** + * algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2" + * see [org.matrix.android.sdk.internal.crypto.keysbackup.MegolmBackupAuthData] + */ + @Json(name = "auth_data") + override val authData: JsonDict? = null +) : KeysAlgorithmAndData diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeyBackupData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeyBackupData.kt new file mode 100644 index 0000000000..b03d51894c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeyBackupData.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.android.sdk.internal.network.parsing.ForceToBoolean + +/** + * Backup data for one key. + */ +@JsonClass(generateAdapter = true) +data class KeyBackupData( + /** + * Required. The index of the first message in the session that the key can decrypt. + */ + @Json(name = "first_message_index") + val firstMessageIndex: Long = 0, + + /** + * Required. The number of times this key has been forwarded. + */ + @Json(name = "forwarded_count") + val forwardedCount: Int = 0, + + /** + * Whether the device backing up the key has verified the device that the key is from. + * Force to boolean because of https://github.com/matrix-org/synapse/issues/6977 + */ + @ForceToBoolean + @Json(name = "is_verified") + val isVerified: Boolean = false, + + /** + * Algorithm-dependent data. + */ + @Json(name = "session_data") + val sessionData: Map? = null +) { + + fun toJsonString(): String { + return MoshiProvider.providesMoshi().adapter(KeyBackupData::class.java).toJson(this) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysAlgorithmAndData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysAlgorithmAndData.kt new file mode 100644 index 0000000000..99031ca458 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysAlgorithmAndData.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.model.rest + +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupAuthData +import org.matrix.android.sdk.internal.di.MoshiProvider + +/** + *
+ *     Example:
+ *
+ *     {
+ *         "algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
+ *         "auth_data": {
+ *             "public_key": "abcdefg",
+ *             "signatures": {
+ *                 "something": {
+ *                     "ed25519:something": "hijklmnop"
+ *                 }
+ *             }
+ *         }
+ *     }
+ * 
+ */ +interface KeysAlgorithmAndData { + + /** + * The algorithm used for storing backups. Currently, only "m.megolm_backup.v1.curve25519-aes-sha2" is defined + */ + val algorithm: String? + + /** + * algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2" see [org.matrix.android.sdk.internal.crypto.keysbackup.MegolmBackupAuthData] + */ + val authData: JsonDict? + + /** + * Facility method to convert authData to a MegolmBackupAuthData object + */ + fun getAuthDataAsMegolmBackupAuthData(): MegolmBackupAuthData? { + return MoshiProvider.providesMoshi() + .adapter(MegolmBackupAuthData::class.java) + .fromJsonValue(authData) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysBackupData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysBackupData.kt new file mode 100644 index 0000000000..34c5d1c531 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysBackupData.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Backup data for several keys in several rooms. + */ +@JsonClass(generateAdapter = true) +data class KeysBackupData( + // the keys are the room IDs, and the values are RoomKeysBackupData + @Json(name = "rooms") + val roomIdToRoomKeysBackupData: MutableMap = HashMap() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysVersion.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysVersion.kt new file mode 100644 index 0000000000..3ca8df3131 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysVersion.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.model.rest + +data class KeysVersion( + // the keys backup version + var version: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysVersionResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysVersionResult.kt new file mode 100644 index 0000000000..fd5d926871 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysVersionResult.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.util.JsonDict + +@JsonClass(generateAdapter = true) +data class KeysVersionResult( + /** + * The algorithm used for storing backups. Currently, only "m.megolm_backup.v1.curve25519-aes-sha2" is defined + */ + @Json(name = "algorithm") + override val algorithm: String? = null, + + /** + * algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2" + * see [org.matrix.android.sdk.internal.crypto.keysbackup.MegolmBackupAuthData] + */ + @Json(name = "auth_data") + override val authData: JsonDict? = null, + + // the backup version + @Json(name = "version") + val version: String? = null, + + // The hash value which is an opaque string representing stored keys in the backup + @Json(name = "hash") + val hash: String? = null, + + // The number of keys stored in the backup. + @Json(name = "count") + val count: Int? = null +) : KeysAlgorithmAndData diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/RoomKeysBackupData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/RoomKeysBackupData.kt new file mode 100644 index 0000000000..7564e54fc0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/RoomKeysBackupData.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Backup data for several keys within a room. + */ +@JsonClass(generateAdapter = true) +data class RoomKeysBackupData( + // the keys are the session IDs, and the values are KeyBackupData + @Json(name = "sessions") + val sessionIdToKeyBackupData: MutableMap = HashMap() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/UpdateKeysBackupVersionBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/UpdateKeysBackupVersionBody.kt new file mode 100644 index 0000000000..6de374d380 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/UpdateKeysBackupVersionBody.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.util.JsonDict + +@JsonClass(generateAdapter = true) +data class UpdateKeysBackupVersionBody( + /** + * The algorithm used for storing backups. Currently, only "m.megolm_backup.v1.curve25519-aes-sha2" is defined + */ + @Json(name = "algorithm") + override val algorithm: String? = null, + + /** + * algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2" + * see [org.matrix.android.sdk.internal.crypto.keysbackup.MegolmBackupAuthData] + */ + @Json(name = "auth_data") + override val authData: JsonDict? = null, + + // the backup version, mandatory + @Json(name = "version") + val version: String +) : KeysAlgorithmAndData diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/CreateKeysBackupVersionTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/CreateKeysBackupVersionTask.kt new file mode 100644 index 0000000000..3b11e91716 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/CreateKeysBackupVersionTask.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.tasks + +import org.matrix.android.sdk.internal.crypto.keysbackup.api.RoomKeysApi +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.CreateKeysBackupVersionBody +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface CreateKeysBackupVersionTask : Task + +internal class DefaultCreateKeysBackupVersionTask @Inject constructor( + private val roomKeysApi: RoomKeysApi, + private val eventBus: EventBus +) : CreateKeysBackupVersionTask { + + override suspend fun execute(params: CreateKeysBackupVersionBody): KeysVersion { + return executeRequest(eventBus) { + apiCall = roomKeysApi.createKeysBackupVersion(params) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteBackupTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteBackupTask.kt new file mode 100644 index 0000000000..25417ef4fe --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteBackupTask.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.tasks + +import org.matrix.android.sdk.internal.crypto.keysbackup.api.RoomKeysApi +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface DeleteBackupTask : Task { + data class Params( + val version: String + ) +} + +internal class DefaultDeleteBackupTask @Inject constructor( + private val roomKeysApi: RoomKeysApi, + private val eventBus: EventBus +) : DeleteBackupTask { + + override suspend fun execute(params: DeleteBackupTask.Params) { + return executeRequest(eventBus) { + apiCall = roomKeysApi.deleteBackup(params.version) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteRoomSessionDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteRoomSessionDataTask.kt new file mode 100644 index 0000000000..12042f6459 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteRoomSessionDataTask.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.tasks + +import org.matrix.android.sdk.internal.crypto.keysbackup.api.RoomKeysApi +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface DeleteRoomSessionDataTask : Task { + data class Params( + val roomId: String, + val sessionId: String, + val version: String + ) +} + +internal class DefaultDeleteRoomSessionDataTask @Inject constructor( + private val roomKeysApi: RoomKeysApi, + private val eventBus: EventBus +) : DeleteRoomSessionDataTask { + + override suspend fun execute(params: DeleteRoomSessionDataTask.Params) { + return executeRequest(eventBus) { + apiCall = roomKeysApi.deleteRoomSessionData( + params.roomId, + params.sessionId, + params.version) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteRoomSessionsDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteRoomSessionsDataTask.kt new file mode 100644 index 0000000000..92e5153d41 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteRoomSessionsDataTask.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.tasks + +import org.matrix.android.sdk.internal.crypto.keysbackup.api.RoomKeysApi +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface DeleteRoomSessionsDataTask : Task { + data class Params( + val roomId: String, + val version: String + ) +} + +internal class DefaultDeleteRoomSessionsDataTask @Inject constructor( + private val roomKeysApi: RoomKeysApi, + private val eventBus: EventBus +) : DeleteRoomSessionsDataTask { + + override suspend fun execute(params: DeleteRoomSessionsDataTask.Params) { + return executeRequest(eventBus) { + apiCall = roomKeysApi.deleteRoomSessionsData( + params.roomId, + params.version) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteSessionsDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteSessionsDataTask.kt new file mode 100644 index 0000000000..66e1fa0203 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/DeleteSessionsDataTask.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.tasks + +import org.matrix.android.sdk.internal.crypto.keysbackup.api.RoomKeysApi +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface DeleteSessionsDataTask : Task { + data class Params( + val version: String + ) +} + +internal class DefaultDeleteSessionsDataTask @Inject constructor( + private val roomKeysApi: RoomKeysApi, + private val eventBus: EventBus +) : DeleteSessionsDataTask { + + override suspend fun execute(params: DeleteSessionsDataTask.Params) { + return executeRequest(eventBus) { + apiCall = roomKeysApi.deleteSessionsData(params.version) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetKeysBackupLastVersionTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetKeysBackupLastVersionTask.kt new file mode 100644 index 0000000000..afd0e85f59 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetKeysBackupLastVersionTask.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.tasks + +import org.matrix.android.sdk.internal.crypto.keysbackup.api.RoomKeysApi +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface GetKeysBackupLastVersionTask : Task + +internal class DefaultGetKeysBackupLastVersionTask @Inject constructor( + private val roomKeysApi: RoomKeysApi, + private val eventBus: EventBus +) : GetKeysBackupLastVersionTask { + + override suspend fun execute(params: Unit): KeysVersionResult { + return executeRequest(eventBus) { + apiCall = roomKeysApi.getKeysBackupLastVersion() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetKeysBackupVersionTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetKeysBackupVersionTask.kt new file mode 100644 index 0000000000..b454a83b89 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetKeysBackupVersionTask.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.tasks + +import org.matrix.android.sdk.internal.crypto.keysbackup.api.RoomKeysApi +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface GetKeysBackupVersionTask : Task + +internal class DefaultGetKeysBackupVersionTask @Inject constructor( + private val roomKeysApi: RoomKeysApi, + private val eventBus: EventBus +) : GetKeysBackupVersionTask { + + override suspend fun execute(params: String): KeysVersionResult { + return executeRequest(eventBus) { + apiCall = roomKeysApi.getKeysBackupVersion(params) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetRoomSessionDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetRoomSessionDataTask.kt new file mode 100644 index 0000000000..5c5d3c3afa --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetRoomSessionDataTask.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.tasks + +import org.matrix.android.sdk.internal.crypto.keysbackup.api.RoomKeysApi +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeyBackupData +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface GetRoomSessionDataTask : Task { + data class Params( + val roomId: String, + val sessionId: String, + val version: String + ) +} + +internal class DefaultGetRoomSessionDataTask @Inject constructor( + private val roomKeysApi: RoomKeysApi, + private val eventBus: EventBus +) : GetRoomSessionDataTask { + + override suspend fun execute(params: GetRoomSessionDataTask.Params): KeyBackupData { + return executeRequest(eventBus) { + apiCall = roomKeysApi.getRoomSessionData( + params.roomId, + params.sessionId, + params.version) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetRoomSessionsDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetRoomSessionsDataTask.kt new file mode 100644 index 0000000000..d8b49d49d4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetRoomSessionsDataTask.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.tasks + +import org.matrix.android.sdk.internal.crypto.keysbackup.api.RoomKeysApi +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.RoomKeysBackupData +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface GetRoomSessionsDataTask : Task { + data class Params( + val roomId: String, + val version: String + ) +} + +internal class DefaultGetRoomSessionsDataTask @Inject constructor( + private val roomKeysApi: RoomKeysApi, + private val eventBus: EventBus +) : GetRoomSessionsDataTask { + + override suspend fun execute(params: GetRoomSessionsDataTask.Params): RoomKeysBackupData { + return executeRequest(eventBus) { + apiCall = roomKeysApi.getRoomSessionsData( + params.roomId, + params.version) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetSessionsDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetSessionsDataTask.kt new file mode 100644 index 0000000000..c0a05eaff9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetSessionsDataTask.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.tasks + +import org.matrix.android.sdk.internal.crypto.keysbackup.api.RoomKeysApi +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysBackupData +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface GetSessionsDataTask : Task { + data class Params( + val version: String + ) +} + +internal class DefaultGetSessionsDataTask @Inject constructor( + private val roomKeysApi: RoomKeysApi, + private val eventBus: EventBus +) : GetSessionsDataTask { + + override suspend fun execute(params: GetSessionsDataTask.Params): KeysBackupData { + return executeRequest(eventBus) { + apiCall = roomKeysApi.getSessionsData(params.version) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreRoomSessionDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreRoomSessionDataTask.kt new file mode 100644 index 0000000000..31a464dc38 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreRoomSessionDataTask.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.tasks + +import org.matrix.android.sdk.internal.crypto.keysbackup.api.RoomKeysApi +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.BackupKeysResult +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeyBackupData +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface StoreRoomSessionDataTask : Task { + data class Params( + val roomId: String, + val sessionId: String, + val version: String, + val keyBackupData: KeyBackupData + ) +} + +internal class DefaultStoreRoomSessionDataTask @Inject constructor( + private val roomKeysApi: RoomKeysApi, + private val eventBus: EventBus +) : StoreRoomSessionDataTask { + + override suspend fun execute(params: StoreRoomSessionDataTask.Params): BackupKeysResult { + return executeRequest(eventBus) { + apiCall = roomKeysApi.storeRoomSessionData( + params.roomId, + params.sessionId, + params.version, + params.keyBackupData) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreRoomSessionsDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreRoomSessionsDataTask.kt new file mode 100644 index 0000000000..057198aaf9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreRoomSessionsDataTask.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.tasks + +import org.matrix.android.sdk.internal.crypto.keysbackup.api.RoomKeysApi +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.BackupKeysResult +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.RoomKeysBackupData +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface StoreRoomSessionsDataTask : Task { + data class Params( + val roomId: String, + val version: String, + val roomKeysBackupData: RoomKeysBackupData + ) +} + +internal class DefaultStoreRoomSessionsDataTask @Inject constructor( + private val roomKeysApi: RoomKeysApi, + private val eventBus: EventBus +) : StoreRoomSessionsDataTask { + + override suspend fun execute(params: StoreRoomSessionsDataTask.Params): BackupKeysResult { + return executeRequest(eventBus) { + apiCall = roomKeysApi.storeRoomSessionsData( + params.roomId, + params.version, + params.roomKeysBackupData) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreSessionsDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreSessionsDataTask.kt new file mode 100644 index 0000000000..33f6a0862d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreSessionsDataTask.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.tasks + +import org.matrix.android.sdk.internal.crypto.keysbackup.api.RoomKeysApi +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.BackupKeysResult +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysBackupData +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface StoreSessionsDataTask : Task { + data class Params( + val version: String, + val keysBackupData: KeysBackupData + ) +} + +internal class DefaultStoreSessionsDataTask @Inject constructor( + private val roomKeysApi: RoomKeysApi, + private val eventBus: EventBus +) : StoreSessionsDataTask { + + override suspend fun execute(params: StoreSessionsDataTask.Params): BackupKeysResult { + return executeRequest(eventBus) { + apiCall = roomKeysApi.storeSessionsData( + params.version, + params.keysBackupData) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/UpdateKeysBackupVersionTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/UpdateKeysBackupVersionTask.kt new file mode 100644 index 0000000000..68725b1eb1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/UpdateKeysBackupVersionTask.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.tasks + +import org.matrix.android.sdk.internal.crypto.keysbackup.api.RoomKeysApi +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.UpdateKeysBackupVersionBody +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface UpdateKeysBackupVersionTask : Task { + data class Params( + val version: String, + val keysBackupVersionBody: UpdateKeysBackupVersionBody + ) +} + +internal class DefaultUpdateKeysBackupVersionTask @Inject constructor( + private val roomKeysApi: RoomKeysApi, + private val eventBus: EventBus +) : UpdateKeysBackupVersionTask { + + override suspend fun execute(params: UpdateKeysBackupVersionTask.Params) { + return executeRequest(eventBus) { + apiCall = roomKeysApi.updateKeysBackupVersion(params.version, params.keysBackupVersionBody) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/Base58.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/Base58.kt new file mode 100644 index 0000000000..adbcd18d12 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/Base58.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2011 Google Inc. + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.util + +import java.math.BigInteger + +/** + * Ref: https://github.com/bitcoin-labs/bitcoin-mobile-android/blob/master/src/bitcoinj/java/com/google/bitcoin/core/Base58.java + * + * + * A custom form of base58 is used to encode BitCoin addresses. Note that this is not the same base58 as used by + * Flickr, which you may see reference to around the internet. + * + * Satoshi says: why base-58 instead of standard base-64 encoding? + * + * * Don't want 0OIl characters that look the same in some fonts and + * could be used to create visually identical looking account numbers. + * * A string with non-alphanumeric characters is not as easily accepted as an account number. + * * E-mail usually won't line-break if there's no punctuation to break at. + * * Doubleclicking selects the whole number as one word if it's all alphanumeric. + * + */ +private const val ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" +private val BASE = BigInteger.valueOf(58) + +/** + * Encode a byte array to a human readable string with base58 chars + */ +fun base58encode(input: ByteArray): String { + var bi = BigInteger(1, input) + val s = StringBuffer() + while (bi >= BASE) { + val mod = bi.mod(BASE) + s.insert(0, ALPHABET[mod.toInt()]) + bi = bi.subtract(mod).divide(BASE) + } + s.insert(0, ALPHABET[bi.toInt()]) + // Convert leading zeros too. + for (anInput in input) { + if (anInput.toInt() == 0) { + s.insert(0, ALPHABET[0]) + } else { + break + } + } + return s.toString() +} + +/** + * Decode a base58 String to a byte array + */ +fun base58decode(input: String): ByteArray { + var result = decodeToBigInteger(input).toByteArray() + + // Remove the first leading zero if any + if (result[0] == 0.toByte()) { + result = result.copyOfRange(1, result.size) + } + + return result +} + +private fun decodeToBigInteger(input: String): BigInteger { + var bi = BigInteger.valueOf(0) + // Work backwards through the string. + for (i in input.length - 1 downTo 0) { + val alphaIndex = ALPHABET.indexOf(input[i]) + bi = bi.add(BigInteger.valueOf(alphaIndex.toLong()).multiply(BASE.pow(input.length - 1 - i))) + } + return bi +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/RecoveryKey.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/RecoveryKey.kt new file mode 100644 index 0000000000..78697ca9ce --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/RecoveryKey.kt @@ -0,0 +1,120 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.util + +import kotlin.experimental.xor + +/** + * See https://github.com/uhoreg/matrix-doc/blob/e2e_backup/proposals/1219-storing-megolm-keys-serverside.md + */ + +private const val CHAR_0 = 0x8B.toByte() +private const val CHAR_1 = 0x01.toByte() + +private const val RECOVERY_KEY_LENGTH = 2 + 32 + 1 + +/** + * Tell if the format of the recovery key is correct + * + * @param recoveryKey + * @return true if the format of the recovery key is correct + */ +fun isValidRecoveryKey(recoveryKey: String?): Boolean { + return extractCurveKeyFromRecoveryKey(recoveryKey) != null +} + +/** + * Compute recovery key from curve25519 key + * + * @param curve25519Key + * @return the recovery key + */ +fun computeRecoveryKey(curve25519Key: ByteArray): String { + // Append header and parity + val data = ByteArray(curve25519Key.size + 3) + + // Header + data[0] = CHAR_0 + data[1] = CHAR_1 + + // Copy key and compute parity + var parity: Byte = CHAR_0 xor CHAR_1 + + for (i in curve25519Key.indices) { + data[i + 2] = curve25519Key[i] + parity = parity xor curve25519Key[i] + } + + // Parity + data[curve25519Key.size + 2] = parity + + // Do not add white space every 4 chars, it's up to the presenter to do it + return base58encode(data) +} + +/** + * Please call [.isValidRecoveryKey] and ensure it returns true before calling this method + * + * @param recoveryKey the recovery key + * @return curveKey, or null in case of error + */ +fun extractCurveKeyFromRecoveryKey(recoveryKey: String?): ByteArray? { + if (recoveryKey == null) { + return null + } + + // Remove any space + val spaceFreeRecoveryKey = recoveryKey.replace("""\s""".toRegex(), "") + + val b58DecodedKey = base58decode(spaceFreeRecoveryKey) + + // Check length + if (b58DecodedKey.size != RECOVERY_KEY_LENGTH) { + return null + } + + // Check first byte + if (b58DecodedKey[0] != CHAR_0) { + return null + } + + // Check second byte + if (b58DecodedKey[1] != CHAR_1) { + return null + } + + // Check parity + var parity: Byte = 0 + + for (i in 0 until RECOVERY_KEY_LENGTH) { + parity = parity xor b58DecodedKey[i] + } + + if (parity != 0.toByte()) { + return null + } + + // Remove header and parity bytes + val result = ByteArray(b58DecodedKey.size - 3) + + for (i in 2 until b58DecodedKey.size - 1) { + result[i - 2] = b58DecodedKey[i] + } + + return result +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoCrossSigningKey.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoCrossSigningKey.kt new file mode 100644 index 0000000000..168258acd2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoCrossSigningKey.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.model + +import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel +import org.matrix.android.sdk.internal.crypto.model.rest.RestKeyInfo + +data class CryptoCrossSigningKey( + override val userId: String, + + val usages: List?, + + override val keys: Map, + + override val signatures: Map>?, + + var trustLevel: DeviceTrustLevel? = null +) : CryptoInfo { + + override fun signalableJSONDictionary(): Map { + val map = HashMap() + userId.let { map["user_id"] = it } + usages?.let { map["usage"] = it } + keys.let { map["keys"] = it } + + return map + } + + val unpaddedBase64PublicKey: String? = keys.values.firstOrNull() + + val isMasterKey = usages?.contains(KeyUsage.MASTER.value) ?: false + val isSelfSigningKey = usages?.contains(KeyUsage.SELF_SIGNING.value) ?: false + val isUserKey = usages?.contains(KeyUsage.USER_SIGNING.value) ?: false + + fun addSignatureAndCopy(userId: String, signedWithNoPrefix: String, signature: String): CryptoCrossSigningKey { + val updated = (signatures?.toMutableMap() ?: HashMap()) + val userMap = updated[userId]?.toMutableMap() + ?: HashMap().also { updated[userId] = it } + userMap["ed25519:$signedWithNoPrefix"] = signature + + return this.copy( + signatures = updated + ) + } + + fun copyForSignature(userId: String, signedWithNoPrefix: String, signature: String): CryptoCrossSigningKey { + return this.copy( + signatures = mapOf(userId to mapOf("ed25519:$signedWithNoPrefix" to signature)) + ) + } + + data class Builder( + val userId: String, + val usage: KeyUsage, + private var base64Pkey: String? = null, + private val signatures: ArrayList> = ArrayList() + ) { + + fun key(publicKeyBase64: String) = apply { + base64Pkey = publicKeyBase64 + } + + fun signature(userId: String, keySignedBase64: String, base64Signature: String) = apply { + signatures.add(Triple(userId, keySignedBase64, base64Signature)) + } + + fun build(): CryptoCrossSigningKey { + val b64key = base64Pkey ?: throw IllegalArgumentException("") + + val signMap = HashMap>() + signatures.forEach { info -> + val uMap = signMap[info.first] + ?: HashMap().also { signMap[info.first] = it } + uMap["ed25519:${info.second}"] = info.third + } + + return CryptoCrossSigningKey( + userId = userId, + usages = listOf(usage.value), + keys = mapOf("ed25519:$b64key" to b64key), + signatures = signMap) + } + } +} + +enum class KeyUsage(val value: String) { + MASTER("master"), + SELF_SIGNING("self_signing"), + USER_SIGNING("user_signing") +} + +internal fun CryptoCrossSigningKey.toRest(): RestKeyInfo { + return CryptoInfoMapper.map(this) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoDeviceInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoDeviceInfo.kt new file mode 100644 index 0000000000..b4bba72718 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoDeviceInfo.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.model + +import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel +import org.matrix.android.sdk.internal.crypto.model.rest.DeviceKeys +import org.matrix.android.sdk.internal.crypto.model.rest.UnsignedDeviceInfo +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMapper +import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity + +data class CryptoDeviceInfo( + val deviceId: String, + override val userId: String, + var algorithms: List? = null, + override val keys: Map? = null, + override val signatures: Map>? = null, + val unsigned: UnsignedDeviceInfo? = null, + var trustLevel: DeviceTrustLevel? = null, + var isBlocked: Boolean = false, + val firstTimeSeenLocalTs: Long? = null +) : CryptoInfo { + + val isVerified: Boolean + get() = trustLevel?.isVerified() == true + + val isUnknown: Boolean + get() = trustLevel == null + + /** + * @return the fingerprint + */ + fun fingerprint(): String? { + return keys + ?.takeIf { !deviceId.isBlank() } + ?.get("ed25519:$deviceId") + } + + /** + * @return the identity key + */ + fun identityKey(): String? { + return keys + ?.takeIf { !deviceId.isBlank() } + ?.get("curve25519:$deviceId") + } + + /** + * @return the display name + */ + fun displayName(): String? { + return unsigned?.deviceDisplayName + } + + override fun signalableJSONDictionary(): Map { + val map = HashMap() + map["device_id"] = deviceId + map["user_id"] = userId + algorithms?.let { map["algorithms"] = it } + keys?.let { map["keys"] = it } + return map + } +} + +internal fun CryptoDeviceInfo.toRest(): DeviceKeys { + return CryptoInfoMapper.map(this) +} + +internal fun CryptoDeviceInfo.toEntity(): DeviceInfoEntity { + return CryptoMapper.mapToEntity(this) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoInfo.kt new file mode 100644 index 0000000000..116205dce4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoInfo.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.model + +/** + * Generic crypto info. + * Can be a device (CryptoDeviceInfo), as well as a CryptoCrossSigningInfo (can be seen as a kind of virtual device) + */ +interface CryptoInfo { + + val userId: String + + val keys: Map? + + val signatures: Map>? + + fun signalableJSONDictionary(): Map +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoInfoMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoInfoMapper.kt new file mode 100644 index 0000000000..ead1dd5457 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoInfoMapper.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.model + +import org.matrix.android.sdk.internal.crypto.model.rest.DeviceKeys +import org.matrix.android.sdk.internal.crypto.model.rest.DeviceKeysWithUnsigned +import org.matrix.android.sdk.internal.crypto.model.rest.RestKeyInfo + +internal object CryptoInfoMapper { + + fun map(deviceKeysWithUnsigned: DeviceKeysWithUnsigned): CryptoDeviceInfo { + return CryptoDeviceInfo( + deviceId = deviceKeysWithUnsigned.deviceId, + userId = deviceKeysWithUnsigned.userId, + algorithms = deviceKeysWithUnsigned.algorithms, + keys = deviceKeysWithUnsigned.keys, + signatures = deviceKeysWithUnsigned.signatures, + unsigned = deviceKeysWithUnsigned.unsigned, + trustLevel = null + ) + } + + fun map(cryptoDeviceInfo: CryptoDeviceInfo): DeviceKeys { + return DeviceKeys( + deviceId = cryptoDeviceInfo.deviceId, + algorithms = cryptoDeviceInfo.algorithms, + keys = cryptoDeviceInfo.keys, + signatures = cryptoDeviceInfo.signatures, + userId = cryptoDeviceInfo.userId + ) + } + + fun map(keyInfo: RestKeyInfo): CryptoCrossSigningKey { + return CryptoCrossSigningKey( + userId = keyInfo.userId, + usages = keyInfo.usages, + keys = keyInfo.keys.orEmpty(), + signatures = keyInfo.signatures, + trustLevel = null + ) + } + + fun map(keyInfo: CryptoCrossSigningKey): RestKeyInfo { + return RestKeyInfo( + userId = keyInfo.userId, + usages = keyInfo.usages, + keys = keyInfo.keys, + signatures = keyInfo.signatures + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/ImportRoomKeysResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/ImportRoomKeysResult.kt new file mode 100644 index 0000000000..0ecda951df --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/ImportRoomKeysResult.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.model + +data class ImportRoomKeysResult(val totalNumberOfKeys: Int, + val successfullyNumberOfImportedKeys: Int) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXDeviceInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXDeviceInfo.kt new file mode 100755 index 0000000000..1733cc3913 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXDeviceInfo.kt @@ -0,0 +1,180 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.crypto.model.rest.DeviceKeys +import java.io.Serializable + +@JsonClass(generateAdapter = true) +data class MXDeviceInfo( + /** + * The id of this device. + */ + @Json(name = "device_id") + val deviceId: String, + + /** + * the user id + */ + @Json(name = "user_id") + val userId: String, + + /** + * The list of algorithms supported by this device. + */ + @Json(name = "algorithms") + val algorithms: List? = null, + + /** + * A map from ":" to "". + */ + @Json(name = "keys") + val keys: Map? = null, + + /** + * The signature of this MXDeviceInfo. + * A map from "" to a map from ":" to "" + */ + @Json(name = "signatures") + val signatures: Map>? = null, + + /* + * Additional data from the home server. + */ + @Json(name = "unsigned") + val unsigned: JsonDict? = null, + + /** + * Verification state of this device. + */ + val verified: Int = DEVICE_VERIFICATION_UNKNOWN +) : Serializable { + /** + * Tells if the device is unknown + * + * @return true if the device is unknown + */ + val isUnknown: Boolean + get() = verified == DEVICE_VERIFICATION_UNKNOWN + + /** + * Tells if the device is verified. + * + * @return true if the device is verified + */ + val isVerified: Boolean + get() = verified == DEVICE_VERIFICATION_VERIFIED + + /** + * Tells if the device is unverified. + * + * @return true if the device is unverified + */ + val isUnverified: Boolean + get() = verified == DEVICE_VERIFICATION_UNVERIFIED + + /** + * Tells if the device is blocked. + * + * @return true if the device is blocked + */ + val isBlocked: Boolean + get() = verified == DEVICE_VERIFICATION_BLOCKED + + /** + * @return the fingerprint + */ + fun fingerprint(): String? { + return keys + ?.takeIf { !deviceId.isBlank() } + ?.get("ed25519:$deviceId") + } + + /** + * @return the identity key + */ + fun identityKey(): String? { + return keys + ?.takeIf { !deviceId.isBlank() } + ?.get("curve25519:$deviceId") + } + + /** + * @return the display name + */ + fun displayName(): String? { + return unsigned?.get("device_display_name") as? String + } + + /** + * @return the signed data map + */ + fun signalableJSONDictionary(): Map { + val map = HashMap() + + map["device_id"] = deviceId + + map["user_id"] = userId + + if (null != algorithms) { + map["algorithms"] = algorithms + } + + if (null != keys) { + map["keys"] = keys + } + + return map + } + + /** + * @return a dictionary of the parameters + */ + fun toDeviceKeys(): DeviceKeys { + return DeviceKeys( + userId = userId, + deviceId = deviceId, + algorithms = algorithms!!, + keys = keys!!, + signatures = signatures!! + ) + } + + override fun toString(): String { + return "MXDeviceInfo $userId:$deviceId" + } + + companion object { + // This device is a new device and the user was not warned it has been added. + const val DEVICE_VERIFICATION_UNKNOWN = -1 + + // The user has not yet verified this device. + const val DEVICE_VERIFICATION_UNVERIFIED = 0 + + // The user has verified this device. + const val DEVICE_VERIFICATION_VERIFIED = 1 + + // The user has blocked this device. + const val DEVICE_VERIFICATION_BLOCKED = 2 + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXEncryptEventContentResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXEncryptEventContentResult.kt new file mode 100755 index 0000000000..0f0b289bc6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXEncryptEventContentResult.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.model + +import org.matrix.android.sdk.api.session.events.model.Content + +data class MXEncryptEventContentResult( + /** + * The encrypted event content + */ + val eventContent: Content, + /** + * the event type + */ + val eventType: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXKey.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXKey.kt new file mode 100755 index 0000000000..8808c83985 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXKey.kt @@ -0,0 +1,123 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.model + +import org.matrix.android.sdk.api.util.JsonDict +import timber.log.Timber + +data class MXKey( + /** + * The type of the key (in the example: "signed_curve25519"). + */ + val type: String, + + /** + * The id of the key (in the example: "AAAAFw"). + */ + private val keyId: String, + + /** + * The key (in the example: "IjwIcskng7YjYcn0tS8TUOT2OHHtBSfMpcfIczCgXj4"). + */ + val value: String, + + /** + * signature user Id to [deviceid][signature] + */ + private val signatures: Map> +) { + + /** + * @return the signed data map + */ + fun signalableJSONDictionary(): Map { + return mapOf("key" to value) + } + + /** + * Returns a signature for an user Id and a signkey + * + * @param userId the user id + * @param signkey the sign key + * @return the signature + */ + fun signatureForUserId(userId: String, signkey: String): String? { + // sanity checks + if (userId.isNotBlank() && signkey.isNotBlank()) { + return signatures[userId]?.get(signkey) + } + + return null + } + + companion object { + /** + * Key types. + */ + const val KEY_CURVE_25519_TYPE = "curve25519" + const val KEY_SIGNED_CURVE_25519_TYPE = "signed_curve25519" + // const val KEY_ED_25519_TYPE = "ed25519" + + /** + * Convert a map to a MXKey + * + * @param map the map to convert + * + * Json Example: + * + *
+         *   "signed_curve25519:AAAAFw": {
+         *     "key": "IjwIcskng7YjYcn0tS8TUOT2OHHtBSfMpcfIczCgXj4",
+         *     "signatures": {
+         *       "@userId:matrix.org": {
+         *         "ed25519:GMJRREOASV": "EUjp6pXzK9u3SDFR\/qLbzpOi3bEREeI6qMnKzXu992HsfuDDZftfJfiUXv9b\/Hqq1og4qM\/vCQJGTHAWMmgkCg"
+         *       }
+         *     }
+         *   }
+         * 
+ * + * into several val members + */ + fun from(map: Map?): MXKey? { + if (map?.isNotEmpty() == true) { + val firstKey = map.keys.first() + + val components = firstKey.split(":").dropLastWhile { it.isEmpty() } + + if (components.size == 2) { + val params = map[firstKey] + if (params != null) { + if (params["key"] is String) { + @Suppress("UNCHECKED_CAST") + return MXKey( + type = components[0], + keyId = components[1], + value = params["key"] as String, + signatures = params["signatures"] as Map> + ) + } + } + } + } + + // Error case + Timber.e("## Unable to parse map") + return null + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXOlmSessionResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXOlmSessionResult.kt new file mode 100755 index 0000000000..30f4a6bba1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXOlmSessionResult.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.model + +import java.io.Serializable + +data class MXOlmSessionResult( + /** + * the device + */ + val deviceInfo: CryptoDeviceInfo, + /** + * Base64 olm session id. + * null if no session could be established. + */ + var sessionId: String?) : Serializable diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXQueuedEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXQueuedEncryption.kt new file mode 100755 index 0000000000..598f16bdf3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXQueuedEncryption.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.model + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.events.model.Content + +class MXQueuedEncryption { + + /** + * The data to encrypt. + */ + var eventContent: Content? = null + var eventType: String? = null + + /** + * the asynchronous callback + */ + var apiCallback: MatrixCallback? = null +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXUsersDevicesMap.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXUsersDevicesMap.kt new file mode 100755 index 0000000000..aa0d9a2e6d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXUsersDevicesMap.kt @@ -0,0 +1,132 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.model + +class MXUsersDevicesMap { + + // A map of maps (userId -> (deviceId -> Object)). + val map = HashMap>() + + /** + * @return the user Ids + */ + val userIds: List + get() = map.keys.toList() + + val isEmpty: Boolean + get() = map.isEmpty() + + /** + * Provides the device ids list for a user id + * FIXME Should maybe return emptyList and not null, to avoid many !! in the code + * + * @param userId the user id + * @return the device ids list + */ + fun getUserDeviceIds(userId: String?): List? { + return if (!userId.isNullOrBlank() && map.containsKey(userId)) { + map[userId]!!.keys.toList() + } else null + } + + /** + * Provides the object for a device id and a user Id + * + * @param deviceId the device id + * @param userId the object id + * @return the object + */ + fun getObject(userId: String?, deviceId: String?): E? { + return if (!userId.isNullOrBlank() && !deviceId.isNullOrBlank()) { + map[userId]?.get(deviceId) + } else null + } + + /** + * Set an object for a dedicated user Id and device Id + * + * @param userId the user Id + * @param deviceId the device id + * @param o the object to set + */ + fun setObject(userId: String?, deviceId: String?, o: E?) { + if (null != o && userId?.isNotBlank() == true && deviceId?.isNotBlank() == true) { + val devices = map.getOrPut(userId) { HashMap() } + devices[deviceId] = o + } + } + + /** + * Defines the objects map for a user Id + * + * @param objectsPerDevices the objects maps + * @param userId the user id + */ + fun setObjects(userId: String?, objectsPerDevices: Map?) { + if (!userId.isNullOrBlank()) { + if (null == objectsPerDevices) { + map.remove(userId) + } else { + map[userId] = HashMap(objectsPerDevices) + } + } + } + + /** + * Removes objects for a dedicated user + * + * @param userId the user id. + */ + fun removeUserObjects(userId: String?) { + if (!userId.isNullOrBlank()) { + map.remove(userId) + } + } + + /** + * Clear the internal dictionary + */ + fun removeAllObjects() { + map.clear() + } + + /** + * Add entries from another MXUsersDevicesMap + * + * @param other the other one + */ + fun addEntriesFromMap(other: MXUsersDevicesMap?) { + if (null != other) { + map.putAll(other.map) + } + } + + override fun toString(): String { + return "MXUsersDevicesMap $map" + } +} + +inline fun MXUsersDevicesMap.forEach(action: (String, String, T) -> Unit) { + userIds.forEach { userId -> + getUserDeviceIds(userId)?.forEach { deviceId -> + getObject(userId, deviceId)?.let { + action(userId, deviceId, it) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmInboundGroupSessionWrapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmInboundGroupSessionWrapper.kt new file mode 100755 index 0000000000..1621db380d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmInboundGroupSessionWrapper.kt @@ -0,0 +1,153 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.model + +import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.internal.crypto.MegolmSessionData +import org.matrix.olm.OlmInboundGroupSession +import timber.log.Timber +import java.io.Serializable + +/** + * This class adds more context to a OlmInboundGroupSession object. + * This allows additional checks. The class implements Serializable so that the context can be stored. + */ +class OlmInboundGroupSessionWrapper : Serializable { + + // The associated olm inbound group session. + var olmInboundGroupSession: OlmInboundGroupSession? = null + + // The room in which this session is used. + var roomId: String? = null + + // The base64-encoded curve25519 key of the sender. + var senderKey: String? = null + + // Other keys the sender claims. + var keysClaimed: Map? = null + + // Devices which forwarded this session to us (normally empty). + var forwardingCurve25519KeyChain: List? = ArrayList() + + /** + * @return the first known message index + */ + val firstKnownIndex: Long? + get() { + if (null != olmInboundGroupSession) { + try { + return olmInboundGroupSession!!.firstKnownIndex + } catch (e: Exception) { + Timber.e(e, "## getFirstKnownIndex() : getFirstKnownIndex failed") + } + } + + return null + } + + /** + * Constructor + * + * @param sessionKey the session key + * @param isImported true if it is an imported session key + */ + constructor(sessionKey: String, isImported: Boolean) { + try { + if (!isImported) { + olmInboundGroupSession = OlmInboundGroupSession(sessionKey) + } else { + olmInboundGroupSession = OlmInboundGroupSession.importSession(sessionKey) + } + } catch (e: Exception) { + Timber.e(e, "Cannot create") + } + } + + /** + * Create a new instance from the provided keys map. + * + * @param megolmSessionData the megolm session data + * @throws Exception if the data are invalid + */ + @Throws(Exception::class) + constructor(megolmSessionData: MegolmSessionData) { + try { + olmInboundGroupSession = OlmInboundGroupSession.importSession(megolmSessionData.sessionKey!!) + + if (olmInboundGroupSession!!.sessionIdentifier() != megolmSessionData.sessionId) { + throw Exception("Mismatched group session Id") + } + + senderKey = megolmSessionData.senderKey + keysClaimed = megolmSessionData.senderClaimedKeys + roomId = megolmSessionData.roomId + } catch (e: Exception) { + throw Exception(e.message) + } + } + + /** + * Export the inbound group session keys + * + * @return the inbound group session as MegolmSessionData if the operation succeeds + */ + fun exportKeys(): MegolmSessionData? { + return try { + if (null == forwardingCurve25519KeyChain) { + forwardingCurve25519KeyChain = ArrayList() + } + + if (keysClaimed == null) { + return null + } + + MegolmSessionData( + senderClaimedEd25519Key = keysClaimed?.get("ed25519"), + forwardingCurve25519KeyChain = ArrayList(forwardingCurve25519KeyChain!!), + senderKey = senderKey, + senderClaimedKeys = keysClaimed, + roomId = roomId, + sessionId = olmInboundGroupSession!!.sessionIdentifier(), + sessionKey = olmInboundGroupSession!!.export(olmInboundGroupSession!!.firstKnownIndex), + algorithm = MXCRYPTO_ALGORITHM_MEGOLM + ) + } catch (e: Exception) { + Timber.e(e, "## export() : senderKey $senderKey failed") + null + } + } + + /** + * Export the session for a message index. + * + * @param messageIndex the message index + * @return the exported data + */ + fun exportSession(messageIndex: Long): String? { + if (null != olmInboundGroupSession) { + try { + return olmInboundGroupSession!!.export(messageIndex) + } catch (e: Exception) { + Timber.e(e, "## exportSession() : export failed") + } + } + + return null + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmInboundGroupSessionWrapper2.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmInboundGroupSessionWrapper2.kt new file mode 100755 index 0000000000..091106c161 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmInboundGroupSessionWrapper2.kt @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.model + +import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.internal.crypto.MegolmSessionData +import org.matrix.olm.OlmInboundGroupSession +import timber.log.Timber +import java.io.Serializable + +/** + * This class adds more context to a OlmInboundGroupSession object. + * This allows additional checks. The class implements Serializable so that the context can be stored. + */ +class OlmInboundGroupSessionWrapper2 : Serializable { + + // The associated olm inbound group session. + var olmInboundGroupSession: OlmInboundGroupSession? = null + + // The room in which this session is used. + var roomId: String? = null + + // The base64-encoded curve25519 key of the sender. + var senderKey: String? = null + + // Other keys the sender claims. + var keysClaimed: Map? = null + + // Devices which forwarded this session to us (normally empty). + var forwardingCurve25519KeyChain: List? = ArrayList() + + /** + * @return the first known message index + */ + val firstKnownIndex: Long? + get() { + if (null != olmInboundGroupSession) { + try { + return olmInboundGroupSession!!.firstKnownIndex + } catch (e: Exception) { + Timber.e(e, "## getFirstKnownIndex() : getFirstKnownIndex failed") + } + } + + return null + } + + /** + * Constructor + * + * @param sessionKey the session key + * @param isImported true if it is an imported session key + */ + constructor(sessionKey: String, isImported: Boolean) { + try { + if (!isImported) { + olmInboundGroupSession = OlmInboundGroupSession(sessionKey) + } else { + olmInboundGroupSession = OlmInboundGroupSession.importSession(sessionKey) + } + } catch (e: Exception) { + Timber.e(e, "Cannot create") + } + } + + constructor() { + // empty + } + /** + * Create a new instance from the provided keys map. + * + * @param megolmSessionData the megolm session data + * @throws Exception if the data are invalid + */ + @Throws(Exception::class) + constructor(megolmSessionData: MegolmSessionData) { + try { + olmInboundGroupSession = OlmInboundGroupSession.importSession(megolmSessionData.sessionKey!!) + + if (olmInboundGroupSession!!.sessionIdentifier() != megolmSessionData.sessionId) { + throw Exception("Mismatched group session Id") + } + + senderKey = megolmSessionData.senderKey + keysClaimed = megolmSessionData.senderClaimedKeys + roomId = megolmSessionData.roomId + } catch (e: Exception) { + throw Exception(e.message) + } + } + + /** + * Export the inbound group session keys + * @param index the index to export. If null, the first known index will be used + * + * @return the inbound group session as MegolmSessionData if the operation succeeds + */ + fun exportKeys(index: Long? = null): MegolmSessionData? { + return try { + if (null == forwardingCurve25519KeyChain) { + forwardingCurve25519KeyChain = ArrayList() + } + + if (keysClaimed == null) { + return null + } + + val wantedIndex = index ?: olmInboundGroupSession!!.firstKnownIndex + + MegolmSessionData( + senderClaimedEd25519Key = keysClaimed?.get("ed25519"), + forwardingCurve25519KeyChain = ArrayList(forwardingCurve25519KeyChain!!), + senderKey = senderKey, + senderClaimedKeys = keysClaimed, + roomId = roomId, + sessionId = olmInboundGroupSession!!.sessionIdentifier(), + sessionKey = olmInboundGroupSession!!.export(wantedIndex), + algorithm = MXCRYPTO_ALGORITHM_MEGOLM + ) + } catch (e: Exception) { + Timber.e(e, "## export() : senderKey $senderKey failed") + null + } + } + + /** + * Export the session for a message index. + * + * @param messageIndex the message index + * @return the exported data + */ + fun exportSession(messageIndex: Long): String? { + if (null != olmInboundGroupSession) { + try { + return olmInboundGroupSession!!.export(messageIndex) + } catch (e: Exception) { + Timber.e(e, "## exportSession() : export failed") + } + } + + return null + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmSessionWrapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmSessionWrapper.kt new file mode 100644 index 0000000000..448043024d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmSessionWrapper.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.model + +import org.matrix.olm.OlmSession + +/** + * Encapsulate a OlmSession and a last received message Timestamp + */ +data class OlmSessionWrapper( + // The associated olm session. + val olmSession: OlmSession, + // Timestamp at which the session last received a message. + var lastReceivedMessageTs: Long = 0) { + + /** + * Notify that a message has been received on this olm session so that it updates `lastReceivedMessageTs` + */ + fun onMessageReceived() { + lastReceivedMessageTs = System.currentTimeMillis() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/EncryptedEventContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/EncryptedEventContent.kt new file mode 100644 index 0000000000..79f86bd28c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/EncryptedEventContent.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.model.event + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent + +/** + * Class representing an encrypted event content + */ +@JsonClass(generateAdapter = true) +data class EncryptedEventContent( + + /** + * the used algorithm + */ + @Json(name = "algorithm") + val algorithm: String? = null, + + /** + * The encrypted event + */ + @Json(name = "ciphertext") + val ciphertext: String? = null, + + /** + * The device id + */ + @Json(name = "device_id") + val deviceId: String? = null, + + /** + * the sender key + */ + @Json(name = "sender_key") + val senderKey: String? = null, + + /** + * The session id + */ + @Json(name = "session_id") + val sessionId: String? = null, + + // Relation context is in clear in encrypted message + @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/EncryptionEventContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/EncryptionEventContent.kt new file mode 100644 index 0000000000..255e5e8d81 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/EncryptionEventContent.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.model.event + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing an encrypted event content + */ +@JsonClass(generateAdapter = true) +data class EncryptionEventContent( + /** + * Required. The encryption algorithm to be used to encrypt messages sent in this room. Must be 'm.megolm.v1.aes-sha2'. + */ + @Json(name = "algorithm") + val algorithm: String, + + /** + * How long the session should be used before changing it. 604800000 (a week) is the recommended default. + */ + @Json(name = "rotation_period_ms") + val rotationPeriodMs: Long? = null, + + /** + * How many messages should be sent before changing the session. 100 is the recommended default. + */ + @Json(name = "rotation_period_msgs") + val rotationPeriodMsgs: Long? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/NewDeviceContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/NewDeviceContent.kt new file mode 100644 index 0000000000..27ccc2d041 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/NewDeviceContent.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.model.event + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class NewDeviceContent( + // the device id + @Json(name = "device_id") + val deviceId: String? = null, + + // the room ids list + @Json(name = "rooms") + val rooms: List? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/OlmEventContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/OlmEventContent.kt new file mode 100644 index 0000000000..f9de805962 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/OlmEventContent.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.model.event + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing an encrypted event content + */ +@JsonClass(generateAdapter = true) +data class OlmEventContent( + /** + * + */ + @Json(name = "ciphertext") + val ciphertext: Map? = null, + + /** + * the sender key + */ + @Json(name = "sender_key") + val senderKey: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/OlmPayloadContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/OlmPayloadContent.kt new file mode 100644 index 0000000000..a3a9ee2e51 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/OlmPayloadContent.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.model.event + +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.di.MoshiProvider + +/** + * Class representing the OLM payload content + */ +@JsonClass(generateAdapter = true) +data class OlmPayloadContent( + /** + * The room id + */ + var room_id: String? = null, + + /** + * The sender + */ + var sender: String? = null, + + /** + * The recipient + */ + var recipient: String? = null, + + /** + * the recipient keys + */ + var recipient_keys: Map? = null, + + /** + * The keys + */ + var keys: Map? = null +) { + fun toJsonString(): String { + return MoshiProvider.providesMoshi().adapter(OlmPayloadContent::class.java).toJson(this) + } + + companion object { + fun fromJsonString(str: String): OlmPayloadContent? { + return MoshiProvider.providesMoshi().adapter(OlmPayloadContent::class.java).fromJson(str) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/RoomKeyContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/RoomKeyContent.kt new file mode 100644 index 0000000000..eeaf52f0e1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/RoomKeyContent.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.model.event + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing an sharekey content + */ +@JsonClass(generateAdapter = true) +data class RoomKeyContent( + + @Json(name = "algorithm") + val algorithm: String? = null, + + @Json(name = "room_id") + val roomId: String? = null, + + @Json(name = "session_id") + val sessionId: String? = null, + + @Json(name = "session_key") + val sessionKey: String? = null, + + // should be a Long but it is sometimes a double + @Json(name = "chain_index") + val chainIndex: Any? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/RoomKeyWithHeldContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/RoomKeyWithHeldContent.kt new file mode 100644 index 0000000000..5d9a1937af --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/RoomKeyWithHeldContent.kt @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.model.event + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing an sharekey content + */ +@JsonClass(generateAdapter = true) +data class RoomKeyWithHeldContent( + + /** + * Required if code is not m.no_olm. The ID of the room that the session belongs to. + */ + @Json(name = "room_id") val roomId: String? = null, + + /** + * Required. The encryption algorithm that the key is for. + */ + @Json(name = "algorithm") val algorithm: String? = null, + + /** + * Required if code is not m.no_olm. The ID of the session. + */ + @Json(name = "session_id") val sessionId: String? = null, + + /** + * Required. The key of the session creator. + */ + @Json(name = "sender_key") val senderKey: String? = null, + + /** + * Required. A machine-readable code for why the key was not sent + */ + @Json(name = "code") val codeString: String? = null, + + /** + * A human-readable reason for why the key was not sent. The receiving client should only use this string if it does not understand the code. + */ + @Json(name = "reason") val reason: String? = null + +) { + val code: WithHeldCode? + get() { + return WithHeldCode.fromCode(codeString) + } +} + +enum class WithHeldCode(val value: String) { + /** + * the user/device was blacklisted + */ + BLACKLISTED("m.blacklisted"), + /** + * the user/devices is unverified + */ + UNVERIFIED("m.unverified"), + /** + * the user/device is not allowed have the key. For example, this would usually be sent in response + * to a key request if the user was not in the room when the message was sent + */ + UNAUTHORISED("m.unauthorised"), + /** + * Sent in reply to a key request if the device that the key is requested from does not have the requested key + */ + UNAVAILABLE("m.unavailable"), + /** + * An olm session could not be established. + * This may happen, for example, if the sender was unable to obtain a one-time key from the recipient. + */ + NO_OLM("m.no_olm"); + + companion object { + fun fromCode(code: String?): WithHeldCode? { + return when (code) { + BLACKLISTED.value -> BLACKLISTED + UNVERIFIED.value -> UNVERIFIED + UNAUTHORISED.value -> UNAUTHORISED + UNAVAILABLE.value -> UNAVAILABLE + NO_OLM.value -> NO_OLM + else -> null + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/SecretSendEventContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/SecretSendEventContent.kt new file mode 100644 index 0000000000..5b7a139488 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/SecretSendEventContent.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.model.event + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing an encrypted event content + */ +@JsonClass(generateAdapter = true) +data class SecretSendEventContent( + @Json(name = "request_id") val requestId: String, + @Json(name = "secret") val secretValue: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDeviceParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDeviceParams.kt new file mode 100644 index 0000000000..4b1530c9c6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDeviceParams.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This class provides the parameter to delete a device + */ +@JsonClass(generateAdapter = true) +internal data class DeleteDeviceParams( + @Json(name = "auth") + val userPasswordAuth: UserPasswordAuth? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeviceInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeviceInfo.kt new file mode 100644 index 0000000000..97c7c59b50 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeviceInfo.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2014 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.interfaces.DatedObject + +/** + * This class describes the device information + */ +@JsonClass(generateAdapter = true) +data class DeviceInfo( + /** + * The owner user id (not documented and useless but the homeserver sent it. You should not need it) + */ + @Json(name = "user_id") + val user_id: String? = null, + + /** + * The device id + */ + @Json(name = "device_id") + val deviceId: String? = null, + + /** + * The device display name + */ + @Json(name = "display_name") + val displayName: String? = null, + + /** + * The last time this device has been seen. + */ + @Json(name = "last_seen_ts") + val lastSeenTs: Long? = null, + + /** + * The last ip address + */ + @Json(name = "last_seen_ip") + val lastSeenIp: String? = null +) : DatedObject { + + override val date: Long + get() = lastSeenTs ?: 0 +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeviceKeys.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeviceKeys.kt new file mode 100644 index 0000000000..efc036c4d8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeviceKeys.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class DeviceKeys( + /** + * Required. The ID of the user the device belongs to. Must match the user ID used when logging in. + */ + @Json(name = "user_id") + val userId: String, + + /** + * Required. The ID of the device these keys belong to. Must match the device ID used when logging in. + */ + @Json(name = "device_id") + val deviceId: String, + + /** + * Required. The encryption algorithms supported by this device. + */ + @Json(name = "algorithms") + val algorithms: List?, + + /** + * Required. Public identity keys. The names of the properties should be in the format :. + * The keys themselves should be encoded as specified by the key algorithm. + */ + @Json(name = "keys") + val keys: Map?, + + /** + * Required. Signatures for the device key object. A map from user ID, to a map from : to the signature. + * The signature is calculated using the process described at https://matrix.org/docs/spec/appendices.html#signing-json. + */ + @Json(name = "signatures") + val signatures: Map>? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeviceKeysWithUnsigned.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeviceKeysWithUnsigned.kt new file mode 100644 index 0000000000..c0f900f6c0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeviceKeysWithUnsigned.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class DeviceKeysWithUnsigned( + /** + * Required. The ID of the user the device belongs to. Must match the user ID used when logging in. + */ + @Json(name = "user_id") + val userId: String, + + /** + * Required. The ID of the device these keys belong to. Must match the device ID used when logging in. + */ + @Json(name = "device_id") + val deviceId: String, + + /** + * Required. The encryption algorithms supported by this device. + */ + @Json(name = "algorithms") + val algorithms: List?, + + /** + * Required. Public identity keys. The names of the properties should be in the format :. + * The keys themselves should be encoded as specified by the key algorithm. + */ + @Json(name = "keys") + val keys: Map?, + + /** + * Required. Signatures for the device key object. A map from user ID, to a map from : to the signature. + * The signature is calculated using the process described at https://matrix.org/docs/spec/appendices.html#signing-json. + */ + @Json(name = "signatures") + val signatures: Map>?, + + /** + * Additional data added to the device key information by intermediate servers, and not covered by the signatures. + */ + @Json(name = "unsigned") + val unsigned: UnsignedDeviceInfo? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DevicesListResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DevicesListResponse.kt new file mode 100644 index 0000000000..934a0cf43c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DevicesListResponse.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2014 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This class describes the response to https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-devices + */ +@JsonClass(generateAdapter = true) +data class DevicesListResponse( + @Json(name = "devices") + val devices: List? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DummyContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DummyContent.kt new file mode 100644 index 0000000000..8464566d7c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DummyContent.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +/** + * Class representing the dummy content + * Ref: https://matrix.org/docs/spec/client_server/latest#id82 + */ +typealias DummyContent = Unit diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedBodyFileInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedBodyFileInfo.kt new file mode 100644 index 0000000000..56156cf749 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedBodyFileInfo.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import org.matrix.olm.OlmPkMessage + +/** + * Build from a OlmPkMessage object + * + * @param olmPkMessage OlmPkMessage + */ +class EncryptedBodyFileInfo(olmPkMessage: OlmPkMessage) { + var ciphertext = olmPkMessage.mCipherText + var mac = olmPkMessage.mMac + var ephemeral = olmPkMessage.mEphemeralKey +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedFileInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedFileInfo.kt new file mode 100644 index 0000000000..65455e9fa3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedFileInfo.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * In Matrix specs: EncryptedFile + */ +@JsonClass(generateAdapter = true) +data class EncryptedFileInfo( + /** + * Required. The URL to the file. + */ + @Json(name = "url") + val url: String? = null, + + /** + * Not documented + */ + @Json(name = "mimetype") + val mimetype: String? = null, + + /** + * Required. A JSON Web Key object. + */ + @Json(name = "key") + val key: EncryptedFileKey? = null, + + /** + * Required. The Initialisation Vector used by AES-CTR, encoded as unpadded base64. + */ + @Json(name = "iv") + val iv: String? = null, + + /** + * Required. A map from an algorithm name to a hash of the ciphertext, encoded as unpadded base64. + * Clients should support the SHA-256 hash, which uses the key "sha256". + */ + @Json(name = "hashes") + val hashes: Map? = null, + + /** + * Required. Version of the encrypted attachments protocol. Must be "v2". + */ + @Json(name = "v") + val v: String? = null +) { + /** + * Check what the spec tells us + */ + fun isValid(): Boolean { + if (url.isNullOrBlank()) { + return false + } + + if (key?.isValid() != true) { + return false + } + + if (iv.isNullOrBlank()) { + return false + } + + if (hashes?.containsKey("sha256") != true) { + return false + } + + if (v != "v2") { + return false + } + + return true + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedFileKey.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedFileKey.kt new file mode 100644 index 0000000000..f0a680cfd3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedFileKey.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class EncryptedFileKey( + /** + * Required. Algorithm. Must be "A256CTR". + */ + @Json(name = "alg") + val alg: String? = null, + + /** + * Required. Extractable. Must be true. This is a W3C extension. + */ + @Json(name = "ext") + val ext: Boolean? = null, + + /** + * Required. Key operations. Must at least contain "encrypt" and "decrypt". + */ + @Json(name = "key_ops") + val key_ops: List? = null, + + /** + * Required. Key type. Must be "oct". + */ + @Json(name = "kty") + val kty: String? = null, + + /** + * Required. The key, encoded as urlsafe unpadded base64. + */ + @Json(name = "k") + val k: String? = null +) { + /** + * Check what the spec tells us + */ + fun isValid(): Boolean { + if (alg != "A256CTR") { + return false + } + + if (ext != true) { + return false + } + + if (key_ops?.contains("encrypt") != true || !key_ops.contains("decrypt")) { + return false + } + + if (kty != "oct") { + return false + } + + if (k.isNullOrBlank()) { + return false + } + + return true + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedMessage.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedMessage.kt new file mode 100644 index 0000000000..d044815542 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedMessage.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class EncryptedMessage( + @Json(name = "algorithm") + val algorithm: String? = null, + + @Json(name = "sender_key") + val senderKey: String? = null, + + @Json(name = "ciphertext") + val cipherText: Map? = null +) : SendToDeviceObject diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/ForwardedRoomKeyContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/ForwardedRoomKeyContent.kt new file mode 100644 index 0000000000..927828c4a0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/ForwardedRoomKeyContent.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing the forward room key request body content + * Ref: https://matrix.org/docs/spec/client_server/latest#m-forwarded-room-key + */ +@JsonClass(generateAdapter = true) +data class ForwardedRoomKeyContent( + /** + * Required. The encryption algorithm the key in this event is to be used with. + */ + @Json(name = "algorithm") + val algorithm: String? = null, + + /** + * Required. The room where the key is used. + */ + @Json(name = "room_id") + val roomId: String? = null, + + /** + * Required. The Curve25519 key of the device which initiated the session originally. + */ + @Json(name = "sender_key") + val senderKey: String? = null, + + /** + * Required. The ID of the session that the key is for. + */ + @Json(name = "session_id") + val sessionId: String? = null, + + /** + * Required. The key to be exchanged. + */ + @Json(name = "session_key") + val sessionKey: String? = null, + + /** + * Required. Chain of Curve25519 keys. It starts out empty, but each time the key is forwarded to another device, + * the previous sender in the chain is added to the end of the list. For example, if the key is forwarded + * from A to B to C, this field is empty between A and B, and contains A's Curve25519 key between B and C. + */ + @Json(name = "forwarding_curve25519_key_chain") + val forwardingCurve25519KeyChain: List? = null, + + /** + * Required. The Ed25519 key of the device which initiated the session originally. It is 'claimed' because the + * receiving device has no way to tell that the original room_key actually came from a device which owns the + * private part of this key unless they have done device verification. + */ + @Json(name = "sender_claimed_ed25519_key") + val senderClaimedEd25519Key: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/GossipingToDeviceObject.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/GossipingToDeviceObject.kt new file mode 100644 index 0000000000..c3b156084d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/GossipingToDeviceObject.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Interface representing an room key action request + * Note: this class cannot be abstract because of [org.matrix.androidsdk.core.JsonUtils.toRoomKeyShare] + */ +interface GossipingToDeviceObject : SendToDeviceObject { + + val action: String? + + val requestingDeviceId: String? + + val requestId: String? + + companion object { + const val ACTION_SHARE_REQUEST = "request" + const val ACTION_SHARE_CANCELLATION = "request_cancellation" + } +} + +@JsonClass(generateAdapter = true) +data class GossipingDefaultContent( + @Json(name = "action") override val action: String?, + @Json(name = "requesting_device_id") override val requestingDeviceId: String?, + @Json(name = "m.request_id") override val requestId: String? = null +) : GossipingToDeviceObject diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyChangesResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyChangesResponse.kt new file mode 100644 index 0000000000..5c677c7123 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyChangesResponse.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2017 Vector Creations Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This class describes the key changes response + */ +@JsonClass(generateAdapter = true) +internal data class KeyChangesResponse( + // list of user ids which have new devices + @Json(name = "changed") + val changed: List? = null, + + // List of user ids who are no more tracked. + @Json(name = "left") + val left: List? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationAccept.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationAccept.kt new file mode 100644 index 0000000000..c4e3dd9297 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationAccept.kt @@ -0,0 +1,88 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoAccept +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoAcceptFactory + +/** + * Sent by Bob to accept a verification from a previously sent m.key.verification.start message. + */ +@JsonClass(generateAdapter = true) +internal data class KeyVerificationAccept( + /** + * string to identify the transaction. + * This string must be unique for the pair of users performing verification for the duration that the transaction is valid. + * Alice’s device should record this ID and use it in future messages in this transaction. + */ + @Json(name = "transaction_id") + override val transactionId: String? = null, + + /** + * The key agreement protocol that Bob’s device has selected to use, out of the list proposed by Alice’s device + */ + @Json(name = "key_agreement_protocol") + override val keyAgreementProtocol: String? = null, + + /** + * The hash algorithm that Bob’s device has selected to use, out of the list proposed by Alice’s device + */ + @Json(name = "hash") + override val hash: String? = null, + + /** + * The message authentication code that Bob’s device has selected to use, out of the list proposed by Alice’s device + */ + @Json(name = "message_authentication_code") + override val messageAuthenticationCode: String? = null, + + /** + * An array of short authentication string methods that Bob’s client (and Bob) understands. Must be a subset of the list proposed by Alice’s device + */ + @Json(name = "short_authentication_string") + override val shortAuthenticationStrings: List? = null, + + /** + * The hash (encoded as unpadded base64) of the concatenation of the device’s ephemeral public key (QB, encoded as unpadded base64) + * and the canonical JSON representation of the m.key.verification.start message. + */ + @Json(name = "commitment") + override var commitment: String? = null +) : SendToDeviceObject, VerificationInfoAccept { + + override fun toSendToDeviceObject() = this + + companion object : VerificationInfoAcceptFactory { + override fun create(tid: String, + keyAgreementProtocol: String, + hash: String, + commitment: String, + messageAuthenticationCode: String, + shortAuthenticationStrings: List): VerificationInfoAccept { + return KeyVerificationAccept( + transactionId = tid, + keyAgreementProtocol = keyAgreementProtocol, + hash = hash, + commitment = commitment, + messageAuthenticationCode = messageAuthenticationCode, + shortAuthenticationStrings = shortAuthenticationStrings + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationCancel.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationCancel.kt new file mode 100644 index 0000000000..ea2cbf214b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationCancel.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.crypto.verification.CancelCode +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoCancel + +/** + * To device event sent by either party to cancel a key verification. + */ +@JsonClass(generateAdapter = true) +internal data class KeyVerificationCancel( + /** + * the transaction ID of the verification to cancel + */ + @Json(name = "transaction_id") + override val transactionId: String? = null, + + /** + * machine-readable reason for cancelling, see #CancelCode + */ + override val code: String? = null, + + /** + * human-readable reason for cancelling. This should only be used if the receiving client does not understand the code given. + */ + override val reason: String? = null +) : SendToDeviceObject, VerificationInfoCancel { + + companion object { + fun create(tid: String, cancelCode: CancelCode): KeyVerificationCancel { + return KeyVerificationCancel( + tid, + cancelCode.value, + cancelCode.humanReadable + ) + } + } + + override fun toSendToDeviceObject() = this +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationDone.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationDone.kt new file mode 100644 index 0000000000..e4d75a0de6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationDone.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoDone + +/** + * Requests a key verification with another user's devices. + */ +@JsonClass(generateAdapter = true) +internal data class KeyVerificationDone( + @Json(name = "transaction_id") override val transactionId: String? = null +) : SendToDeviceObject, VerificationInfoDone { + + override fun toSendToDeviceObject() = this +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationKey.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationKey.kt new file mode 100644 index 0000000000..bf1ded002e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationKey.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoKey +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoKeyFactory + +/** + * Sent by both devices to send their ephemeral Curve25519 public key to the other device. + */ +@JsonClass(generateAdapter = true) +internal data class KeyVerificationKey( + /** + * the ID of the transaction that the message is part of + */ + @Json(name = "transaction_id") override val transactionId: String? = null, + + /** + * The device’s ephemeral public key, as an unpadded base64 string + */ + @Json(name = "key") override val key: String? = null + +) : SendToDeviceObject, VerificationInfoKey { + + companion object : VerificationInfoKeyFactory { + override fun create(tid: String, pubKey: String): KeyVerificationKey { + return KeyVerificationKey(tid, pubKey) + } + } + + override fun toSendToDeviceObject() = this +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationMac.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationMac.kt new file mode 100644 index 0000000000..001cabaa4e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationMac.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoMac +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoMacFactory + +/** + * Sent by both devices to send the MAC of their device key to the other device. + */ +@JsonClass(generateAdapter = true) +internal data class KeyVerificationMac( + @Json(name = "transaction_id") override val transactionId: String? = null, + @Json(name = "mac") override val mac: Map? = null, + @Json(name = "keys") override val keys: String? = null + +) : SendToDeviceObject, VerificationInfoMac { + + override fun toSendToDeviceObject(): SendToDeviceObject? = this + + companion object : VerificationInfoMacFactory { + override fun create(tid: String, mac: Map, keys: String): VerificationInfoMac { + return KeyVerificationMac(tid, mac, keys) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationReady.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationReady.kt new file mode 100644 index 0000000000..25d6984560 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationReady.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoReady + +/** + * Requests a key verification with another user's devices. + */ +@JsonClass(generateAdapter = true) +internal data class KeyVerificationReady( + @Json(name = "from_device") override val fromDevice: String?, + @Json(name = "methods") override val methods: List?, + @Json(name = "transaction_id") override val transactionId: String? = null +) : SendToDeviceObject, VerificationInfoReady { + + override fun toSendToDeviceObject() = this +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationRequest.kt new file mode 100644 index 0000000000..1bf1d1a5c2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationRequest.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoRequest + +/** + * Requests a key verification with another user's devices. + */ +@JsonClass(generateAdapter = true) +internal data class KeyVerificationRequest( + @Json(name = "from_device") override val fromDevice: String?, + @Json(name = "methods") override val methods: List, + @Json(name = "timestamp") override val timestamp: Long?, + @Json(name = "transaction_id") override val transactionId: String? = null +) : SendToDeviceObject, VerificationInfoRequest { + + override fun toSendToDeviceObject() = this +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationStart.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationStart.kt new file mode 100644 index 0000000000..bc99c71f09 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationStart.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoStart +import org.matrix.android.sdk.internal.util.JsonCanonicalizer + +/** + * Sent by Alice to initiate an interactive key verification. + */ +@JsonClass(generateAdapter = true) +internal data class KeyVerificationStart( + @Json(name = "from_device") override val fromDevice: String? = null, + @Json(name = "method") override val method: String? = null, + @Json(name = "transaction_id") override val transactionId: String? = null, + @Json(name = "key_agreement_protocols") override val keyAgreementProtocols: List? = null, + @Json(name = "hashes") override val hashes: List? = null, + @Json(name = "message_authentication_codes") override val messageAuthenticationCodes: List? = null, + @Json(name = "short_authentication_string") override val shortAuthenticationStrings: List? = null, + // For QR code verification + @Json(name = "secret") override val sharedSecret: String? = null +) : SendToDeviceObject, VerificationInfoStart { + + override fun toCanonicalJson(): String { + return JsonCanonicalizer.getCanonicalJson(KeyVerificationStart::class.java, this) + } + + override fun toSendToDeviceObject() = this +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysClaimBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysClaimBody.kt new file mode 100644 index 0000000000..f48936a80e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysClaimBody.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This class represents the response to /keys/claim request made by claimOneTimeKeysForUsersDevices. + */ +@JsonClass(generateAdapter = true) +internal data class KeysClaimBody( + /** + * The time (in milliseconds) to wait when downloading keys from remote servers. 10 seconds is the recommended default. + */ + @Json(name = "timeout") + val timeout: Int? = null, + + /** + * Required. The keys to be claimed. A map from user ID, to a map from device ID to algorithm name. + */ + @Json(name = "one_time_keys") + val oneTimeKeys: Map> +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysClaimResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysClaimResponse.kt new file mode 100644 index 0000000000..a1eebcb694 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysClaimResponse.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This class represents the response to /keys/claim request made by claimOneTimeKeysForUsersDevices. + */ +@JsonClass(generateAdapter = true) +internal data class KeysClaimResponse( + /** + * The requested keys ordered by device by user. + * TODO Type does not match spec, should be Map + */ + @Json(name = "one_time_keys") + val oneTimeKeys: Map>>>? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysQueryBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysQueryBody.kt new file mode 100644 index 0000000000..4232225cbe --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysQueryBody.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This class represents the body to /keys/query + */ +@JsonClass(generateAdapter = true) +internal data class KeysQueryBody( + /** + * The time (in milliseconds) to wait when downloading keys from remote servers. 10 seconds is the recommended default. + */ + @Json(name = "timeout") + val timeout: Int? = null, + + /** + * Required. The keys to be downloaded. + * A map from user ID, to a list of device IDs, or to an empty list to indicate all devices for the corresponding user. + */ + @Json(name = "device_keys") + val deviceKeys: Map>, + + /** + * If the client is fetching keys as a result of a device update received in a sync request, this should be the 'since' token + * of that sync request, or any later sync token. This allows the server to ensure its response contains the keys advertised + * by the notification in that sync. + */ + @Json(name = "token") + val token: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysQueryResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysQueryResponse.kt new file mode 100644 index 0000000000..a099419c3c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysQueryResponse.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This class represents the response to /keys/query request made by downloadKeysForUsers + * + * After uploading cross-signing keys, they will be included under the /keys/query endpoint under the master_keys, + * self_signing_keys and user_signing_keys properties. + * + * The user_signing_keys property will only be included when a user requests their own keys. + */ +@JsonClass(generateAdapter = true) +internal data class KeysQueryResponse( + /** + * Information on the queried devices. A map from user ID, to a map from device ID to device information. + * For each device, the information returned will be the same as uploaded via /keys/upload, with the addition of an unsigned property. + */ + @Json(name = "device_keys") + val deviceKeys: Map>? = null, + + /** + * If any remote homeservers could not be reached, they are recorded here. The names of the + * properties are the names of the unreachable servers. + * + * If the homeserver could be reached, but the user or device was unknown, no failure is recorded. + * Instead, the corresponding user or device is missing from the device_keys result. + */ + val failures: Map>? = null, + + @Json(name = "master_keys") + val masterKeys: Map? = null, + + @Json(name = "self_signing_keys") + val selfSigningKeys: Map? = null, + + @Json(name = "user_signing_keys") + val userSigningKeys: Map? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysUploadBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysUploadBody.kt new file mode 100644 index 0000000000..3d9652e328 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysUploadBody.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.util.JsonDict + +/** + * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-keys-upload + */ +@JsonClass(generateAdapter = true) +internal data class KeysUploadBody( + /** + * Identity keys for the device. + * + * May be absent if no new identity keys are required. + */ + @Json(name = "device_keys") + val deviceKeys: DeviceKeys? = null, + + /** + * One-time public keys for "pre-key" messages. The names of the properties should be in the + * format :. The format of the key is determined by the key algorithm. + * + * May be absent if no new one-time keys are required. + */ + @Json(name = "one_time_keys") + val oneTimeKeys: JsonDict? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysUploadResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysUploadResponse.kt new file mode 100644 index 0000000000..07904969f0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysUploadResponse.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This class represents the response to /keys/upload request made by uploadKeys. + */ +@JsonClass(generateAdapter = true) +internal data class KeysUploadResponse( + /** + * Required. For each key algorithm, the number of unclaimed one-time keys + * of that type currently held on the server for this device. + */ + @Json(name = "one_time_key_counts") + val oneTimeKeyCounts: Map? = null +) { + /** + * Helper methods to extract information from 'oneTimeKeyCounts' + * + * @param algorithm the expected algorithm + * @return the time key counts + */ + fun oneTimeKeyCountsForAlgorithm(algorithm: String): Int { + return oneTimeKeyCounts?.get(algorithm) ?: 0 + } + + /** + * Tells if there is a oneTimeKeys for a dedicated algorithm. + * + * @param algorithm the algorithm + * @return true if it is found + */ + fun hasOneTimeKeyCountsForAlgorithm(algorithm: String): Boolean { + return oneTimeKeyCounts?.containsKey(algorithm) == true + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/RestKeyInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/RestKeyInfo.kt new file mode 100644 index 0000000000..cfdb300f16 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/RestKeyInfo.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey +import org.matrix.android.sdk.internal.crypto.model.CryptoInfoMapper + +@JsonClass(generateAdapter = true) +internal data class RestKeyInfo( + /** + * The user who owns the key + */ + @Json(name = "user_id") + val userId: String, + + /** + * Allowed uses for the key. + * Must contain "master" for master keys, "self_signing" for self-signing keys, and "user_signing" for user-signing keys. + * See CrossSigningKeyInfo#KEY_USAGE_* constants + */ + @Json(name = "usage") + val usages: List?, + + /** + * An object that must have one entry, + * whose name is "ed25519:" followed by the unpadded base64 encoding of the public key, + * and whose value is the unpadded base64 encoding of the public key. + */ + @Json(name = "keys") + val keys: Map?, + + /** + * Signatures of the key. + * A self-signing or user-signing key must be signed by the master key. + * A master key may be signed by a device. + */ + @Json(name = "signatures") + val signatures: Map>? = null +) { + fun toCryptoModel(): CryptoCrossSigningKey { + return CryptoInfoMapper.map(this) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/RoomKeyRequestBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/RoomKeyRequestBody.kt new file mode 100644 index 0000000000..e3a65df5d0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/RoomKeyRequestBody.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.di.MoshiProvider + +/** + * Class representing an room key request body content + */ +@JsonClass(generateAdapter = true) +data class RoomKeyRequestBody( + @Json(name = "algorithm") + val algorithm: String? = null, + + @Json(name = "room_id") + val roomId: String? = null, + + @Json(name = "sender_key") + val senderKey: String? = null, + + @Json(name = "session_id") + val sessionId: String? = null +) { + fun toJson(): String { + return MoshiProvider.providesMoshi().adapter(RoomKeyRequestBody::class.java).toJson(this) + } + + companion object { + fun fromJson(json: String?): RoomKeyRequestBody? { + return json?.let { MoshiProvider.providesMoshi().adapter(RoomKeyRequestBody::class.java).fromJson(it) } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/RoomKeyShareRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/RoomKeyShareRequest.kt new file mode 100644 index 0000000000..299c084819 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/RoomKeyShareRequest.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing a room key request content + */ +@JsonClass(generateAdapter = true) +data class RoomKeyShareRequest( + @Json(name = "action") + override val action: String? = GossipingToDeviceObject.ACTION_SHARE_REQUEST, + + @Json(name = "requesting_device_id") + override val requestingDeviceId: String? = null, + + @Json(name = "request_id") + override val requestId: String? = null, + + @Json(name = "body") + val body: RoomKeyRequestBody? = null +) : GossipingToDeviceObject diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/SecretShareRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/SecretShareRequest.kt new file mode 100644 index 0000000000..98a586d136 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/SecretShareRequest.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing a room key request content + */ +@JsonClass(generateAdapter = true) +data class SecretShareRequest( + @Json(name = "action") + override val action: String? = GossipingToDeviceObject.ACTION_SHARE_REQUEST, + + @Json(name = "requesting_device_id") + override val requestingDeviceId: String? = null, + + @Json(name = "request_id") + override val requestId: String? = null, + + @Json(name = "name") + val secretName: String? = null +) : GossipingToDeviceObject diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/SendToDeviceBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/SendToDeviceBody.kt new file mode 100644 index 0000000000..e7df20ee5e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/SendToDeviceBody.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.model.rest + +internal data class SendToDeviceBody( + /** + * `Any` should implement [SendToDeviceObject], but we cannot use interface here because of Json serialization + * + * The messages to send. A map from user ID, to a map from device ID to message body. + * The device ID may also be *, meaning all known devices for the user. + */ + val messages: Map>? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/SendToDeviceObject.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/SendToDeviceObject.kt new file mode 100644 index 0000000000..a018d62ab4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/SendToDeviceObject.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.model.rest + +interface SendToDeviceObject diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/ShareRequestCancellation.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/ShareRequestCancellation.kt new file mode 100644 index 0000000000..b6449fe8ed --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/ShareRequestCancellation.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.crypto.model.rest.GossipingToDeviceObject.Companion.ACTION_SHARE_CANCELLATION + +/** + * Class representing a room key request cancellation content + */ +@JsonClass(generateAdapter = true) +internal data class ShareRequestCancellation( + @Json(name = "action") + override val action: String? = ACTION_SHARE_CANCELLATION, + + @Json(name = "requesting_device_id") + override val requestingDeviceId: String? = null, + + @Json(name = "request_id") + override val requestId: String? = null +) : GossipingToDeviceObject diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/SignatureUploadResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/SignatureUploadResponse.kt new file mode 100644 index 0000000000..e21fd8fbd4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/SignatureUploadResponse.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Upload Signature response + */ +@JsonClass(generateAdapter = true) +internal data class SignatureUploadResponse( + /** + * The response contains a failures property, which is a map of user ID to device ID to failure reason, + * if any of the uploaded keys failed. + * The homeserver should verify that the signatures on the uploaded keys are valid. + * If a signature is not valid, the homeserver should set the corresponding entry in failures to a JSON object + * with the errcode property set to M_INVALID_SIGNATURE. + */ + val failures: Map>? = null +) + +@JsonClass(generateAdapter = true) +internal data class UploadResponseFailure( + @Json(name = "status") + val status: Int, + + @Json(name = "errcode") + val errCode: String, + + @Json(name = "message") + val message: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UnsignedDeviceInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UnsignedDeviceInfo.kt new file mode 100644 index 0000000000..1fc0d417e8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UnsignedDeviceInfo.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class UnsignedDeviceInfo( + /** + * The display name which the user set on the device. + */ + @Json(name = "device_display_name") + val deviceDisplayName: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UpdateDeviceInfoBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UpdateDeviceInfoBody.kt new file mode 100644 index 0000000000..ac691e6e6e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UpdateDeviceInfoBody.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class UpdateDeviceInfoBody( + /** + * The new display name for this device. If not given, the display name is unchanged. + */ + @Json(name = "display_name") + val displayName: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UploadSignatureQueryBuilder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UploadSignatureQueryBuilder.kt new file mode 100644 index 0000000000..dbb2822cdd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UploadSignatureQueryBuilder.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.toRest + +/** + * Helper class to build CryptoApi#uploadSignatures params + */ +internal data class UploadSignatureQueryBuilder( + private val deviceInfoList: MutableList = mutableListOf(), + private val signingKeyInfoList: MutableList = mutableListOf() +) { + + fun withDeviceInfo(deviceInfo: CryptoDeviceInfo) = apply { + deviceInfoList.add(deviceInfo) + } + + fun withSigningKeyInfo(info: CryptoCrossSigningKey) = apply { + signingKeyInfoList.add(info) + } + + fun build(): Map> { + val map = HashMap>() + + val usersList = (deviceInfoList.map { it.userId } + signingKeyInfoList.map { it.userId }) + .distinct() + + usersList.forEach { userID -> + val userMap = HashMap() + deviceInfoList.filter { it.userId == userID }.forEach { deviceInfo -> + userMap[deviceInfo.deviceId] = deviceInfo.toRest() + } + signingKeyInfoList.filter { it.userId == userID }.forEach { keyInfo -> + keyInfo.unpaddedBase64PublicKey?.let { base64Key -> + userMap[base64Key] = keyInfo.toRest() + } + } + map[userID] = userMap + } + + return map + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UploadSigningKeysBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UploadSigningKeysBody.kt new file mode 100644 index 0000000000..a7a61b282f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UploadSigningKeysBody.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class UploadSigningKeysBody( + @Json(name = "master_key") + val masterKey: RestKeyInfo? = null, + + @Json(name = "self_signing_key") + val selfSigningKey: RestKeyInfo? = null, + + @Json(name = "user_signing_key") + val userSigningKey: RestKeyInfo? = null, + + @Json(name = "auth") + val auth: UserPasswordAuth? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UserPasswordAuth.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UserPasswordAuth.kt new file mode 100644 index 0000000000..018f707105 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UserPasswordAuth.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes + +/** + * This class provides the authentication data by using user and password + */ +@JsonClass(generateAdapter = true) +data class UserPasswordAuth( + + // device device session id + @Json(name = "session") + val session: String? = null, + + // registration information + @Json(name = "type") + val type: String? = LoginFlowTypes.PASSWORD, + + @Json(name = "user") + val user: String? = null, + + @Json(name = "password") + val password: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/VerificationMethodValues.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/VerificationMethodValues.kt new file mode 100644 index 0000000000..99bbebf7eb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/VerificationMethodValues.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.model.rest + +import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod + +internal const val VERIFICATION_METHOD_SAS = "m.sas.v1" + +// Qr code +// Ref: https://github.com/uhoreg/matrix-doc/blob/qr_key_verification/proposals/1543-qr_code_key_verification.md#verification-methods +internal const val VERIFICATION_METHOD_QR_CODE_SHOW = "m.qr_code.show.v1" +internal const val VERIFICATION_METHOD_QR_CODE_SCAN = "m.qr_code.scan.v1" +internal const val VERIFICATION_METHOD_RECIPROCATE = "m.reciprocate.v1" + +internal fun VerificationMethod.toValue(): String { + return when (this) { + VerificationMethod.SAS -> VERIFICATION_METHOD_SAS + VerificationMethod.QR_CODE_SCAN -> VERIFICATION_METHOD_QR_CODE_SCAN + VerificationMethod.QR_CODE_SHOW -> VERIFICATION_METHOD_QR_CODE_SHOW + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/repository/WarnOnUnknownDeviceRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/repository/WarnOnUnknownDeviceRepository.kt new file mode 100644 index 0000000000..20b8ff1840 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/repository/WarnOnUnknownDeviceRepository.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.repository + +import org.matrix.android.sdk.internal.session.SessionScope +import javax.inject.Inject + +@SessionScope +internal class WarnOnUnknownDeviceRepository @Inject constructor() { + + // TODO: set it back to true by default. Need UI + // Warn the user if some new devices are detected while encrypting a message. + private var warnOnUnknownDevices = false + + /** + * Tells if the encryption must fail if some unknown devices are detected. + * + * @return true to warn when some unknown devices are detected. + */ + fun warnOnUnknownDevices() = warnOnUnknownDevices + + /** + * Update the warn status when some unknown devices are detected. + * + * @param warn true to warn when some unknown devices are detected. + */ + fun setWarnOnUnknownDevices(warn: Boolean) { + warnOnUnknownDevices = warn + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt new file mode 100644 index 0000000000..7186bc3cd0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt @@ -0,0 +1,447 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.secrets + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.listeners.ProgressListener +import org.matrix.android.sdk.api.session.accountdata.AccountDataService +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.securestorage.EncryptedSecretContent +import org.matrix.android.sdk.api.session.securestorage.IntegrityResult +import org.matrix.android.sdk.api.session.securestorage.KeyInfo +import org.matrix.android.sdk.api.session.securestorage.KeyInfoResult +import org.matrix.android.sdk.api.session.securestorage.KeySigner +import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec +import org.matrix.android.sdk.api.session.securestorage.SecretStorageKeyContent +import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageError +import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService +import org.matrix.android.sdk.api.session.securestorage.SsssKeyCreationInfo +import org.matrix.android.sdk.api.session.securestorage.SsssKeySpec +import org.matrix.android.sdk.api.session.securestorage.SsssPassphrase +import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.SSSS_ALGORITHM_AES_HMAC_SHA2 +import org.matrix.android.sdk.internal.crypto.SSSS_ALGORITHM_CURVE25519_AES_SHA2 +import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64 +import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding +import org.matrix.android.sdk.internal.crypto.keysbackup.generatePrivateKeyWithPassword +import org.matrix.android.sdk.internal.crypto.keysbackup.util.computeRecoveryKey +import org.matrix.android.sdk.internal.crypto.tools.HkdfSha256 +import org.matrix.android.sdk.internal.crypto.tools.withOlmDecryption +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.extensions.foldToCallback +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.matrix.olm.OlmPkMessage +import java.security.SecureRandom +import javax.crypto.Cipher +import javax.crypto.Mac +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec +import javax.inject.Inject +import kotlin.experimental.and + +internal class DefaultSharedSecretStorageService @Inject constructor( + @UserId private val userId: String, + private val accountDataService: AccountDataService, + private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val cryptoCoroutineScope: CoroutineScope +) : SharedSecretStorageService { + + override fun generateKey(keyId: String, + key: SsssKeySpec?, + keyName: String, + keySigner: KeySigner?, + callback: MatrixCallback) { + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + val bytes = try { + (key as? RawBytesKeySpec)?.privateKey + ?: ByteArray(32).also { + SecureRandom().nextBytes(it) + } + } catch (failure: Throwable) { + callback.onFailure(failure) + return@launch + } + + val storageKeyContent = SecretStorageKeyContent( + name = keyName, + algorithm = SSSS_ALGORITHM_AES_HMAC_SHA2, + passphrase = null + ) + + val signedContent = keySigner?.sign(storageKeyContent.canonicalSignable())?.let { + storageKeyContent.copy( + signatures = it + ) + } ?: storageKeyContent + + accountDataService.updateAccountData( + "$KEY_ID_BASE.$keyId", + signedContent.toContent(), + object : MatrixCallback { + override fun onFailure(failure: Throwable) { + callback.onFailure(failure) + } + + override fun onSuccess(data: Unit) { + callback.onSuccess(SsssKeyCreationInfo( + keyId = keyId, + content = storageKeyContent, + recoveryKey = computeRecoveryKey(bytes), + keySpec = RawBytesKeySpec(bytes) + )) + } + } + ) + } + } + + override fun generateKeyWithPassphrase(keyId: String, + keyName: String, + passphrase: String, + keySigner: KeySigner, + progressListener: ProgressListener?, + callback: MatrixCallback) { + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + val privatePart = generatePrivateKeyWithPassword(passphrase, progressListener) + + val storageKeyContent = SecretStorageKeyContent( + algorithm = SSSS_ALGORITHM_AES_HMAC_SHA2, + passphrase = SsssPassphrase(algorithm = "m.pbkdf2", iterations = privatePart.iterations, salt = privatePart.salt) + ) + + val signedContent = keySigner.sign(storageKeyContent.canonicalSignable())?.let { + storageKeyContent.copy( + signatures = it + ) + } ?: storageKeyContent + + accountDataService.updateAccountData( + "$KEY_ID_BASE.$keyId", + signedContent.toContent(), + object : MatrixCallback { + override fun onFailure(failure: Throwable) { + callback.onFailure(failure) + } + + override fun onSuccess(data: Unit) { + callback.onSuccess(SsssKeyCreationInfo( + keyId = keyId, + content = storageKeyContent, + recoveryKey = computeRecoveryKey(privatePart.privateKey), + keySpec = RawBytesKeySpec(privatePart.privateKey) + )) + } + } + ) + } + } + + override fun hasKey(keyId: String): Boolean { + return accountDataService.getAccountDataEvent("$KEY_ID_BASE.$keyId") != null + } + + override fun getKey(keyId: String): KeyInfoResult { + val accountData = accountDataService.getAccountDataEvent("$KEY_ID_BASE.$keyId") + ?: return KeyInfoResult.Error(SharedSecretStorageError.UnknownKey(keyId)) + return SecretStorageKeyContent.fromJson(accountData.content)?.let { + KeyInfoResult.Success( + KeyInfo(id = keyId, content = it) + ) + } ?: KeyInfoResult.Error(SharedSecretStorageError.UnknownAlgorithm(keyId)) + } + + override fun setDefaultKey(keyId: String, callback: MatrixCallback) { + val existingKey = getKey(keyId) + if (existingKey is KeyInfoResult.Success) { + accountDataService.updateAccountData(DEFAULT_KEY_ID, + mapOf("key" to keyId), + callback + ) + } else { + callback.onFailure(SharedSecretStorageError.UnknownKey(keyId)) + } + } + + override fun getDefaultKey(): KeyInfoResult { + val accountData = accountDataService.getAccountDataEvent(DEFAULT_KEY_ID) + ?: return KeyInfoResult.Error(SharedSecretStorageError.UnknownKey(DEFAULT_KEY_ID)) + val keyId = accountData.content["key"] as? String + ?: return KeyInfoResult.Error(SharedSecretStorageError.UnknownKey(DEFAULT_KEY_ID)) + return getKey(keyId) + } + + override fun storeSecret(name: String, secretBase64: String, keys: List, callback: MatrixCallback) { + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + val encryptedContents = HashMap() + try { + keys.forEach { + val keyId = it.keyId + // encrypt the content + when (val key = keyId?.let { getKey(keyId) } ?: getDefaultKey()) { + is KeyInfoResult.Success -> { + if (key.keyInfo.content.algorithm == SSSS_ALGORITHM_AES_HMAC_SHA2) { + encryptAesHmacSha2(it.keySpec!!, name, secretBase64).let { + encryptedContents[key.keyInfo.id] = it + } + } else { + // Unknown algorithm + callback.onFailure(SharedSecretStorageError.UnknownAlgorithm(key.keyInfo.content.algorithm ?: "")) + return@launch + } + } + is KeyInfoResult.Error -> { + callback.onFailure(key.error) + return@launch + } + } + } + + accountDataService.updateAccountData( + type = name, + content = mapOf( + "encrypted" to encryptedContents + ), + callback = callback + ) + } catch (failure: Throwable) { + callback.onFailure(failure) + } + } + } + + /** + * Encryption algorithm m.secret_storage.v1.aes-hmac-sha2 + * Secrets are encrypted using AES-CTR-256 and MACed using HMAC-SHA-256. The data is encrypted and MACed as follows: + * + * Given the secret storage key, generate 64 bytes by performing an HKDF with SHA-256 as the hash, a salt of 32 bytes + * of 0, and with the secret name as the info. + * + * The first 32 bytes are used as the AES key, and the next 32 bytes are used as the MAC key + * + * Generate 16 random bytes, set bit 63 to 0 (in order to work around differences in AES-CTR implementations), and use + * this as the AES initialization vector. + * This becomes the iv property, encoded using base64. + * + * Encrypt the data using AES-CTR-256 using the AES key generated above. + * + * This encrypted data, encoded using base64, becomes the ciphertext property. + * + * Pass the raw encrypted data (prior to base64 encoding) through HMAC-SHA-256 using the MAC key generated above. + * The resulting MAC is base64-encoded and becomes the mac property. + * (We use AES-CTR to match file encryption and key exports.) + */ + @Throws + private fun encryptAesHmacSha2(secretKey: SsssKeySpec, secretName: String, clearDataBase64: String): EncryptedSecretContent { + secretKey as RawBytesKeySpec + val pseudoRandomKey = HkdfSha256.deriveSecret( + secretKey.privateKey, + ByteArray(32) { 0.toByte() }, + secretName.toByteArray(), + 64) + + // The first 32 bytes are used as the AES key, and the next 32 bytes are used as the MAC key + val aesKey = pseudoRandomKey.copyOfRange(0, 32) + val macKey = pseudoRandomKey.copyOfRange(32, 64) + + val secureRandom = SecureRandom() + val iv = ByteArray(16) + secureRandom.nextBytes(iv) + + // clear bit 63 of the salt to stop us hitting the 64-bit counter boundary + // (which would mean we wouldn't be able to decrypt on Android). The loss + // of a single bit of salt is a price we have to pay. + iv[9] = iv[9] and 0x7f + + val cipher = Cipher.getInstance("AES/CTR/NoPadding") + + val secretKeySpec = SecretKeySpec(aesKey, "AES") + val ivParameterSpec = IvParameterSpec(iv) + cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec) + // secret are not that big, just do Final + val cipherBytes = cipher.doFinal(clearDataBase64.toByteArray()) + require(cipherBytes.isNotEmpty()) + + val macKeySpec = SecretKeySpec(macKey, "HmacSHA256") + val mac = Mac.getInstance("HmacSHA256") + mac.init(macKeySpec) + val digest = mac.doFinal(cipherBytes) + + return EncryptedSecretContent( + ciphertext = cipherBytes.toBase64NoPadding(), + initializationVector = iv.toBase64NoPadding(), + mac = digest.toBase64NoPadding() + ) + } + + private fun decryptAesHmacSha2(secretKey: SsssKeySpec, secretName: String, cipherContent: EncryptedSecretContent): String { + secretKey as RawBytesKeySpec + val pseudoRandomKey = HkdfSha256.deriveSecret( + secretKey.privateKey, + ByteArray(32) { 0.toByte() }, + secretName.toByteArray(), + 64) + + // The first 32 bytes are used as the AES key, and the next 32 bytes are used as the MAC key + val aesKey = pseudoRandomKey.copyOfRange(0, 32) + val macKey = pseudoRandomKey.copyOfRange(32, 64) + + val iv = cipherContent.initializationVector?.fromBase64() ?: ByteArray(16) + + val cipherRawBytes = cipherContent.ciphertext?.fromBase64() ?: throw SharedSecretStorageError.BadCipherText + + // Check Signature + val macKeySpec = SecretKeySpec(macKey, "HmacSHA256") + val mac = Mac.getInstance("HmacSHA256").apply { init(macKeySpec) } + val digest = mac.doFinal(cipherRawBytes) + + if (!cipherContent.mac?.fromBase64()?.contentEquals(digest).orFalse()) { + throw SharedSecretStorageError.BadMac + } + + val cipher = Cipher.getInstance("AES/CTR/NoPadding") + + val secretKeySpec = SecretKeySpec(aesKey, "AES") + val ivParameterSpec = IvParameterSpec(iv) + cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec) + // secret are not that big, just do Final + val decryptedSecret = cipher.doFinal(cipherRawBytes) + + require(decryptedSecret.isNotEmpty()) + + return String(decryptedSecret, Charsets.UTF_8) + } + + override fun getAlgorithmsForSecret(name: String): List { + val accountData = accountDataService.getAccountDataEvent(name) + ?: return listOf(KeyInfoResult.Error(SharedSecretStorageError.UnknownSecret(name))) + val encryptedContent = accountData.content[ENCRYPTED] as? Map<*, *> + ?: return listOf(KeyInfoResult.Error(SharedSecretStorageError.SecretNotEncrypted(name))) + + val results = ArrayList() + encryptedContent.keys.forEach { + (it as? String)?.let { keyId -> + results.add(getKey(keyId)) + } + } + return results + } + + override fun getSecret(name: String, keyId: String?, secretKey: SsssKeySpec, callback: MatrixCallback) { + val accountData = accountDataService.getAccountDataEvent(name) ?: return Unit.also { + callback.onFailure(SharedSecretStorageError.UnknownSecret(name)) + } + val encryptedContent = accountData.content[ENCRYPTED] as? Map<*, *> ?: return Unit.also { + callback.onFailure(SharedSecretStorageError.SecretNotEncrypted(name)) + } + val key = keyId?.let { getKey(it) } as? KeyInfoResult.Success ?: getDefaultKey() as? KeyInfoResult.Success ?: return Unit.also { + callback.onFailure(SharedSecretStorageError.UnknownKey(name)) + } + + val encryptedForKey = encryptedContent[key.keyInfo.id] ?: return Unit.also { + callback.onFailure(SharedSecretStorageError.SecretNotEncryptedWithKey(name, key.keyInfo.id)) + } + + val secretContent = EncryptedSecretContent.fromJson(encryptedForKey) + ?: return Unit.also { + callback.onFailure(SharedSecretStorageError.ParsingError) + } + + val algorithm = key.keyInfo.content + if (SSSS_ALGORITHM_CURVE25519_AES_SHA2 == algorithm.algorithm) { + val keySpec = secretKey as? RawBytesKeySpec ?: return Unit.also { + callback.onFailure(SharedSecretStorageError.BadKeyFormat) + } + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + kotlin.runCatching { + // decrypt from recovery key + withOlmDecryption { olmPkDecryption -> + olmPkDecryption.setPrivateKey(keySpec.privateKey) + olmPkDecryption.decrypt(OlmPkMessage() + .apply { + mCipherText = secretContent.ciphertext + mEphemeralKey = secretContent.ephemeral + mMac = secretContent.mac + } + ) + } + }.foldToCallback(callback) + } + } else if (SSSS_ALGORITHM_AES_HMAC_SHA2 == algorithm.algorithm) { + val keySpec = secretKey as? RawBytesKeySpec ?: return Unit.also { + callback.onFailure(SharedSecretStorageError.BadKeyFormat) + } + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + kotlin.runCatching { + decryptAesHmacSha2(keySpec, name, secretContent) + }.foldToCallback(callback) + } + } else { + callback.onFailure(SharedSecretStorageError.UnsupportedAlgorithm(algorithm.algorithm ?: "")) + } + } + + companion object { + const val KEY_ID_BASE = "m.secret_storage.key" + const val ENCRYPTED = "encrypted" + const val DEFAULT_KEY_ID = "m.secret_storage.default_key" + } + + override fun checkShouldBeAbleToAccessSecrets(secretNames: List, keyId: String?): IntegrityResult { + if (secretNames.isEmpty()) { + return IntegrityResult.Error(SharedSecretStorageError.UnknownSecret("none")) + } + + val keyInfoResult = if (keyId == null) { + getDefaultKey() + } else { + getKey(keyId) + } + + val keyInfo = (keyInfoResult as? KeyInfoResult.Success)?.keyInfo + ?: return IntegrityResult.Error(SharedSecretStorageError.UnknownKey(keyId ?: "")) + + if (keyInfo.content.algorithm != SSSS_ALGORITHM_AES_HMAC_SHA2 + && keyInfo.content.algorithm != SSSS_ALGORITHM_CURVE25519_AES_SHA2) { + // Unsupported algorithm + return IntegrityResult.Error( + SharedSecretStorageError.UnsupportedAlgorithm(keyInfo.content.algorithm ?: "") + ) + } + + secretNames.forEach { secretName -> + val secretEvent = accountDataService.getAccountDataEvent(secretName) + ?: return IntegrityResult.Error(SharedSecretStorageError.UnknownSecret(secretName)) + if ((secretEvent.content["encrypted"] as? Map<*, *>)?.get(keyInfo.id) == null) { + return IntegrityResult.Error(SharedSecretStorageError.SecretNotEncryptedWithKey(secretName, keyInfo.id)) + } + } + + return IntegrityResult.Success(keyInfo.content.passphrase != null) + } + + override fun requestSecret(name: String, myOtherDeviceId: String) { + outgoingGossipingRequestManager.sendSecretShareRequest( + name, + mapOf(userId to listOf(myOtherDeviceId)) + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt new file mode 100644 index 0000000000..f248e464c2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt @@ -0,0 +1,440 @@ + +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.store + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.internal.crypto.GossipingRequestState +import org.matrix.android.sdk.internal.crypto.IncomingRoomKeyRequest +import org.matrix.android.sdk.internal.crypto.IncomingShareRequestCommon +import org.matrix.android.sdk.internal.crypto.NewSessionListener +import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestState +import org.matrix.android.sdk.internal.crypto.OutgoingRoomKeyRequest +import org.matrix.android.sdk.internal.crypto.OutgoingSecretRequest +import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 +import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper +import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent +import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo +import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody +import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity +import org.matrix.olm.OlmAccount + +/** + * the crypto data store + */ +internal interface IMXCryptoStore { + + /** + * @return the device id + */ + fun getDeviceId(): String + + /** + * @return the olm account + */ + fun getOlmAccount(): OlmAccount + + fun getOrCreateOlmAccount(): OlmAccount + + /** + * Retrieve the known inbound group sessions. + * + * @return the list of all known group sessions, to export them. + */ + fun getInboundGroupSessions(): List + + /** + * @return true to unilaterally blacklist all unverified devices. + */ + fun getGlobalBlacklistUnverifiedDevices(): Boolean + + /** + * Set the global override for whether the client should ever send encrypted + * messages to unverified devices. + * If false, it can still be overridden per-room. + * If true, it overrides the per-room settings. + * + * @param block true to unilaterally blacklist all + */ + fun setGlobalBlacklistUnverifiedDevices(block: Boolean) + + /** + * Provides the rooms ids list in which the messages are not encrypted for the unverified devices. + * + * @return the room Ids list + */ + fun getRoomsListBlacklistUnverifiedDevices(): List + + /** + * Updates the rooms ids list in which the messages are not encrypted for the unverified devices. + * + * @param roomIds the room ids list + */ + fun setRoomsListBlacklistUnverifiedDevices(roomIds: List) + + /** + * Get the current keys backup version + */ + fun getKeyBackupVersion(): String? + + /** + * Set the current keys backup version + * + * @param keyBackupVersion the keys backup version or null to delete it + */ + fun setKeyBackupVersion(keyBackupVersion: String?) + + /** + * Get the current keys backup local data + */ + fun getKeysBackupData(): KeysBackupDataEntity? + + /** + * Set the keys backup local data + * + * @param keysBackupData the keys backup local data, or null to erase data + */ + fun setKeysBackupData(keysBackupData: KeysBackupDataEntity?) + + /** + * @return the devices statuses map (userId -> tracking status) + */ + fun getDeviceTrackingStatuses(): Map + + /** + * @return the pending IncomingRoomKeyRequest requests + */ + fun getPendingIncomingRoomKeyRequests(): List + + fun getPendingIncomingGossipingRequests(): List + fun storeIncomingGossipingRequest(request: IncomingShareRequestCommon, ageLocalTS: Long?) +// fun getPendingIncomingSecretShareRequests(): List + + /** + * Indicate if the store contains data for the passed account. + * + * @return true means that the user enabled the crypto in a previous session + */ + fun hasData(): Boolean + + /** + * Delete the crypto store for the passed credentials. + */ + fun deleteStore() + + /** + * open any existing crypto store + */ + fun open() + + /** + * Close the store + */ + fun close() + + /** + * Store the device id. + * + * @param deviceId the device id + */ + fun storeDeviceId(deviceId: String) + + /** + * Store the end to end account for the logged-in user. + * + * @param account the account to save + */ + fun saveOlmAccount() + + /** + * Retrieve a device for a user. + * + * @param deviceId the device id. + * @param userId the user's id. + * @return the device + */ + fun getUserDevice(userId: String, deviceId: String): CryptoDeviceInfo? + + /** + * Retrieve a device by its identity key. + * + * @param identityKey the device identity key (`MXDeviceInfo.identityKey`) + * @return the device or null if not found + */ + fun deviceWithIdentityKey(identityKey: String): CryptoDeviceInfo? + + /** + * Store the known devices for a user. + * + * @param userId The user's id. + * @param devices A map from device id to 'MXDevice' object for the device. + */ + fun storeUserDevices(userId: String, devices: Map?) + + fun storeUserCrossSigningKeys(userId: String, + masterKey: CryptoCrossSigningKey?, + selfSigningKey: CryptoCrossSigningKey?, + userSigningKey: CryptoCrossSigningKey?) + + /** + * Retrieve the known devices for a user. + * + * @param userId The user's id. + * @return The devices map if some devices are known, else null + */ + fun getUserDevices(userId: String): Map? + + fun getUserDeviceList(userId: String): List? + + fun getLiveDeviceList(userId: String): LiveData> + + fun getLiveDeviceList(userIds: List): LiveData> + + // TODO temp + fun getLiveDeviceList(): LiveData> + + fun getMyDevicesInfo() : List + + fun getLiveMyDevicesInfo() : LiveData> + + fun saveMyDevicesInfo(info: List) + /** + * Store the crypto algorithm for a room. + * + * @param roomId the id of the room. + * @param algorithm the algorithm. + */ + fun storeRoomAlgorithm(roomId: String, algorithm: String) + + /** + * Provides the algorithm used in a dedicated room. + * + * @param roomId the room id + * @return the algorithm, null is the room is not encrypted + */ + fun getRoomAlgorithm(roomId: String): String? + + fun shouldEncryptForInvitedMembers(roomId: String): Boolean + + fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean) + + /** + * Store a session between the logged-in user and another device. + * + * @param olmSessionWrapper the end-to-end session. + * @param deviceKey the public key of the other device. + */ + fun storeSession(olmSessionWrapper: OlmSessionWrapper, deviceKey: String) + + /** + * Retrieve the end-to-end session ids between the logged-in user and another + * device. + * + * @param deviceKey the public key of the other device. + * @return A set of sessionId, or null if device is not known + */ + fun getDeviceSessionIds(deviceKey: String): Set? + + /** + * Retrieve an end-to-end session between the logged-in user and another + * device. + * + * @param sessionId the session Id. + * @param deviceKey the public key of the other device. + * @return The Base64 end-to-end session, or null if not found + */ + fun getDeviceSession(sessionId: String, deviceKey: String): OlmSessionWrapper? + + /** + * Retrieve the last used sessionId, regarding `lastReceivedMessageTs`, or null if no session exist + * + * @param deviceKey the public key of the other device. + * @return last used sessionId, or null if not found + */ + fun getLastUsedSessionId(deviceKey: String): String? + + /** + * Store inbound group sessions. + * + * @param sessions the inbound group sessions to store. + */ + fun storeInboundGroupSessions(sessions: List) + + /** + * Retrieve an inbound group session. + * + * @param sessionId the session identifier. + * @param senderKey the base64-encoded curve25519 key of the sender. + * @return an inbound group session. + */ + fun getInboundGroupSession(sessionId: String, senderKey: String): OlmInboundGroupSessionWrapper2? + + /** + * Remove an inbound group session + * + * @param sessionId the session identifier. + * @param senderKey the base64-encoded curve25519 key of the sender. + */ + fun removeInboundGroupSession(sessionId: String, senderKey: String) + + /* ========================================================================================== + * Keys backup + * ========================================================================================== */ + + /** + * Mark all inbound group sessions as not backed up. + */ + fun resetBackupMarkers() + + /** + * Mark inbound group sessions as backed up on the user homeserver. + * + * @param sessions the sessions + */ + fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List) + + /** + * Retrieve inbound group sessions that are not yet backed up. + * + * @param limit the maximum number of sessions to return. + * @return an array of non backed up inbound group sessions. + */ + fun inboundGroupSessionsToBackup(limit: Int): List + + /** + * Number of stored inbound group sessions. + * + * @param onlyBackedUp if true, count only session marked as backed up. + * @return a count. + */ + fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int + + /** + * Save the device statuses + * + * @param deviceTrackingStatuses the device tracking statuses + */ + fun saveDeviceTrackingStatuses(deviceTrackingStatuses: Map) + + /** + * Get the tracking status of a specified userId devices. + * + * @param userId the user id + * @param defaultValue the default value + * @return the tracking status + */ + fun getDeviceTrackingStatus(userId: String, defaultValue: Int): Int + + /** + * Look for an existing outgoing room key request, and if none is found, return null + * + * @param requestBody the request body + * @return an OutgoingRoomKeyRequest instance or null + */ + fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingRoomKeyRequest? + + /** + * Look for an existing outgoing room key request, and if none is found, add a new one. + * + * @param request the request + * @return either the same instance as passed in, or the existing one. + */ + fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map>): OutgoingRoomKeyRequest? + + fun getOrAddOutgoingSecretShareRequest(secretName: String, recipients: Map>): OutgoingSecretRequest? + + fun saveGossipingEvent(event: Event) + + fun updateGossipingRequestState(request: IncomingShareRequestCommon, state: GossipingRequestState) + + /** + * Search an IncomingRoomKeyRequest + * + * @param userId the user id + * @param deviceId the device id + * @param requestId the request id + * @return an IncomingRoomKeyRequest if it exists, else null + */ + fun getIncomingRoomKeyRequest(userId: String, deviceId: String, requestId: String): IncomingRoomKeyRequest? + + fun updateOutgoingGossipingRequestState(requestId: String, state: OutgoingGossipingRequestState) + + fun addNewSessionListener(listener: NewSessionListener) + + fun removeSessionListener(listener: NewSessionListener) + + // ============================================= + // CROSS SIGNING + // ============================================= + + /** + * Gets the current crosssigning info + */ + fun getMyCrossSigningInfo(): MXCrossSigningInfo? + + fun setMyCrossSigningInfo(info: MXCrossSigningInfo?) + + fun getCrossSigningInfo(userId: String): MXCrossSigningInfo? + fun getLiveCrossSigningInfo(userId: String): LiveData> + fun setCrossSigningInfo(userId: String, info: MXCrossSigningInfo?) + + fun markMyMasterKeyAsLocallyTrusted(trusted: Boolean) + + fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?) + fun storeMSKPrivateKey(msk: String?) + fun storeSSKPrivateKey(ssk: String?) + fun storeUSKPrivateKey(usk: String?) + + fun getCrossSigningPrivateKeys(): PrivateKeysInfo? + fun getLiveCrossSigningPrivateKeys(): LiveData> + + fun saveBackupRecoveryKey(recoveryKey: String?, version: String?) + fun getKeyBackupRecoveryKeyInfo() : SavedKeyBackupKeyInfo? + + fun setUserKeysAsTrusted(userId: String, trusted: Boolean = true) + fun setDeviceTrust(userId: String, deviceId: String, crossSignedVerified: Boolean, locallyVerified: Boolean?) + + fun clearOtherUserTrust() + + fun updateUsersTrust(check: (String) -> Boolean) + + fun addWithHeldMegolmSession(withHeldContent: RoomKeyWithHeldContent) + fun getWithHeldMegolmSession(roomId: String, sessionId: String) : RoomKeyWithHeldContent? + + fun markedSessionAsShared(roomId: String?, sessionId: String, userId: String, deviceId: String, chainIndex: Int) + fun wasSessionSharedWithUser(roomId: String?, sessionId: String, userId: String, deviceId: String) : SharedSessionResult + data class SharedSessionResult(val found: Boolean, val chainIndex: Int?) + fun getSharedWithInfo(roomId: String?, sessionId: String) : MXUsersDevicesMap + // Dev tools + + fun getOutgoingRoomKeyRequests(): List + fun getOutgoingSecretKeyRequests(): List + fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest? + fun getIncomingRoomKeyRequests(): List + fun getGossipingEventsTrail(): List + + fun setDeviceKeysUploaded(uploaded: Boolean) + fun getDeviceKeysUploaded(): Boolean +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/PrivateKeysInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/PrivateKeysInfo.kt new file mode 100644 index 0000000000..5c8476ea1f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/PrivateKeysInfo.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.store + +data class PrivateKeysInfo( + val master: String? = null, + val selfSigned: String? = null, + val user: String? = null +) { + fun allKnown() = master != null && selfSigned != null && user != null +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/SavedKeyBackupKeyInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/SavedKeyBackupKeyInfo.kt new file mode 100644 index 0000000000..2d0c53a584 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/SavedKeyBackupKeyInfo.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.store + +data class SavedKeyBackupKeyInfo( + val recoveryKey : String, + val version: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/Helper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/Helper.kt new file mode 100644 index 0000000000..98098686cc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/Helper.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.store.db + +import android.util.Base64 +import org.matrix.android.sdk.internal.util.CompatUtil +import io.realm.Realm +import io.realm.RealmConfiguration +import io.realm.RealmObject +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.ObjectOutputStream +import java.util.zip.GZIPInputStream + +/** + * Get realm, invoke the action, close realm, and return the result of the action + */ +fun doWithRealm(realmConfiguration: RealmConfiguration, action: (Realm) -> T): T { + return Realm.getInstance(realmConfiguration).use { realm -> + action.invoke(realm) + } +} + +/** + * Get realm, do the query, copy from realm, close realm, and return the copied result + */ +fun doRealmQueryAndCopy(realmConfiguration: RealmConfiguration, action: (Realm) -> T?): T? { + return Realm.getInstance(realmConfiguration).use { realm -> + action.invoke(realm)?.let { realm.copyFromRealm(it) } + } +} + +/** + * Get realm, do the list query, copy from realm, close realm, and return the copied result + */ +fun doRealmQueryAndCopyList(realmConfiguration: RealmConfiguration, action: (Realm) -> Iterable): Iterable { + return Realm.getInstance(realmConfiguration).use { realm -> + action.invoke(realm).let { realm.copyFromRealm(it) } + } +} + +/** + * Get realm instance, invoke the action in a transaction and close realm + */ +fun doRealmTransaction(realmConfiguration: RealmConfiguration, action: (Realm) -> Unit) { + Realm.getInstance(realmConfiguration).use { realm -> + realm.executeTransaction { action.invoke(it) } + } +} + +fun doRealmTransactionAsync(realmConfiguration: RealmConfiguration, action: (Realm) -> Unit) { + Realm.getInstance(realmConfiguration).use { realm -> + realm.executeTransactionAsync { action.invoke(it) } + } +} + +/** + * Serialize any Serializable object, zip it and convert to Base64 String + */ +fun serializeForRealm(o: Any?): String? { + if (o == null) { + return null + } + + val baos = ByteArrayOutputStream() + val gzis = CompatUtil.createGzipOutputStream(baos) + val out = ObjectOutputStream(gzis) + out.use { + it.writeObject(o) + } + return Base64.encodeToString(baos.toByteArray(), Base64.DEFAULT) +} + +/** + * Do the opposite of serializeForRealm. + */ +@Suppress("UNCHECKED_CAST") +fun deserializeFromRealm(string: String?): T? { + if (string == null) { + return null + } + val decodedB64 = Base64.decode(string.toByteArray(), Base64.DEFAULT) + + val bais = ByteArrayInputStream(decodedB64) + val gzis = GZIPInputStream(bais) + val ois = SafeObjectInputStream(gzis) + return ois.use { + it.readObject() as T + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt new file mode 100644 index 0000000000..a7b9503a84 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt @@ -0,0 +1,1513 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.store.db + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.LocalEcho +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.toOptional +import org.matrix.android.sdk.internal.crypto.GossipRequestType +import org.matrix.android.sdk.internal.crypto.GossipingRequestState +import org.matrix.android.sdk.internal.crypto.IncomingRoomKeyRequest +import org.matrix.android.sdk.internal.crypto.IncomingSecretShareRequest +import org.matrix.android.sdk.internal.crypto.IncomingShareRequestCommon +import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.internal.crypto.NewSessionListener +import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestState +import org.matrix.android.sdk.internal.crypto.OutgoingRoomKeyRequest +import org.matrix.android.sdk.internal.crypto.OutgoingSecretRequest +import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult +import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 +import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper +import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent +import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo +import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody +import org.matrix.android.sdk.internal.crypto.model.toEntity +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo +import org.matrix.android.sdk.internal.crypto.store.SavedKeyBackupKeyInfo +import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper +import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMapper +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMetadataEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.SharedSessionEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.WithHeldSessionEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.createPrimaryKey +import org.matrix.android.sdk.internal.crypto.store.db.query.create +import org.matrix.android.sdk.internal.crypto.store.db.query.delete +import org.matrix.android.sdk.internal.crypto.store.db.query.get +import org.matrix.android.sdk.internal.crypto.store.db.query.getById +import org.matrix.android.sdk.internal.crypto.store.db.query.getOrCreate +import org.matrix.android.sdk.internal.database.mapper.ContentMapper +import org.matrix.android.sdk.internal.di.CryptoDatabase +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.android.sdk.internal.session.SessionScope +import io.realm.Realm +import io.realm.RealmConfiguration +import io.realm.Sort +import io.realm.kotlin.where +import org.matrix.olm.OlmAccount +import org.matrix.olm.OlmException +import timber.log.Timber +import javax.inject.Inject +import kotlin.collections.set + +@SessionScope +internal class RealmCryptoStore @Inject constructor( + @CryptoDatabase private val realmConfiguration: RealmConfiguration, + private val crossSigningKeysMapper: CrossSigningKeysMapper, + private val credentials: Credentials) : IMXCryptoStore { + + /* ========================================================================================== + * Memory cache, to correctly release JNI objects + * ========================================================================================== */ + + // A realm instance, for faster future getInstance. Do not use it + private var realmLocker: Realm? = null + + // The olm account + private var olmAccount: OlmAccount? = null + + // Cache for OlmSession, to release them properly + private val olmSessionsToRelease = HashMap() + + // Cache for InboundGroupSession, to release them properly + private val inboundGroupSessionToRelease = HashMap() + + private val newSessionListeners = ArrayList() + + override fun addNewSessionListener(listener: NewSessionListener) { + if (!newSessionListeners.contains(listener)) newSessionListeners.add(listener) + } + + override fun removeSessionListener(listener: NewSessionListener) { + newSessionListeners.remove(listener) + } + + private val monarchy = Monarchy.Builder() + .setRealmConfiguration(realmConfiguration) + .build() + + init { + // Ensure CryptoMetadataEntity is inserted in DB + doRealmTransaction(realmConfiguration) { realm -> + var currentMetadata = realm.where().findFirst() + + var deleteAll = false + + if (currentMetadata != null) { + // Check credentials + // The device id may not have been provided in credentials. + // Check it only if provided, else trust the stored one. + if (currentMetadata.userId != credentials.userId + || (credentials.deviceId != null && credentials.deviceId != currentMetadata.deviceId)) { + Timber.w("## open() : Credentials do not match, close this store and delete data") + deleteAll = true + currentMetadata = null + } + } + + if (currentMetadata == null) { + if (deleteAll) { + realm.deleteAll() + } + + // Metadata not found, or database cleaned, create it + realm.createObject(CryptoMetadataEntity::class.java, credentials.userId).apply { + deviceId = credentials.deviceId + } + } + } + } + /* ========================================================================================== + * Other data + * ========================================================================================== */ + + override fun hasData(): Boolean { + return doWithRealm(realmConfiguration) { + !it.isEmpty + // Check if there is a MetaData object + && it.where().count() > 0 + } + } + + override fun deleteStore() { + doRealmTransaction(realmConfiguration) { + it.deleteAll() + } + } + + override fun open() { + synchronized(this) { + if (realmLocker == null) { + realmLocker = Realm.getInstance(realmConfiguration) + } + } + } + + override fun close() { + olmSessionsToRelease.forEach { + it.value.olmSession.releaseSession() + } + olmSessionsToRelease.clear() + + inboundGroupSessionToRelease.forEach { + it.value.olmInboundGroupSession?.releaseSession() + } + inboundGroupSessionToRelease.clear() + + olmAccount?.releaseAccount() + + realmLocker?.close() + realmLocker = null + } + + override fun storeDeviceId(deviceId: String) { + doRealmTransaction(realmConfiguration) { + it.where().findFirst()?.deviceId = deviceId + } + } + + override fun getDeviceId(): String { + return doWithRealm(realmConfiguration) { + it.where().findFirst()?.deviceId + } ?: "" + } + + override fun saveOlmAccount() { + doRealmTransaction(realmConfiguration) { + it.where().findFirst()?.putOlmAccount(olmAccount) + } + } + + override fun getOlmAccount(): OlmAccount { + return olmAccount!! + } + + override fun getOrCreateOlmAccount(): OlmAccount { + doRealmTransaction(realmConfiguration) { + val metaData = it.where().findFirst() + val existing = metaData!!.getOlmAccount() + if (existing == null) { + Timber.d("## Crypto Creating olm account") + val created = OlmAccount() + metaData.putOlmAccount(created) + olmAccount = created + } else { + Timber.d("## Crypto Access existing account") + olmAccount = existing + } + } + return olmAccount!! + } + + override fun getUserDevice(userId: String, deviceId: String): CryptoDeviceInfo? { + return doWithRealm(realmConfiguration) { + it.where() + .equalTo(DeviceInfoEntityFields.PRIMARY_KEY, DeviceInfoEntity.createPrimaryKey(userId, deviceId)) + .findFirst() + ?.let { deviceInfo -> + CryptoMapper.mapToModel(deviceInfo) + } + } + } + + override fun deviceWithIdentityKey(identityKey: String): CryptoDeviceInfo? { + return doWithRealm(realmConfiguration) { + it.where() + .equalTo(DeviceInfoEntityFields.IDENTITY_KEY, identityKey) + .findFirst() + ?.let { deviceInfo -> + CryptoMapper.mapToModel(deviceInfo) + } + } + } + + override fun storeUserDevices(userId: String, devices: Map?) { + doRealmTransaction(realmConfiguration) { realm -> + if (devices == null) { + // Remove the user + UserEntity.delete(realm, userId) + } else { + UserEntity.getOrCreate(realm, userId) + .let { u -> + // Add the devices + val currentKnownDevices = u.devices.toList() + val new = devices.map { entry -> entry.value.toEntity() } + new.forEach { entity -> + // Maintain first time seen + val existing = currentKnownDevices.firstOrNull { it.deviceId == entity.deviceId && it.identityKey == entity.identityKey } + entity.firstTimeSeenLocalTs = existing?.firstTimeSeenLocalTs ?: System.currentTimeMillis() + realm.insertOrUpdate(entity) + } + // Ensure all other devices are deleted + u.devices.deleteAllFromRealm() + u.devices.addAll(new) + } + } + } + } + + override fun storeUserCrossSigningKeys(userId: String, + masterKey: CryptoCrossSigningKey?, + selfSigningKey: CryptoCrossSigningKey?, + userSigningKey: CryptoCrossSigningKey?) { + doRealmTransaction(realmConfiguration) { realm -> + UserEntity.getOrCreate(realm, userId) + .let { userEntity -> + if (masterKey == null || selfSigningKey == null) { + // The user has disabled cross signing? + userEntity.crossSigningInfoEntity?.deleteFromRealm() + userEntity.crossSigningInfoEntity = null + } else { + CrossSigningInfoEntity.getOrCreate(realm, userId).let { signingInfo -> + // What should we do if we detect a change of the keys? + val existingMaster = signingInfo.getMasterKey() + if (existingMaster != null && existingMaster.publicKeyBase64 == masterKey.unpaddedBase64PublicKey) { + crossSigningKeysMapper.update(existingMaster, masterKey) + } else { + val keyEntity = crossSigningKeysMapper.map(masterKey) + signingInfo.setMasterKey(keyEntity) + } + + val existingSelfSigned = signingInfo.getSelfSignedKey() + if (existingSelfSigned != null && existingSelfSigned.publicKeyBase64 == selfSigningKey.unpaddedBase64PublicKey) { + crossSigningKeysMapper.update(existingSelfSigned, selfSigningKey) + } else { + val keyEntity = crossSigningKeysMapper.map(selfSigningKey) + signingInfo.setSelfSignedKey(keyEntity) + } + + // Only for me + if (userSigningKey != null) { + val existingUSK = signingInfo.getUserSigningKey() + if (existingUSK != null && existingUSK.publicKeyBase64 == userSigningKey.unpaddedBase64PublicKey) { + crossSigningKeysMapper.update(existingUSK, userSigningKey) + } else { + val keyEntity = crossSigningKeysMapper.map(userSigningKey) + signingInfo.setUserSignedKey(keyEntity) + } + } + userEntity.crossSigningInfoEntity = signingInfo + } + } + } + } + } + + override fun getCrossSigningPrivateKeys(): PrivateKeysInfo? { + return doWithRealm(realmConfiguration) { realm -> + realm.where() + .findFirst() + ?.let { + PrivateKeysInfo( + master = it.xSignMasterPrivateKey, + selfSigned = it.xSignSelfSignedPrivateKey, + user = it.xSignUserPrivateKey + ) + } + } + } + + override fun getLiveCrossSigningPrivateKeys(): LiveData> { + val liveData = monarchy.findAllMappedWithChanges( + { realm: Realm -> + realm + .where() + }, + { + PrivateKeysInfo( + master = it.xSignMasterPrivateKey, + selfSigned = it.xSignSelfSignedPrivateKey, + user = it.xSignUserPrivateKey + ) + } + ) + return Transformations.map(liveData) { + it.firstOrNull().toOptional() + } + } + + override fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where().findFirst()?.apply { + xSignMasterPrivateKey = msk + xSignUserPrivateKey = usk + xSignSelfSignedPrivateKey = ssk + } + } + } + + override fun saveBackupRecoveryKey(recoveryKey: String?, version: String?) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where().findFirst()?.apply { + keyBackupRecoveryKey = recoveryKey + keyBackupRecoveryKeyVersion = version + } + } + } + + override fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo? { + return doWithRealm(realmConfiguration) { realm -> + realm.where() + .findFirst() + ?.let { + val key = it.keyBackupRecoveryKey + val version = it.keyBackupRecoveryKeyVersion + if (!key.isNullOrBlank() && !version.isNullOrBlank()) { + SavedKeyBackupKeyInfo(recoveryKey = key, version = version) + } else { + null + } + } + } + } + + override fun storeMSKPrivateKey(msk: String?) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where().findFirst()?.apply { + xSignMasterPrivateKey = msk + } + } + } + + override fun storeSSKPrivateKey(ssk: String?) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where().findFirst()?.apply { + xSignSelfSignedPrivateKey = ssk + } + } + } + + override fun storeUSKPrivateKey(usk: String?) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where().findFirst()?.apply { + xSignUserPrivateKey = usk + } + } + } + + override fun getUserDevices(userId: String): Map? { + return doWithRealm(realmConfiguration) { + it.where() + .equalTo(UserEntityFields.USER_ID, userId) + .findFirst() + ?.devices + ?.map { deviceInfo -> + CryptoMapper.mapToModel(deviceInfo) + } + ?.associateBy { cryptoDevice -> + cryptoDevice.deviceId + } + } + } + + override fun getUserDeviceList(userId: String): List? { + return doWithRealm(realmConfiguration) { + it.where() + .equalTo(UserEntityFields.USER_ID, userId) + .findFirst() + ?.devices + ?.map { deviceInfo -> + CryptoMapper.mapToModel(deviceInfo) + } + } + } + + override fun getLiveDeviceList(userId: String): LiveData> { + val liveData = monarchy.findAllMappedWithChanges( + { realm: Realm -> + realm + .where() + .equalTo(UserEntityFields.USER_ID, userId) + }, + { entity -> + entity.devices.map { CryptoMapper.mapToModel(it) } + } + ) + return Transformations.map(liveData) { + it.firstOrNull().orEmpty() + } + } + + override fun getLiveDeviceList(userIds: List): LiveData> { + val liveData = monarchy.findAllMappedWithChanges( + { realm: Realm -> + realm + .where() + .`in`(UserEntityFields.USER_ID, userIds.distinct().toTypedArray()) + }, + { entity -> + entity.devices.map { CryptoMapper.mapToModel(it) } + } + ) + return Transformations.map(liveData) { + it.flatten() + } + } + + override fun getLiveDeviceList(): LiveData> { + val liveData = monarchy.findAllMappedWithChanges( + { realm: Realm -> + realm.where() + }, + { entity -> + entity.devices.map { CryptoMapper.mapToModel(it) } + } + ) + return Transformations.map(liveData) { + it.firstOrNull().orEmpty() + } + } + + override fun getMyDevicesInfo(): List { + return monarchy.fetchAllCopiedSync { + it.where() + }.map { + DeviceInfo( + deviceId = it.deviceId, + lastSeenIp = it.lastSeenIp, + lastSeenTs = it.lastSeenTs, + displayName = it.displayName + ) + } + } + + override fun getLiveMyDevicesInfo(): LiveData> { + return monarchy.findAllMappedWithChanges( + { realm: Realm -> + realm.where() + }, + { entity -> + DeviceInfo( + deviceId = entity.deviceId, + lastSeenIp = entity.lastSeenIp, + lastSeenTs = entity.lastSeenTs, + displayName = entity.displayName + ) + } + ) + } + + override fun saveMyDevicesInfo(info: List) { + val entities = info.map { + MyDeviceLastSeenInfoEntity( + lastSeenTs = it.lastSeenTs, + lastSeenIp = it.lastSeenIp, + displayName = it.displayName, + deviceId = it.deviceId + ) + } + monarchy.writeAsync { realm -> + realm.where().findAll().deleteAllFromRealm() + entities.forEach { + realm.insertOrUpdate(it) + } + } + } + + override fun storeRoomAlgorithm(roomId: String, algorithm: String) { + doRealmTransaction(realmConfiguration) { + CryptoRoomEntity.getOrCreate(it, roomId).algorithm = algorithm + } + } + + override fun getRoomAlgorithm(roomId: String): String? { + return doWithRealm(realmConfiguration) { + CryptoRoomEntity.getById(it, roomId)?.algorithm + } + } + + override fun shouldEncryptForInvitedMembers(roomId: String): Boolean { + return doWithRealm(realmConfiguration) { + CryptoRoomEntity.getById(it, roomId)?.shouldEncryptForInvitedMembers + } + ?: false + } + + override fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean) { + doRealmTransaction(realmConfiguration) { + CryptoRoomEntity.getOrCreate(it, roomId).shouldEncryptForInvitedMembers = shouldEncryptForInvitedMembers + } + } + + override fun storeSession(olmSessionWrapper: OlmSessionWrapper, deviceKey: String) { + var sessionIdentifier: String? = null + + try { + sessionIdentifier = olmSessionWrapper.olmSession.sessionIdentifier() + } catch (e: OlmException) { + Timber.e(e, "## storeSession() : sessionIdentifier failed") + } + + if (sessionIdentifier != null) { + val key = OlmSessionEntity.createPrimaryKey(sessionIdentifier, deviceKey) + + // Release memory of previously known session, if it is not the same one + if (olmSessionsToRelease[key]?.olmSession != olmSessionWrapper.olmSession) { + olmSessionsToRelease[key]?.olmSession?.releaseSession() + } + + olmSessionsToRelease[key] = olmSessionWrapper + + doRealmTransaction(realmConfiguration) { + val realmOlmSession = OlmSessionEntity().apply { + primaryKey = key + sessionId = sessionIdentifier + this.deviceKey = deviceKey + putOlmSession(olmSessionWrapper.olmSession) + lastReceivedMessageTs = olmSessionWrapper.lastReceivedMessageTs + } + + it.insertOrUpdate(realmOlmSession) + } + } + } + + override fun getDeviceSession(sessionId: String, deviceKey: String): OlmSessionWrapper? { + val key = OlmSessionEntity.createPrimaryKey(sessionId, deviceKey) + + // If not in cache (or not found), try to read it from realm + if (olmSessionsToRelease[key] == null) { + doRealmQueryAndCopy(realmConfiguration) { + it.where() + .equalTo(OlmSessionEntityFields.PRIMARY_KEY, key) + .findFirst() + } + ?.let { + val olmSession = it.getOlmSession() + if (olmSession != null && it.sessionId != null) { + olmSessionsToRelease[key] = OlmSessionWrapper(olmSession, it.lastReceivedMessageTs) + } + } + } + + return olmSessionsToRelease[key] + } + + override fun getLastUsedSessionId(deviceKey: String): String? { + return doWithRealm(realmConfiguration) { + it.where() + .equalTo(OlmSessionEntityFields.DEVICE_KEY, deviceKey) + .sort(OlmSessionEntityFields.LAST_RECEIVED_MESSAGE_TS, Sort.DESCENDING) + .findFirst() + ?.sessionId + } + } + + override fun getDeviceSessionIds(deviceKey: String): MutableSet { + return doWithRealm(realmConfiguration) { + it.where() + .equalTo(OlmSessionEntityFields.DEVICE_KEY, deviceKey) + .findAll() + .mapNotNull { sessionEntity -> + sessionEntity.sessionId + } + } + .toMutableSet() + } + + override fun storeInboundGroupSessions(sessions: List) { + if (sessions.isEmpty()) { + return + } + + doRealmTransaction(realmConfiguration) { realm -> + sessions.forEach { session -> + var sessionIdentifier: String? = null + + try { + sessionIdentifier = session.olmInboundGroupSession?.sessionIdentifier() + } catch (e: OlmException) { + Timber.e(e, "## storeInboundGroupSession() : sessionIdentifier failed") + } + + if (sessionIdentifier != null) { + val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionIdentifier, session.senderKey) + + // Release memory of previously known session, if it is not the same one + if (inboundGroupSessionToRelease[key] != session) { + inboundGroupSessionToRelease[key]?.olmInboundGroupSession?.releaseSession() + } + + inboundGroupSessionToRelease[key] = session + + val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply { + primaryKey = key + sessionId = sessionIdentifier + senderKey = session.senderKey + putInboundGroupSession(session) + } + + realm.insertOrUpdate(realmOlmInboundGroupSession) + } + } + } + } + + override fun getInboundGroupSession(sessionId: String, senderKey: String): OlmInboundGroupSessionWrapper2? { + val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey) + + // If not in cache (or not found), try to read it from realm + if (inboundGroupSessionToRelease[key] == null) { + doWithRealm(realmConfiguration) { + it.where() + .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) + .findFirst() + ?.getInboundGroupSession() + } + ?.let { + inboundGroupSessionToRelease[key] = it + } + } + + return inboundGroupSessionToRelease[key] + } + + /** + * Note: the result will be only use to export all the keys and not to use the OlmInboundGroupSessionWrapper2, + * so there is no need to use or update `inboundGroupSessionToRelease` for native memory management + */ + override fun getInboundGroupSessions(): MutableList { + return doWithRealm(realmConfiguration) { + it.where() + .findAll() + .mapNotNull { inboundGroupSessionEntity -> + inboundGroupSessionEntity.getInboundGroupSession() + } + } + .toMutableList() + } + + override fun removeInboundGroupSession(sessionId: String, senderKey: String) { + val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey) + + // Release memory of previously known session + inboundGroupSessionToRelease[key]?.olmInboundGroupSession?.releaseSession() + inboundGroupSessionToRelease.remove(key) + + doRealmTransaction(realmConfiguration) { + it.where() + .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) + .findAll() + .deleteAllFromRealm() + } + } + + /* ========================================================================================== + * Keys backup + * ========================================================================================== */ + + override fun getKeyBackupVersion(): String? { + return doRealmQueryAndCopy(realmConfiguration) { + it.where().findFirst() + }?.backupVersion + } + + override fun setKeyBackupVersion(keyBackupVersion: String?) { + doRealmTransaction(realmConfiguration) { + it.where().findFirst()?.backupVersion = keyBackupVersion + } + } + + override fun getKeysBackupData(): KeysBackupDataEntity? { + return doRealmQueryAndCopy(realmConfiguration) { + it.where().findFirst() + } + } + + override fun setKeysBackupData(keysBackupData: KeysBackupDataEntity?) { + doRealmTransaction(realmConfiguration) { + if (keysBackupData == null) { + // Clear the table + it.where() + .findAll() + .deleteAllFromRealm() + } else { + // Only one object + it.copyToRealmOrUpdate(keysBackupData) + } + } + } + + override fun resetBackupMarkers() { + doRealmTransaction(realmConfiguration) { + it.where() + .findAll() + .map { inboundGroupSession -> + inboundGroupSession.backedUp = false + } + } + } + + override fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List) { + if (olmInboundGroupSessionWrappers.isEmpty()) { + return + } + + doRealmTransaction(realmConfiguration) { + olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper -> + try { + val key = OlmInboundGroupSessionEntity.createPrimaryKey( + olmInboundGroupSessionWrapper.olmInboundGroupSession?.sessionIdentifier(), + olmInboundGroupSessionWrapper.senderKey) + + it.where() + .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) + .findFirst() + ?.backedUp = true + } catch (e: OlmException) { + Timber.e(e, "OlmException") + } + } + } + } + + override fun inboundGroupSessionsToBackup(limit: Int): List { + return doWithRealm(realmConfiguration) { + it.where() + .equalTo(OlmInboundGroupSessionEntityFields.BACKED_UP, false) + .limit(limit.toLong()) + .findAll() + .mapNotNull { inboundGroupSession -> + inboundGroupSession.getInboundGroupSession() + } + } + } + + override fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int { + return doWithRealm(realmConfiguration) { + it.where() + .apply { + if (onlyBackedUp) { + equalTo(OlmInboundGroupSessionEntityFields.BACKED_UP, true) + } + } + .count() + .toInt() + } + } + + override fun setGlobalBlacklistUnverifiedDevices(block: Boolean) { + doRealmTransaction(realmConfiguration) { + it.where().findFirst()?.globalBlacklistUnverifiedDevices = block + } + } + + override fun getGlobalBlacklistUnverifiedDevices(): Boolean { + return doWithRealm(realmConfiguration) { + it.where().findFirst()?.globalBlacklistUnverifiedDevices + } ?: false + } + + override fun setDeviceKeysUploaded(uploaded: Boolean) { + doRealmTransaction(realmConfiguration) { + it.where().findFirst()?.deviceKeysSentToServer = uploaded + } + } + + override fun getDeviceKeysUploaded(): Boolean { + return doWithRealm(realmConfiguration) { + it.where().findFirst()?.deviceKeysSentToServer + } ?: false + } + + override fun setRoomsListBlacklistUnverifiedDevices(roomIds: List) { + doRealmTransaction(realmConfiguration) { + // Reset all + it.where() + .findAll() + .forEach { room -> + room.blacklistUnverifiedDevices = false + } + + // Enable those in the list + it.where() + .`in`(CryptoRoomEntityFields.ROOM_ID, roomIds.toTypedArray()) + .findAll() + .forEach { room -> + room.blacklistUnverifiedDevices = true + } + } + } + + override fun getRoomsListBlacklistUnverifiedDevices(): MutableList { + return doWithRealm(realmConfiguration) { + it.where() + .equalTo(CryptoRoomEntityFields.BLACKLIST_UNVERIFIED_DEVICES, true) + .findAll() + .mapNotNull { cryptoRoom -> + cryptoRoom.roomId + } + } + .toMutableList() + } + + override fun getDeviceTrackingStatuses(): MutableMap { + return doWithRealm(realmConfiguration) { + it.where() + .findAll() + .associateBy { user -> + user.userId!! + } + .mapValues { entry -> + entry.value.deviceTrackingStatus + } + } + .toMutableMap() + } + + override fun saveDeviceTrackingStatuses(deviceTrackingStatuses: Map) { + doRealmTransaction(realmConfiguration) { + deviceTrackingStatuses + .map { entry -> + UserEntity.getOrCreate(it, entry.key) + .deviceTrackingStatus = entry.value + } + } + } + + override fun getDeviceTrackingStatus(userId: String, defaultValue: Int): Int { + return doWithRealm(realmConfiguration) { + it.where() + .equalTo(UserEntityFields.USER_ID, userId) + .findFirst() + ?.deviceTrackingStatus + } + ?: defaultValue + } + + override fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingRoomKeyRequest? { + return monarchy.fetchAllCopiedSync { realm -> + realm.where() + .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) + }.mapNotNull { + it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest + }.firstOrNull { + it.requestBody?.algorithm == requestBody.algorithm + && it.requestBody?.roomId == requestBody.roomId + && it.requestBody?.senderKey == requestBody.senderKey + && it.requestBody?.sessionId == requestBody.sessionId + } + } + + override fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest? { + return monarchy.fetchAllCopiedSync { realm -> + realm.where() + .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name) + .equalTo(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, secretName) + }.mapNotNull { + it.toOutgoingGossipingRequest() as? OutgoingSecretRequest + }.firstOrNull() + } + + override fun getIncomingRoomKeyRequests(): List { + return monarchy.fetchAllCopiedSync { realm -> + realm.where() + .equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) + }.mapNotNull { + it.toIncomingGossipingRequest() as? IncomingRoomKeyRequest + } + } + + override fun getGossipingEventsTrail(): List { + return monarchy.fetchAllCopiedSync { realm -> + realm.where() + }.map { + it.toModel() + } + } + + override fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map>): OutgoingRoomKeyRequest? { + // Insert the request and return the one passed in parameter + var request: OutgoingRoomKeyRequest? = null + doRealmTransaction(realmConfiguration) { realm -> + + val existing = realm.where() + .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) + .findAll() + .mapNotNull { + it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest + }.firstOrNull { + it.requestBody?.algorithm == requestBody.algorithm + && it.requestBody?.sessionId == requestBody.sessionId + && it.requestBody?.senderKey == requestBody.senderKey + && it.requestBody?.roomId == requestBody.roomId + } + + if (existing == null) { + request = realm.createObject(OutgoingGossipingRequestEntity::class.java).apply { + this.requestId = LocalEcho.createLocalEchoId() + this.setRecipients(recipients) + this.requestState = OutgoingGossipingRequestState.UNSENT + this.type = GossipRequestType.KEY + this.requestedInfoStr = requestBody.toJson() + }.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest + } else { + request = existing + } + } + return request + } + + override fun getOrAddOutgoingSecretShareRequest(secretName: String, recipients: Map>): OutgoingSecretRequest? { + var request: OutgoingSecretRequest? = null + + // Insert the request and return the one passed in parameter + doRealmTransaction(realmConfiguration) { realm -> + val existing = realm.where() + .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name) + .equalTo(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, secretName) + .findAll() + .mapNotNull { + it.toOutgoingGossipingRequest() as? OutgoingSecretRequest + }.firstOrNull() + if (existing == null) { + request = realm.createObject(OutgoingGossipingRequestEntity::class.java).apply { + this.type = GossipRequestType.SECRET + setRecipients(recipients) + this.requestState = OutgoingGossipingRequestState.UNSENT + this.requestId = LocalEcho.createLocalEchoId() + this.requestedInfoStr = secretName + }.toOutgoingGossipingRequest() as? OutgoingSecretRequest + } else { + request = existing + } + } + + return request + } + + override fun saveGossipingEvent(event: Event) { + val now = System.currentTimeMillis() + val ageLocalTs = event.unsignedData?.age?.let { now - it } ?: now + val entity = GossipingEventEntity( + type = event.type, + sender = event.senderId, + ageLocalTs = ageLocalTs, + content = ContentMapper.map(event.content) + ).apply { + sendState = SendState.SYNCED + decryptionResultJson = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).toJson(event.mxDecryptionResult) + decryptionErrorCode = event.mCryptoError?.name + } + doRealmTransaction(realmConfiguration) { realm -> + realm.insertOrUpdate(entity) + } + } + +// override fun getOutgoingRoomKeyRequestByState(states: Set): OutgoingRoomKeyRequest? { +// val statesIndex = states.map { it.ordinal }.toTypedArray() +// return doRealmQueryAndCopy(realmConfiguration) { realm -> +// realm.where() +// .equalTo(GossipingEventEntityFields.SENDER, credentials.userId) +// .findAll() +// .filter {entity -> +// states.any { it == entity.requestState} +// } +// }.mapNotNull { +// ContentMapper.map(it.content)?.toModel() +// } +// ?.toOutgoingRoomKeyRequest() +// } +// +// override fun getOutgoingSecretShareRequestByState(states: Set): OutgoingSecretRequest? { +// val statesIndex = states.map { it.ordinal }.toTypedArray() +// return doRealmQueryAndCopy(realmConfiguration) { +// it.where() +// .`in`(OutgoingSecretRequestEntityFields.STATE, statesIndex) +// .findFirst() +// } +// ?.toOutgoingSecretRequest() +// } + +// override fun updateOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest) { +// doRealmTransaction(realmConfiguration) { +// val obj = OutgoingRoomKeyRequestEntity().apply { +// requestId = request.requestId +// cancellationTxnId = request.cancellationTxnId +// state = request.state.ordinal +// putRecipients(request.recipients) +// putRequestBody(request.requestBody) +// } +// +// it.insertOrUpdate(obj) +// } +// } + +// override fun deleteOutgoingRoomKeyRequest(transactionId: String) { +// doRealmTransaction(realmConfiguration) { +// it.where() +// .equalTo(OutgoingRoomKeyRequestEntityFields.REQUEST_ID, transactionId) +// .findFirst() +// ?.deleteFromRealm() +// } +// } + +// override fun storeIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingRoomKeyRequest?) { +// if (incomingRoomKeyRequest == null) { +// return +// } +// +// doRealmTransaction(realmConfiguration) { +// // Delete any previous store request with the same parameters +// it.where() +// .equalTo(IncomingRoomKeyRequestEntityFields.USER_ID, incomingRoomKeyRequest.userId) +// .equalTo(IncomingRoomKeyRequestEntityFields.DEVICE_ID, incomingRoomKeyRequest.deviceId) +// .equalTo(IncomingRoomKeyRequestEntityFields.REQUEST_ID, incomingRoomKeyRequest.requestId) +// .findAll() +// .deleteAllFromRealm() +// +// // Then store it +// it.createObject(IncomingRoomKeyRequestEntity::class.java).apply { +// userId = incomingRoomKeyRequest.userId +// deviceId = incomingRoomKeyRequest.deviceId +// requestId = incomingRoomKeyRequest.requestId +// putRequestBody(incomingRoomKeyRequest.requestBody) +// } +// } +// } + +// override fun deleteIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingShareRequestCommon) { +// doRealmTransaction(realmConfiguration) { +// it.where() +// .equalTo(GossipingEventEntityFields.TYPE, EventType.ROOM_KEY_REQUEST) +// .notEqualTo(GossipingEventEntityFields.SENDER, credentials.userId) +// .findAll() +// .filter { +// ContentMapper.map(it.content).toModel()?.let { +// +// } +// } +// // .equalTo(IncomingRoomKeyRequestEntityFields.USER_ID, incomingRoomKeyRequest.userId) +// // .equalTo(IncomingRoomKeyRequestEntityFields.DEVICE_ID, incomingRoomKeyRequest.deviceId) +// // .equalTo(IncomingRoomKeyRequestEntityFields.REQUEST_ID, incomingRoomKeyRequest.requestId) +// // .findAll() +// // .deleteAllFromRealm() +// } +// } + + override fun updateGossipingRequestState(request: IncomingShareRequestCommon, state: GossipingRequestState) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where() + .equalTo(IncomingGossipingRequestEntityFields.OTHER_USER_ID, request.userId) + .equalTo(IncomingGossipingRequestEntityFields.OTHER_DEVICE_ID, request.deviceId) + .equalTo(IncomingGossipingRequestEntityFields.REQUEST_ID, request.requestId) + .findAll().forEach { + it.requestState = state + } + } + } + + override fun updateOutgoingGossipingRequestState(requestId: String, state: OutgoingGossipingRequestState) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where() + .equalTo(OutgoingGossipingRequestEntityFields.REQUEST_ID, requestId) + .findAll().forEach { + it.requestState = state + } + } + } + + override fun getIncomingRoomKeyRequest(userId: String, deviceId: String, requestId: String): IncomingRoomKeyRequest? { + return doWithRealm(realmConfiguration) { realm -> + realm.where() + .equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) + .equalTo(IncomingGossipingRequestEntityFields.OTHER_DEVICE_ID, deviceId) + .equalTo(IncomingGossipingRequestEntityFields.OTHER_USER_ID, userId) + .findAll() + .mapNotNull { entity -> + entity.toIncomingGossipingRequest() as? IncomingRoomKeyRequest + } + .firstOrNull() + } + } + + override fun getPendingIncomingRoomKeyRequests(): List { + return doWithRealm(realmConfiguration) { + it.where() + .equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) + .equalTo(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, GossipingRequestState.PENDING.name) + .findAll() + .map { entity -> + IncomingRoomKeyRequest( + userId = entity.otherUserId, + deviceId = entity.otherDeviceId, + requestId = entity.requestId, + requestBody = entity.getRequestedKeyInfo(), + localCreationTimestamp = entity.localCreationTimestamp + ) + } + } + } + + override fun getPendingIncomingGossipingRequests(): List { + return doWithRealm(realmConfiguration) { + it.where() + .equalTo(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, GossipingRequestState.PENDING.name) + .findAll() + .mapNotNull { entity -> + when (entity.type) { + GossipRequestType.KEY -> { + IncomingRoomKeyRequest( + userId = entity.otherUserId, + deviceId = entity.otherDeviceId, + requestId = entity.requestId, + requestBody = entity.getRequestedKeyInfo(), + localCreationTimestamp = entity.localCreationTimestamp + ) + } + GossipRequestType.SECRET -> { + IncomingSecretShareRequest( + userId = entity.otherUserId, + deviceId = entity.otherDeviceId, + requestId = entity.requestId, + secretName = entity.getRequestedSecretName(), + localCreationTimestamp = entity.localCreationTimestamp + ) + } + } + } + } + } + + override fun storeIncomingGossipingRequest(request: IncomingShareRequestCommon, ageLocalTS: Long?) { + doRealmTransactionAsync(realmConfiguration) { realm -> + + // After a clear cache, we might have a + + realm.createObject(IncomingGossipingRequestEntity::class.java).let { + it.otherDeviceId = request.deviceId + it.otherUserId = request.userId + it.requestId = request.requestId ?: "" + it.requestState = GossipingRequestState.PENDING + it.localCreationTimestamp = ageLocalTS ?: System.currentTimeMillis() + if (request is IncomingSecretShareRequest) { + it.type = GossipRequestType.SECRET + it.requestedInfoStr = request.secretName + } else if (request is IncomingRoomKeyRequest) { + it.type = GossipRequestType.KEY + it.requestedInfoStr = request.requestBody?.toJson() + } + } + } + } + +// override fun getPendingIncomingSecretShareRequests(): List { +// return doRealmQueryAndCopyList(realmConfiguration) { +// it.where() +// .findAll() +// }.map { +// it.toIncomingSecretShareRequest() +// } +// } + + /* ========================================================================================== + * Cross Signing + * ========================================================================================== */ + override fun getMyCrossSigningInfo(): MXCrossSigningInfo? { + return doWithRealm(realmConfiguration) { + it.where().findFirst()?.userId + }?.let { + getCrossSigningInfo(it) + } + } + + override fun setMyCrossSigningInfo(info: MXCrossSigningInfo?) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where().findFirst()?.userId?.let { userId -> + addOrUpdateCrossSigningInfo(realm, userId, info) + } + } + } + + override fun setUserKeysAsTrusted(userId: String, trusted: Boolean) { + doRealmTransaction(realmConfiguration) { realm -> + val xInfoEntity = realm.where(CrossSigningInfoEntity::class.java) + .equalTo(CrossSigningInfoEntityFields.USER_ID, userId) + .findFirst() + xInfoEntity?.crossSigningKeys?.forEach { info -> + val level = info.trustLevelEntity + if (level == null) { + val newLevel = realm.createObject(TrustLevelEntity::class.java) + newLevel.locallyVerified = trusted + newLevel.crossSignedVerified = trusted + info.trustLevelEntity = newLevel + } else { + level.locallyVerified = trusted + level.crossSignedVerified = trusted + } + } + } + } + + override fun setDeviceTrust(userId: String, deviceId: String, crossSignedVerified: Boolean, locallyVerified: Boolean?) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where(DeviceInfoEntity::class.java) + .equalTo(DeviceInfoEntityFields.PRIMARY_KEY, DeviceInfoEntity.createPrimaryKey(userId, deviceId)) + .findFirst()?.let { deviceInfoEntity -> + val trustEntity = deviceInfoEntity.trustLevelEntity + if (trustEntity == null) { + realm.createObject(TrustLevelEntity::class.java).let { + it.locallyVerified = locallyVerified + it.crossSignedVerified = crossSignedVerified + deviceInfoEntity.trustLevelEntity = it + } + } else { + locallyVerified?.let { trustEntity.locallyVerified = it } + trustEntity.crossSignedVerified = crossSignedVerified + } + } + } + } + + override fun clearOtherUserTrust() { + doRealmTransaction(realmConfiguration) { realm -> + val xInfoEntities = realm.where(CrossSigningInfoEntity::class.java) + .findAll() + xInfoEntities?.forEach { info -> + // Need to ignore mine + if (info.userId != credentials.userId) { + info.crossSigningKeys.forEach { + it.trustLevelEntity = null + } + } + } + } + } + + override fun updateUsersTrust(check: (String) -> Boolean) { + doRealmTransaction(realmConfiguration) { realm -> + val xInfoEntities = realm.where(CrossSigningInfoEntity::class.java) + .findAll() + xInfoEntities?.forEach { xInfoEntity -> + // Need to ignore mine + if (xInfoEntity.userId == credentials.userId) return@forEach + val mapped = mapCrossSigningInfoEntity(xInfoEntity) + val currentTrust = mapped.isTrusted() + val newTrust = check(mapped.userId) + if (currentTrust != newTrust) { + xInfoEntity.crossSigningKeys.forEach { info -> + val level = info.trustLevelEntity + if (level == null) { + val newLevel = realm.createObject(TrustLevelEntity::class.java) + newLevel.locallyVerified = newTrust + newLevel.crossSignedVerified = newTrust + info.trustLevelEntity = newLevel + } else { + level.locallyVerified = newTrust + level.crossSignedVerified = newTrust + } + } + } + } + } + } + + override fun getOutgoingRoomKeyRequests(): List { + return monarchy.fetchAllMappedSync({ realm -> + realm + .where(OutgoingGossipingRequestEntity::class.java) + .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) + }, { entity -> + entity.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest + }) + .filterNotNull() + } + + override fun getOutgoingSecretKeyRequests(): List { + return monarchy.fetchAllMappedSync({ realm -> + realm + .where(OutgoingGossipingRequestEntity::class.java) + .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name) + }, { entity -> + entity.toOutgoingGossipingRequest() as? OutgoingSecretRequest + }) + .filterNotNull() + } + + override fun getCrossSigningInfo(userId: String): MXCrossSigningInfo? { + return doWithRealm(realmConfiguration) { realm -> + val crossSigningInfo = realm.where(CrossSigningInfoEntity::class.java) + .equalTo(CrossSigningInfoEntityFields.USER_ID, userId) + .findFirst() + if (crossSigningInfo == null) { + null + } else { + mapCrossSigningInfoEntity(crossSigningInfo) + } + } + } + + private fun mapCrossSigningInfoEntity(xsignInfo: CrossSigningInfoEntity): MXCrossSigningInfo { + val userId = xsignInfo.userId ?: "" + return MXCrossSigningInfo( + userId = userId, + crossSigningKeys = xsignInfo.crossSigningKeys.mapNotNull { + crossSigningKeysMapper.map(userId, it) + } + ) + } + + override fun getLiveCrossSigningInfo(userId: String): LiveData> { + val liveData = monarchy.findAllMappedWithChanges( + { realm: Realm -> + realm.where() + .equalTo(UserEntityFields.USER_ID, userId) + }, + { mapCrossSigningInfoEntity(it) } + ) + return Transformations.map(liveData) { + it.firstOrNull().toOptional() + } + } + + override fun setCrossSigningInfo(userId: String, info: MXCrossSigningInfo?) { + doRealmTransaction(realmConfiguration) { realm -> + addOrUpdateCrossSigningInfo(realm, userId, info) + } + } + + override fun markMyMasterKeyAsLocallyTrusted(trusted: Boolean) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where().findFirst()?.userId?.let { myUserId -> + CrossSigningInfoEntity.get(realm, myUserId)?.getMasterKey()?.let { xInfoEntity -> + val level = xInfoEntity.trustLevelEntity + if (level == null) { + val newLevel = realm.createObject(TrustLevelEntity::class.java) + newLevel.locallyVerified = trusted + xInfoEntity.trustLevelEntity = newLevel + } else { + level.locallyVerified = trusted + } + } + } + } + } + + private fun addOrUpdateCrossSigningInfo(realm: Realm, userId: String, info: MXCrossSigningInfo?): CrossSigningInfoEntity? { + if (info == null) { + // Delete known if needed + CrossSigningInfoEntity.get(realm, userId)?.deleteFromRealm() + return null + // TODO notify, we might need to untrust things? + } else { + // Just override existing, caller should check and untrust id needed + val existing = CrossSigningInfoEntity.getOrCreate(realm, userId) + existing.crossSigningKeys.deleteAllFromRealm() + existing.crossSigningKeys.addAll( + info.crossSigningKeys.map { + crossSigningKeysMapper.map(it) + } + ) + return existing + } + } + + override fun addWithHeldMegolmSession(withHeldContent: RoomKeyWithHeldContent) { + val roomId = withHeldContent.roomId ?: return + val sessionId = withHeldContent.sessionId ?: return + if (withHeldContent.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return + doRealmTransaction(realmConfiguration) { realm -> + WithHeldSessionEntity.getOrCreate(realm, roomId, sessionId)?.let { + it.code = withHeldContent.code + it.senderKey = withHeldContent.senderKey + it.reason = withHeldContent.reason + } + } + } + + override fun getWithHeldMegolmSession(roomId: String, sessionId: String): RoomKeyWithHeldContent? { + return doWithRealm(realmConfiguration) { realm -> + WithHeldSessionEntity.get(realm, roomId, sessionId)?.let { + RoomKeyWithHeldContent( + roomId = roomId, + sessionId = sessionId, + algorithm = it.algorithm, + codeString = it.codeString, + reason = it.reason, + senderKey = it.senderKey + ) + } + } + } + + override fun markedSessionAsShared(roomId: String?, sessionId: String, userId: String, deviceId: String, chainIndex: Int) { + doRealmTransaction(realmConfiguration) { realm -> + SharedSessionEntity.create( + realm = realm, + roomId = roomId, + sessionId = sessionId, + userId = userId, + deviceId = deviceId, + chainIndex = chainIndex + ) + } + } + + override fun wasSessionSharedWithUser(roomId: String?, sessionId: String, userId: String, deviceId: String): IMXCryptoStore.SharedSessionResult { + return doWithRealm(realmConfiguration) { realm -> + SharedSessionEntity.get(realm, roomId, sessionId, userId, deviceId)?.let { + IMXCryptoStore.SharedSessionResult(true, it.chainIndex) + } ?: IMXCryptoStore.SharedSessionResult(false, null) + } + } + + override fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap { + return doWithRealm(realmConfiguration) { realm -> + val result = MXUsersDevicesMap() + SharedSessionEntity.get(realm, roomId, sessionId) + .groupBy { it.userId } + .forEach { (userId, shared) -> + shared.forEach { + result.setObject(userId, it.deviceId, it.chainIndex) + } + } + + result + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt new file mode 100644 index 0000000000..fbc1eb6bb1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt @@ -0,0 +1,458 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.store.db + +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.crypto.model.MXDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper +import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 +import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper +import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMetadataEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.KeyInfoEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.SharedSessionEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.WithHeldSessionEntityFields +import org.matrix.android.sdk.internal.di.SerializeNulls +import io.realm.DynamicRealm +import io.realm.RealmMigration +import org.matrix.androidsdk.crypto.data.MXOlmInboundGroupSession2 +import timber.log.Timber +import javax.inject.Inject +import org.matrix.androidsdk.crypto.data.MXDeviceInfo as LegacyMXDeviceInfo + +internal class RealmCryptoStoreMigration @Inject constructor(private val crossSigningKeysMapper: CrossSigningKeysMapper) : RealmMigration { + + companion object { + // 0, 1, 2: legacy Riot-Android + // 3: migrate to RiotX schema + // 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6) + const val CRYPTO_STORE_SCHEMA_VERSION = 11L + } + + override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { + Timber.v("Migrating Realm Crypto from $oldVersion to $newVersion") + + if (oldVersion <= 0) migrateTo1Legacy(realm) + if (oldVersion <= 1) migrateTo2Legacy(realm) + if (oldVersion <= 2) migrateTo3RiotX(realm) + if (oldVersion <= 3) migrateTo4(realm) + if (oldVersion <= 4) migrateTo5(realm) + if (oldVersion <= 5) migrateTo6(realm) + if (oldVersion <= 6) migrateTo7(realm) + if (oldVersion <= 7) migrateTo8(realm) + if (oldVersion <= 8) migrateTo9(realm) + if (oldVersion <= 9) migrateTo10(realm) + if (oldVersion <= 10) migrateTo11(realm) + } + + private fun migrateTo1Legacy(realm: DynamicRealm) { + Timber.d("Step 0 -> 1") + Timber.d("Add field lastReceivedMessageTs (Long) and set the value to 0") + + realm.schema.get("OlmSessionEntity") + ?.addField(OlmSessionEntityFields.LAST_RECEIVED_MESSAGE_TS, Long::class.java) + ?.transform { + it.setLong(OlmSessionEntityFields.LAST_RECEIVED_MESSAGE_TS, 0) + } + } + + private fun migrateTo2Legacy(realm: DynamicRealm) { + Timber.d("Step 1 -> 2") + Timber.d("Update IncomingRoomKeyRequestEntity format: requestBodyString field is exploded into several fields") + + realm.schema.get("IncomingRoomKeyRequestEntity") + ?.addField("requestBodyAlgorithm", String::class.java) + ?.addField("requestBodyRoomId", String::class.java) + ?.addField("requestBodySenderKey", String::class.java) + ?.addField("requestBodySessionId", String::class.java) + ?.transform { dynamicObject -> + val requestBodyString = dynamicObject.getString("requestBodyString") + try { + // It was a map before + val map: Map? = deserializeFromRealm(requestBodyString) + + map?.let { + dynamicObject.setString("requestBodyAlgorithm", it["algorithm"]) + dynamicObject.setString("requestBodyRoomId", it["room_id"]) + dynamicObject.setString("requestBodySenderKey", it["sender_key"]) + dynamicObject.setString("requestBodySessionId", it["session_id"]) + } + } catch (e: Exception) { + Timber.e(e, "Error") + } + } + ?.removeField("requestBodyString") + + Timber.d("Update IncomingRoomKeyRequestEntity format: requestBodyString field is exploded into several fields") + + realm.schema.get("OutgoingRoomKeyRequestEntity") + ?.addField("requestBodyAlgorithm", String::class.java) + ?.addField("requestBodyRoomId", String::class.java) + ?.addField("requestBodySenderKey", String::class.java) + ?.addField("requestBodySessionId", String::class.java) + ?.transform { dynamicObject -> + val requestBodyString = dynamicObject.getString("requestBodyString") + try { + // It was a map before + val map: Map? = deserializeFromRealm(requestBodyString) + + map?.let { + dynamicObject.setString("requestBodyAlgorithm", it["algorithm"]) + dynamicObject.setString("requestBodyRoomId", it["room_id"]) + dynamicObject.setString("requestBodySenderKey", it["sender_key"]) + dynamicObject.setString("requestBodySessionId", it["session_id"]) + } + } catch (e: Exception) { + Timber.e(e, "Error") + } + } + ?.removeField("requestBodyString") + + Timber.d("Create KeysBackupDataEntity") + + realm.schema.create("KeysBackupDataEntity") + .addField(KeysBackupDataEntityFields.PRIMARY_KEY, Integer::class.java) + .addPrimaryKey(KeysBackupDataEntityFields.PRIMARY_KEY) + .setRequired(KeysBackupDataEntityFields.PRIMARY_KEY, true) + .addField(KeysBackupDataEntityFields.BACKUP_LAST_SERVER_HASH, String::class.java) + .addField(KeysBackupDataEntityFields.BACKUP_LAST_SERVER_NUMBER_OF_KEYS, Integer::class.java) + } + + private fun migrateTo3RiotX(realm: DynamicRealm) { + Timber.d("Step 2 -> 3") + Timber.d("Migrate to RiotX model") + + realm.schema.get("CryptoRoomEntity") + ?.addField(CryptoRoomEntityFields.SHOULD_ENCRYPT_FOR_INVITED_MEMBERS, Boolean::class.java) + ?.setRequired(CryptoRoomEntityFields.SHOULD_ENCRYPT_FOR_INVITED_MEMBERS, false) + + // Convert format of MXDeviceInfo, package has to be the same. + realm.schema.get("DeviceInfoEntity") + ?.transform { obj -> + try { + val oldSerializedData = obj.getString("deviceInfoData") + deserializeFromRealm(oldSerializedData)?.let { legacyMxDeviceInfo -> + val newMxDeviceInfo = MXDeviceInfo( + deviceId = legacyMxDeviceInfo.deviceId, + userId = legacyMxDeviceInfo.userId, + algorithms = legacyMxDeviceInfo.algorithms, + keys = legacyMxDeviceInfo.keys, + signatures = legacyMxDeviceInfo.signatures, + unsigned = legacyMxDeviceInfo.unsigned, + verified = legacyMxDeviceInfo.mVerified + ) + + obj.setString("deviceInfoData", serializeForRealm(newMxDeviceInfo)) + } + } catch (e: Exception) { + Timber.e(e, "Error") + } + } + + // Convert MXOlmInboundGroupSession2 to OlmInboundGroupSessionWrapper + realm.schema.get("OlmInboundGroupSessionEntity") + ?.transform { obj -> + try { + val oldSerializedData = obj.getString("olmInboundGroupSessionData") + deserializeFromRealm(oldSerializedData)?.let { mxOlmInboundGroupSession2 -> + val sessionKey = mxOlmInboundGroupSession2.mSession.sessionIdentifier() + val newOlmInboundGroupSessionWrapper = OlmInboundGroupSessionWrapper(sessionKey, false) + .apply { + olmInboundGroupSession = mxOlmInboundGroupSession2.mSession + roomId = mxOlmInboundGroupSession2.mRoomId + senderKey = mxOlmInboundGroupSession2.mSenderKey + keysClaimed = mxOlmInboundGroupSession2.mKeysClaimed + forwardingCurve25519KeyChain = mxOlmInboundGroupSession2.mForwardingCurve25519KeyChain + } + + obj.setString("olmInboundGroupSessionData", serializeForRealm(newOlmInboundGroupSessionWrapper)) + } + } catch (e: Exception) { + Timber.e(e, "Error") + } + } + } + + // Version 4L added Cross Signing info persistence + private fun migrateTo4(realm: DynamicRealm) { + Timber.d("Step 3 -> 4") + Timber.d("Create KeyInfoEntity") + + val trustLevelEntityEntitySchema = realm.schema.create("TrustLevelEntity") + .addField(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, Boolean::class.java) + .setNullable(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, true) + .addField(TrustLevelEntityFields.LOCALLY_VERIFIED, Boolean::class.java) + .setNullable(TrustLevelEntityFields.LOCALLY_VERIFIED, true) + + val keyInfoEntitySchema = realm.schema.create("KeyInfoEntity") + .addField(KeyInfoEntityFields.PUBLIC_KEY_BASE64, String::class.java) + .addField(KeyInfoEntityFields.SIGNATURES, String::class.java) + .addRealmListField(KeyInfoEntityFields.USAGES.`$`, String::class.java) + .addRealmObjectField(KeyInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevelEntityEntitySchema) + + Timber.d("Create CrossSigningInfoEntity") + + val crossSigningInfoSchema = realm.schema.create("CrossSigningInfoEntity") + .addField(CrossSigningInfoEntityFields.USER_ID, String::class.java) + .addPrimaryKey(CrossSigningInfoEntityFields.USER_ID) + .addRealmListField(CrossSigningInfoEntityFields.CROSS_SIGNING_KEYS.`$`, keyInfoEntitySchema) + + Timber.d("Updating UserEntity table") + realm.schema.get("UserEntity") + ?.addRealmObjectField(UserEntityFields.CROSS_SIGNING_INFO_ENTITY.`$`, crossSigningInfoSchema) + + Timber.d("Updating CryptoMetadataEntity table") + realm.schema.get("CryptoMetadataEntity") + ?.addField(CryptoMetadataEntityFields.X_SIGN_MASTER_PRIVATE_KEY, String::class.java) + ?.addField(CryptoMetadataEntityFields.X_SIGN_USER_PRIVATE_KEY, String::class.java) + ?.addField(CryptoMetadataEntityFields.X_SIGN_SELF_SIGNED_PRIVATE_KEY, String::class.java) + + val moshi = Moshi.Builder().add(SerializeNulls.JSON_ADAPTER_FACTORY).build() + val listMigrationAdapter = moshi.adapter>(Types.newParameterizedType( + List::class.java, + String::class.java, + Any::class.java + )) + val mapMigrationAdapter = moshi.adapter(Types.newParameterizedType( + Map::class.java, + String::class.java, + Any::class.java + )) + + realm.schema.get("DeviceInfoEntity") + ?.addField(DeviceInfoEntityFields.USER_ID, String::class.java) + ?.addField(DeviceInfoEntityFields.ALGORITHM_LIST_JSON, String::class.java) + ?.addField(DeviceInfoEntityFields.KEYS_MAP_JSON, String::class.java) + ?.addField(DeviceInfoEntityFields.SIGNATURE_MAP_JSON, String::class.java) + ?.addField(DeviceInfoEntityFields.UNSIGNED_MAP_JSON, String::class.java) + ?.addField(DeviceInfoEntityFields.IS_BLOCKED, Boolean::class.java) + ?.setNullable(DeviceInfoEntityFields.IS_BLOCKED, true) + ?.addRealmObjectField(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevelEntityEntitySchema) + ?.transform { obj -> + + try { + val oldSerializedData = obj.getString("deviceInfoData") + deserializeFromRealm(oldSerializedData)?.let { oldDevice -> + + val trustLevel = realm.createObject("TrustLevelEntity") + when (oldDevice.verified) { + MXDeviceInfo.DEVICE_VERIFICATION_UNKNOWN -> { + obj.setNull(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`) + } + MXDeviceInfo.DEVICE_VERIFICATION_BLOCKED -> { + trustLevel.setNull(TrustLevelEntityFields.LOCALLY_VERIFIED) + trustLevel.setNull(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED) + obj.setBoolean(DeviceInfoEntityFields.IS_BLOCKED, oldDevice.isBlocked) + obj.setObject(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevel) + } + MXDeviceInfo.DEVICE_VERIFICATION_UNVERIFIED -> { + trustLevel.setBoolean(TrustLevelEntityFields.LOCALLY_VERIFIED, false) + trustLevel.setBoolean(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, false) + obj.setObject(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevel) + } + MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED -> { + trustLevel.setBoolean(TrustLevelEntityFields.LOCALLY_VERIFIED, true) + trustLevel.setBoolean(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, false) + obj.setObject(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevel) + } + } + + obj.setString(DeviceInfoEntityFields.USER_ID, oldDevice.userId) + obj.setString(DeviceInfoEntityFields.IDENTITY_KEY, oldDevice.identityKey()) + obj.setString(DeviceInfoEntityFields.ALGORITHM_LIST_JSON, listMigrationAdapter.toJson(oldDevice.algorithms)) + obj.setString(DeviceInfoEntityFields.KEYS_MAP_JSON, mapMigrationAdapter.toJson(oldDevice.keys)) + obj.setString(DeviceInfoEntityFields.SIGNATURE_MAP_JSON, mapMigrationAdapter.toJson(oldDevice.signatures)) + obj.setString(DeviceInfoEntityFields.UNSIGNED_MAP_JSON, mapMigrationAdapter.toJson(oldDevice.unsigned)) + } + } catch (failure: Throwable) { + Timber.w(failure, "Crypto Data base migration error") + // an unfortunate refactor did modify that class, making deserialization failing + // so we just skip and ignore.. + } + } + ?.removeField("deviceInfoData") + } + + private fun migrateTo5(realm: DynamicRealm) { + Timber.d("Step 4 -> 5") + realm.schema.remove("OutgoingRoomKeyRequestEntity") + realm.schema.remove("IncomingRoomKeyRequestEntity") + + // Not need to migrate existing request, just start fresh? + + realm.schema.create("GossipingEventEntity") + .addField(GossipingEventEntityFields.TYPE, String::class.java) + .addIndex(GossipingEventEntityFields.TYPE) + .addField(GossipingEventEntityFields.CONTENT, String::class.java) + .addField(GossipingEventEntityFields.SENDER, String::class.java) + .addIndex(GossipingEventEntityFields.SENDER) + .addField(GossipingEventEntityFields.DECRYPTION_RESULT_JSON, String::class.java) + .addField(GossipingEventEntityFields.DECRYPTION_ERROR_CODE, String::class.java) + .addField(GossipingEventEntityFields.AGE_LOCAL_TS, Long::class.java) + .setNullable(GossipingEventEntityFields.AGE_LOCAL_TS, true) + .addField(GossipingEventEntityFields.SEND_STATE_STR, String::class.java) + + realm.schema.create("IncomingGossipingRequestEntity") + .addField(IncomingGossipingRequestEntityFields.REQUEST_ID, String::class.java) + .addIndex(IncomingGossipingRequestEntityFields.REQUEST_ID) + .addField(IncomingGossipingRequestEntityFields.TYPE_STR, String::class.java) + .addIndex(IncomingGossipingRequestEntityFields.TYPE_STR) + .addField(IncomingGossipingRequestEntityFields.OTHER_USER_ID, String::class.java) + .addField(IncomingGossipingRequestEntityFields.REQUESTED_INFO_STR, String::class.java) + .addField(IncomingGossipingRequestEntityFields.OTHER_DEVICE_ID, String::class.java) + .addField(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, String::class.java) + .addField(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, Long::class.java) + .setNullable(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, true) + + realm.schema.create("OutgoingGossipingRequestEntity") + .addField(OutgoingGossipingRequestEntityFields.REQUEST_ID, String::class.java) + .addIndex(OutgoingGossipingRequestEntityFields.REQUEST_ID) + .addField(OutgoingGossipingRequestEntityFields.RECIPIENTS_DATA, String::class.java) + .addField(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, String::class.java) + .addField(OutgoingGossipingRequestEntityFields.TYPE_STR, String::class.java) + .addIndex(OutgoingGossipingRequestEntityFields.TYPE_STR) + .addField(OutgoingGossipingRequestEntityFields.REQUEST_STATE_STR, String::class.java) + } + + private fun migrateTo6(realm: DynamicRealm) { + Timber.d("Step 5 -> 6") + Timber.d("Updating CryptoMetadataEntity table") + realm.schema.get("CryptoMetadataEntity") + ?.addField(CryptoMetadataEntityFields.KEY_BACKUP_RECOVERY_KEY, String::class.java) + ?.addField(CryptoMetadataEntityFields.KEY_BACKUP_RECOVERY_KEY_VERSION, String::class.java) + } + + private fun migrateTo7(realm: DynamicRealm) { + Timber.d("Step 6 -> 7") + Timber.d("Updating KeyInfoEntity table") + val keyInfoEntities = realm.where("KeyInfoEntity").findAll() + try { + keyInfoEntities.forEach { + val stringSignatures = it.getString(KeyInfoEntityFields.SIGNATURES) + val objectSignatures: Map>? = deserializeFromRealm(stringSignatures) + val jsonSignatures = crossSigningKeysMapper.serializeSignatures(objectSignatures) + it.setString(KeyInfoEntityFields.SIGNATURES, jsonSignatures) + } + } catch (failure: Throwable) { + } + + // Migrate frozen classes + val inboundGroupSessions = realm.where("OlmInboundGroupSessionEntity").findAll() + inboundGroupSessions.forEach { dynamicObject -> + dynamicObject.getString(OlmInboundGroupSessionEntityFields.OLM_INBOUND_GROUP_SESSION_DATA)?.let { serializedObject -> + try { + deserializeFromRealm(serializedObject)?.let { oldFormat -> + val newFormat = oldFormat.exportKeys()?.let { + OlmInboundGroupSessionWrapper2(it) + } + dynamicObject.setString(OlmInboundGroupSessionEntityFields.OLM_INBOUND_GROUP_SESSION_DATA, serializeForRealm(newFormat)) + } + } catch (failure: Throwable) { + Timber.e(failure, "## OlmInboundGroupSessionEntity migration failed") + } + } + } + } + + private fun migrateTo8(realm: DynamicRealm) { + Timber.d("Step 7 -> 8") + realm.schema.create("MyDeviceLastSeenInfoEntity") + .addField(MyDeviceLastSeenInfoEntityFields.DEVICE_ID, String::class.java) + .addPrimaryKey(MyDeviceLastSeenInfoEntityFields.DEVICE_ID) + .addField(MyDeviceLastSeenInfoEntityFields.DISPLAY_NAME, String::class.java) + .addField(MyDeviceLastSeenInfoEntityFields.LAST_SEEN_IP, String::class.java) + .addField(MyDeviceLastSeenInfoEntityFields.LAST_SEEN_TS, Long::class.java) + .setNullable(MyDeviceLastSeenInfoEntityFields.LAST_SEEN_TS, true) + + val now = System.currentTimeMillis() + realm.schema.get("DeviceInfoEntity") + ?.addField(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, Long::class.java) + ?.setNullable(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, true) + ?.transform { deviceInfoEntity -> + tryThis { + deviceInfoEntity.setLong(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, now) + } + } + } + + // Fixes duplicate devices in UserEntity#devices + private fun migrateTo9(realm: DynamicRealm) { + Timber.d("Step 8 -> 9") + val userEntities = realm.where("UserEntity").findAll() + userEntities.forEach { + try { + val deviceList = it.getList(UserEntityFields.DEVICES.`$`) + ?: return@forEach + val distinct = deviceList.distinctBy { it.getString(DeviceInfoEntityFields.DEVICE_ID) } + if (distinct.size != deviceList.size) { + deviceList.clear() + deviceList.addAll(distinct) + } + } catch (failure: Throwable) { + Timber.w(failure, "Crypto Data base migration error for migrateTo9") + } + } + } + + // Version 10L added WithHeld Keys Info (MSC2399) + private fun migrateTo10(realm: DynamicRealm) { + Timber.d("Step 9 -> 10") + realm.schema.create("WithHeldSessionEntity") + .addField(WithHeldSessionEntityFields.ROOM_ID, String::class.java) + .addField(WithHeldSessionEntityFields.ALGORITHM, String::class.java) + .addField(WithHeldSessionEntityFields.SESSION_ID, String::class.java) + .addIndex(WithHeldSessionEntityFields.SESSION_ID) + .addField(WithHeldSessionEntityFields.SENDER_KEY, String::class.java) + .addIndex(WithHeldSessionEntityFields.SENDER_KEY) + .addField(WithHeldSessionEntityFields.CODE_STRING, String::class.java) + .addField(WithHeldSessionEntityFields.REASON, String::class.java) + + realm.schema.create("SharedSessionEntity") + .addField(SharedSessionEntityFields.ROOM_ID, String::class.java) + .addField(SharedSessionEntityFields.ALGORITHM, String::class.java) + .addField(SharedSessionEntityFields.SESSION_ID, String::class.java) + .addIndex(SharedSessionEntityFields.SESSION_ID) + .addField(SharedSessionEntityFields.USER_ID, String::class.java) + .addIndex(SharedSessionEntityFields.USER_ID) + .addField(SharedSessionEntityFields.DEVICE_ID, String::class.java) + .addIndex(SharedSessionEntityFields.DEVICE_ID) + .addField(SharedSessionEntityFields.CHAIN_INDEX, Long::class.java) + .setNullable(SharedSessionEntityFields.CHAIN_INDEX, true) + } + + // Version 11L added deviceKeysSentToServer boolean to CryptoMetadataEntity + private fun migrateTo11(realm: DynamicRealm) { + Timber.d("Step 10 -> 11") + realm.schema.get("CryptoMetadataEntity") + ?.addField(CryptoMetadataEntityFields.DEVICE_KEYS_SENT_TO_SERVER, Boolean::class.java) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreModule.kt new file mode 100644 index 0000000000..1103e69bbc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreModule.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.store.db + +import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMetadataEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.KeyInfoEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.SharedSessionEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.WithHeldSessionEntity +import io.realm.annotations.RealmModule + +/** + * Realm module for Crypto store classes + */ +@RealmModule(library = true, + classes = [ + CryptoMetadataEntity::class, + CryptoRoomEntity::class, + DeviceInfoEntity::class, + KeysBackupDataEntity::class, + OlmInboundGroupSessionEntity::class, + OlmSessionEntity::class, + UserEntity::class, + KeyInfoEntity::class, + CrossSigningInfoEntity::class, + TrustLevelEntity::class, + GossipingEventEntity::class, + IncomingGossipingRequestEntity::class, + OutgoingGossipingRequestEntity::class, + MyDeviceLastSeenInfoEntity::class, + WithHeldSessionEntity::class, + SharedSessionEntity::class + ]) +internal class RealmCryptoStoreModule diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/SafeObjectInputStream.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/SafeObjectInputStream.kt new file mode 100644 index 0000000000..17538c7cbe --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/SafeObjectInputStream.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.store.db + +import java.io.IOException +import java.io.InputStream +import java.io.ObjectInputStream +import java.io.ObjectStreamClass + +/** + * Package has been renamed from `im.vector.matrix.android` to `org.matrix.android.sdk` + * so ensure deserialization of previously stored objects still works + * + * Ref: https://stackoverflow.com/questions/3884492/how-can-i-change-package-for-a-bunch-of-java-serializable-classes + */ +internal class SafeObjectInputStream(`in`: InputStream) : ObjectInputStream(`in`) { + + init { + enableResolveObject(true) + } + + @Throws(IOException::class, ClassNotFoundException::class) + override fun readClassDescriptor(): ObjectStreamClass { + val read = super.readClassDescriptor() + if (read.name.startsWith("im.vector.matrix.android.")) { + return ObjectStreamClass.lookup(Class.forName(read.name.replace("im.vector.matrix.android.", "org.matrix.android.sdk."))) + } + return read + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/CrossSigningKeysMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/CrossSigningKeysMapper.kt new file mode 100644 index 0000000000..4a303de81c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/CrossSigningKeysMapper.kt @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.mapper + +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel +import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey +import org.matrix.android.sdk.internal.crypto.store.db.model.KeyInfoEntity +import io.realm.RealmList +import timber.log.Timber +import javax.inject.Inject + +internal class CrossSigningKeysMapper @Inject constructor(moshi: Moshi) { + + private val signaturesAdapter = moshi.adapter>>(Types.newParameterizedType( + Map::class.java, + String::class.java, + Any::class.java + )) + + fun update(keyInfo: KeyInfoEntity, cryptoCrossSigningKey: CryptoCrossSigningKey) { + // update signatures? + keyInfo.signatures = serializeSignatures(cryptoCrossSigningKey.signatures) + keyInfo.usages = cryptoCrossSigningKey.usages?.toTypedArray()?.let { RealmList(*it) } + ?: RealmList() + } + + fun map(userId: String?, keyInfo: KeyInfoEntity?): CryptoCrossSigningKey? { + val pubKey = keyInfo?.publicKeyBase64 ?: return null + return CryptoCrossSigningKey( + userId = userId ?: "", + keys = mapOf("ed25519:$pubKey" to pubKey), + usages = keyInfo.usages.map { it }, + signatures = deserializeSignatures(keyInfo.signatures), + trustLevel = keyInfo.trustLevelEntity?.let { + DeviceTrustLevel( + crossSigningVerified = it.crossSignedVerified ?: false, + locallyVerified = it.locallyVerified ?: false + ) + } + ) + } + + fun map(keyInfo: CryptoCrossSigningKey): KeyInfoEntity { + return KeyInfoEntity().apply { + publicKeyBase64 = keyInfo.unpaddedBase64PublicKey + usages = keyInfo.usages?.let { RealmList(*it.toTypedArray()) } ?: RealmList() + signatures = serializeSignatures(keyInfo.signatures) + // TODO how to handle better, check if same keys? + // reset trust + trustLevelEntity = null + } + } + + fun serializeSignatures(signatures: Map>?): String { + return signaturesAdapter.toJson(signatures) + } + + fun deserializeSignatures(signatures: String?): Map>? { + if (signatures == null) { + return null + } + return try { + signaturesAdapter.fromJson(signatures) + } catch (failure: Throwable) { + Timber.e(failure) + null + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CrossSigningInfoEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CrossSigningInfoEntity.kt new file mode 100644 index 0000000000..94db368e05 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CrossSigningInfoEntity.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.model + +import org.matrix.android.sdk.internal.crypto.model.KeyUsage +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class CrossSigningInfoEntity( + @PrimaryKey + var userId: String? = null, + var crossSigningKeys: RealmList = RealmList() +) : RealmObject() { + + companion object + + fun getMasterKey() = crossSigningKeys.firstOrNull { it.usages.contains(KeyUsage.MASTER.value) } + + fun setMasterKey(info: KeyInfoEntity?) { + crossSigningKeys + .filter { it.usages.contains(KeyUsage.MASTER.value) } + .forEach { crossSigningKeys.remove(it) } + info?.let { crossSigningKeys.add(it) } + } + + fun getSelfSignedKey() = crossSigningKeys.firstOrNull { it.usages.contains(KeyUsage.SELF_SIGNING.value) } + + fun setSelfSignedKey(info: KeyInfoEntity?) { + crossSigningKeys + .filter { it.usages.contains(KeyUsage.SELF_SIGNING.value) } + .forEach { crossSigningKeys.remove(it) } + info?.let { crossSigningKeys.add(it) } + } + + fun getUserSigningKey() = crossSigningKeys.firstOrNull { it.usages.contains(KeyUsage.USER_SIGNING.value) } + + fun setUserSignedKey(info: KeyInfoEntity?) { + crossSigningKeys + .filter { it.usages.contains(KeyUsage.USER_SIGNING.value) } + .forEach { crossSigningKeys.remove(it) } + info?.let { crossSigningKeys.add(it) } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMapper.kt new file mode 100644 index 0000000000..3e3c12f20b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMapper.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.store.db.model + +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.rest.UnsignedDeviceInfo +import org.matrix.android.sdk.internal.di.SerializeNulls +import timber.log.Timber + +object CryptoMapper { + + private val moshi = Moshi.Builder().add(SerializeNulls.JSON_ADAPTER_FACTORY).build() + private val listMigrationAdapter = moshi.adapter>(Types.newParameterizedType( + List::class.java, + String::class.java, + Any::class.java + )) + private val mapMigrationAdapter = moshi.adapter(Types.newParameterizedType( + Map::class.java, + String::class.java, + Any::class.java + )) + private val mapOfStringMigrationAdapter = moshi.adapter>>(Types.newParameterizedType( + Map::class.java, + String::class.java, + Any::class.java + )) + + internal fun mapToEntity(deviceInfo: CryptoDeviceInfo): DeviceInfoEntity { + return DeviceInfoEntity( + primaryKey = DeviceInfoEntity.createPrimaryKey(deviceInfo.userId, deviceInfo.deviceId), + userId = deviceInfo.userId, + deviceId = deviceInfo.deviceId, + algorithmListJson = listMigrationAdapter.toJson(deviceInfo.algorithms), + keysMapJson = mapMigrationAdapter.toJson(deviceInfo.keys), + signatureMapJson = mapMigrationAdapter.toJson(deviceInfo.signatures), + isBlocked = deviceInfo.isBlocked, + trustLevelEntity = deviceInfo.trustLevel?.let { + TrustLevelEntity( + crossSignedVerified = it.crossSigningVerified, + locallyVerified = it.locallyVerified + ) + }, + // We store the device name if present now + unsignedMapJson = deviceInfo.unsigned?.deviceDisplayName + ) + } + + internal fun mapToModel(deviceInfoEntity: DeviceInfoEntity): CryptoDeviceInfo { + return CryptoDeviceInfo( + userId = deviceInfoEntity.userId ?: "", + deviceId = deviceInfoEntity.deviceId ?: "", + isBlocked = deviceInfoEntity.isBlocked ?: false, + trustLevel = deviceInfoEntity.trustLevelEntity?.let { + DeviceTrustLevel(it.crossSignedVerified ?: false, it.locallyVerified) + }, + unsigned = deviceInfoEntity.unsignedMapJson?.let { UnsignedDeviceInfo(deviceDisplayName = it) }, + signatures = deviceInfoEntity.signatureMapJson?.let { + try { + mapOfStringMigrationAdapter.fromJson(it) + } catch (failure: Throwable) { + Timber.e(failure) + null + } + }, + keys = deviceInfoEntity.keysMapJson?.let { + try { + moshi.adapter>(Types.newParameterizedType( + Map::class.java, + String::class.java, + Any::class.java + )).fromJson(it) + } catch (failure: Throwable) { + Timber.e(failure) + null + } + }, + algorithms = deviceInfoEntity.algorithmListJson?.let { + try { + listMigrationAdapter.fromJson(it) + } catch (failure: Throwable) { + Timber.e(failure) + null + } + }, + firstTimeSeenLocalTs = deviceInfoEntity.firstTimeSeenLocalTs + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMetadataEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMetadataEntity.kt new file mode 100644 index 0000000000..eb79af4747 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMetadataEntity.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.model + +import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm +import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey +import org.matrix.olm.OlmAccount + +internal open class CryptoMetadataEntity( + // The current user id. + @PrimaryKey var userId: String? = null, + // The current device id. + var deviceId: String? = null, + // Serialized OlmAccount + var olmAccountData: String? = null, + // The sync token corresponding to the device list. // TODO? + var deviceSyncToken: String? = null, + // Settings for blacklisting unverified devices. + var globalBlacklistUnverifiedDevices: Boolean = false, + // The keys backup version currently used. Null means no backup. + var backupVersion: String? = null, + + // The device keys has been sent to the homeserver + var deviceKeysSentToServer: Boolean = false, + + var xSignMasterPrivateKey: String? = null, + var xSignUserPrivateKey: String? = null, + var xSignSelfSignedPrivateKey: String? = null, + var keyBackupRecoveryKey: String? = null, + var keyBackupRecoveryKeyVersion: String? = null + +// var crossSigningInfoEntity: CrossSigningInfoEntity? = null +) : RealmObject() { + + // Deserialize data + fun getOlmAccount(): OlmAccount? { + return deserializeFromRealm(olmAccountData) + } + + // Serialize data + fun putOlmAccount(olmAccount: OlmAccount?) { + olmAccountData = serializeForRealm(olmAccount) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoRoomEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoRoomEntity.kt new file mode 100644 index 0000000000..e1881e9157 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoRoomEntity.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.model + +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class CryptoRoomEntity( + @PrimaryKey var roomId: String? = null, + var algorithm: String? = null, + var shouldEncryptForInvitedMembers: Boolean? = null, + var blacklistUnverifiedDevices: Boolean = false) + : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/DeviceInfoEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/DeviceInfoEntity.kt new file mode 100644 index 0000000000..d0f4d49545 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/DeviceInfoEntity.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.model + +import io.realm.RealmObject +import io.realm.RealmResults +import io.realm.annotations.LinkingObjects +import io.realm.annotations.PrimaryKey + +internal fun DeviceInfoEntity.Companion.createPrimaryKey(userId: String, deviceId: String) = "$userId|$deviceId" + +internal open class DeviceInfoEntity( + @PrimaryKey var primaryKey: String = "", + var deviceId: String? = null, + var identityKey: String? = null, + var userId: String? = null, + var isBlocked: Boolean? = null, + var algorithmListJson: String? = null, + var keysMapJson: String? = null, + var signatureMapJson: String? = null, + // Will contain the device name from unsigned data if present + var unsignedMapJson: String? = null, + var trustLevelEntity: TrustLevelEntity? = null, + /** + * We use that to make distinction between old devices (there before mine) + * and new ones. Used for example to detect new unverified login + */ + var firstTimeSeenLocalTs: Long? = null +) : RealmObject() { + + @LinkingObjects("devices") + val users: RealmResults? = null + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/GossipingEventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/GossipingEventEntity.kt new file mode 100644 index 0000000000..c0a4625826 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/GossipingEventEntity.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.model + +import com.squareup.moshi.JsonDataException +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult +import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult +import org.matrix.android.sdk.internal.database.mapper.ContentMapper +import org.matrix.android.sdk.internal.di.MoshiProvider +import io.realm.RealmObject +import io.realm.annotations.Index +import timber.log.Timber + +/** + * Keep track of gossiping event received in toDevice messages + * (room key request, or sss secret sharing, as well as cancellations) + * + */ +internal open class GossipingEventEntity(@Index var type: String? = "", + var content: String? = null, + @Index var sender: String? = null, + var decryptionResultJson: String? = null, + var decryptionErrorCode: String? = null, + var ageLocalTs: Long? = null) : RealmObject() { + + private var sendStateStr: String = SendState.UNKNOWN.name + + var sendState: SendState + get() { + return SendState.valueOf(sendStateStr) + } + set(value) { + sendStateStr = value.name + } + + companion object + + fun setDecryptionResult(result: MXEventDecryptionResult) { + val decryptionResult = OlmDecryptionResult( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + ) + val adapter = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java) + decryptionResultJson = adapter.toJson(decryptionResult) + decryptionErrorCode = null + } + + fun toModel(): Event { + return Event( + type = this.type ?: "", + content = ContentMapper.map(this.content), + senderId = this.sender + ).also { + it.ageLocalTs = this.ageLocalTs + it.sendState = this.sendState + this.decryptionResultJson?.let { json -> + try { + it.mxDecryptionResult = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).fromJson(json) + } catch (t: JsonDataException) { + Timber.e(t, "Failed to parse decryption result") + } + } + // TODO get the full crypto error object + it.mCryptoError = this.decryptionErrorCode?.let { errorCode -> + MXCryptoError.ErrorType.valueOf(errorCode) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/IncomingGossipingRequestEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/IncomingGossipingRequestEntity.kt new file mode 100644 index 0000000000..c15df27874 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/IncomingGossipingRequestEntity.kt @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.model + +import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.internal.crypto.GossipRequestType +import org.matrix.android.sdk.internal.crypto.GossipingRequestState +import org.matrix.android.sdk.internal.crypto.IncomingRoomKeyRequest +import org.matrix.android.sdk.internal.crypto.IncomingSecretShareRequest +import org.matrix.android.sdk.internal.crypto.IncomingShareRequestCommon +import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody +import io.realm.RealmObject +import io.realm.annotations.Index + +internal open class IncomingGossipingRequestEntity(@Index var requestId: String? = "", + @Index var typeStr: String? = null, + var otherUserId: String? = null, + var requestedInfoStr: String? = null, + var otherDeviceId: String? = null, + var localCreationTimestamp: Long? = null +) : RealmObject() { + + fun getRequestedSecretName(): String? = if (type == GossipRequestType.SECRET) { + requestedInfoStr + } else null + + fun getRequestedKeyInfo(): RoomKeyRequestBody? = if (type == GossipRequestType.KEY) { + RoomKeyRequestBody.fromJson(requestedInfoStr) + } else null + + var type: GossipRequestType + get() { + return tryThis { typeStr?.let { GossipRequestType.valueOf(it) } } ?: GossipRequestType.KEY + } + set(value) { + typeStr = value.name + } + + private var requestStateStr: String = GossipingRequestState.NONE.name + + var requestState: GossipingRequestState + get() { + return tryThis { GossipingRequestState.valueOf(requestStateStr) } + ?: GossipingRequestState.NONE + } + set(value) { + requestStateStr = value.name + } + + companion object + + fun toIncomingGossipingRequest(): IncomingShareRequestCommon { + return when (type) { + GossipRequestType.KEY -> { + IncomingRoomKeyRequest( + requestBody = getRequestedKeyInfo(), + deviceId = otherDeviceId, + userId = otherUserId, + requestId = requestId, + state = requestState, + localCreationTimestamp = localCreationTimestamp + ) + } + GossipRequestType.SECRET -> { + IncomingSecretShareRequest( + secretName = getRequestedSecretName(), + deviceId = otherDeviceId, + userId = otherUserId, + requestId = requestId, + localCreationTimestamp = localCreationTimestamp + ) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/KeyInfoEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/KeyInfoEntity.kt new file mode 100644 index 0000000000..125fbd8118 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/KeyInfoEntity.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.model + +import io.realm.RealmList +import io.realm.RealmObject + +internal open class KeyInfoEntity( + var publicKeyBase64: String? = null, +// var isTrusted: Boolean = false, + var usages: RealmList = RealmList(), + /** + * The signature of this MXDeviceInfo. + * A map from "" to a map from ":" to "" + */ + var signatures: String? = null, + var trustLevelEntity: TrustLevelEntity? = null +) : RealmObject() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/KeysBackupDataEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/KeysBackupDataEntity.kt new file mode 100644 index 0000000000..0155ed9cce --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/KeysBackupDataEntity.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.model + +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class KeysBackupDataEntity( + // Primary key to update this object. There is only one object, so it's a constant, please do not set it + @PrimaryKey + var primaryKey: Int = 0, + // The last known hash of the backed up keys on the server + var backupLastServerHash: String? = null, + // The last known number of backed up keys on the server + var backupLastServerNumberOfKeys: Int? = null +) : RealmObject() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/MyDeviceLastSeenInfoEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/MyDeviceLastSeenInfoEntity.kt new file mode 100644 index 0000000000..64b04827d6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/MyDeviceLastSeenInfoEntity.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.model + +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class MyDeviceLastSeenInfoEntity( + /**The device id*/ + @PrimaryKey var deviceId: String? = null, + /** The device display name*/ + var displayName: String? = null, + /** The last time this device has been seen. */ + var lastSeenTs: Long? = null, + /** The last ip address*/ + var lastSeenIp: String? = null +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt new file mode 100644 index 0000000000..7d20b7582d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.model + +import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 +import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm +import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey +import timber.log.Timber + +internal fun OlmInboundGroupSessionEntity.Companion.createPrimaryKey(sessionId: String?, senderKey: String?) = "$sessionId|$senderKey" + +internal open class OlmInboundGroupSessionEntity( + // Combined value to build a primary key + @PrimaryKey var primaryKey: String? = null, + var sessionId: String? = null, + var senderKey: String? = null, + // olmInboundGroupSessionData contains Json + var olmInboundGroupSessionData: String? = null, + // Indicate if the key has been backed up to the homeserver + var backedUp: Boolean = false) + : RealmObject() { + + fun getInboundGroupSession(): OlmInboundGroupSessionWrapper2? { + return try { + deserializeFromRealm(olmInboundGroupSessionData) + } catch (failure: Throwable) { + Timber.e(failure, "## Deserialization failure") + return null + } + } + + fun putInboundGroupSession(olmInboundGroupSessionWrapper: OlmInboundGroupSessionWrapper2?) { + olmInboundGroupSessionData = serializeForRealm(olmInboundGroupSessionWrapper) + } + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OlmSessionEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OlmSessionEntity.kt new file mode 100644 index 0000000000..f804a64182 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OlmSessionEntity.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.model + +import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm +import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey +import org.matrix.olm.OlmSession + +internal fun OlmSessionEntity.Companion.createPrimaryKey(sessionId: String, deviceKey: String) = "$sessionId|$deviceKey" + +// olmSessionData is a serialized OlmSession +internal open class OlmSessionEntity(@PrimaryKey var primaryKey: String = "", + var sessionId: String? = null, + var deviceKey: String? = null, + var olmSessionData: String? = null, + var lastReceivedMessageTs: Long = 0) + : RealmObject() { + + fun getOlmSession(): OlmSession? { + return deserializeFromRealm(olmSessionData) + } + + fun putOlmSession(olmSession: OlmSession?) { + olmSessionData = serializeForRealm(olmSession) + } + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingGossipingRequestEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingGossipingRequestEntity.kt new file mode 100644 index 0000000000..2880735d6b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingGossipingRequestEntity.kt @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.model + +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Types +import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.internal.crypto.GossipRequestType +import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequest +import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestState +import org.matrix.android.sdk.internal.crypto.OutgoingRoomKeyRequest +import org.matrix.android.sdk.internal.crypto.OutgoingSecretRequest +import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody +import org.matrix.android.sdk.internal.di.MoshiProvider +import io.realm.RealmObject +import io.realm.annotations.Index + +internal open class OutgoingGossipingRequestEntity( + @Index var requestId: String? = null, + var recipientsData: String? = null, + var requestedInfoStr: String? = null, + @Index var typeStr: String? = null +) : RealmObject() { + + fun getRequestedSecretName(): String? = if (type == GossipRequestType.SECRET) { + requestedInfoStr + } else null + + fun getRequestedKeyInfo(): RoomKeyRequestBody? = if (type == GossipRequestType.KEY) { + RoomKeyRequestBody.fromJson(requestedInfoStr) + } else null + + var type: GossipRequestType + get() { + return tryThis { typeStr?.let { GossipRequestType.valueOf(it) } } ?: GossipRequestType.KEY + } + set(value) { + typeStr = value.name + } + + private var requestStateStr: String = OutgoingGossipingRequestState.UNSENT.name + + var requestState: OutgoingGossipingRequestState + get() { + return tryThis { OutgoingGossipingRequestState.valueOf(requestStateStr) } + ?: OutgoingGossipingRequestState.UNSENT + } + set(value) { + requestStateStr = value.name + } + + companion object { + + private val recipientsDataMapper: JsonAdapter>> = + MoshiProvider + .providesMoshi() + .adapter>>( + Types.newParameterizedType(Map::class.java, String::class.java, List::class.java) + ) + } + + fun toOutgoingGossipingRequest(): OutgoingGossipingRequest { + return when (type) { + GossipRequestType.KEY -> { + OutgoingRoomKeyRequest( + requestBody = getRequestedKeyInfo(), + recipients = getRecipients().orEmpty(), + requestId = requestId ?: "", + state = requestState + ) + } + GossipRequestType.SECRET -> { + OutgoingSecretRequest( + secretName = getRequestedSecretName(), + recipients = getRecipients().orEmpty(), + requestId = requestId ?: "", + state = requestState + ) + } + } + } + + private fun getRecipients(): Map>? { + return this.recipientsData?.let { recipientsDataMapper.fromJson(it) } + } + + fun setRecipients(recipients: Map>) { + this.recipientsData = recipientsDataMapper.toJson(recipients) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/SharedSessionEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/SharedSessionEntity.kt new file mode 100644 index 0000000000..aa647d02c1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/SharedSessionEntity.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.model + +import io.realm.RealmObject +import io.realm.annotations.Index + +/** + * Keep a record of to whom (user/device) a given session should have been shared. + * It will be used to reply to keyshare requests from other users, in order to see if + * this session was originaly shared with a given user + */ +internal open class SharedSessionEntity( + var roomId: String? = null, + var algorithm: String? = null, + @Index var sessionId: String? = null, + @Index var userId: String? = null, + @Index var deviceId: String? = null, + var chainIndex: Int? = null +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/TrustLevelEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/TrustLevelEntity.kt new file mode 100644 index 0000000000..cb2933e3c4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/TrustLevelEntity.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.model + +import io.realm.RealmObject + +internal open class TrustLevelEntity( + var crossSignedVerified: Boolean? = null, + var locallyVerified: Boolean? = null +) : RealmObject() { + + companion object + + fun isVerified(): Boolean = crossSignedVerified == true || locallyVerified == true +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/UserEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/UserEntity.kt new file mode 100644 index 0000000000..2820f72ef4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/UserEntity.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.model + +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class UserEntity( + @PrimaryKey var userId: String? = null, + var devices: RealmList = RealmList(), + var crossSigningInfoEntity: CrossSigningInfoEntity? = null, + var deviceTrackingStatus: Int = 0) + : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/WithHeldSessionEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/WithHeldSessionEntity.kt new file mode 100644 index 0000000000..36ffe85183 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/WithHeldSessionEntity.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.model + +import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode +import io.realm.RealmObject +import io.realm.annotations.Index + +/** + * When an encrypted message is sent in a room, the megolm key might not be sent to all devices present in the room. + * Sometimes this may be inadvertent (for example, if the sending device is not aware of some devices that have joined), + * but some times, this may be purposeful. + * For example, the sender may have blacklisted certain devices or users, + * or may be choosing to not send the megolm key to devices that they have not verified yet. + */ +internal open class WithHeldSessionEntity( + var roomId: String? = null, + var algorithm: String? = null, + @Index var sessionId: String? = null, + @Index var senderKey: String? = null, + var codeString: String? = null, + var reason: String? = null +) : RealmObject() { + + var code: WithHeldCode? + get() { + return WithHeldCode.fromCode(codeString) + } + set(code) { + codeString = code?.value + } + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/CrossSigningInfoEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/CrossSigningInfoEntityQueries.kt new file mode 100644 index 0000000000..5864455027 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/CrossSigningInfoEntityQueries.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.query + +import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntityFields +import io.realm.Realm +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +internal fun CrossSigningInfoEntity.Companion.getOrCreate(realm: Realm, userId: String): CrossSigningInfoEntity { + return realm.where() + .equalTo(UserEntityFields.USER_ID, userId) + .findFirst() + ?: realm.createObject(userId) +} + +internal fun CrossSigningInfoEntity.Companion.get(realm: Realm, userId: String): CrossSigningInfoEntity? { + return realm.where() + .equalTo(UserEntityFields.USER_ID, userId) + .findFirst() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/CryptoRoomEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/CryptoRoomEntityQueries.kt new file mode 100644 index 0000000000..f65b1a3c71 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/CryptoRoomEntityQueries.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.query + +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntityFields +import io.realm.Realm +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +/** + * Get or create a room + */ +internal fun CryptoRoomEntity.Companion.getOrCreate(realm: Realm, roomId: String): CryptoRoomEntity { + return getById(realm, roomId) ?: realm.createObject(roomId) +} + +/** + * Get a room + */ +internal fun CryptoRoomEntity.Companion.getById(realm: Realm, roomId: String): CryptoRoomEntity? { + return realm.where() + .equalTo(CryptoRoomEntityFields.ROOM_ID, roomId) + .findFirst() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/DeviceInfoEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/DeviceInfoEntityQueries.kt new file mode 100644 index 0000000000..b0e677e078 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/DeviceInfoEntityQueries.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.query + +import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.createPrimaryKey +import io.realm.Realm +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +/** + * Get or create a device info + */ +internal fun DeviceInfoEntity.Companion.getOrCreate(realm: Realm, userId: String, deviceId: String): DeviceInfoEntity { + val key = DeviceInfoEntity.createPrimaryKey(userId, deviceId) + + return realm.where() + .equalTo(DeviceInfoEntityFields.PRIMARY_KEY, key) + .findFirst() + ?: realm.createObject(key) + .apply { + this.deviceId = deviceId + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/SharedSessionQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/SharedSessionQueries.kt new file mode 100644 index 0000000000..885cadb5e5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/SharedSessionQueries.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.query + +import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.internal.crypto.store.db.model.SharedSessionEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.SharedSessionEntityFields +import io.realm.Realm +import io.realm.RealmResults +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +internal fun SharedSessionEntity.Companion.get(realm: Realm, roomId: String?, sessionId: String, userId: String, deviceId: String) + : SharedSessionEntity? { + return realm.where() + .equalTo(SharedSessionEntityFields.ROOM_ID, roomId) + .equalTo(SharedSessionEntityFields.SESSION_ID, sessionId) + .equalTo(SharedSessionEntityFields.ALGORITHM, MXCRYPTO_ALGORITHM_MEGOLM) + .equalTo(SharedSessionEntityFields.USER_ID, userId) + .equalTo(SharedSessionEntityFields.DEVICE_ID, deviceId) + .findFirst() +} + +internal fun SharedSessionEntity.Companion.get(realm: Realm, roomId: String?, sessionId: String) + : RealmResults { + return realm.where() + .equalTo(SharedSessionEntityFields.ROOM_ID, roomId) + .equalTo(SharedSessionEntityFields.SESSION_ID, sessionId) + .equalTo(SharedSessionEntityFields.ALGORITHM, MXCRYPTO_ALGORITHM_MEGOLM) + .findAll() +} + +internal fun SharedSessionEntity.Companion.create(realm: Realm, roomId: String?, sessionId: String, userId: String, deviceId: String, chainIndex: Int) + : SharedSessionEntity { + return realm.createObject().apply { + this.roomId = roomId + this.algorithm = MXCRYPTO_ALGORITHM_MEGOLM + this.sessionId = sessionId + this.userId = userId + this.deviceId = deviceId + this.chainIndex = chainIndex + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/UserEntitiesQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/UserEntitiesQueries.kt new file mode 100644 index 0000000000..e64dcb815d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/UserEntitiesQueries.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.query + +import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntityFields +import io.realm.Realm +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +/** + * Get or create a user + */ +internal fun UserEntity.Companion.getOrCreate(realm: Realm, userId: String): UserEntity { + return realm.where() + .equalTo(UserEntityFields.USER_ID, userId) + .findFirst() + ?: realm.createObject(userId) +} + +/** + * Delete a user + */ +internal fun UserEntity.Companion.delete(realm: Realm, userId: String) { + realm.where() + .equalTo(UserEntityFields.USER_ID, userId) + .findFirst() + ?.deleteFromRealm() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/WithHeldSessionQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/WithHeldSessionQueries.kt new file mode 100644 index 0000000000..b3a5560dd4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/WithHeldSessionQueries.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.query + +import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.internal.crypto.store.db.model.WithHeldSessionEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.WithHeldSessionEntityFields +import io.realm.Realm +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +internal fun WithHeldSessionEntity.Companion.get(realm: Realm, roomId: String, sessionId: String): WithHeldSessionEntity? { + return realm.where() + .equalTo(WithHeldSessionEntityFields.ROOM_ID, roomId) + .equalTo(WithHeldSessionEntityFields.SESSION_ID, sessionId) + .equalTo(WithHeldSessionEntityFields.ALGORITHM, MXCRYPTO_ALGORITHM_MEGOLM) + .findFirst() +} + +internal fun WithHeldSessionEntity.Companion.getOrCreate(realm: Realm, roomId: String, sessionId: String): WithHeldSessionEntity? { + return get(realm, roomId, sessionId) + ?: realm.createObject().apply { + this.roomId = roomId + this.algorithm = MXCRYPTO_ALGORITHM_MEGOLM + this.sessionId = sessionId + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/ClaimOneTimeKeysForUsersDeviceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/ClaimOneTimeKeysForUsersDeviceTask.kt new file mode 100644 index 0000000000..f5ee6aa9bf --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/ClaimOneTimeKeysForUsersDeviceTask.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.tasks + +import org.matrix.android.sdk.internal.crypto.api.CryptoApi +import org.matrix.android.sdk.internal.crypto.model.MXKey +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimBody +import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimResponse +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import javax.inject.Inject + +internal interface ClaimOneTimeKeysForUsersDeviceTask : Task> { + data class Params( + // a list of users, devices and key types to retrieve keys for. + val usersDevicesKeyTypesMap: MXUsersDevicesMap + ) +} + +internal class DefaultClaimOneTimeKeysForUsersDevice @Inject constructor( + private val cryptoApi: CryptoApi, + private val eventBus: EventBus +) : ClaimOneTimeKeysForUsersDeviceTask { + + override suspend fun execute(params: ClaimOneTimeKeysForUsersDeviceTask.Params): MXUsersDevicesMap { + val body = KeysClaimBody(oneTimeKeys = params.usersDevicesKeyTypesMap.map) + + val keysClaimResponse = executeRequest(eventBus) { + apiCall = cryptoApi.claimOneTimeKeysForUsersDevices(body) + } + val map = MXUsersDevicesMap() + keysClaimResponse.oneTimeKeys?.let { oneTimeKeys -> + for ((userId, mapByUserId) in oneTimeKeys) { + for ((deviceId, deviceKey) in mapByUserId) { + val mxKey = MXKey.from(deviceKey) + + if (mxKey != null) { + map.setObject(userId, deviceId, mxKey) + } else { + Timber.e("## claimOneTimeKeysForUsersDevices : fail to create a MXKey") + } + } + } + } + return map + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt new file mode 100644 index 0000000000..1f0d9eaaf9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.tasks + +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse +import org.matrix.android.sdk.internal.crypto.api.CryptoApi +import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface DeleteDeviceTask : Task { + data class Params( + val deviceId: String + ) +} + +internal class DefaultDeleteDeviceTask @Inject constructor( + private val cryptoApi: CryptoApi, + private val eventBus: EventBus +) : DeleteDeviceTask { + + override suspend fun execute(params: DeleteDeviceTask.Params) { + try { + executeRequest(eventBus) { + apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams()) + } + } catch (throwable: Throwable) { + throw throwable.toRegistrationFlowResponse() + ?.let { Failure.RegistrationFlowError(it) } + ?: throwable + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt new file mode 100644 index 0000000000..0f67ec666d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.tasks + +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes +import org.matrix.android.sdk.internal.crypto.api.CryptoApi +import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams +import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface DeleteDeviceWithUserPasswordTask : Task { + data class Params( + val deviceId: String, + val authSession: String?, + val password: String + ) +} + +internal class DefaultDeleteDeviceWithUserPasswordTask @Inject constructor( + private val cryptoApi: CryptoApi, + @UserId private val userId: String, + private val eventBus: EventBus +) : DeleteDeviceWithUserPasswordTask { + + override suspend fun execute(params: DeleteDeviceWithUserPasswordTask.Params) { + return executeRequest(eventBus) { + apiCall = cryptoApi.deleteDevice(params.deviceId, + DeleteDeviceParams( + userPasswordAuth = UserPasswordAuth( + type = LoginFlowTypes.PASSWORD, + session = params.authSession, + user = userId, + password = params.password + ) + ) + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DownloadKeysForUsersTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DownloadKeysForUsersTask.kt new file mode 100644 index 0000000000..f053900598 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DownloadKeysForUsersTask.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.tasks + +import org.matrix.android.sdk.internal.crypto.api.CryptoApi +import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryBody +import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface DownloadKeysForUsersTask : Task { + data class Params( + // the list of users to get keys for. + val userIds: List, + // the up-to token + val token: String? + ) +} + +internal class DefaultDownloadKeysForUsers @Inject constructor( + private val cryptoApi: CryptoApi, + private val eventBus: EventBus +) : DownloadKeysForUsersTask { + + override suspend fun execute(params: DownloadKeysForUsersTask.Params): KeysQueryResponse { + val downloadQuery = params.userIds.associateWith { emptyList() } + + val body = KeysQueryBody( + deviceKeys = downloadQuery, + token = params.token?.takeIf { it.isNotEmpty() } + ) + + return executeRequest(eventBus) { + apiCall = cryptoApi.downloadKeysForUsers(body) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt new file mode 100644 index 0000000000..e0a85d50c0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.tasks + +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.crypto.model.MXEncryptEventContentResult +import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitCallback +import javax.inject.Inject + +internal interface EncryptEventTask : Task { + data class Params(val roomId: String, + val event: Event, + /**Do not encrypt these keys, keep them as is in encrypted content (e.g. m.relates_to)*/ + val keepKeys: List? = null, + val crypto: CryptoService + ) +} + +internal class DefaultEncryptEventTask @Inject constructor( +// private val crypto: CryptoService + private val localEchoRepository: LocalEchoRepository +) : EncryptEventTask { + override suspend fun execute(params: EncryptEventTask.Params): Event { + if (!params.crypto.isRoomEncrypted(params.roomId)) return params.event + val localEvent = params.event + if (localEvent.eventId == null) { + throw IllegalArgumentException() + } + + localEchoRepository.updateSendState(localEvent.eventId, SendState.ENCRYPTING) + + val localMutableContent = localEvent.content?.toMutableMap() ?: mutableMapOf() + params.keepKeys?.forEach { + localMutableContent.remove(it) + } + +// try { + awaitCallback { + params.crypto.encryptEventContent(localMutableContent, localEvent.type, params.roomId, it) + }.let { result -> + val modifiedContent = HashMap(result.eventContent) + params.keepKeys?.forEach { toKeep -> + localEvent.content?.get(toKeep)?.let { + // put it back in the encrypted thing + modifiedContent[toKeep] = it + } + } + val safeResult = result.copy(eventContent = modifiedContent) + return localEvent.copy( + type = safeResult.eventType, + content = safeResult.eventContent + ) + } +// } catch (throwable: Throwable) { +// val sendState = when (throwable) { +// is Failure.CryptoError -> SendState.FAILED_UNKNOWN_DEVICES +// else -> SendState.UNDELIVERED +// } +// localEchoUpdater.updateSendState(localEvent.eventId, sendState) +// throw throwable +// } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/GetDeviceInfoTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/GetDeviceInfoTask.kt new file mode 100644 index 0000000000..e1db5e0c98 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/GetDeviceInfoTask.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.tasks + +import org.matrix.android.sdk.internal.crypto.api.CryptoApi +import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface GetDeviceInfoTask : Task { + data class Params(val deviceId: String) +} + +internal class DefaultGetDeviceInfoTask @Inject constructor( + private val cryptoApi: CryptoApi, + private val eventBus: EventBus +) : GetDeviceInfoTask { + + override suspend fun execute(params: GetDeviceInfoTask.Params): DeviceInfo { + return executeRequest(eventBus) { + apiCall = cryptoApi.getDeviceInfo(params.deviceId) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/GetDevicesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/GetDevicesTask.kt new file mode 100644 index 0000000000..ea8be725f0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/GetDevicesTask.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.tasks + +import org.matrix.android.sdk.internal.crypto.api.CryptoApi +import org.matrix.android.sdk.internal.crypto.model.rest.DevicesListResponse +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface GetDevicesTask : Task + +internal class DefaultGetDevicesTask @Inject constructor( + private val cryptoApi: CryptoApi, + private val eventBus: EventBus +) : GetDevicesTask { + + override suspend fun execute(params: Unit): DevicesListResponse { + return executeRequest(eventBus) { + apiCall = cryptoApi.getDevices() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/GetKeyChangesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/GetKeyChangesTask.kt new file mode 100644 index 0000000000..57a4881a51 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/GetKeyChangesTask.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.tasks + +import org.matrix.android.sdk.internal.crypto.api.CryptoApi +import org.matrix.android.sdk.internal.crypto.model.rest.KeyChangesResponse +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface GetKeyChangesTask : Task { + data class Params( + // the start token. + val from: String, + // the up-to token. + val to: String + ) +} + +internal class DefaultGetKeyChangesTask @Inject constructor( + private val cryptoApi: CryptoApi, + private val eventBus: EventBus +) : GetKeyChangesTask { + + override suspend fun execute(params: GetKeyChangesTask.Params): KeyChangesResponse { + return executeRequest(eventBus) { + apiCall = cryptoApi.getKeyChanges(params.from, params.to) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt new file mode 100644 index 0000000000..d2c7e87b67 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.tasks + +import dagger.Lazy +import org.matrix.android.sdk.internal.crypto.MXOlmDevice +import org.matrix.android.sdk.internal.crypto.MyDeviceInfoHolder +import org.matrix.android.sdk.internal.crypto.crosssigning.canonicalSignable +import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding +import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey +import org.matrix.android.sdk.internal.crypto.model.KeyUsage +import org.matrix.android.sdk.internal.crypto.model.rest.UploadSignatureQueryBuilder +import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.JsonCanonicalizer +import org.matrix.olm.OlmPkSigning +import timber.log.Timber +import javax.inject.Inject + +internal interface InitializeCrossSigningTask : Task { + data class Params( + val authParams: UserPasswordAuth? + ) + + data class Result( + val masterKeyPK: String, + val userKeyPK: String, + val selfSigningKeyPK: String, + val masterKeyInfo: CryptoCrossSigningKey, + val userKeyInfo: CryptoCrossSigningKey, + val selfSignedKeyInfo: CryptoCrossSigningKey + ) +} + +internal class DefaultInitializeCrossSigningTask @Inject constructor( + @UserId private val userId: String, + private val olmDevice: MXOlmDevice, + private val myDeviceInfoHolder: Lazy, + private val uploadSigningKeysTask: UploadSigningKeysTask, + private val uploadSignaturesTask: UploadSignaturesTask +) : InitializeCrossSigningTask { + + override suspend fun execute(params: InitializeCrossSigningTask.Params): InitializeCrossSigningTask.Result { + var masterPkOlm: OlmPkSigning? = null + var userSigningPkOlm: OlmPkSigning? = null + var selfSigningPkOlm: OlmPkSigning? = null + + try { + // ================= + // MASTER KEY + // ================= + + masterPkOlm = OlmPkSigning() + val masterKeyPrivateKey = OlmPkSigning.generateSeed() + val masterPublicKey = masterPkOlm.initWithSeed(masterKeyPrivateKey) + + Timber.v("## CrossSigning - masterPublicKey:$masterPublicKey") + + // ================= + // USER KEY + // ================= + userSigningPkOlm = OlmPkSigning() + val uskPrivateKey = OlmPkSigning.generateSeed() + val uskPublicKey = userSigningPkOlm.initWithSeed(uskPrivateKey) + + Timber.v("## CrossSigning - uskPublicKey:$uskPublicKey") + + // Sign userSigningKey with master + val signedUSK = CryptoCrossSigningKey.Builder(userId, KeyUsage.USER_SIGNING) + .key(uskPublicKey) + .build() + .canonicalSignable() + .let { masterPkOlm.sign(it) } + + // ================= + // SELF SIGNING KEY + // ================= + selfSigningPkOlm = OlmPkSigning() + val sskPrivateKey = OlmPkSigning.generateSeed() + val sskPublicKey = selfSigningPkOlm.initWithSeed(sskPrivateKey) + + Timber.v("## CrossSigning - sskPublicKey:$sskPublicKey") + + // Sign userSigningKey with master + val signedSSK = CryptoCrossSigningKey.Builder(userId, KeyUsage.SELF_SIGNING) + .key(sskPublicKey) + .build() + .canonicalSignable() + .let { masterPkOlm.sign(it) } + + // I need to upload the keys + val mskCrossSigningKeyInfo = CryptoCrossSigningKey.Builder(userId, KeyUsage.MASTER) + .key(masterPublicKey) + .build() + val uploadSigningKeysParams = UploadSigningKeysTask.Params( + masterKey = mskCrossSigningKeyInfo, + userKey = CryptoCrossSigningKey.Builder(userId, KeyUsage.USER_SIGNING) + .key(uskPublicKey) + .signature(userId, masterPublicKey, signedUSK) + .build(), + selfSignedKey = CryptoCrossSigningKey.Builder(userId, KeyUsage.SELF_SIGNING) + .key(sskPublicKey) + .signature(userId, masterPublicKey, signedSSK) + .build(), + userPasswordAuth = params.authParams + ) + + uploadSigningKeysTask.execute(uploadSigningKeysParams) + + // Sign the current device with SSK + val uploadSignatureQueryBuilder = UploadSignatureQueryBuilder() + + val myDevice = myDeviceInfoHolder.get().myDevice + val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, myDevice.signalableJSONDictionary()) + val signedDevice = selfSigningPkOlm.sign(canonicalJson) + val updateSignatures = (myDevice.signatures?.toMutableMap() ?: HashMap()) + .also { + it[userId] = (it[userId] + ?: HashMap()) + mapOf("ed25519:$sskPublicKey" to signedDevice) + } + myDevice.copy(signatures = updateSignatures).let { + uploadSignatureQueryBuilder.withDeviceInfo(it) + } + + // sign MSK with device key (migration) and upload signatures + val message = JsonCanonicalizer.getCanonicalJson(Map::class.java, mskCrossSigningKeyInfo.signalableJSONDictionary()) + olmDevice.signMessage(message)?.let { sign -> + val mskUpdatedSignatures = (mskCrossSigningKeyInfo.signatures?.toMutableMap() + ?: HashMap()).also { + it[userId] = (it[userId] + ?: HashMap()) + mapOf("ed25519:${myDevice.deviceId}" to sign) + } + mskCrossSigningKeyInfo.copy( + signatures = mskUpdatedSignatures + ).let { + uploadSignatureQueryBuilder.withSigningKeyInfo(it) + } + } + + // TODO should we ignore failure of that? + uploadSignaturesTask.execute(UploadSignaturesTask.Params(uploadSignatureQueryBuilder.build())) + + return InitializeCrossSigningTask.Result( + masterKeyPK = masterKeyPrivateKey.toBase64NoPadding(), + userKeyPK = uskPrivateKey.toBase64NoPadding(), + selfSigningKeyPK = sskPrivateKey.toBase64NoPadding(), + masterKeyInfo = uploadSigningKeysParams.masterKey, + userKeyInfo = uploadSigningKeysParams.userKey, + selfSignedKeyInfo = uploadSigningKeysParams.selfSignedKey + ) + } finally { + masterPkOlm?.releaseSigning() + userSigningPkOlm?.releaseSigning() + selfSigningPkOlm?.releaseSigning() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt new file mode 100644 index 0000000000..870980bde2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.tasks + +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository +import org.matrix.android.sdk.internal.session.room.send.SendResponse +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface SendEventTask : Task { + data class Params( + val event: Event, + val cryptoService: CryptoService? + ) +} + +internal class DefaultSendEventTask @Inject constructor( + private val localEchoRepository: LocalEchoRepository, + private val encryptEventTask: DefaultEncryptEventTask, + private val roomAPI: RoomAPI, + private val eventBus: EventBus) : SendEventTask { + + override suspend fun execute(params: SendEventTask.Params): String { + val event = handleEncryption(params) + val localId = event.eventId!! + + try { + localEchoRepository.updateSendState(localId, SendState.SENDING) + val executeRequest = executeRequest(eventBus) { + apiCall = roomAPI.send( + localId, + roomId = event.roomId ?: "", + content = event.content, + eventType = event.type + ) + } + localEchoRepository.updateSendState(localId, SendState.SENT) + return executeRequest.eventId + } catch (e: Throwable) { + localEchoRepository.updateSendState(localId, SendState.UNDELIVERED) + throw e + } + } + + private suspend fun handleEncryption(params: SendEventTask.Params): Event { + if (params.cryptoService?.isRoomEncrypted(params.event.roomId ?: "") == true) { + try { + return encryptEventTask.execute(EncryptEventTask.Params( + params.event.roomId ?: "", + params.event, + listOf("m.relates_to"), + params.cryptoService + )) + } catch (throwable: Throwable) { + // We said it's ok to send verification request in clear + } + } + return params.event + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendToDeviceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendToDeviceTask.kt new file mode 100644 index 0000000000..20153ef460 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendToDeviceTask.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.tasks + +import org.matrix.android.sdk.internal.crypto.api.CryptoApi +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.model.rest.SendToDeviceBody +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject +import kotlin.random.Random + +internal interface SendToDeviceTask : Task { + data class Params( + // the type of event to send + val eventType: String, + // the content to send. Map from user_id to device_id to content dictionary. + val contentMap: MXUsersDevicesMap, + // the transactionId + val transactionId: String? = null + ) +} + +internal class DefaultSendToDeviceTask @Inject constructor( + private val cryptoApi: CryptoApi, + private val eventBus: EventBus +) : SendToDeviceTask { + + override suspend fun execute(params: SendToDeviceTask.Params) { + val sendToDeviceBody = SendToDeviceBody( + messages = params.contentMap.map + ) + + return executeRequest(eventBus) { + apiCall = cryptoApi.sendToDevice( + params.eventType, + params.transactionId ?: Random.nextInt(Integer.MAX_VALUE).toString(), + sendToDeviceBody + ) + isRetryable = true + maxRetryCount = 3 + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt new file mode 100644 index 0000000000..09baf88e59 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.tasks + +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository +import org.matrix.android.sdk.internal.session.room.send.SendResponse +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface SendVerificationMessageTask : Task { + data class Params( + val event: Event, + val cryptoService: CryptoService? + ) +} + +internal class DefaultSendVerificationMessageTask @Inject constructor( + private val localEchoRepository: LocalEchoRepository, + private val encryptEventTask: DefaultEncryptEventTask, + private val roomAPI: RoomAPI, + private val eventBus: EventBus) : SendVerificationMessageTask { + + override suspend fun execute(params: SendVerificationMessageTask.Params): String { + val event = handleEncryption(params) + val localId = event.eventId!! + + try { + localEchoRepository.updateSendState(localId, SendState.SENDING) + val executeRequest = executeRequest(eventBus) { + apiCall = roomAPI.send( + localId, + roomId = event.roomId ?: "", + content = event.content, + eventType = event.type + ) + } + localEchoRepository.updateSendState(localId, SendState.SENT) + return executeRequest.eventId + } catch (e: Throwable) { + localEchoRepository.updateSendState(localId, SendState.UNDELIVERED) + throw e + } + } + + private suspend fun handleEncryption(params: SendVerificationMessageTask.Params): Event { + if (params.cryptoService?.isRoomEncrypted(params.event.roomId ?: "") == true) { + try { + return encryptEventTask.execute(EncryptEventTask.Params( + params.event.roomId ?: "", + params.event, + listOf("m.relates_to"), + params.cryptoService + )) + } catch (throwable: Throwable) { + // We said it's ok to send verification request in clear + } + } + return params.event + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SetDeviceNameTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SetDeviceNameTask.kt new file mode 100644 index 0000000000..d3900550c5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SetDeviceNameTask.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.tasks + +import org.matrix.android.sdk.internal.crypto.api.CryptoApi +import org.matrix.android.sdk.internal.crypto.model.rest.UpdateDeviceInfoBody +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface SetDeviceNameTask : Task { + data class Params( + // the device id + val deviceId: String, + // the device name + val deviceName: String + ) +} + +internal class DefaultSetDeviceNameTask @Inject constructor( + private val cryptoApi: CryptoApi, + private val eventBus: EventBus +) : SetDeviceNameTask { + + override suspend fun execute(params: SetDeviceNameTask.Params) { + val body = UpdateDeviceInfoBody( + displayName = params.deviceName + ) + return executeRequest(eventBus) { + apiCall = cryptoApi.updateDeviceInfo(params.deviceId, body) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadKeysTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadKeysTask.kt new file mode 100644 index 0000000000..b41dcf6dd0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadKeysTask.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.tasks + +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.crypto.api.CryptoApi +import org.matrix.android.sdk.internal.crypto.model.rest.DeviceKeys +import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadBody +import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadResponse +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import javax.inject.Inject + +internal interface UploadKeysTask : Task { + data class Params( + // the device keys to send. + val deviceKeys: DeviceKeys?, + // the one-time keys to send. + val oneTimeKeys: JsonDict? + ) +} + +internal class DefaultUploadKeysTask @Inject constructor( + private val cryptoApi: CryptoApi, + private val eventBus: EventBus +) : UploadKeysTask { + + override suspend fun execute(params: UploadKeysTask.Params): KeysUploadResponse { + val body = KeysUploadBody( + deviceKeys = params.deviceKeys, + oneTimeKeys = params.oneTimeKeys + ) + + Timber.i("## Uploading device keys -> $body") + + return executeRequest(eventBus) { + apiCall = cryptoApi.uploadKeys(body) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSignaturesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSignaturesTask.kt new file mode 100644 index 0000000000..255d06ea7c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSignaturesTask.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.tasks + +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.internal.crypto.api.CryptoApi +import org.matrix.android.sdk.internal.crypto.model.rest.SignatureUploadResponse +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface UploadSignaturesTask : Task { + data class Params( + val signatures: Map> + ) +} + +internal class DefaultUploadSignaturesTask @Inject constructor( + private val cryptoApi: CryptoApi, + private val eventBus: EventBus +) : UploadSignaturesTask { + + override suspend fun execute(params: UploadSignaturesTask.Params) { + try { + val response = executeRequest(eventBus) { + this.isRetryable = true + this.maxRetryCount = 10 + this.apiCall = cryptoApi.uploadSignatures(params.signatures) + } + if (response.failures?.isNotEmpty() == true) { + throw Throwable(response.failures.toString()) + } + return + } catch (f: Failure) { + throw f + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSigningKeysTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSigningKeysTask.kt new file mode 100644 index 0000000000..c7844fbfe4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSigningKeysTask.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.tasks + +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse +import org.matrix.android.sdk.internal.crypto.api.CryptoApi +import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey +import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse +import org.matrix.android.sdk.internal.crypto.model.rest.UploadSigningKeysBody +import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth +import org.matrix.android.sdk.internal.crypto.model.toRest +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface UploadSigningKeysTask : Task { + data class Params( + // the MSK + val masterKey: CryptoCrossSigningKey, + // the USK + val userKey: CryptoCrossSigningKey, + // the SSK + val selfSignedKey: CryptoCrossSigningKey, + /** + * - If null: + * - no retry will be performed + * - If not null, it may or may not contain a sessionId: + * - If sessionId is null: + * - password should not be null: the task will perform a first request to get a sessionId, and then a second one + * - If sessionId is not null: + * - password should not be null as well, and no retry will be performed + */ + val userPasswordAuth: UserPasswordAuth? + ) +} + +data class UploadSigningKeys(val failures: Map?) : Failure.FeatureFailure() + +internal class DefaultUploadSigningKeysTask @Inject constructor( + private val cryptoApi: CryptoApi, + private val eventBus: EventBus +) : UploadSigningKeysTask { + + override suspend fun execute(params: UploadSigningKeysTask.Params) { + val paramsHaveSessionId = params.userPasswordAuth?.session != null + + val uploadQuery = UploadSigningKeysBody( + masterKey = params.masterKey.toRest(), + userSigningKey = params.userKey.toRest(), + selfSigningKey = params.selfSignedKey.toRest(), + // If sessionId is provided, use the userPasswordAuth + auth = params.userPasswordAuth.takeIf { paramsHaveSessionId } + ) + try { + doRequest(uploadQuery) + } catch (throwable: Throwable) { + val registrationFlowResponse = throwable.toRegistrationFlowResponse() + if (registrationFlowResponse != null + && registrationFlowResponse.flows.orEmpty().any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true } + && params.userPasswordAuth?.password != null + && !paramsHaveSessionId + ) { + // Retry with authentication + doRequest(uploadQuery.copy(auth = params.userPasswordAuth.copy(session = registrationFlowResponse.session))) + } else { + // Other error + throw throwable + } + } + } + + private suspend fun doRequest(uploadQuery: UploadSigningKeysBody) { + val keysQueryResponse = executeRequest(eventBus) { + apiCall = cryptoApi.uploadSigningKeys(uploadQuery) + } + if (keysQueryResponse.failures?.isNotEmpty() == true) { + throw UploadSigningKeys(keysQueryResponse.failures) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tools/HkdfSha256.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tools/HkdfSha256.kt new file mode 100644 index 0000000000..f93dc7126a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tools/HkdfSha256.kt @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * Copyright (C) 2015 Square, Inc. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.tools + +import java.io.ByteArrayOutputStream +import java.nio.ByteBuffer +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec +import kotlin.math.ceil + +/** + * HMAC-based Extract-and-Expand Key Derivation Function (HkdfSha256) + * [RFC-5869] https://tools.ietf.org/html/rfc5869 + */ +object HkdfSha256 { + + fun deriveSecret(inputKeyMaterial: ByteArray, salt: ByteArray?, info: ByteArray, outputLength: Int): ByteArray { + return expand(extract(salt, inputKeyMaterial), info, outputLength) + } + + /** + * HkdfSha256-Extract(salt, IKM) -> PRK + * + * @param salt optional salt value (a non-secret random value); + * if not provided, it is set to a string of HashLen (size in octets) zeros. + * @param ikm input keying material + */ + private fun extract(salt: ByteArray?, ikm: ByteArray): ByteArray { + val mac = initMac(salt ?: ByteArray(HASH_LEN) { 0.toByte() }) + return mac.doFinal(ikm) + } + + /** + * HkdfSha256-Expand(PRK, info, L) -> OKM + * + * @param prk a pseudorandom key of at least HashLen bytes (usually, the output from the extract step) + * @param info optional context and application specific information (can be empty) + * @param outputLength length of output keying material in bytes (<= 255*HashLen) + * @return OKM output keying material + */ + private fun expand(prk: ByteArray, info: ByteArray = ByteArray(0), outputLength: Int): ByteArray { + require(outputLength <= 255 * HASH_LEN) { "outputLength must be less than or equal to 255*HashLen" } + + /* + The output OKM is calculated as follows: + Notation | -> When the message is composed of several elements we use concatenation (denoted |) in the second argument; + + + N = ceil(L/HashLen) + T = T(1) | T(2) | T(3) | ... | T(N) + OKM = first L octets of T + + where: + T(0) = empty string (zero length) + T(1) = HMAC-Hash(PRK, T(0) | info | 0x01) + T(2) = HMAC-Hash(PRK, T(1) | info | 0x02) + T(3) = HMAC-Hash(PRK, T(2) | info | 0x03) + ... + */ + val n = ceil(outputLength.toDouble() / HASH_LEN.toDouble()).toInt() + + var stepHash = ByteArray(0) // T(0) empty string (zero length) + + val generatedBytes = ByteArrayOutputStream() // ByteBuffer.allocate(Math.multiplyExact(n, HASH_LEN)) + val mac = initMac(prk) + for (roundNum in 1..n) { + mac.reset() + val t = ByteBuffer.allocate(stepHash.size + info.size + 1).apply { + put(stepHash) + put(info) + put(roundNum.toByte()) + } + stepHash = mac.doFinal(t.array()) + generatedBytes.write(stepHash) + } + + return generatedBytes.toByteArray().sliceArray(0 until outputLength) + } + + private fun initMac(secret: ByteArray): Mac { + val mac = Mac.getInstance(HASH_ALG) + mac.init(SecretKeySpec(secret, HASH_ALG)) + return mac + } + + private const val HASH_LEN = 32 + private const val HASH_ALG = "HmacSHA256" +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tools/Tools.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tools/Tools.kt new file mode 100644 index 0000000000..1bd9e1282f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tools/Tools.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.tools + +import org.matrix.olm.OlmPkDecryption +import org.matrix.olm.OlmPkEncryption +import org.matrix.olm.OlmPkSigning +import org.matrix.olm.OlmUtility + +fun withOlmEncryption(block: (OlmPkEncryption) -> T): T { + val olmPkEncryption = OlmPkEncryption() + try { + return block(olmPkEncryption) + } finally { + olmPkEncryption.releaseEncryption() + } +} + +fun withOlmDecryption(block: (OlmPkDecryption) -> T): T { + val olmPkDecryption = OlmPkDecryption() + try { + return block(olmPkDecryption) + } finally { + olmPkDecryption.releaseDecryption() + } +} + +fun withOlmSigning(block: (OlmPkSigning) -> T): T { + val olmPkSigning = OlmPkSigning() + try { + return block(olmPkSigning) + } finally { + olmPkSigning.releaseSigning() + } +} + +fun withOlmUtility(block: (OlmUtility) -> T): T { + val olmUtility = OlmUtility() + try { + return block(olmUtility) + } finally { + olmUtility.releaseUtility() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt new file mode 100644 index 0000000000..009979db49 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt @@ -0,0 +1,265 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.verification + +import android.util.Base64 +import org.matrix.android.sdk.BuildConfig +import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService +import org.matrix.android.sdk.api.session.crypto.verification.CancelCode +import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction +import org.matrix.android.sdk.api.session.crypto.verification.SasMode +import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import timber.log.Timber + +internal class DefaultIncomingSASDefaultVerificationTransaction( + setDeviceVerificationAction: SetDeviceVerificationAction, + override val userId: String, + override val deviceId: String?, + private val cryptoStore: IMXCryptoStore, + crossSigningService: CrossSigningService, + outgoingGossipingRequestManager: OutgoingGossipingRequestManager, + incomingGossipingRequestManager: IncomingGossipingRequestManager, + deviceFingerprint: String, + transactionId: String, + otherUserID: String, + private val autoAccept: Boolean = false +) : SASDefaultVerificationTransaction( + setDeviceVerificationAction, + userId, + deviceId, + cryptoStore, + crossSigningService, + outgoingGossipingRequestManager, + incomingGossipingRequestManager, + deviceFingerprint, + transactionId, + otherUserID, + null, + isIncoming = true), + IncomingSasVerificationTransaction { + + override val uxState: IncomingSasVerificationTransaction.UxState + get() { + return when (val immutableState = state) { + is VerificationTxState.OnStarted -> IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT + is VerificationTxState.SendingAccept, + is VerificationTxState.Accepted, + is VerificationTxState.OnKeyReceived, + is VerificationTxState.SendingKey, + is VerificationTxState.KeySent -> IncomingSasVerificationTransaction.UxState.WAIT_FOR_KEY_AGREEMENT + is VerificationTxState.ShortCodeReady -> IncomingSasVerificationTransaction.UxState.SHOW_SAS + is VerificationTxState.ShortCodeAccepted, + is VerificationTxState.SendingMac, + is VerificationTxState.MacSent, + is VerificationTxState.Verifying -> IncomingSasVerificationTransaction.UxState.WAIT_FOR_VERIFICATION + is VerificationTxState.Verified -> IncomingSasVerificationTransaction.UxState.VERIFIED + is VerificationTxState.Cancelled -> { + if (immutableState.byMe) { + IncomingSasVerificationTransaction.UxState.CANCELLED_BY_ME + } else { + IncomingSasVerificationTransaction.UxState.CANCELLED_BY_OTHER + } + } + else -> IncomingSasVerificationTransaction.UxState.UNKNOWN + } + } + + override fun onVerificationStart(startReq: ValidVerificationInfoStart.SasVerificationInfoStart) { + Timber.v("## SAS I: received verification request from state $state") + if (state != VerificationTxState.None) { + Timber.e("## SAS I: received verification request from invalid state") + // should I cancel?? + throw IllegalStateException("Interactive Key verification already started") + } + this.startReq = startReq + state = VerificationTxState.OnStarted + this.otherDeviceId = startReq.fromDevice + + if (autoAccept) { + performAccept() + } + } + + override fun performAccept() { + if (state != VerificationTxState.OnStarted) { + Timber.e("## SAS Cannot perform accept from state $state") + return + } + + // Select a key agreement protocol, a hash algorithm, a message authentication code, + // and short authentication string methods out of the lists given in requester's message. + val agreedProtocol = startReq!!.keyAgreementProtocols.firstOrNull { KNOWN_AGREEMENT_PROTOCOLS.contains(it) } + val agreedHash = startReq!!.hashes.firstOrNull { KNOWN_HASHES.contains(it) } + val agreedMac = startReq!!.messageAuthenticationCodes.firstOrNull { KNOWN_MACS.contains(it) } + val agreedShortCode = startReq!!.shortAuthenticationStrings.filter { KNOWN_SHORT_CODES.contains(it) } + + // No common key sharing/hashing/hmac/SAS methods. + // If a device is unable to complete the verification because the devices are unable to find a common key sharing, + // hashing, hmac, or SAS method, then it should send a m.key.verification.cancel message + if (listOf(agreedProtocol, agreedHash, agreedMac).any { it.isNullOrBlank() } + || agreedShortCode.isNullOrEmpty()) { + // Failed to find agreement + Timber.e("## SAS Failed to find agreement ") + cancel(CancelCode.UnknownMethod) + return + } + + // Bob’s device ensures that it has a copy of Alice’s device key. + val mxDeviceInfo = cryptoStore.getUserDevice(userId = otherUserId, deviceId = otherDeviceId!!) + + if (mxDeviceInfo?.fingerprint() == null) { + Timber.e("## SAS Failed to find device key ") + // TODO force download keys!! + // would be probably better to download the keys + // for now I cancel + cancel(CancelCode.User) + } else { + // val otherKey = info.identityKey() + // need to jump back to correct thread + val accept = transport.createAccept( + tid = transactionId, + keyAgreementProtocol = agreedProtocol!!, + hash = agreedHash!!, + messageAuthenticationCode = agreedMac!!, + shortAuthenticationStrings = agreedShortCode, + commitment = Base64.encodeToString("temporary commitment".toByteArray(), Base64.DEFAULT) + ) + doAccept(accept) + } + } + + private fun doAccept(accept: VerificationInfoAccept) { + this.accepted = accept.asValidObject() + Timber.v("## SAS incoming accept request id:$transactionId") + + // The hash commitment is the hash (using the selected hash algorithm) of the unpadded base64 representation of QB, + // concatenated with the canonical JSON representation of the content of the m.key.verification.start message + val concat = getSAS().publicKey + startReq!!.canonicalJson + accept.commitment = hashUsingAgreedHashMethod(concat) ?: "" + // we need to send this to other device now + state = VerificationTxState.SendingAccept + sendToOther(EventType.KEY_VERIFICATION_ACCEPT, accept, VerificationTxState.Accepted, CancelCode.User) { + if (state == VerificationTxState.SendingAccept) { + // It is possible that we receive the next event before this one :/, in this case we should keep state + state = VerificationTxState.Accepted + } + } + } + + override fun onVerificationAccept(accept: ValidVerificationInfoAccept) { + Timber.v("## SAS invalid message for incoming request id:$transactionId") + cancel(CancelCode.UnexpectedMessage) + } + + override fun onKeyVerificationKey(vKey: ValidVerificationInfoKey) { + Timber.v("## SAS received key for request id:$transactionId") + if (state != VerificationTxState.SendingAccept && state != VerificationTxState.Accepted) { + Timber.e("## SAS received key from invalid state $state") + cancel(CancelCode.UnexpectedMessage) + return + } + + otherKey = vKey.key + // Upon receipt of the m.key.verification.key message from Alice’s device, + // Bob’s device replies with a to_device message with type set to m.key.verification.key, + // sending Bob’s public key QB + val pubKey = getSAS().publicKey + + val keyToDevice = transport.createKey(transactionId, pubKey) + // we need to send this to other device now + state = VerificationTxState.SendingKey + this.sendToOther(EventType.KEY_VERIFICATION_KEY, keyToDevice, VerificationTxState.KeySent, CancelCode.User) { + if (state == VerificationTxState.SendingKey) { + // It is possible that we receive the next event before this one :/, in this case we should keep state + state = VerificationTxState.KeySent + } + } + + // Alice’s and Bob’s devices perform an Elliptic-curve Diffie-Hellman + // (calculate the point (x,y)=dAQB=dBQA and use x as the result of the ECDH), + // using the result as the shared secret. + + getSAS().setTheirPublicKey(otherKey) + + shortCodeBytes = calculateSASBytes() + + if (BuildConfig.LOG_PRIVATE_DATA) { + Timber.v("************ BOB CODE ${getDecimalCodeRepresentation(shortCodeBytes!!)}") + Timber.v("************ BOB EMOJI CODE ${getShortCodeRepresentation(SasMode.EMOJI)}") + } + + state = VerificationTxState.ShortCodeReady + } + + private fun calculateSASBytes(): ByteArray { + when (accepted?.keyAgreementProtocol) { + KEY_AGREEMENT_V1 -> { + // (Note: In all of the following HKDF is as defined in RFC 5869, and uses the previously agreed-on hash function as the hash function, + // the shared secret as the input keying material, no salt, and with the input parameter set to the concatenation of: + // - the string “MATRIX_KEY_VERIFICATION_SAS”, + // - the Matrix ID of the user who sent the m.key.verification.start message, + // - the device ID of the device that sent the m.key.verification.start message, + // - the Matrix ID of the user who sent the m.key.verification.accept message, + // - he device ID of the device that sent the m.key.verification.accept message + // - the transaction ID. + val sasInfo = "MATRIX_KEY_VERIFICATION_SAS$otherUserId$otherDeviceId$userId$deviceId$transactionId" + + // decimal: generate five bytes by using HKDF. + // emoji: generate six bytes by using HKDF. + return getSAS().generateShortCode(sasInfo, 6) + } + KEY_AGREEMENT_V2 -> { + // Adds the SAS public key, and separate by | + val sasInfo = "MATRIX_KEY_VERIFICATION_SAS|$otherUserId|$otherDeviceId|$otherKey|$userId|$deviceId|${getSAS().publicKey}|$transactionId" + return getSAS().generateShortCode(sasInfo, 6) + } + else -> { + // Protocol has been checked earlier + throw IllegalArgumentException() + } + } + } + + override fun onKeyVerificationMac(vMac: ValidVerificationInfoMac) { + Timber.v("## SAS I: received mac for request id:$transactionId") + // Check for state? + if (state != VerificationTxState.SendingKey + && state != VerificationTxState.KeySent + && state != VerificationTxState.ShortCodeReady + && state != VerificationTxState.ShortCodeAccepted + && state != VerificationTxState.SendingMac + && state != VerificationTxState.MacSent) { + Timber.e("## SAS I: received key from invalid state $state") + cancel(CancelCode.UnexpectedMessage) + return + } + + theirMac = vMac + + // Do I have my Mac? + if (myMac != null) { + // I can check + verifyMacs(vMac) + } + // Wait for ShortCode Accepted + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt new file mode 100644 index 0000000000..07e98f52b7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt @@ -0,0 +1,257 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.verification + +import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService +import org.matrix.android.sdk.api.session.crypto.verification.CancelCode +import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction +import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import timber.log.Timber + +internal class DefaultOutgoingSASDefaultVerificationTransaction( + setDeviceVerificationAction: SetDeviceVerificationAction, + userId: String, + deviceId: String?, + cryptoStore: IMXCryptoStore, + crossSigningService: CrossSigningService, + outgoingGossipingRequestManager: OutgoingGossipingRequestManager, + incomingGossipingRequestManager: IncomingGossipingRequestManager, + deviceFingerprint: String, + transactionId: String, + otherUserId: String, + otherDeviceId: String +) : SASDefaultVerificationTransaction( + setDeviceVerificationAction, + userId, + deviceId, + cryptoStore, + crossSigningService, + outgoingGossipingRequestManager, + incomingGossipingRequestManager, + deviceFingerprint, + transactionId, + otherUserId, + otherDeviceId, + isIncoming = false), + OutgoingSasVerificationTransaction { + + override val uxState: OutgoingSasVerificationTransaction.UxState + get() { + return when (val immutableState = state) { + is VerificationTxState.None -> OutgoingSasVerificationTransaction.UxState.WAIT_FOR_START + is VerificationTxState.SendingStart, + is VerificationTxState.Started, + is VerificationTxState.OnAccepted, + is VerificationTxState.SendingKey, + is VerificationTxState.KeySent, + is VerificationTxState.OnKeyReceived -> OutgoingSasVerificationTransaction.UxState.WAIT_FOR_KEY_AGREEMENT + is VerificationTxState.ShortCodeReady -> OutgoingSasVerificationTransaction.UxState.SHOW_SAS + is VerificationTxState.ShortCodeAccepted, + is VerificationTxState.SendingMac, + is VerificationTxState.MacSent, + is VerificationTxState.Verifying -> OutgoingSasVerificationTransaction.UxState.WAIT_FOR_VERIFICATION + is VerificationTxState.Verified -> OutgoingSasVerificationTransaction.UxState.VERIFIED + is VerificationTxState.Cancelled -> { + if (immutableState.byMe) { + OutgoingSasVerificationTransaction.UxState.CANCELLED_BY_OTHER + } else { + OutgoingSasVerificationTransaction.UxState.CANCELLED_BY_ME + } + } + else -> OutgoingSasVerificationTransaction.UxState.UNKNOWN + } + } + + override fun onVerificationStart(startReq: ValidVerificationInfoStart.SasVerificationInfoStart) { + Timber.e("## SAS O: onVerificationStart - unexpected id:$transactionId") + cancel(CancelCode.UnexpectedMessage) + } + + fun start() { + if (state != VerificationTxState.None) { + Timber.e("## SAS O: start verification from invalid state") + // should I cancel?? + throw IllegalStateException("Interactive Key verification already started") + } + + val startMessage = transport.createStartForSas( + deviceId ?: "", + transactionId, + KNOWN_AGREEMENT_PROTOCOLS, + KNOWN_HASHES, + KNOWN_MACS, + KNOWN_SHORT_CODES + ) + + startReq = startMessage.asValidObject() as? ValidVerificationInfoStart.SasVerificationInfoStart + state = VerificationTxState.SendingStart + + sendToOther( + EventType.KEY_VERIFICATION_START, + startMessage, + VerificationTxState.Started, + CancelCode.User, + null + ) + } + +// fun request() { +// if (state != VerificationTxState.None) { +// Timber.e("## start verification from invalid state") +// // should I cancel?? +// throw IllegalStateException("Interactive Key verification already started") +// } +// +// val requestMessage = KeyVerificationRequest( +// fromDevice = session.sessionParams.deviceId ?: "", +// methods = listOf(KeyVerificationStart.VERIF_METHOD_SAS), +// timestamp = System.currentTimeMillis().toInt(), +// transactionId = transactionId +// ) +// +// sendToOther( +// EventType.KEY_VERIFICATION_REQUEST, +// requestMessage, +// VerificationTxState.None, +// CancelCode.User, +// null +// ) +// } + + override fun onVerificationAccept(accept: ValidVerificationInfoAccept) { + Timber.v("## SAS O: onVerificationAccept id:$transactionId") + if (state != VerificationTxState.Started && state != VerificationTxState.SendingStart) { + Timber.e("## SAS O: received accept request from invalid state $state") + cancel(CancelCode.UnexpectedMessage) + return + } + // Check that the agreement is correct + if (!KNOWN_AGREEMENT_PROTOCOLS.contains(accept.keyAgreementProtocol) + || !KNOWN_HASHES.contains(accept.hash) + || !KNOWN_MACS.contains(accept.messageAuthenticationCode) + || accept.shortAuthenticationStrings.intersect(KNOWN_SHORT_CODES).isEmpty()) { + Timber.e("## SAS O: received invalid accept") + cancel(CancelCode.UnknownMethod) + return + } + + // Upon receipt of the m.key.verification.accept message from Bob’s device, + // Alice’s device stores the commitment value for later use. + accepted = accept + state = VerificationTxState.OnAccepted + + // Alice’s device creates an ephemeral Curve25519 key pair (dA,QA), + // and replies with a to_device message with type set to “m.key.verification.key”, sending Alice’s public key QA + val pubKey = getSAS().publicKey + + val keyToDevice = transport.createKey(transactionId, pubKey) + // we need to send this to other device now + state = VerificationTxState.SendingKey + sendToOther(EventType.KEY_VERIFICATION_KEY, keyToDevice, VerificationTxState.KeySent, CancelCode.User) { + // It is possible that we receive the next event before this one :/, in this case we should keep state + if (state == VerificationTxState.SendingKey) { + state = VerificationTxState.KeySent + } + } + } + + override fun onKeyVerificationKey(vKey: ValidVerificationInfoKey) { + Timber.v("## SAS O: onKeyVerificationKey id:$transactionId") + if (state != VerificationTxState.SendingKey && state != VerificationTxState.KeySent) { + Timber.e("## received key from invalid state $state") + cancel(CancelCode.UnexpectedMessage) + return + } + + otherKey = vKey.key + // Upon receipt of the m.key.verification.key message from Bob’s device, + // Alice’s device checks that the commitment property from the Bob’s m.key.verification.accept + // message is the same as the expected value based on the value of the key property received + // in Bob’s m.key.verification.key and the content of Alice’s m.key.verification.start message. + + // check commitment + val concat = vKey.key + startReq!!.canonicalJson + val otherCommitment = hashUsingAgreedHashMethod(concat) ?: "" + + if (accepted!!.commitment.equals(otherCommitment)) { + getSAS().setTheirPublicKey(otherKey) + shortCodeBytes = calculateSASBytes() + state = VerificationTxState.ShortCodeReady + } else { + // bad commitment + cancel(CancelCode.MismatchedCommitment) + } + } + + private fun calculateSASBytes(): ByteArray { + when (accepted?.keyAgreementProtocol) { + KEY_AGREEMENT_V1 -> { + // (Note: In all of the following HKDF is as defined in RFC 5869, and uses the previously agreed-on hash function as the hash function, + // the shared secret as the input keying material, no salt, and with the input parameter set to the concatenation of: + // - the string “MATRIX_KEY_VERIFICATION_SAS”, + // - the Matrix ID of the user who sent the m.key.verification.start message, + // - the device ID of the device that sent the m.key.verification.start message, + // - the Matrix ID of the user who sent the m.key.verification.accept message, + // - he device ID of the device that sent the m.key.verification.accept message + // - the transaction ID. + val sasInfo = "MATRIX_KEY_VERIFICATION_SAS$userId$deviceId$otherUserId$otherDeviceId$transactionId" + + // decimal: generate five bytes by using HKDF. + // emoji: generate six bytes by using HKDF. + return getSAS().generateShortCode(sasInfo, 6) + } + KEY_AGREEMENT_V2 -> { + // Adds the SAS public key, and separate by | + val sasInfo = "MATRIX_KEY_VERIFICATION_SAS|$userId|$deviceId|${getSAS().publicKey}|$otherUserId|$otherDeviceId|$otherKey|$transactionId" + return getSAS().generateShortCode(sasInfo, 6) + } + else -> { + // Protocol has been checked earlier + throw IllegalArgumentException() + } + } + } + + override fun onKeyVerificationMac(vMac: ValidVerificationInfoMac) { + Timber.v("## SAS O: onKeyVerificationMac id:$transactionId") + // There is starting to be a huge amount of state / race here :/ + if (state != VerificationTxState.OnKeyReceived + && state != VerificationTxState.ShortCodeReady + && state != VerificationTxState.ShortCodeAccepted + && state != VerificationTxState.KeySent + && state != VerificationTxState.SendingMac + && state != VerificationTxState.MacSent) { + Timber.e("## SAS O: received mac from invalid state $state") + cancel(CancelCode.UnexpectedMessage) + return + } + + theirMac = vMac + + // Do I have my Mac? + if (myMac != null) { + // I can check + verifyMacs(vMac) + } + // Wait for ShortCode Accepted + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt new file mode 100644 index 0000000000..e61497fd31 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt @@ -0,0 +1,1479 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.verification + +import android.os.Handler +import android.os.Looper +import dagger.Lazy +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService +import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.verification.CancelCode +import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest +import org.matrix.android.sdk.api.session.crypto.verification.QrCodeVerificationTransaction +import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction +import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoReady +import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod +import org.matrix.android.sdk.api.session.crypto.verification.VerificationService +import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction +import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState +import org.matrix.android.sdk.api.session.crypto.verification.safeValueOf +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.LocalEcho +import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent +import org.matrix.android.sdk.api.session.room.model.message.MessageType +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationAcceptContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationCancelContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationDoneContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationKeyContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationMacContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationReadyContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationStartContent +import org.matrix.android.sdk.api.session.room.model.message.ValidVerificationDone +import org.matrix.android.sdk.internal.crypto.DeviceListManager +import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.MyDeviceInfoHolder +import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction +import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent +import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationAccept +import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationCancel +import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationDone +import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationKey +import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationMac +import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationReady +import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationRequest +import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationStart +import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SCAN +import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SHOW +import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_RECIPROCATE +import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_SAS +import org.matrix.android.sdk.internal.crypto.model.rest.toValue +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.verification.qrcode.DefaultQrCodeVerificationTransaction +import org.matrix.android.sdk.internal.crypto.verification.qrcode.QrCodeData +import org.matrix.android.sdk.internal.crypto.verification.qrcode.generateSharedSecretV2 +import org.matrix.android.sdk.internal.di.DeviceId +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import timber.log.Timber +import java.util.UUID +import javax.inject.Inject +import kotlin.collections.set + +@SessionScope +internal class DefaultVerificationService @Inject constructor( + @UserId private val userId: String, + @DeviceId private val deviceId: String?, + private val cryptoStore: IMXCryptoStore, + private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, + private val incomingGossipingRequestManager: IncomingGossipingRequestManager, + private val myDeviceInfoHolder: Lazy, + private val deviceListManager: DeviceListManager, + private val setDeviceVerificationAction: SetDeviceVerificationAction, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val verificationTransportRoomMessageFactory: VerificationTransportRoomMessageFactory, + private val verificationTransportToDeviceFactory: VerificationTransportToDeviceFactory, + private val crossSigningService: CrossSigningService, + private val cryptoCoroutineScope: CoroutineScope, + private val taskExecutor: TaskExecutor +) : DefaultVerificationTransaction.Listener, VerificationService { + + private val uiHandler = Handler(Looper.getMainLooper()) + + // Cannot be injected in constructor as it creates a dependency cycle + lateinit var cryptoService: CryptoService + + // map [sender : [transaction]] + private val txMap = HashMap>() + + // we need to keep track of finished transaction + // It will be used for gossiping (to send request after request is completed and 'done' by other) + private val pastTransactions = HashMap>() + + /** + * Map [sender: [PendingVerificationRequest]] + * For now we keep all requests (even terminated ones) during the lifetime of the app. + */ + private val pendingRequests = HashMap>() + + // Event received from the sync + fun onToDeviceEvent(event: Event) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + when (event.getClearType()) { + EventType.KEY_VERIFICATION_START -> { + onStartRequestReceived(event) + } + EventType.KEY_VERIFICATION_CANCEL -> { + onCancelReceived(event) + } + EventType.KEY_VERIFICATION_ACCEPT -> { + onAcceptReceived(event) + } + EventType.KEY_VERIFICATION_KEY -> { + onKeyReceived(event) + } + EventType.KEY_VERIFICATION_MAC -> { + onMacReceived(event) + } + EventType.KEY_VERIFICATION_READY -> { + onReadyReceived(event) + } + EventType.KEY_VERIFICATION_DONE -> { + onDoneReceived(event) + } + MessageType.MSGTYPE_VERIFICATION_REQUEST -> { + onRequestReceived(event) + } + else -> { + // ignore + } + } + } + } + + fun onRoomEvent(event: Event) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + when (event.getClearType()) { + EventType.KEY_VERIFICATION_START -> { + onRoomStartRequestReceived(event) + } + EventType.KEY_VERIFICATION_CANCEL -> { + // MultiSessions | ignore events if i didn't sent the start from this device, or accepted from this device + onRoomCancelReceived(event) + } + EventType.KEY_VERIFICATION_ACCEPT -> { + onRoomAcceptReceived(event) + } + EventType.KEY_VERIFICATION_KEY -> { + onRoomKeyRequestReceived(event) + } + EventType.KEY_VERIFICATION_MAC -> { + onRoomMacReceived(event) + } + EventType.KEY_VERIFICATION_READY -> { + onRoomReadyReceived(event) + } + EventType.KEY_VERIFICATION_DONE -> { + onRoomDoneReceived(event) + } + EventType.MESSAGE -> { + if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel()?.msgType) { + onRoomRequestReceived(event) + } + } + else -> { + // ignore + } + } + } + } + + private var listeners = ArrayList() + + override fun addListener(listener: VerificationService.Listener) { + uiHandler.post { + if (!listeners.contains(listener)) { + listeners.add(listener) + } + } + } + + override fun removeListener(listener: VerificationService.Listener) { + uiHandler.post { + listeners.remove(listener) + } + } + + private fun dispatchTxAdded(tx: VerificationTransaction) { + uiHandler.post { + listeners.forEach { + try { + it.transactionCreated(tx) + } catch (e: Throwable) { + Timber.e(e, "## Error while notifying listeners") + } + } + } + } + + private fun dispatchTxUpdated(tx: VerificationTransaction) { + uiHandler.post { + listeners.forEach { + try { + it.transactionUpdated(tx) + } catch (e: Throwable) { + Timber.e(e, "## Error while notifying listeners") + } + } + } + } + + private fun dispatchRequestAdded(tx: PendingVerificationRequest) { + uiHandler.post { + listeners.forEach { + try { + it.verificationRequestCreated(tx) + } catch (e: Throwable) { + Timber.e(e, "## Error while notifying listeners") + } + } + } + } + + private fun dispatchRequestUpdated(tx: PendingVerificationRequest) { + uiHandler.post { + listeners.forEach { + try { + it.verificationRequestUpdated(tx) + } catch (e: Throwable) { + Timber.e(e, "## Error while notifying listeners") + } + } + } + } + + override fun markedLocallyAsManuallyVerified(userId: String, deviceID: String) { + setDeviceVerificationAction.handle(DeviceTrustLevel(false, true), + userId, + deviceID) + + listeners.forEach { + try { + it.markedAsManuallyVerified(userId, deviceID) + } catch (e: Throwable) { + Timber.e(e, "## Error while notifying listeners") + } + } + } + + fun onRoomRequestHandledByOtherDevice(event: Event) { + val requestInfo = event.content.toModel() + ?: return + val requestId = requestInfo.relatesTo?.eventId ?: return + getExistingVerificationRequestInRoom(event.roomId ?: "", requestId)?.let { + updatePendingRequest( + it.copy( + handledByOtherSession = true + ) + ) + } + } + + private fun onRequestReceived(event: Event) { + val validRequestInfo = event.getClearContent().toModel()?.asValidObject() + + if (validRequestInfo == null) { + // ignore + Timber.e("## SAS Received invalid key request") + return + } + val senderId = event.senderId ?: return + + // We don't want to block here + val otherDeviceId = validRequestInfo.fromDevice + + cryptoCoroutineScope.launch { + if (checkKeysAreDownloaded(senderId, otherDeviceId) == null) { + Timber.e("## Verification device $otherDeviceId is not known") + } + } + + // Remember this request + val requestsForUser = pendingRequests.getOrPut(senderId) { mutableListOf() } + + val pendingVerificationRequest = PendingVerificationRequest( + ageLocalTs = event.ageLocalTs ?: System.currentTimeMillis(), + isIncoming = true, + otherUserId = senderId, // requestInfo.toUserId, + roomId = null, + transactionId = validRequestInfo.transactionId, + localId = validRequestInfo.transactionId, + requestInfo = validRequestInfo + ) + requestsForUser.add(pendingVerificationRequest) + dispatchRequestAdded(pendingVerificationRequest) + } + + suspend fun onRoomRequestReceived(event: Event) { + Timber.v("## SAS Verification request from ${event.senderId} in room ${event.roomId}") + val requestInfo = event.getClearContent().toModel() ?: return + val validRequestInfo = requestInfo + // copy the EventId to the transactionId + .copy(transactionId = event.eventId) + .asValidObject() ?: return + + val senderId = event.senderId ?: return + + if (requestInfo.toUserId != userId) { + // I should ignore this, it's not for me + Timber.w("## SAS Verification ignoring request from ${event.senderId}, not sent to me") + return + } + + // We don't want to block here + taskExecutor.executorScope.launch { + if (checkKeysAreDownloaded(senderId, validRequestInfo.fromDevice) == null) { + Timber.e("## SAS Verification device ${validRequestInfo.fromDevice} is not known") + } + } + + // Remember this request + val requestsForUser = pendingRequests.getOrPut(senderId) { mutableListOf() } + + val pendingVerificationRequest = PendingVerificationRequest( + ageLocalTs = event.ageLocalTs ?: System.currentTimeMillis(), + isIncoming = true, + otherUserId = senderId, // requestInfo.toUserId, + roomId = event.roomId, + transactionId = event.eventId, + localId = event.eventId!!, + requestInfo = validRequestInfo + ) + requestsForUser.add(pendingVerificationRequest) + dispatchRequestAdded(pendingVerificationRequest) + + /* + * After the m.key.verification.ready event is sent, either party can send an m.key.verification.start event + * to begin the verification. + * If both parties send an m.key.verification.start event, and they both specify the same verification method, + * then the event sent by the user whose user ID is the smallest is used, and the other m.key.verification.start + * event is ignored. + * In the case of a single user verifying two of their devices, the device ID is compared instead. + * If both parties send an m.key.verification.start event, but they specify different verification methods, + * the verification should be cancelled with a code of m.unexpected_message. + */ + } + + override fun onPotentiallyInterestingEventRoomFailToDecrypt(event: Event) { + // When Should/Can we cancel?? + val relationContent = event.content.toModel()?.relatesTo + if (relationContent?.type == RelationType.REFERENCE) { + val relatedId = relationContent.eventId ?: return + // at least if request was sent by me, I can safely cancel without interfering + pendingRequests[event.senderId]?.firstOrNull { + it.transactionId == relatedId && !it.isIncoming + }?.let { pr -> + verificationTransportRoomMessageFactory.createTransport(event.roomId ?: "", null) + .cancelTransaction( + relatedId, + event.senderId ?: "", + event.getSenderKey() ?: "", + CancelCode.InvalidMessage + ) + updatePendingRequest(pr.copy(cancelConclusion = CancelCode.InvalidMessage)) + } + } + } + + private suspend fun onRoomStartRequestReceived(event: Event) { + val startReq = event.getClearContent().toModel() + ?.copy( + // relates_to is in clear in encrypted payload + relatesTo = event.content.toModel()?.relatesTo + ) + + val validStartReq = startReq?.asValidObject() + + val otherUserId = event.senderId + if (validStartReq == null) { + Timber.e("## received invalid verification request") + if (startReq?.transactionId != null) { + verificationTransportRoomMessageFactory.createTransport(event.roomId ?: "", null) + .cancelTransaction( + startReq.transactionId ?: "", + otherUserId!!, + startReq.fromDevice ?: event.getSenderKey()!!, + CancelCode.UnknownMethod + ) + } + return + } + + handleStart(otherUserId, validStartReq) { + it.transport = verificationTransportRoomMessageFactory.createTransport(event.roomId ?: "", it) + }?.let { + verificationTransportRoomMessageFactory.createTransport(event.roomId ?: "", null) + .cancelTransaction( + validStartReq.transactionId, + otherUserId!!, + validStartReq.fromDevice, + it + ) + } + } + + private suspend fun onStartRequestReceived(event: Event) { + Timber.e("## SAS received Start request ${event.eventId}") + val startReq = event.getClearContent().toModel() + val validStartReq = startReq?.asValidObject() + Timber.v("## SAS received Start request $startReq") + + val otherUserId = event.senderId!! + if (validStartReq == null) { + Timber.e("## SAS received invalid verification request") + if (startReq?.transactionId != null) { + verificationTransportToDeviceFactory.createTransport(null).cancelTransaction( + startReq.transactionId, + otherUserId, + startReq.fromDevice ?: event.getSenderKey()!!, + CancelCode.UnknownMethod + ) + } + return + } + // Download device keys prior to everything + handleStart(otherUserId, validStartReq) { + it.transport = verificationTransportToDeviceFactory.createTransport(it) + }?.let { + verificationTransportToDeviceFactory.createTransport(null).cancelTransaction( + validStartReq.transactionId, + otherUserId, + validStartReq.fromDevice, + it + ) + } + } + + /** + * Return a CancelCode to make the caller cancel the verification. Else return null + */ + private suspend fun handleStart(otherUserId: String?, + startReq: ValidVerificationInfoStart, + txConfigure: (DefaultVerificationTransaction) -> Unit): CancelCode? { + Timber.d("## SAS onStartRequestReceived $startReq") + if (otherUserId?.let { checkKeysAreDownloaded(it, startReq.fromDevice) } != null) { + val tid = startReq.transactionId + var existing = getExistingTransaction(otherUserId, tid) + + // After the m.key.verification.ready event is sent, either party can send an + // m.key.verification.start event to begin the verification. If both parties + // send an m.key.verification.start event, and they both specify the same + // verification method, then the event sent by the user whose user ID is the + // smallest is used, and the other m.key.verification.start event is ignored. + // In the case of a single user verifying two of their devices, the device ID is + // compared instead . + if (existing is DefaultOutgoingSASDefaultVerificationTransaction) { + val readyRequest = getExistingVerificationRequest(otherUserId, tid) + if (readyRequest?.isReady == true) { + if (isOtherPrioritary(otherUserId, existing.otherDeviceId ?: "")) { + Timber.d("## SAS concurrent start isOtherPrioritary, clear") + // The other is prioritary! + // I should replace my outgoing with an incoming + removeTransaction(otherUserId, tid) + existing = null + } else { + Timber.d("## SAS concurrent start i am prioritary, ignore") + // i am prioritary, ignore this start event! + return null + } + } + } + + when (startReq) { + is ValidVerificationInfoStart.SasVerificationInfoStart -> { + when (existing) { + is SasVerificationTransaction -> { + // should cancel both! + Timber.v("## SAS onStartRequestReceived - Request exist with same id ${startReq.transactionId}") + existing.cancel(CancelCode.UnexpectedMessage) + // Already cancelled, so return null + return null + } + is QrCodeVerificationTransaction -> { + // Nothing to do? + } + null -> { + getExistingTransactionsForUser(otherUserId) + ?.filterIsInstance(SasVerificationTransaction::class.java) + ?.takeIf { it.isNotEmpty() } + ?.also { + // Multiple keyshares between two devices: + // any two devices may only have at most one key verification in flight at a time. + Timber.v("## SAS onStartRequestReceived - Already a transaction with this user ${startReq.transactionId}") + } + ?.forEach { + it.cancel(CancelCode.UnexpectedMessage) + } + ?.also { + return CancelCode.UnexpectedMessage + } + } + } + + // Ok we can create a SAS transaction + Timber.v("## SAS onStartRequestReceived - request accepted ${startReq.transactionId}") + // If there is a corresponding request, we can auto accept + // as we are the one requesting in first place (or we accepted the request) + // I need to check if the pending request was related to this device also + val autoAccept = getExistingVerificationRequest(otherUserId)?.any { + it.transactionId == startReq.transactionId + && (it.requestInfo?.fromDevice == this.deviceId || it.readyInfo?.fromDevice == this.deviceId) + } + ?: false + val tx = DefaultIncomingSASDefaultVerificationTransaction( +// this, + setDeviceVerificationAction, + userId, + deviceId, + cryptoStore, + crossSigningService, + outgoingGossipingRequestManager, + incomingGossipingRequestManager, + myDeviceInfoHolder.get().myDevice.fingerprint()!!, + startReq.transactionId, + otherUserId, + autoAccept).also { txConfigure(it) } + addTransaction(tx) + tx.onVerificationStart(startReq) + return null + } + is ValidVerificationInfoStart.ReciprocateVerificationInfoStart -> { + // Other user has scanned my QR code + if (existing is DefaultQrCodeVerificationTransaction) { + existing.onStartReceived(startReq) + return null + } else { + Timber.w("## SAS onStartRequestReceived - unexpected message ${startReq.transactionId} / $existing") + return CancelCode.UnexpectedMessage + } + } + } + } else { + return CancelCode.UnexpectedMessage + } + } + + private fun isOtherPrioritary(otherUserId: String, otherDeviceId: String): Boolean { + if (userId < otherUserId) { + return false + } else if (userId > otherUserId) { + return true + } else { + return otherDeviceId < deviceId ?: "" + } + } + + // TODO Refacto: It could just return a boolean + private suspend fun checkKeysAreDownloaded(otherUserId: String, + otherDeviceId: String): MXUsersDevicesMap? { + return try { + var keys = deviceListManager.downloadKeys(listOf(otherUserId), false) + if (keys.getUserDeviceIds(otherUserId)?.contains(otherDeviceId) == true) { + return keys + } else { + // force download + keys = deviceListManager.downloadKeys(listOf(otherUserId), true) + return keys.takeIf { keys.getUserDeviceIds(otherUserId)?.contains(otherDeviceId) == true } + } + } catch (e: Exception) { + null + } + } + + private fun onRoomCancelReceived(event: Event) { + val cancelReq = event.getClearContent().toModel() + ?.copy( + // relates_to is in clear in encrypted payload + relatesTo = event.content.toModel()?.relatesTo + ) + + val validCancelReq = cancelReq?.asValidObject() + + if (validCancelReq == null) { + // ignore + Timber.e("## SAS Received invalid cancel request") + // TODO should we cancel? + return + } + getExistingVerificationRequest(event.senderId ?: "", validCancelReq.transactionId)?.let { + updatePendingRequest(it.copy(cancelConclusion = safeValueOf(validCancelReq.code))) + // Should we remove it from the list? + } + handleOnCancel(event.senderId!!, validCancelReq) + } + + private fun onCancelReceived(event: Event) { + Timber.v("## SAS onCancelReceived") + val cancelReq = event.getClearContent().toModel()?.asValidObject() + + if (cancelReq == null) { + // ignore + Timber.e("## SAS Received invalid cancel request") + return + } + val otherUserId = event.senderId!! + + handleOnCancel(otherUserId, cancelReq) + } + + private fun handleOnCancel(otherUserId: String, cancelReq: ValidVerificationInfoCancel) { + Timber.v("## SAS onCancelReceived otherUser: $otherUserId reason: ${cancelReq.reason}") + + val existingTransaction = getExistingTransaction(otherUserId, cancelReq.transactionId) + val existingRequest = getExistingVerificationRequest(otherUserId, cancelReq.transactionId) + + if (existingRequest != null) { + // Mark this request as cancelled + updatePendingRequest(existingRequest.copy( + cancelConclusion = safeValueOf(cancelReq.code) + )) + } + + existingTransaction?.state = VerificationTxState.Cancelled(safeValueOf(cancelReq.code), false) + } + + private fun onRoomAcceptReceived(event: Event) { + Timber.d("## SAS Received Accept via DM $event") + val accept = event.getClearContent().toModel() + ?.copy( + // relates_to is in clear in encrypted payload + relatesTo = event.content.toModel()?.relatesTo + ) + ?: return + + val validAccept = accept.asValidObject() ?: return + + handleAccept(validAccept, event.senderId!!) + } + + private fun onAcceptReceived(event: Event) { + Timber.d("## SAS Received Accept $event") + val acceptReq = event.getClearContent().toModel()?.asValidObject() ?: return + handleAccept(acceptReq, event.senderId!!) + } + + private fun handleAccept(acceptReq: ValidVerificationInfoAccept, senderId: String) { + val otherUserId = senderId + val existing = getExistingTransaction(otherUserId, acceptReq.transactionId) + if (existing == null) { + Timber.e("## SAS Received invalid accept request") + return + } + + if (existing is SASDefaultVerificationTransaction) { + existing.onVerificationAccept(acceptReq) + } else { + // not other types now + } + } + + private fun onRoomKeyRequestReceived(event: Event) { + val keyReq = event.getClearContent().toModel() + ?.copy( + // relates_to is in clear in encrypted payload + relatesTo = event.content.toModel()?.relatesTo + ) + ?.asValidObject() + if (keyReq == null) { + // ignore + Timber.e("## SAS Received invalid key request") + // TODO should we cancel? + return + } + handleKeyReceived(event, keyReq) + } + + private fun onKeyReceived(event: Event) { + val keyReq = event.getClearContent().toModel()?.asValidObject() + + if (keyReq == null) { + // ignore + Timber.e("## SAS Received invalid key request") + return + } + handleKeyReceived(event, keyReq) + } + + private fun handleKeyReceived(event: Event, keyReq: ValidVerificationInfoKey) { + Timber.d("## SAS Received Key from ${event.senderId} with info $keyReq") + val otherUserId = event.senderId!! + val existing = getExistingTransaction(otherUserId, keyReq.transactionId) + if (existing == null) { + Timber.e("## SAS Received invalid key request") + return + } + if (existing is SASDefaultVerificationTransaction) { + existing.onKeyVerificationKey(keyReq) + } else { + // not other types now + } + } + + private fun onRoomMacReceived(event: Event) { + val macReq = event.getClearContent().toModel() + ?.copy( + // relates_to is in clear in encrypted payload + relatesTo = event.content.toModel()?.relatesTo + ) + ?.asValidObject() + if (macReq == null || event.senderId == null) { + // ignore + Timber.e("## SAS Received invalid mac request") + // TODO should we cancel? + return + } + handleMacReceived(event.senderId, macReq) + } + + private suspend fun onRoomReadyReceived(event: Event) { + val readyReq = event.getClearContent().toModel() + ?.copy( + // relates_to is in clear in encrypted payload + relatesTo = event.content.toModel()?.relatesTo + ) + ?.asValidObject() + if (readyReq == null || event.senderId == null) { + // ignore + Timber.e("## SAS Received invalid ready request") + // TODO should we cancel? + return + } + if (checkKeysAreDownloaded(event.senderId, readyReq.fromDevice) == null) { + Timber.e("## SAS Verification device ${readyReq.fromDevice} is not known") + // TODO cancel? + return + } + + handleReadyReceived(event.senderId, readyReq) { + verificationTransportRoomMessageFactory.createTransport(event.roomId!!, it) + } + } + + private suspend fun onReadyReceived(event: Event) { + val readyReq = event.getClearContent().toModel()?.asValidObject() + Timber.v("## SAS onReadyReceived $readyReq") + + if (readyReq == null || event.senderId == null) { + // ignore + Timber.e("## SAS Received invalid ready request") + // TODO should we cancel? + return + } + if (checkKeysAreDownloaded(event.senderId, readyReq.fromDevice) == null) { + Timber.e("## SAS Verification device ${readyReq.fromDevice} is not known") + // TODO cancel? + return + } + + handleReadyReceived(event.senderId, readyReq) { + verificationTransportToDeviceFactory.createTransport(it) + } + } + + private fun onDoneReceived(event: Event) { + Timber.v("## onDoneReceived") + val doneReq = event.getClearContent().toModel()?.asValidObject() + if (doneReq == null || event.senderId == null) { + // ignore + Timber.e("## SAS Received invalid done request") + return + } + + handleDoneReceived(event.senderId, doneReq) + + if (event.senderId == userId) { + // We only send gossiping request when the other sent us a done + // We can ask without checking too much thinks (like trust), because we will check validity of secret on reception + getExistingTransaction(userId, doneReq.transactionId) + ?: getOldTransaction(userId, doneReq.transactionId) + ?.let { vt -> + val otherDeviceId = vt.otherDeviceId + if (!crossSigningService.canCrossSign()) { + outgoingGossipingRequestManager.sendSecretShareRequest(MASTER_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId + ?: "*"))) + outgoingGossipingRequestManager.sendSecretShareRequest(SELF_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId + ?: "*"))) + outgoingGossipingRequestManager.sendSecretShareRequest(USER_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId + ?: "*"))) + } + outgoingGossipingRequestManager.sendSecretShareRequest(KEYBACKUP_SECRET_SSSS_NAME, mapOf(userId to listOf(otherDeviceId + ?: "*"))) + } + } + } + + private fun handleDoneReceived(senderId: String, doneReq: ValidVerificationDone) { + Timber.v("## SAS Done received $doneReq") + val existing = getExistingTransaction(senderId, doneReq.transactionId) + if (existing == null) { + Timber.e("## SAS Received invalid Done request") + return + } + if (existing is DefaultQrCodeVerificationTransaction) { + existing.onDoneReceived() + } else { + // SAS do not care for now? + } + + // Now transactions are udated, let's also update Requests + val existingRequest = getExistingVerificationRequest(senderId)?.find { it.transactionId == doneReq.transactionId } + if (existingRequest == null) { + Timber.e("## SAS Received Done for unknown request txId:${doneReq.transactionId}") + return + } + updatePendingRequest(existingRequest.copy(isSuccessful = true)) + } + + private fun onRoomDoneReceived(event: Event) { + val doneReq = event.getClearContent().toModel() + ?.copy( + // relates_to is in clear in encrypted payload + relatesTo = event.content.toModel()?.relatesTo + ) + ?.asValidObject() + + if (doneReq == null || event.senderId == null) { + // ignore + Timber.e("## SAS Received invalid Done request") + // TODO should we cancel? + return + } + + handleDoneReceived(event.senderId, doneReq) + } + + private fun onMacReceived(event: Event) { + val macReq = event.getClearContent().toModel()?.asValidObject() + + if (macReq == null || event.senderId == null) { + // ignore + Timber.e("## SAS Received invalid mac request") + return + } + handleMacReceived(event.senderId, macReq) + } + + private fun handleMacReceived(senderId: String, macReq: ValidVerificationInfoMac) { + Timber.v("## SAS Received $macReq") + val existing = getExistingTransaction(senderId, macReq.transactionId) + if (existing == null) { + Timber.e("## SAS Received invalid Mac request") + return + } + if (existing is SASDefaultVerificationTransaction) { + existing.onKeyVerificationMac(macReq) + } else { + // not other types known for now + } + } + + private fun handleReadyReceived(senderId: String, + readyReq: ValidVerificationInfoReady, + transportCreator: (DefaultVerificationTransaction) -> VerificationTransport) { + val existingRequest = getExistingVerificationRequest(senderId)?.find { it.transactionId == readyReq.transactionId } + if (existingRequest == null) { + Timber.e("## SAS Received Ready for unknown request txId:${readyReq.transactionId} fromDevice ${readyReq.fromDevice}") + return + } + + val qrCodeData = readyReq.methods + // Check if other user is able to scan QR code + .takeIf { it.contains(VERIFICATION_METHOD_QR_CODE_SCAN) } + ?.let { + createQrCodeData(existingRequest.transactionId, existingRequest.otherUserId, readyReq.fromDevice) + } + + if (readyReq.methods.contains(VERIFICATION_METHOD_RECIPROCATE)) { + // Create the pending transaction + val tx = DefaultQrCodeVerificationTransaction( + setDeviceVerificationAction = setDeviceVerificationAction, + transactionId = readyReq.transactionId, + otherUserId = senderId, + otherDeviceId = readyReq.fromDevice, + crossSigningService = crossSigningService, + outgoingGossipingRequestManager = outgoingGossipingRequestManager, + incomingGossipingRequestManager = incomingGossipingRequestManager, + cryptoStore = cryptoStore, + qrCodeData = qrCodeData, + userId = userId, + deviceId = deviceId ?: "", + isIncoming = false) + + tx.transport = transportCreator.invoke(tx) + + addTransaction(tx) + } + + updatePendingRequest(existingRequest.copy( + readyInfo = readyReq + )) + } + + private fun createQrCodeData(requestId: String?, otherUserId: String, otherDeviceId: String?): QrCodeData? { + requestId ?: run { + Timber.w("## Unknown requestId") + return null + } + + return when { + userId != otherUserId -> + createQrCodeDataForDistinctUser(requestId, otherUserId) + crossSigningService.isCrossSigningVerified() -> + // This is a self verification and I am the old device (Osborne2) + createQrCodeDataForVerifiedDevice(requestId, otherDeviceId) + else -> + // This is a self verification and I am the new device (Dynabook) + createQrCodeDataForUnVerifiedDevice(requestId) + } + } + + private fun createQrCodeDataForDistinctUser(requestId: String, otherUserId: String): QrCodeData.VerifyingAnotherUser? { + val myMasterKey = crossSigningService.getMyCrossSigningKeys() + ?.masterKey() + ?.unpaddedBase64PublicKey + ?: run { + Timber.w("## Unable to get my master key") + return null + } + + val otherUserMasterKey = crossSigningService.getUserCrossSigningKeys(otherUserId) + ?.masterKey() + ?.unpaddedBase64PublicKey + ?: run { + Timber.w("## Unable to get other user master key") + return null + } + + return QrCodeData.VerifyingAnotherUser( + transactionId = requestId, + userMasterCrossSigningPublicKey = myMasterKey, + otherUserMasterCrossSigningPublicKey = otherUserMasterKey, + sharedSecret = generateSharedSecretV2() + ) + } + + // Create a QR code to display on the old device (Osborne2) + private fun createQrCodeDataForVerifiedDevice(requestId: String, otherDeviceId: String?): QrCodeData.SelfVerifyingMasterKeyTrusted? { + val myMasterKey = crossSigningService.getMyCrossSigningKeys() + ?.masterKey() + ?.unpaddedBase64PublicKey + ?: run { + Timber.w("## Unable to get my master key") + return null + } + + val otherDeviceKey = otherDeviceId + ?.let { + cryptoStore.getUserDevice(userId, otherDeviceId)?.fingerprint() + } + ?: run { + Timber.w("## Unable to get other device data") + return null + } + + return QrCodeData.SelfVerifyingMasterKeyTrusted( + transactionId = requestId, + userMasterCrossSigningPublicKey = myMasterKey, + otherDeviceKey = otherDeviceKey, + sharedSecret = generateSharedSecretV2() + ) + } + + // Create a QR code to display on the new device (Dynabook) + private fun createQrCodeDataForUnVerifiedDevice(requestId: String): QrCodeData.SelfVerifyingMasterKeyNotTrusted? { + val myMasterKey = crossSigningService.getMyCrossSigningKeys() + ?.masterKey() + ?.unpaddedBase64PublicKey + ?: run { + Timber.w("## Unable to get my master key") + return null + } + + val myDeviceKey = myDeviceInfoHolder.get().myDevice.fingerprint() + ?: run { + Timber.w("## Unable to get my fingerprint") + return null + } + + return QrCodeData.SelfVerifyingMasterKeyNotTrusted( + transactionId = requestId, + deviceKey = myDeviceKey, + userMasterCrossSigningPublicKey = myMasterKey, + sharedSecret = generateSharedSecretV2() + ) + } + +// private fun handleDoneReceived(senderId: String, doneInfo: ValidVerificationDone) { +// val existingRequest = getExistingVerificationRequest(senderId)?.find { it.transactionId == doneInfo.transactionId } +// if (existingRequest == null) { +// Timber.e("## SAS Received Done for unknown request txId:${doneInfo.transactionId}") +// return +// } +// updatePendingRequest(existingRequest.copy(isSuccessful = true)) +// } + + // TODO All this methods should be delegated to a TransactionStore + override fun getExistingTransaction(otherUserId: String, tid: String): VerificationTransaction? { + synchronized(lock = txMap) { + return txMap[otherUserId]?.get(tid) + } + } + + override fun getExistingVerificationRequest(otherUserId: String): List? { + synchronized(lock = pendingRequests) { + return pendingRequests[otherUserId] + } + } + + override fun getExistingVerificationRequest(otherUserId: String, tid: String?): PendingVerificationRequest? { + synchronized(lock = pendingRequests) { + return tid?.let { tid -> pendingRequests[otherUserId]?.firstOrNull { it.transactionId == tid } } + } + } + + override fun getExistingVerificationRequestInRoom(roomId: String, tid: String?): PendingVerificationRequest? { + synchronized(lock = pendingRequests) { + return tid?.let { tid -> + pendingRequests.flatMap { entry -> + entry.value.filter { it.roomId == roomId && it.transactionId == tid } + }.firstOrNull() + } + } + } + + private fun getExistingTransactionsForUser(otherUser: String): Collection? { + synchronized(txMap) { + return txMap[otherUser]?.values + } + } + + private fun removeTransaction(otherUser: String, tid: String) { + synchronized(txMap) { + txMap[otherUser]?.remove(tid)?.also { + it.removeListener(this) + } + }?.let { + rememberOldTransaction(it) + } + } + + private fun addTransaction(tx: DefaultVerificationTransaction) { + synchronized(txMap) { + val txInnerMap = txMap.getOrPut(tx.otherUserId) { HashMap() } + txInnerMap[tx.transactionId] = tx + dispatchTxAdded(tx) + tx.addListener(this) + } + } + + private fun rememberOldTransaction(tx: DefaultVerificationTransaction) { + synchronized(pastTransactions) { + pastTransactions.getOrPut(tx.otherUserId) { HashMap() }[tx.transactionId] = tx + } + } + + private fun getOldTransaction(userId: String, tid: String?): DefaultVerificationTransaction? { + return tid?.let { + synchronized(pastTransactions) { + pastTransactions[userId]?.get(it) + } + } + } + + override fun beginKeyVerification(method: VerificationMethod, otherUserId: String, otherDeviceId: String, transactionId: String?): String? { + val txID = transactionId?.takeIf { it.isNotEmpty() } ?: createUniqueIDForTransaction(otherUserId, otherDeviceId) + // should check if already one (and cancel it) + if (method == VerificationMethod.SAS) { + val tx = DefaultOutgoingSASDefaultVerificationTransaction( + setDeviceVerificationAction, + userId, + deviceId, + cryptoStore, + crossSigningService, + outgoingGossipingRequestManager, + incomingGossipingRequestManager, + myDeviceInfoHolder.get().myDevice.fingerprint()!!, + txID, + otherUserId, + otherDeviceId) + tx.transport = verificationTransportToDeviceFactory.createTransport(tx) + addTransaction(tx) + + tx.start() + return txID + } else { + throw IllegalArgumentException("Unknown verification method") + } + } + + override fun requestKeyVerificationInDMs(methods: List, otherUserId: String, roomId: String, localId: String?) + : PendingVerificationRequest { + Timber.i("## SAS Requesting verification to user: $otherUserId in room $roomId") + + val requestsForUser = pendingRequests.getOrPut(otherUserId) { mutableListOf() } + + val transport = verificationTransportRoomMessageFactory.createTransport(roomId, null) + + // Cancel existing pending requests? + requestsForUser.toList().forEach { existingRequest -> + existingRequest.transactionId?.let { tid -> + if (!existingRequest.isFinished) { + Timber.d("## SAS, cancelling pending requests to start a new one") + updatePendingRequest(existingRequest.copy(cancelConclusion = CancelCode.User)) + transport.cancelTransaction(tid, existingRequest.otherUserId, "", CancelCode.User) + } + } + } + + val validLocalId = localId ?: LocalEcho.createLocalEchoId() + + val verificationRequest = PendingVerificationRequest( + ageLocalTs = System.currentTimeMillis(), + isIncoming = false, + roomId = roomId, + localId = validLocalId, + otherUserId = otherUserId + ) + + // We can SCAN or SHOW QR codes only if cross-signing is verified + val methodValues = if (crossSigningService.isCrossSigningVerified()) { + // Add reciprocate method if application declares it can scan or show QR codes + // Not sure if it ok to do that (?) + val reciprocateMethod = methods + .firstOrNull { it == VerificationMethod.QR_CODE_SCAN || it == VerificationMethod.QR_CODE_SHOW } + ?.let { listOf(VERIFICATION_METHOD_RECIPROCATE) }.orEmpty() + methods.map { it.toValue() } + reciprocateMethod + } else { + // Filter out SCAN and SHOW qr code method + methods + .filter { it != VerificationMethod.QR_CODE_SHOW && it != VerificationMethod.QR_CODE_SCAN } + .map { it.toValue() } + } + .distinct() + + transport.sendVerificationRequest(methodValues, validLocalId, otherUserId, roomId, null) { syncedId, info -> + // We need to update with the syncedID + updatePendingRequest(verificationRequest.copy( + transactionId = syncedId, + // localId stays different + requestInfo = info + )) + } + + requestsForUser.add(verificationRequest) + dispatchRequestAdded(verificationRequest) + + return verificationRequest + } + + override fun cancelVerificationRequest(request: PendingVerificationRequest) { + if (request.roomId != null) { + val transport = verificationTransportRoomMessageFactory.createTransport(request.roomId, null) + transport.cancelTransaction(request.transactionId ?: "", request.otherUserId, null, CancelCode.User) + } else { + val transport = verificationTransportToDeviceFactory.createTransport(null) + request.targetDevices?.forEach { deviceId -> + transport.cancelTransaction(request.transactionId ?: "", request.otherUserId, deviceId, CancelCode.User) + } + } + } + + override fun requestKeyVerification(methods: List, otherUserId: String, otherDevices: List?): PendingVerificationRequest { + // TODO refactor this with the DM one + Timber.i("## Requesting verification to user: $otherUserId with device list $otherDevices") + + val targetDevices = otherDevices ?: cryptoService.getUserDevices(otherUserId).map { it.deviceId } + val requestsForUser = pendingRequests.getOrPut(otherUserId) { mutableListOf() } + + val transport = verificationTransportToDeviceFactory.createTransport(null) + + // Cancel existing pending requests? + requestsForUser.toList().forEach { existingRequest -> + existingRequest.transactionId?.let { tid -> + if (!existingRequest.isFinished) { + Timber.d("## SAS, cancelling pending requests to start a new one") + updatePendingRequest(existingRequest.copy(cancelConclusion = CancelCode.User)) + existingRequest.targetDevices?.forEach { + transport.cancelTransaction(tid, existingRequest.otherUserId, it, CancelCode.User) + } + } + } + } + + val localId = LocalEcho.createLocalEchoId() + + val verificationRequest = PendingVerificationRequest( + transactionId = localId, + ageLocalTs = System.currentTimeMillis(), + isIncoming = false, + roomId = null, + localId = localId, + otherUserId = otherUserId, + targetDevices = targetDevices + ) + + // We can SCAN or SHOW QR codes only if cross-signing is enabled + val methodValues = if (crossSigningService.isCrossSigningInitialized()) { + // Add reciprocate method if application declares it can scan or show QR codes + // Not sure if it ok to do that (?) + val reciprocateMethod = methods + .firstOrNull { it == VerificationMethod.QR_CODE_SCAN || it == VerificationMethod.QR_CODE_SHOW } + ?.let { listOf(VERIFICATION_METHOD_RECIPROCATE) }.orEmpty() + methods.map { it.toValue() } + reciprocateMethod + } else { + // Filter out SCAN and SHOW qr code method + methods + .filter { it != VerificationMethod.QR_CODE_SHOW && it != VerificationMethod.QR_CODE_SCAN } + .map { it.toValue() } + } + .distinct() + + transport.sendVerificationRequest(methodValues, localId, otherUserId, null, targetDevices) { _, info -> + // Nothing special to do in to device mode + updatePendingRequest(verificationRequest.copy( + // localId stays different + requestInfo = info + )) + } + + requestsForUser.add(verificationRequest) + dispatchRequestAdded(verificationRequest) + + return verificationRequest + } + + override fun declineVerificationRequestInDMs(otherUserId: String, transactionId: String, roomId: String) { + verificationTransportRoomMessageFactory.createTransport(roomId, null) + .cancelTransaction(transactionId, otherUserId, null, CancelCode.User) + + getExistingVerificationRequest(otherUserId, transactionId)?.let { + updatePendingRequest(it.copy( + cancelConclusion = CancelCode.User + )) + } + } + + private fun updatePendingRequest(updated: PendingVerificationRequest) { + val requestsForUser = pendingRequests.getOrPut(updated.otherUserId) { mutableListOf() } + val index = requestsForUser.indexOfFirst { + it.transactionId == updated.transactionId + || it.transactionId == null && it.localId == updated.localId + } + if (index != -1) { + requestsForUser.removeAt(index) + } + requestsForUser.add(updated) + dispatchRequestUpdated(updated) + } + + override fun beginKeyVerificationInDMs(method: VerificationMethod, + transactionId: String, + roomId: String, + otherUserId: String, + otherDeviceId: String, + callback: MatrixCallback?): String? { + if (method == VerificationMethod.SAS) { + val tx = DefaultOutgoingSASDefaultVerificationTransaction( + setDeviceVerificationAction, + userId, + deviceId, + cryptoStore, + crossSigningService, + outgoingGossipingRequestManager, + incomingGossipingRequestManager, + myDeviceInfoHolder.get().myDevice.fingerprint()!!, + transactionId, + otherUserId, + otherDeviceId) + tx.transport = verificationTransportRoomMessageFactory.createTransport(roomId, tx) + addTransaction(tx) + + tx.start() + return transactionId + } else { + throw IllegalArgumentException("Unknown verification method") + } + } + + override fun readyPendingVerificationInDMs(methods: List, + otherUserId: String, + roomId: String, + transactionId: String): Boolean { + Timber.v("## SAS readyPendingVerificationInDMs $otherUserId room:$roomId tx:$transactionId") + // Let's find the related request + val existingRequest = getExistingVerificationRequest(otherUserId, transactionId) + if (existingRequest != null) { + // we need to send a ready event, with matching methods + val transport = verificationTransportRoomMessageFactory.createTransport(roomId, null) + val computedMethods = computeReadyMethods( + transactionId, + otherUserId, + existingRequest.requestInfo?.fromDevice ?: "", + existingRequest.requestInfo?.methods, + methods) { + verificationTransportRoomMessageFactory.createTransport(roomId, it) + } + if (methods.isNullOrEmpty()) { + Timber.i("Cannot ready this request, no common methods found txId:$transactionId") + // TODO buttons should not be shown in this case? + return false + } + // TODO this is not yet related to a transaction, maybe we should use another method like for cancel? + val readyMsg = transport.createReady(transactionId, deviceId ?: "", computedMethods) + transport.sendToOther(EventType.KEY_VERIFICATION_READY, + readyMsg, + VerificationTxState.None, + CancelCode.User, + null // TODO handle error? + ) + updatePendingRequest(existingRequest.copy(readyInfo = readyMsg.asValidObject())) + return true + } else { + Timber.e("## SAS readyPendingVerificationInDMs Verification not found") + // :/ should not be possible... unless live observer very slow + return false + } + } + + override fun readyPendingVerification(methods: List, + otherUserId: String, + transactionId: String): Boolean { + Timber.v("## SAS readyPendingVerification $otherUserId tx:$transactionId") + // Let's find the related request + val existingRequest = getExistingVerificationRequest(otherUserId, transactionId) + if (existingRequest != null) { + // we need to send a ready event, with matching methods + val transport = verificationTransportToDeviceFactory.createTransport(null) + val computedMethods = computeReadyMethods( + transactionId, + otherUserId, + existingRequest.requestInfo?.fromDevice ?: "", + existingRequest.requestInfo?.methods, + methods) { + verificationTransportToDeviceFactory.createTransport(it) + } + if (methods.isNullOrEmpty()) { + Timber.i("Cannot ready this request, no common methods found txId:$transactionId") + // TODO buttons should not be shown in this case? + return false + } + // TODO this is not yet related to a transaction, maybe we should use another method like for cancel? + val readyMsg = transport.createReady(transactionId, deviceId ?: "", computedMethods) + transport.sendVerificationReady( + readyMsg, + otherUserId, + existingRequest.requestInfo?.fromDevice ?: "", + null // TODO handle error? + ) + updatePendingRequest(existingRequest.copy(readyInfo = readyMsg.asValidObject())) + return true + } else { + Timber.e("## SAS readyPendingVerification Verification not found") + // :/ should not be possible... unless live observer very slow + return false + } + } + + private fun computeReadyMethods( + transactionId: String, + otherUserId: String, + otherDeviceId: String, + otherUserMethods: List?, + methods: List, + transportCreator: (DefaultVerificationTransaction) -> VerificationTransport): List { + if (otherUserMethods.isNullOrEmpty()) { + return emptyList() + } + + val result = mutableSetOf() + + if (VERIFICATION_METHOD_SAS in otherUserMethods && VerificationMethod.SAS in methods) { + // Other can do SAS and so do I + result.add(VERIFICATION_METHOD_SAS) + } + + if (VERIFICATION_METHOD_QR_CODE_SCAN in otherUserMethods || VERIFICATION_METHOD_QR_CODE_SHOW in otherUserMethods) { + // Other user wants to verify using QR code. Cross-signing has to be setup + val qrCodeData = createQrCodeData(transactionId, otherUserId, otherDeviceId) + + if (qrCodeData != null) { + if (VERIFICATION_METHOD_QR_CODE_SCAN in otherUserMethods && VerificationMethod.QR_CODE_SHOW in methods) { + // Other can Scan and I can show QR code + result.add(VERIFICATION_METHOD_QR_CODE_SHOW) + result.add(VERIFICATION_METHOD_RECIPROCATE) + } + if (VERIFICATION_METHOD_QR_CODE_SHOW in otherUserMethods && VerificationMethod.QR_CODE_SCAN in methods) { + // Other can show and I can scan QR code + result.add(VERIFICATION_METHOD_QR_CODE_SCAN) + result.add(VERIFICATION_METHOD_RECIPROCATE) + } + } + + if (VERIFICATION_METHOD_RECIPROCATE in result) { + // Create the pending transaction + val tx = DefaultQrCodeVerificationTransaction( + setDeviceVerificationAction = setDeviceVerificationAction, + transactionId = transactionId, + otherUserId = otherUserId, + otherDeviceId = otherDeviceId, + crossSigningService = crossSigningService, + outgoingGossipingRequestManager = outgoingGossipingRequestManager, + incomingGossipingRequestManager = incomingGossipingRequestManager, + cryptoStore = cryptoStore, + qrCodeData = qrCodeData, + userId = userId, + deviceId = deviceId ?: "", + isIncoming = false) + + tx.transport = transportCreator.invoke(tx) + + addTransaction(tx) + } + } + + return result.toList() + } + + /** + * This string must be unique for the pair of users performing verification for the duration that the transaction is valid + */ + private fun createUniqueIDForTransaction(otherUserId: String, otherDeviceID: String): String { + return buildString { + append(userId).append("|") + append(deviceId).append("|") + append(otherUserId).append("|") + append(otherDeviceID).append("|") + append(UUID.randomUUID().toString()) + } + } + + override fun transactionUpdated(tx: VerificationTransaction) { + dispatchTxUpdated(tx) + if (tx.state is VerificationTxState.TerminalTxState) { + // remove + this.removeTransaction(tx.otherUserId, tx.transactionId) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationTransaction.kt new file mode 100644 index 0000000000..e4f559767c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationTransaction.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.verification + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService +import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction +import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState +import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction +import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel +import timber.log.Timber + +/** + * Generic interactive key verification transaction + */ +internal abstract class DefaultVerificationTransaction( + private val setDeviceVerificationAction: SetDeviceVerificationAction, + private val crossSigningService: CrossSigningService, + private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, + private val incomingGossipingRequestManager: IncomingGossipingRequestManager, + private val userId: String, + override val transactionId: String, + override val otherUserId: String, + override var otherDeviceId: String? = null, + override val isIncoming: Boolean) : VerificationTransaction { + + lateinit var transport: VerificationTransport + + interface Listener { + fun transactionUpdated(tx: VerificationTransaction) + } + + protected var listeners = ArrayList() + + fun addListener(listener: Listener) { + if (!listeners.contains(listener)) listeners.add(listener) + } + + fun removeListener(listener: Listener) { + listeners.remove(listener) + } + + protected fun trust(canTrustOtherUserMasterKey: Boolean, + toVerifyDeviceIds: List, + eventuallyMarkMyMasterKeyAsTrusted: Boolean, autoDone : Boolean = true) { + Timber.d("## Verification: trust ($otherUserId,$otherDeviceId) , verifiedDevices:$toVerifyDeviceIds") + Timber.d("## Verification: trust Mark myMSK trusted $eventuallyMarkMyMasterKeyAsTrusted") + + // TODO what if the otherDevice is not in this list? and should we + toVerifyDeviceIds.forEach { + setDeviceVerified(otherUserId, it) + } + + // If not me sign his MSK and upload the signature + if (canTrustOtherUserMasterKey) { + // we should trust this master key + // And check verification MSK -> SSK? + if (otherUserId != userId) { + crossSigningService.trustUser(otherUserId, object : MatrixCallback { + override fun onFailure(failure: Throwable) { + Timber.e(failure, "## Verification: Failed to trust User $otherUserId") + } + }) + } else { + // Notice other master key is mine because other is me + if (eventuallyMarkMyMasterKeyAsTrusted) { + // Mark my keys as trusted locally + crossSigningService.markMyMasterKeyAsTrusted() + } + } + } + + if (otherUserId == userId) { + incomingGossipingRequestManager.onVerificationCompleteForDevice(otherDeviceId!!) + + // If me it's reasonable to sign and upload the device signature + // Notice that i might not have the private keys, so may not be able to do it + crossSigningService.trustDevice(otherDeviceId!!, object : MatrixCallback { + override fun onFailure(failure: Throwable) { + Timber.w("## Verification: Failed to sign new device $otherDeviceId, ${failure.localizedMessage}") + } + }) + } + + if (autoDone) { + state = VerificationTxState.Verified + transport.done(transactionId) {} + } + } + + private fun setDeviceVerified(userId: String, deviceId: String) { + // TODO should not override cross sign status + setDeviceVerificationAction.handle(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), + userId, + deviceId) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt new file mode 100644 index 0000000000..7ebd3b51b0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt @@ -0,0 +1,421 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.verification + +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService +import org.matrix.android.sdk.api.session.crypto.verification.CancelCode +import org.matrix.android.sdk.api.session.crypto.verification.EmojiRepresentation +import org.matrix.android.sdk.api.session.crypto.verification.SasMode +import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction +import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.extensions.toUnsignedInt +import org.matrix.android.sdk.internal.util.withoutPrefix +import org.matrix.olm.OlmSAS +import org.matrix.olm.OlmUtility +import timber.log.Timber + +/** + * Represents an ongoing short code interactive key verification between two devices. + */ +internal abstract class SASDefaultVerificationTransaction( + setDeviceVerificationAction: SetDeviceVerificationAction, + open val userId: String, + open val deviceId: String?, + private val cryptoStore: IMXCryptoStore, + crossSigningService: CrossSigningService, + outgoingGossipingRequestManager: OutgoingGossipingRequestManager, + incomingGossipingRequestManager: IncomingGossipingRequestManager, + private val deviceFingerprint: String, + transactionId: String, + otherUserId: String, + otherDeviceId: String?, + isIncoming: Boolean +) : DefaultVerificationTransaction( + setDeviceVerificationAction, + crossSigningService, + outgoingGossipingRequestManager, + incomingGossipingRequestManager, + userId, + transactionId, + otherUserId, + otherDeviceId, + isIncoming), + SasVerificationTransaction { + + companion object { + const val SAS_MAC_SHA256_LONGKDF = "hmac-sha256" + const val SAS_MAC_SHA256 = "hkdf-hmac-sha256" + + // Deprecated maybe removed later, use V2 + const val KEY_AGREEMENT_V1 = "curve25519" + const val KEY_AGREEMENT_V2 = "curve25519-hkdf-sha256" + // ordered by preferred order + val KNOWN_AGREEMENT_PROTOCOLS = listOf(KEY_AGREEMENT_V2, KEY_AGREEMENT_V1) + // ordered by preferred order + val KNOWN_HASHES = listOf("sha256") + // ordered by preferred order + val KNOWN_MACS = listOf(SAS_MAC_SHA256, SAS_MAC_SHA256_LONGKDF) + + // older devices have limited support of emoji but SDK offers images for the 64 verification emojis + // so always send that we support EMOJI + val KNOWN_SHORT_CODES = listOf(SasMode.EMOJI, SasMode.DECIMAL) + } + + override var state: VerificationTxState = VerificationTxState.None + set(newState) { + field = newState + + listeners.forEach { + try { + it.transactionUpdated(this) + } catch (e: Throwable) { + Timber.e(e, "## Error while notifying listeners") + } + } + + if (newState is VerificationTxState.TerminalTxState) { + releaseSAS() + } + } + + private var olmSas: OlmSAS? = null + + // Visible for test + var startReq: ValidVerificationInfoStart.SasVerificationInfoStart? = null + // Visible for test + var accepted: ValidVerificationInfoAccept? = null + protected var otherKey: String? = null + protected var shortCodeBytes: ByteArray? = null + + protected var myMac: ValidVerificationInfoMac? = null + protected var theirMac: ValidVerificationInfoMac? = null + + protected fun getSAS(): OlmSAS { + if (olmSas == null) olmSas = OlmSAS() + return olmSas!! + } + + // To override finalize(), all you need to do is simply declare it, without using the override keyword: + protected fun finalize() { + releaseSAS() + } + + private fun releaseSAS() { + // finalization logic + olmSas?.releaseSas() + olmSas = null + } + + /** + * To be called by the client when the user has verified that + * both short codes do match + */ + override fun userHasVerifiedShortCode() { + Timber.v("## SAS short code verified by user for id:$transactionId") + if (state != VerificationTxState.ShortCodeReady) { + // ignore and cancel? + Timber.e("## Accepted short code from invalid state $state") + cancel(CancelCode.UnexpectedMessage) + return + } + + state = VerificationTxState.ShortCodeAccepted + // Alice and Bob’ devices calculate the HMAC of their own device keys and a comma-separated, + // sorted list of the key IDs that they wish the other user to verify, + // the shared secret as the input keying material, no salt, and with the input parameter set to the concatenation of: + // - the string “MATRIX_KEY_VERIFICATION_MAC”, + // - the Matrix ID of the user whose key is being MAC-ed, + // - the device ID of the device sending the MAC, + // - the Matrix ID of the other user, + // - the device ID of the device receiving the MAC, + // - the transaction ID, and + // - the key ID of the key being MAC-ed, or the string “KEY_IDS” if the item being MAC-ed is the list of key IDs. + val baseInfo = "MATRIX_KEY_VERIFICATION_MAC$userId$deviceId$otherUserId$otherDeviceId$transactionId" + + // Previously, with SAS verification, the m.key.verification.mac message only contained the user's device key. + // It should now contain both the device key and the MSK. + // So when Alice and Bob verify with SAS, the verification will verify the MSK. + + val keyMap = HashMap() + + val keyId = "ed25519:$deviceId" + val macString = macUsingAgreedMethod(deviceFingerprint, baseInfo + keyId) + + if (macString.isNullOrBlank()) { + // Should not happen + Timber.e("## SAS verification [$transactionId] failed to send KeyMac, empty key hashes.") + cancel(CancelCode.UnexpectedMessage) + return + } + + keyMap[keyId] = macString + + cryptoStore.getMyCrossSigningInfo()?.takeIf { it.isTrusted() } + ?.masterKey() + ?.unpaddedBase64PublicKey + ?.let { masterPublicKey -> + val crossSigningKeyId = "ed25519:$masterPublicKey" + macUsingAgreedMethod(masterPublicKey, baseInfo + crossSigningKeyId)?.let { MSKMacString -> + keyMap[crossSigningKeyId] = MSKMacString + } + } + + val keyStrings = macUsingAgreedMethod(keyMap.keys.sorted().joinToString(","), baseInfo + "KEY_IDS") + + if (macString.isNullOrBlank() || keyStrings.isNullOrBlank()) { + // Should not happen + Timber.e("## SAS verification [$transactionId] failed to send KeyMac, empty key hashes.") + cancel(CancelCode.UnexpectedMessage) + return + } + + val macMsg = transport.createMac(transactionId, keyMap, keyStrings) + myMac = macMsg.asValidObject() + state = VerificationTxState.SendingMac + sendToOther(EventType.KEY_VERIFICATION_MAC, macMsg, VerificationTxState.MacSent, CancelCode.User) { + if (state == VerificationTxState.SendingMac) { + // It is possible that we receive the next event before this one :/, in this case we should keep state + state = VerificationTxState.MacSent + } + } + + // Do I already have their Mac? + theirMac?.let { verifyMacs(it) } + // if not wait for it + } + + override fun shortCodeDoesNotMatch() { + Timber.v("## SAS short code do not match for id:$transactionId") + cancel(CancelCode.MismatchedSas) + } + + override fun isToDeviceTransport(): Boolean { + return transport is VerificationTransportToDevice + } + + abstract fun onVerificationStart(startReq: ValidVerificationInfoStart.SasVerificationInfoStart) + + abstract fun onVerificationAccept(accept: ValidVerificationInfoAccept) + + abstract fun onKeyVerificationKey(vKey: ValidVerificationInfoKey) + + abstract fun onKeyVerificationMac(vMac: ValidVerificationInfoMac) + + protected fun verifyMacs(theirMacSafe: ValidVerificationInfoMac) { + Timber.v("## SAS verifying macs for id:$transactionId") + state = VerificationTxState.Verifying + + // Keys have been downloaded earlier in process + val otherUserKnownDevices = cryptoStore.getUserDevices(otherUserId) + + // Bob’s device calculates the HMAC (as above) of its copies of Alice’s keys given in the message (as identified by their key ID), + // as well as the HMAC of the comma-separated, sorted list of the key IDs given in the message. + // Bob’s device compares these with the HMAC values given in the m.key.verification.mac message. + // If everything matches, then consider Alice’s device keys as verified. + val baseInfo = "MATRIX_KEY_VERIFICATION_MAC$otherUserId$otherDeviceId$userId$deviceId$transactionId" + + val commaSeparatedListOfKeyIds = theirMacSafe.mac.keys.sorted().joinToString(",") + + val keyStrings = macUsingAgreedMethod(commaSeparatedListOfKeyIds, baseInfo + "KEY_IDS") + if (theirMacSafe.keys != keyStrings) { + // WRONG! + cancel(CancelCode.MismatchedKeys) + return + } + + val verifiedDevices = ArrayList() + + // cannot be empty because it has been validated + theirMacSafe.mac.keys.forEach { + val keyIDNoPrefix = it.withoutPrefix("ed25519:") + val otherDeviceKey = otherUserKnownDevices?.get(keyIDNoPrefix)?.fingerprint() + if (otherDeviceKey == null) { + Timber.w("## SAS Verification: Could not find device $keyIDNoPrefix to verify") + // just ignore and continue + return@forEach + } + val mac = macUsingAgreedMethod(otherDeviceKey, baseInfo + it) + if (mac != theirMacSafe.mac[it]) { + // WRONG! + Timber.e("## SAS Verification: mac mismatch for $otherDeviceKey with id $keyIDNoPrefix") + cancel(CancelCode.MismatchedKeys) + return + } + verifiedDevices.add(keyIDNoPrefix) + } + + var otherMasterKeyIsVerified = false + val otherMasterKey = cryptoStore.getCrossSigningInfo(otherUserId)?.masterKey() + val otherCrossSigningMasterKeyPublic = otherMasterKey?.unpaddedBase64PublicKey + if (otherCrossSigningMasterKeyPublic != null) { + // Did the user signed his master key + theirMacSafe.mac.keys.forEach { + val keyIDNoPrefix = it.withoutPrefix("ed25519:") + if (keyIDNoPrefix == otherCrossSigningMasterKeyPublic) { + // Check the signature + val mac = macUsingAgreedMethod(otherCrossSigningMasterKeyPublic, baseInfo + it) + if (mac != theirMacSafe.mac[it]) { + // WRONG! + Timber.e("## SAS Verification: mac mismatch for MasterKey with id $keyIDNoPrefix") + cancel(CancelCode.MismatchedKeys) + return + } else { + otherMasterKeyIsVerified = true + } + } + } + } + + // if none of the keys could be verified, then error because the app + // should be informed about that + if (verifiedDevices.isEmpty() && !otherMasterKeyIsVerified) { + Timber.e("## SAS Verification: No devices verified") + cancel(CancelCode.MismatchedKeys) + return + } + + trust(otherMasterKeyIsVerified, + verifiedDevices, + eventuallyMarkMyMasterKeyAsTrusted = otherMasterKey?.trustLevel?.isVerified() == false) + } + + override fun cancel() { + cancel(CancelCode.User) + } + + override fun cancel(code: CancelCode) { + state = VerificationTxState.Cancelled(code, true) + transport.cancelTransaction(transactionId, otherUserId, otherDeviceId ?: "", code) + } + + protected fun sendToOther(type: String, + keyToDevice: VerificationInfo, + nextState: VerificationTxState, + onErrorReason: CancelCode, + onDone: (() -> Unit)?) { + transport.sendToOther(type, keyToDevice, nextState, onErrorReason, onDone) + } + + fun getShortCodeRepresentation(shortAuthenticationStringMode: String): String? { + if (shortCodeBytes == null) { + return null + } + when (shortAuthenticationStringMode) { + SasMode.DECIMAL -> { + if (shortCodeBytes!!.size < 5) return null + return getDecimalCodeRepresentation(shortCodeBytes!!) + } + SasMode.EMOJI -> { + if (shortCodeBytes!!.size < 6) return null + return getEmojiCodeRepresentation(shortCodeBytes!!).joinToString(" ") { it.emoji } + } + else -> return null + } + } + + override fun supportsEmoji(): Boolean { + return accepted?.shortAuthenticationStrings?.contains(SasMode.EMOJI).orFalse() + } + + override fun supportsDecimal(): Boolean { + return accepted?.shortAuthenticationStrings?.contains(SasMode.DECIMAL).orFalse() + } + + protected fun hashUsingAgreedHashMethod(toHash: String): String? { + if ("sha256".toLowerCase() == accepted?.hash?.toLowerCase()) { + val olmUtil = OlmUtility() + val hashBytes = olmUtil.sha256(toHash) + olmUtil.releaseUtility() + return hashBytes + } + return null + } + + private fun macUsingAgreedMethod(message: String, info: String): String? { + if (SAS_MAC_SHA256_LONGKDF.toLowerCase() == accepted?.messageAuthenticationCode?.toLowerCase()) { + return getSAS().calculateMacLongKdf(message, info) + } else if (SAS_MAC_SHA256.toLowerCase() == accepted?.messageAuthenticationCode?.toLowerCase()) { + return getSAS().calculateMac(message, info) + } + return null + } + + override fun getDecimalCodeRepresentation(): String { + return getDecimalCodeRepresentation(shortCodeBytes!!) + } + + /** + * decimal: generate five bytes by using HKDF. + * Take the first 13 bits and convert it to a decimal number (which will be a number between 0 and 8191 inclusive), + * and add 1000 (resulting in a number between 1000 and 9191 inclusive). + * Do the same with the second 13 bits, and the third 13 bits, giving three 4-digit numbers. + * In other words, if the five bytes are B0, B1, B2, B3, and B4, then the first number is (B0 << 5 | B1 >> 3) + 1000, + * the second number is ((B1 & 0x7) << 10 | B2 << 2 | B3 >> 6) + 1000, and the third number is ((B3 & 0x3f) << 7 | B4 >> 1) + 1000. + * (This method of converting 13 bits at a time is used to avoid requiring 32-bit clients to do big-number arithmetic, + * and adding 1000 to the number avoids having clients to worry about properly zero-padding the number when displaying to the user.) + * The three 4-digit numbers are displayed to the user either with dashes (or another appropriate separator) separating the three numbers, + * or with the three numbers on separate lines. + */ + fun getDecimalCodeRepresentation(byteArray: ByteArray): String { + val b0 = byteArray[0].toUnsignedInt() // need unsigned byte + val b1 = byteArray[1].toUnsignedInt() // need unsigned byte + val b2 = byteArray[2].toUnsignedInt() // need unsigned byte + val b3 = byteArray[3].toUnsignedInt() // need unsigned byte + val b4 = byteArray[4].toUnsignedInt() // need unsigned byte + // (B0 << 5 | B1 >> 3) + 1000 + val first = (b0.shl(5) or b1.shr(3)) + 1000 + // ((B1 & 0x7) << 10 | B2 << 2 | B3 >> 6) + 1000 + val second = ((b1 and 0x7).shl(10) or b2.shl(2) or b3.shr(6)) + 1000 + // ((B3 & 0x3f) << 7 | B4 >> 1) + 1000 + val third = ((b3 and 0x3f).shl(7) or b4.shr(1)) + 1000 + return "$first $second $third" + } + + override fun getEmojiCodeRepresentation(): List { + return getEmojiCodeRepresentation(shortCodeBytes!!) + } + + /** + * emoji: generate six bytes by using HKDF. + * Split the first 42 bits into 7 groups of 6 bits, as one would do when creating a base64 encoding. + * For each group of 6 bits, look up the emoji from Appendix A corresponding + * to that number 7 emoji are selected from a list of 64 emoji (see Appendix A) + */ + private fun getEmojiCodeRepresentation(byteArray: ByteArray): List { + val b0 = byteArray[0].toUnsignedInt() + val b1 = byteArray[1].toUnsignedInt() + val b2 = byteArray[2].toUnsignedInt() + val b3 = byteArray[3].toUnsignedInt() + val b4 = byteArray[4].toUnsignedInt() + val b5 = byteArray[5].toUnsignedInt() + return listOf( + getEmojiForCode((b0 and 0xFC).shr(2)), + getEmojiForCode((b0 and 0x3).shl(4) or (b1 and 0xF0).shr(4)), + getEmojiForCode((b1 and 0xF).shl(2) or (b2 and 0xC0).shr(6)), + getEmojiForCode((b2 and 0x3F)), + getEmojiForCode((b3 and 0xFC).shr(2)), + getEmojiForCode((b3 and 0x3).shl(4) or (b4 and 0xF0).shr(4)), + getEmojiForCode((b4 and 0xF).shl(2) or (b5 and 0xC0).shr(6)) + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SendVerificationMessageWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SendVerificationMessageWorker.kt new file mode 100644 index 0000000000..2b7d26e76b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SendVerificationMessageWorker.kt @@ -0,0 +1,92 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.verification + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.Data +import androidx.work.WorkerParameters +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.failure.shouldBeRetried +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask +import org.matrix.android.sdk.internal.worker.SessionWorkerParams +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import org.matrix.android.sdk.internal.worker.getSessionComponent +import timber.log.Timber +import javax.inject.Inject + +/** + * Possible previous worker: None + * Possible next worker : None + */ +internal class SendVerificationMessageWorker(context: Context, + params: WorkerParameters) + : CoroutineWorker(context, params) { + + @JsonClass(generateAdapter = true) + internal data class Params( + override val sessionId: String, + val event: Event, + override val lastFailureMessage: String? = null + ) : SessionWorkerParams + + @Inject + lateinit var sendVerificationMessageTask: SendVerificationMessageTask + + @Inject + lateinit var cryptoService: CryptoService + + override suspend fun doWork(): Result { + val errorOutputData = Data.Builder().putBoolean(OUTPUT_KEY_FAILED, true).build() + val params = WorkerParamsFactory.fromData(inputData) + ?: return Result.success(errorOutputData) + + val sessionComponent = getSessionComponent(params.sessionId) + ?: return Result.success(errorOutputData).also { + // TODO, can this happen? should I update local echo? + Timber.e("Unknown Session, cannot send message, sessionId: ${params.sessionId}") + } + sessionComponent.inject(this) + val localId = params.event.eventId ?: "" + return try { + val eventId = sendVerificationMessageTask.execute( + SendVerificationMessageTask.Params( + event = params.event, + cryptoService = cryptoService + ) + ) + + Result.success(Data.Builder().putString(localId, eventId).build()) + } catch (exception: Throwable) { + if (exception.shouldBeRetried()) { + Result.retry() + } else { + Result.success(errorOutputData) + } + } + } + + companion object { + private const val OUTPUT_KEY_FAILED = "failed" + + fun hasFailed(outputData: Data): Boolean { + return outputData.getBoolean(OUTPUT_KEY_FAILED, false) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationEmoji.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationEmoji.kt new file mode 100644 index 0000000000..5a55ec2a9c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationEmoji.kt @@ -0,0 +1,89 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.verification + +import org.matrix.android.sdk.R +import org.matrix.android.sdk.api.session.crypto.verification.EmojiRepresentation + +internal fun getEmojiForCode(code: Int): EmojiRepresentation { + return when (code % 64) { + 0 -> EmojiRepresentation("🐶", R.string.verification_emoji_dog, R.drawable.ic_verification_dog) + 1 -> EmojiRepresentation("🐱", R.string.verification_emoji_cat, R.drawable.ic_verification_cat) + 2 -> EmojiRepresentation("🦁", R.string.verification_emoji_lion, R.drawable.ic_verification_lion) + 3 -> EmojiRepresentation("🐎", R.string.verification_emoji_horse, R.drawable.ic_verification_horse) + 4 -> EmojiRepresentation("🦄", R.string.verification_emoji_unicorn, R.drawable.ic_verification_unicorn) + 5 -> EmojiRepresentation("🐷", R.string.verification_emoji_pig, R.drawable.ic_verification_pig) + 6 -> EmojiRepresentation("🐘", R.string.verification_emoji_elephant, R.drawable.ic_verification_elephant) + 7 -> EmojiRepresentation("🐰", R.string.verification_emoji_rabbit, R.drawable.ic_verification_rabbit) + 8 -> EmojiRepresentation("🐼", R.string.verification_emoji_panda, R.drawable.ic_verification_panda) + 9 -> EmojiRepresentation("🐓", R.string.verification_emoji_rooster, R.drawable.ic_verification_rooster) + 10 -> EmojiRepresentation("🐧", R.string.verification_emoji_penguin, R.drawable.ic_verification_penguin) + 11 -> EmojiRepresentation("🐢", R.string.verification_emoji_turtle, R.drawable.ic_verification_turtle) + 12 -> EmojiRepresentation("🐟", R.string.verification_emoji_fish, R.drawable.ic_verification_fish) + 13 -> EmojiRepresentation("🐙", R.string.verification_emoji_octopus, R.drawable.ic_verification_octopus) + 14 -> EmojiRepresentation("🦋", R.string.verification_emoji_butterfly, R.drawable.ic_verification_butterfly) + 15 -> EmojiRepresentation("🌷", R.string.verification_emoji_flower, R.drawable.ic_verification_flower) + 16 -> EmojiRepresentation("🌳", R.string.verification_emoji_tree, R.drawable.ic_verification_tree) + 17 -> EmojiRepresentation("🌵", R.string.verification_emoji_cactus, R.drawable.ic_verification_cactus) + 18 -> EmojiRepresentation("🍄", R.string.verification_emoji_mushroom, R.drawable.ic_verification_mushroom) + 19 -> EmojiRepresentation("🌏", R.string.verification_emoji_globe, R.drawable.ic_verification_globe) + 20 -> EmojiRepresentation("🌙", R.string.verification_emoji_moon, R.drawable.ic_verification_moon) + 21 -> EmojiRepresentation("☁️", R.string.verification_emoji_cloud, R.drawable.ic_verification_cloud) + 22 -> EmojiRepresentation("🔥", R.string.verification_emoji_fire, R.drawable.ic_verification_fire) + 23 -> EmojiRepresentation("🍌", R.string.verification_emoji_banana, R.drawable.ic_verification_banana) + 24 -> EmojiRepresentation("🍎", R.string.verification_emoji_apple, R.drawable.ic_verification_apple) + 25 -> EmojiRepresentation("🍓", R.string.verification_emoji_strawberry, R.drawable.ic_verification_strawberry) + 26 -> EmojiRepresentation("🌽", R.string.verification_emoji_corn, R.drawable.ic_verification_corn) + 27 -> EmojiRepresentation("🍕", R.string.verification_emoji_pizza, R.drawable.ic_verification_pizza) + 28 -> EmojiRepresentation("🎂", R.string.verification_emoji_cake, R.drawable.ic_verification_cake) + 29 -> EmojiRepresentation("❤️", R.string.verification_emoji_heart, R.drawable.ic_verification_heart) + 30 -> EmojiRepresentation("🙂", R.string.verification_emoji_smiley, R.drawable.ic_verification_smiley) + 31 -> EmojiRepresentation("🤖", R.string.verification_emoji_robot, R.drawable.ic_verification_robot) + 32 -> EmojiRepresentation("🎩", R.string.verification_emoji_hat, R.drawable.ic_verification_hat) + 33 -> EmojiRepresentation("👓", R.string.verification_emoji_glasses, R.drawable.ic_verification_glasses) + 34 -> EmojiRepresentation("🔧", R.string.verification_emoji_wrench, R.drawable.ic_verification_wrench) + 35 -> EmojiRepresentation("🎅", R.string.verification_emoji_santa, R.drawable.ic_verification_santa) + 36 -> EmojiRepresentation("👍", R.string.verification_emoji_thumbsup, R.drawable.ic_verification_thumbs_up) + 37 -> EmojiRepresentation("☂️", R.string.verification_emoji_umbrella, R.drawable.ic_verification_umbrella) + 38 -> EmojiRepresentation("⌛", R.string.verification_emoji_hourglass, R.drawable.ic_verification_hourglass) + 39 -> EmojiRepresentation("⏰", R.string.verification_emoji_clock, R.drawable.ic_verification_clock) + 40 -> EmojiRepresentation("🎁", R.string.verification_emoji_gift, R.drawable.ic_verification_gift) + 41 -> EmojiRepresentation("💡", R.string.verification_emoji_lightbulb, R.drawable.ic_verification_light_bulb) + 42 -> EmojiRepresentation("📕", R.string.verification_emoji_book, R.drawable.ic_verification_book) + 43 -> EmojiRepresentation("✏️", R.string.verification_emoji_pencil, R.drawable.ic_verification_pencil) + 44 -> EmojiRepresentation("📎", R.string.verification_emoji_paperclip, R.drawable.ic_verification_paperclip) + 45 -> EmojiRepresentation("✂️", R.string.verification_emoji_scissors, R.drawable.ic_verification_scissors) + 46 -> EmojiRepresentation("🔒", R.string.verification_emoji_lock, R.drawable.ic_verification_lock) + 47 -> EmojiRepresentation("🔑", R.string.verification_emoji_key, R.drawable.ic_verification_key) + 48 -> EmojiRepresentation("🔨", R.string.verification_emoji_hammer, R.drawable.ic_verification_hammer) + 49 -> EmojiRepresentation("☎️", R.string.verification_emoji_telephone, R.drawable.ic_verification_phone) + 50 -> EmojiRepresentation("🏁", R.string.verification_emoji_flag, R.drawable.ic_verification_flag) + 51 -> EmojiRepresentation("🚂", R.string.verification_emoji_train, R.drawable.ic_verification_train) + 52 -> EmojiRepresentation("🚲", R.string.verification_emoji_bicycle, R.drawable.ic_verification_bicycle) + 53 -> EmojiRepresentation("✈️", R.string.verification_emoji_airplane, R.drawable.ic_verification_airplane) + 54 -> EmojiRepresentation("🚀", R.string.verification_emoji_rocket, R.drawable.ic_verification_rocket) + 55 -> EmojiRepresentation("🏆", R.string.verification_emoji_trophy, R.drawable.ic_verification_trophy) + 56 -> EmojiRepresentation("⚽", R.string.verification_emoji_ball, R.drawable.ic_verification_ball) + 57 -> EmojiRepresentation("🎸", R.string.verification_emoji_guitar, R.drawable.ic_verification_guitar) + 58 -> EmojiRepresentation("🎺", R.string.verification_emoji_trumpet, R.drawable.ic_verification_trumpet) + 59 -> EmojiRepresentation("🔔", R.string.verification_emoji_bell, R.drawable.ic_verification_bell) + 60 -> EmojiRepresentation("⚓", R.string.verification_emoji_anchor, R.drawable.ic_verification_anchor) + 61 -> EmojiRepresentation("🎧", R.string.verification_emoji_headphone, R.drawable.ic_verification_headphone) + 62 -> EmojiRepresentation("📁", R.string.verification_emoji_folder, R.drawable.ic_verification_folder) + /* 63 */ else -> EmojiRepresentation("📌", R.string.verification_emoji_pin, R.drawable.ic_verification_pin) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfo.kt new file mode 100644 index 0000000000..2f4c4e9c93 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfo.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.verification + +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.internal.crypto.model.rest.SendToDeviceObject + +interface VerificationInfo { + fun toEventContent(): Content? = null + fun toSendToDeviceObject(): SendToDeviceObject? = null + + fun asValidObject(): ValidObjectType? + + /** + * String to identify the transaction. + * This string must be unique for the pair of users performing verification for the duration that the transaction is valid. + * Alice’s device should record this ID and use it in future messages in this transaction. + */ + val transactionId: String? +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoAccept.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoAccept.kt new file mode 100644 index 0000000000..5c6435c1cd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoAccept.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.verification + +internal interface VerificationInfoAccept : VerificationInfo { + /** + * The key agreement protocol that Bob’s device has selected to use, out of the list proposed by Alice’s device + */ + val keyAgreementProtocol: String? + + /** + * The hash algorithm that Bob’s device has selected to use, out of the list proposed by Alice’s device + */ + val hash: String? + + /** + * The message authentication code that Bob’s device has selected to use, out of the list proposed by Alice’s device + */ + val messageAuthenticationCode: String? + + /** + * An array of short authentication string methods that Bob’s client (and Bob) understands. Must be a subset of the list proposed by Alice’s device + */ + val shortAuthenticationStrings: List? + + /** + * The hash (encoded as unpadded base64) of the concatenation of the device’s ephemeral public key (QB, encoded as unpadded base64) + * and the canonical JSON representation of the m.key.verification.start message. + */ + var commitment: String? + + override fun asValidObject(): ValidVerificationInfoAccept? { + val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null + val validKeyAgreementProtocol = keyAgreementProtocol?.takeIf { it.isNotEmpty() } ?: return null + val validHash = hash?.takeIf { it.isNotEmpty() } ?: return null + val validMessageAuthenticationCode = messageAuthenticationCode?.takeIf { it.isNotEmpty() } ?: return null + val validShortAuthenticationStrings = shortAuthenticationStrings?.takeIf { it.isNotEmpty() } ?: return null + val validCommitment = commitment?.takeIf { it.isNotEmpty() } ?: return null + + return ValidVerificationInfoAccept( + validTransactionId, + validKeyAgreementProtocol, + validHash, + validMessageAuthenticationCode, + validShortAuthenticationStrings, + validCommitment + ) + } +} + +internal interface VerificationInfoAcceptFactory { + + fun create(tid: String, + keyAgreementProtocol: String, + hash: String, + commitment: String, + messageAuthenticationCode: String, + shortAuthenticationStrings: List): VerificationInfoAccept +} + +internal data class ValidVerificationInfoAccept( + val transactionId: String, + val keyAgreementProtocol: String, + val hash: String, + val messageAuthenticationCode: String, + val shortAuthenticationStrings: List, + var commitment: String? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoCancel.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoCancel.kt new file mode 100644 index 0000000000..68282cb925 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoCancel.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.verification + +internal interface VerificationInfoCancel : VerificationInfo { + /** + * machine-readable reason for cancelling, see [CancelCode] + */ + val code: String? + + /** + * human-readable reason for cancelling. This should only be used if the receiving client does not understand the code given. + */ + val reason: String? + + override fun asValidObject(): ValidVerificationInfoCancel? { + val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null + val validCode = code?.takeIf { it.isNotEmpty() } ?: return null + + return ValidVerificationInfoCancel( + validTransactionId, + validCode, + reason + ) + } +} + +internal data class ValidVerificationInfoCancel( + val transactionId: String, + val code: String, + val reason: String? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoDone.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoDone.kt new file mode 100644 index 0000000000..7dce847e30 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoDone.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.verification + +import org.matrix.android.sdk.api.session.room.model.message.ValidVerificationDone + +internal interface VerificationInfoDone : VerificationInfo { + + override fun asValidObject(): ValidVerificationDone? { + val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null + return ValidVerificationDone(validTransactionId) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoKey.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoKey.kt new file mode 100644 index 0000000000..745309df79 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoKey.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.verification + +/** + * Sent by both devices to send their ephemeral Curve25519 public key to the other device. + */ +internal interface VerificationInfoKey : VerificationInfo { + /** + * The device’s ephemeral public key, as an unpadded base64 string + */ + val key: String? + + override fun asValidObject(): ValidVerificationInfoKey? { + val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null + val validKey = key?.takeIf { it.isNotEmpty() } ?: return null + + return ValidVerificationInfoKey( + validTransactionId, + validKey + ) + } +} + +internal interface VerificationInfoKeyFactory { + fun create(tid: String, pubKey: String): VerificationInfoKey +} + +internal data class ValidVerificationInfoKey( + val transactionId: String, + val key: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoMac.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoMac.kt new file mode 100644 index 0000000000..6ffd0556f5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoMac.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.verification + +internal interface VerificationInfoMac : VerificationInfo { + /** + * A map of key ID to the MAC of the key, as an unpadded base64 string, calculated using the MAC key + */ + val mac: Map? + + /** + * The MAC of the comma-separated, sorted list of key IDs given in the mac property, + * as an unpadded base64 string, calculated using the MAC key. + * For example, if the mac property gives MACs for the keys ed25519:ABCDEFG and ed25519:HIJKLMN, then this property will + * give the MAC of the string “ed25519:ABCDEFG,ed25519:HIJKLMN”. + */ + val keys: String? + + override fun asValidObject(): ValidVerificationInfoMac? { + val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null + val validMac = mac?.takeIf { it.isNotEmpty() } ?: return null + val validKeys = keys?.takeIf { it.isNotEmpty() } ?: return null + + return ValidVerificationInfoMac( + validTransactionId, + validMac, + validKeys + ) + } +} + +internal interface VerificationInfoMacFactory { + fun create(tid: String, mac: Map, keys: String): VerificationInfoMac +} + +internal data class ValidVerificationInfoMac( + val transactionId: String, + val mac: Map, + val keys: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoReady.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoReady.kt new file mode 100644 index 0000000000..6617b6b7c2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoReady.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.verification + +import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoReady + +/** + * A new event type is added to the key verification framework: m.key.verification.ready, + * which may be sent by the target of the m.key.verification.request message, upon receipt of the m.key.verification.request event. + * + * The m.key.verification.ready event is optional; the recipient of the m.key.verification.request event may respond directly + * with a m.key.verification.start event instead. + */ + +internal interface VerificationInfoReady : VerificationInfo { + /** + * The ID of the device that sent the m.key.verification.ready message + */ + val fromDevice: String? + + /** + * An array of verification methods that the device supports + */ + val methods: List? + + override fun asValidObject(): ValidVerificationInfoReady? { + val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null + val validFromDevice = fromDevice?.takeIf { it.isNotEmpty() } ?: return null + val validMethods = methods?.takeIf { it.isNotEmpty() } ?: return null + + return ValidVerificationInfoReady( + validTransactionId, + validFromDevice, + validMethods + ) + } +} + +internal interface MessageVerificationReadyFactory { + fun create(tid: String, methods: List, fromDevice: String): VerificationInfoReady +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoRequest.kt new file mode 100644 index 0000000000..43843d8e29 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoRequest.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.verification + +import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoRequest + +internal interface VerificationInfoRequest : VerificationInfo { + + /** + * Required. The device ID which is initiating the request. + */ + val fromDevice: String? + + /** + * Required. The verification methods supported by the sender. + */ + val methods: List? + + /** + * The POSIX timestamp in milliseconds for when the request was made. + * If the request is in the future by more than 5 minutes or more than 10 minutes in the past, + * the message should be ignored by the receiver. + */ + val timestamp: Long? + + override fun asValidObject(): ValidVerificationInfoRequest? { + // FIXME No check on Timestamp? + val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null + val validFromDevice = fromDevice?.takeIf { it.isNotEmpty() } ?: return null + val validMethods = methods?.takeIf { it.isNotEmpty() } ?: return null + + return ValidVerificationInfoRequest( + validTransactionId, + validFromDevice, + validMethods, + timestamp + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoStart.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoStart.kt new file mode 100644 index 0000000000..9ac15a1056 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoStart.kt @@ -0,0 +1,125 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.verification + +import org.matrix.android.sdk.api.session.crypto.verification.SasMode +import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_RECIPROCATE +import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_SAS + +internal interface VerificationInfoStart : VerificationInfo { + + val method: String? + + /** + * Alice’s device ID + */ + val fromDevice: String? + + /** + * An array of key agreement protocols that Alice’s client understands. + * Must include “curve25519”. + * Other methods may be defined in the future + */ + val keyAgreementProtocols: List? + + /** + * An array of hashes that Alice’s client understands. + * Must include “sha256”. Other methods may be defined in the future. + */ + val hashes: List? + + /** + * An array of message authentication codes that Alice’s client understands. + * Must include “hkdf-hmac-sha256”. + * Other methods may be defined in the future. + */ + val messageAuthenticationCodes: List? + + /** + * An array of short authentication string methods that Alice’s client (and Alice) understands. + * Must include “decimal”. + * This document also describes the “emoji” method. + * Other methods may be defined in the future + */ + val shortAuthenticationStrings: List? + + /** + * Shared secret, when starting verification with QR code + */ + val sharedSecret: String? + + fun toCanonicalJson(): String + + override fun asValidObject(): ValidVerificationInfoStart? { + val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null + val validFromDevice = fromDevice?.takeIf { it.isNotEmpty() } ?: return null + + return when (method) { + VERIFICATION_METHOD_SAS -> { + val validKeyAgreementProtocols = keyAgreementProtocols?.takeIf { it.isNotEmpty() } ?: return null + val validHashes = hashes?.takeIf { it.contains("sha256") } ?: return null + val validMessageAuthenticationCodes = messageAuthenticationCodes + ?.takeIf { + it.contains(SASDefaultVerificationTransaction.SAS_MAC_SHA256) + || it.contains(SASDefaultVerificationTransaction.SAS_MAC_SHA256_LONGKDF) + } + ?: return null + val validShortAuthenticationStrings = shortAuthenticationStrings?.takeIf { it.contains(SasMode.DECIMAL) } ?: return null + + ValidVerificationInfoStart.SasVerificationInfoStart( + validTransactionId, + validFromDevice, + validKeyAgreementProtocols, + validHashes, + validMessageAuthenticationCodes, + validShortAuthenticationStrings, + canonicalJson = toCanonicalJson() + ) + } + VERIFICATION_METHOD_RECIPROCATE -> { + val validSharedSecret = sharedSecret?.takeIf { it.isNotEmpty() } ?: return null + + ValidVerificationInfoStart.ReciprocateVerificationInfoStart( + validTransactionId, + validFromDevice, + validSharedSecret + ) + } + else -> null + } + } +} + +sealed class ValidVerificationInfoStart( + open val transactionId: String, + open val fromDevice: String) { + data class SasVerificationInfoStart( + override val transactionId: String, + override val fromDevice: String, + val keyAgreementProtocols: List, + val hashes: List, + val messageAuthenticationCodes: List, + val shortAuthenticationStrings: List, + val canonicalJson: String + ) : ValidVerificationInfoStart(transactionId, fromDevice) + + data class ReciprocateVerificationInfoStart( + override val transactionId: String, + override val fromDevice: String, + val sharedSecret: String + ) : ValidVerificationInfoStart(transactionId, fromDevice) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt new file mode 100644 index 0000000000..0c16fd970f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt @@ -0,0 +1,169 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.verification + +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.crypto.verification.VerificationService +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.LocalEcho +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent +import org.matrix.android.sdk.api.session.room.model.message.MessageType +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationReadyContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationStartContent +import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult +import org.matrix.android.sdk.internal.database.model.EventInsertType +import org.matrix.android.sdk.internal.di.DeviceId +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor +import io.realm.Realm +import timber.log.Timber +import java.util.ArrayList +import javax.inject.Inject + +internal class VerificationMessageProcessor @Inject constructor( + private val cryptoService: CryptoService, + private val verificationService: DefaultVerificationService, + @UserId private val userId: String, + @DeviceId private val deviceId: String? +) : EventInsertLiveProcessor { + + private val transactionsHandledByOtherDevice = ArrayList() + + private val allowedTypes = listOf( + EventType.KEY_VERIFICATION_START, + EventType.KEY_VERIFICATION_ACCEPT, + EventType.KEY_VERIFICATION_KEY, + EventType.KEY_VERIFICATION_MAC, + EventType.KEY_VERIFICATION_CANCEL, + EventType.KEY_VERIFICATION_DONE, + EventType.KEY_VERIFICATION_READY, + EventType.MESSAGE, + EventType.ENCRYPTED + ) + + override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean { + if (insertType != EventInsertType.INCREMENTAL_SYNC) { + return false + } + return allowedTypes.contains(eventType) && !LocalEcho.isLocalEchoId(eventId) + } + + override suspend fun process(realm: Realm, event: Event) { + Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}") + + // If the request is in the future by more than 5 minutes or more than 10 minutes in the past, + // the message should be ignored by the receiver. + + if (!VerificationService.isValidRequest(event.ageLocalTs + ?: event.originServerTs)) return Unit.also { + Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is outdated") + } + + // decrypt if needed? + if (event.isEncrypted() && event.mxDecryptionResult == null) { + // TODO use a global event decryptor? attache to session and that listen to new sessionId? + // for now decrypt sync + try { + val result = cryptoService.decryptEvent(event, "") + event.mxDecryptionResult = OlmDecryptionResult( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + ) + } catch (e: MXCryptoError) { + Timber.e("## SAS Failed to decrypt event: ${event.eventId}") + verificationService.onPotentiallyInterestingEventRoomFailToDecrypt(event) + } + } + Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}") + + // Relates to is not encrypted + val relatesToEventId = event.content.toModel()?.relatesTo?.eventId + + if (event.senderId == userId) { + // If it's send from me, we need to keep track of Requests or Start + // done from another device of mine + + if (EventType.MESSAGE == event.getClearType()) { + val msgType = event.getClearContent().toModel()?.msgType + if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) { + event.getClearContent().toModel()?.let { + if (it.fromDevice != deviceId) { + // The verification is requested from another device + Timber.v("## SAS Verification live observer: Transaction requested from other device tid:${event.eventId} ") + event.eventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) } + } + } + } + } else if (EventType.KEY_VERIFICATION_START == event.getClearType()) { + event.getClearContent().toModel()?.let { + if (it.fromDevice != deviceId) { + // The verification is started from another device + Timber.v("## SAS Verification live observer: Transaction started by other device tid:$relatesToEventId ") + relatesToEventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) } + verificationService.onRoomRequestHandledByOtherDevice(event) + } + } + } else if (EventType.KEY_VERIFICATION_READY == event.getClearType()) { + event.getClearContent().toModel()?.let { + if (it.fromDevice != deviceId) { + // The verification is started from another device + Timber.v("## SAS Verification live observer: Transaction started by other device tid:$relatesToEventId ") + relatesToEventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) } + verificationService.onRoomRequestHandledByOtherDevice(event) + } + } + } else if (EventType.KEY_VERIFICATION_CANCEL == event.getClearType() || EventType.KEY_VERIFICATION_DONE == event.getClearType()) { + relatesToEventId?.let { + transactionsHandledByOtherDevice.remove(it) + verificationService.onRoomRequestHandledByOtherDevice(event) + } + } + + Timber.v("## SAS Verification ignoring message sent by me: ${event.eventId} type: ${event.getClearType()}") + return + } + + if (relatesToEventId != null && transactionsHandledByOtherDevice.contains(relatesToEventId)) { + // Ignore this event, it is directed to another of my devices + Timber.v("## SAS Verification live observer: Ignore Transaction handled by other device tid:$relatesToEventId ") + return + } + when (event.getClearType()) { + EventType.KEY_VERIFICATION_START, + EventType.KEY_VERIFICATION_ACCEPT, + EventType.KEY_VERIFICATION_KEY, + EventType.KEY_VERIFICATION_MAC, + EventType.KEY_VERIFICATION_CANCEL, + EventType.KEY_VERIFICATION_READY, + EventType.KEY_VERIFICATION_DONE -> { + verificationService.onRoomEvent(event) + } + EventType.MESSAGE -> { + if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel()?.msgType) { + verificationService.onRoomRequestReceived(event) + } + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransport.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransport.kt new file mode 100644 index 0000000000..ffe0709932 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransport.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.verification + +import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoRequest +import org.matrix.android.sdk.api.session.crypto.verification.CancelCode +import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState + +/** + * Verification can be performed using toDevice events or via DM. + * This class abstracts the concept of transport for verification + */ +internal interface VerificationTransport { + + /** + * Sends a message + */ + fun sendToOther(type: String, + verificationInfo: VerificationInfo, + nextState: VerificationTxState, + onErrorReason: CancelCode, + onDone: (() -> Unit)?) + + /** + * @param callback will be called with eventId and ValidVerificationInfoRequest in case of success + */ + fun sendVerificationRequest(supportedMethods: List, + localId: String, + otherUserId: String, + roomId: String?, + toDevices: List?, + callback: (String?, ValidVerificationInfoRequest?) -> Unit) + + fun cancelTransaction(transactionId: String, + otherUserId: String, + otherUserDeviceId: String?, + code: CancelCode) + + fun done(transactionId: String, + onDone: (() -> Unit)?) + + /** + * Creates an accept message suitable for this transport + */ + fun createAccept(tid: String, + keyAgreementProtocol: String, + hash: String, + commitment: String, + messageAuthenticationCode: String, + shortAuthenticationStrings: List): VerificationInfoAccept + + fun createKey(tid: String, + pubKey: String): VerificationInfoKey + + /** + * Create start for SAS verification + */ + fun createStartForSas(fromDevice: String, + transactionId: String, + keyAgreementProtocols: List, + hashes: List, + messageAuthenticationCodes: List, + shortAuthenticationStrings: List): VerificationInfoStart + + /** + * Create start for QR code verification + */ + fun createStartForQrCode(fromDevice: String, + transactionId: String, + sharedSecret: String): VerificationInfoStart + + fun createMac(tid: String, mac: Map, keys: String): VerificationInfoMac + + fun createReady(tid: String, + fromDevice: String, + methods: List): VerificationInfoReady + + // TODO Refactor + fun sendVerificationReady(keyReq: VerificationInfoReady, + otherUserId: String, + otherDeviceId: String?, + callback: (() -> Unit)?) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt new file mode 100644 index 0000000000..69f00ce359 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt @@ -0,0 +1,405 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.verification + +import androidx.lifecycle.Observer +import androidx.work.BackoffPolicy +import androidx.work.Data +import androidx.work.ExistingWorkPolicy +import androidx.work.Operation +import androidx.work.WorkInfo +import org.matrix.android.sdk.R +import org.matrix.android.sdk.api.session.crypto.verification.CancelCode +import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoRequest +import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.LocalEcho +import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.events.model.UnsignedData +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationAcceptContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationCancelContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationDoneContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationKeyContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationMacContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationReadyContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationStartContent +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_RECIPROCATE +import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_SAS +import org.matrix.android.sdk.internal.di.DeviceId +import org.matrix.android.sdk.internal.di.SessionId +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.di.WorkManagerProvider +import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.util.StringProvider +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import timber.log.Timber +import java.util.UUID +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +internal class VerificationTransportRoomMessage( + private val workManagerProvider: WorkManagerProvider, + private val stringProvider: StringProvider, + private val sessionId: String, + private val userId: String, + private val userDeviceId: String?, + private val roomId: String, + private val localEchoEventFactory: LocalEchoEventFactory, + private val tx: DefaultVerificationTransaction?, + private val coroutineScope: CoroutineScope +) : VerificationTransport { + + override fun sendToOther(type: String, + verificationInfo: VerificationInfo, + nextState: VerificationTxState, + onErrorReason: CancelCode, + onDone: (() -> Unit)?) { + Timber.d("## SAS sending msg type $type") + Timber.v("## SAS sending msg info $verificationInfo") + val event = createEventAndLocalEcho( + type = type, + roomId = roomId, + content = verificationInfo.toEventContent()!! + ) + + val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params( + sessionId = sessionId, + event = event + )) + val enqueueInfo = enqueueSendWork(workerParams) + + // I cannot just listen to the given work request, because when used in a uniqueWork, + // The callback is called while it is still Running ... + +// Futures.addCallback(enqueueInfo.first.result, object : FutureCallback { +// override fun onSuccess(result: Operation.State.SUCCESS?) { +// if (onDone != null) { +// onDone() +// } else { +// tx?.state = nextState +// } +// } +// +// override fun onFailure(t: Throwable) { +// Timber.e("## SAS verification [${tx?.transactionId}] failed to send toDevice in state : ${tx?.state}, reason: ${t.localizedMessage}") +// tx?.cancel(onErrorReason) +// } +// }, listenerExecutor) + + val workLiveData = workManagerProvider.workManager + .getWorkInfosForUniqueWorkLiveData(uniqueQueueName()) + + val observer = object : Observer> { + override fun onChanged(workInfoList: List?) { + workInfoList + ?.filter { it.state == WorkInfo.State.SUCCEEDED } + ?.firstOrNull { it.id == enqueueInfo.second } + ?.let { wInfo -> + if (SendVerificationMessageWorker.hasFailed(wInfo.outputData)) { + Timber.e("## SAS verification [${tx?.transactionId}] failed to send verification message in state : ${tx?.state}") + tx?.cancel(onErrorReason) + } else { + if (onDone != null) { + onDone() + } else { + tx?.state = nextState + } + } + workLiveData.removeObserver(this) + } + } + } + + // TODO listen to DB to get synced info + coroutineScope.launch(Dispatchers.Main) { + workLiveData.observeForever(observer) + } + } + + override fun sendVerificationRequest(supportedMethods: List, + localId: String, + otherUserId: String, + roomId: String?, + toDevices: List?, + callback: (String?, ValidVerificationInfoRequest?) -> Unit) { + Timber.d("## SAS sending verification request with supported methods: $supportedMethods") + // This transport requires a room + requireNotNull(roomId) + + val validInfo = ValidVerificationInfoRequest( + transactionId = "", + fromDevice = userDeviceId ?: "", + methods = supportedMethods, + timestamp = System.currentTimeMillis() + ) + + val info = MessageVerificationRequestContent( + body = stringProvider.getString(R.string.key_verification_request_fallback_message, userId), + fromDevice = validInfo.fromDevice, + toUserId = otherUserId, + timestamp = validInfo.timestamp, + methods = validInfo.methods + ) + val content = info.toContent() + + val event = createEventAndLocalEcho( + localId, + EventType.MESSAGE, + roomId, + content + ) + + val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params( + sessionId = sessionId, + event = event + )) + + val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder() + .setConstraints(WorkManagerProvider.workConstraints) + .setInputData(workerParams) + .setBackoffCriteria(BackoffPolicy.LINEAR, 2_000L, TimeUnit.MILLISECONDS) + .build() + + workManagerProvider.workManager + .beginUniqueWork("${roomId}_VerificationWork", ExistingWorkPolicy.APPEND, workRequest) + .enqueue() + + // I cannot just listen to the given work request, because when used in a uniqueWork, + // The callback is called while it is still Running ... + + val workLiveData = workManagerProvider.workManager + .getWorkInfosForUniqueWorkLiveData("${roomId}_VerificationWork") + + val observer = object : Observer> { + override fun onChanged(workInfoList: List?) { + workInfoList + ?.filter { it.state == WorkInfo.State.SUCCEEDED } + ?.firstOrNull { it.id == workRequest.id } + ?.let { wInfo -> + if (SendVerificationMessageWorker.hasFailed(wInfo.outputData)) { + callback(null, null) + } else { + val eventId = wInfo.outputData.getString(localId) + if (eventId != null) { + callback(eventId, validInfo) + } else { + callback(null, null) + } + } + workLiveData.removeObserver(this) + } + } + } + + // TODO listen to DB to get synced info + coroutineScope.launch(Dispatchers.Main) { + workLiveData.observeForever(observer) + } + } + + override fun cancelTransaction(transactionId: String, otherUserId: String, otherUserDeviceId: String?, code: CancelCode) { + Timber.d("## SAS canceling transaction $transactionId for reason $code") + val event = createEventAndLocalEcho( + type = EventType.KEY_VERIFICATION_CANCEL, + roomId = roomId, + content = MessageVerificationCancelContent.create(transactionId, code).toContent() + ) + val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params( + sessionId = sessionId, + event = event + )) + enqueueSendWork(workerParams) + } + + override fun done(transactionId: String, + onDone: (() -> Unit)?) { + Timber.d("## SAS sending done for $transactionId") + val event = createEventAndLocalEcho( + type = EventType.KEY_VERIFICATION_DONE, + roomId = roomId, + content = MessageVerificationDoneContent( + relatesTo = RelationDefaultContent( + RelationType.REFERENCE, + transactionId + ) + ).toContent() + ) + val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params( + sessionId = sessionId, + event = event + )) + val enqueueInfo = enqueueSendWork(workerParams) + + val workLiveData = workManagerProvider.workManager + .getWorkInfosForUniqueWorkLiveData(uniqueQueueName()) + val observer = object : Observer> { + override fun onChanged(workInfoList: List?) { + workInfoList + ?.filter { it.state == WorkInfo.State.SUCCEEDED } + ?.firstOrNull { it.id == enqueueInfo.second } + ?.let { _ -> + onDone?.invoke() + workLiveData.removeObserver(this) + } + } + } + + // TODO listen to DB to get synced info + coroutineScope.launch(Dispatchers.Main) { + workLiveData.observeForever(observer) + } + } + + private fun enqueueSendWork(workerParams: Data): Pair { + val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder() + .setConstraints(WorkManagerProvider.workConstraints) + .setInputData(workerParams) + .setBackoffCriteria(BackoffPolicy.LINEAR, 2_000L, TimeUnit.MILLISECONDS) + .build() + return workManagerProvider.workManager + .beginUniqueWork(uniqueQueueName(), ExistingWorkPolicy.APPEND, workRequest) + .enqueue() to workRequest.id + } + + private fun uniqueQueueName() = "${roomId}_VerificationWork" + + override fun createAccept(tid: String, + keyAgreementProtocol: String, + hash: String, + commitment: String, + messageAuthenticationCode: String, + shortAuthenticationStrings: List) + : VerificationInfoAccept = MessageVerificationAcceptContent.create( + tid, + keyAgreementProtocol, + hash, + commitment, + messageAuthenticationCode, + shortAuthenticationStrings + ) + + override fun createKey(tid: String, pubKey: String): VerificationInfoKey = MessageVerificationKeyContent.create(tid, pubKey) + + override fun createMac(tid: String, mac: Map, keys: String) = MessageVerificationMacContent.create(tid, mac, keys) + + override fun createStartForSas(fromDevice: String, + transactionId: String, + keyAgreementProtocols: List, + hashes: List, + messageAuthenticationCodes: List, + shortAuthenticationStrings: List): VerificationInfoStart { + return MessageVerificationStartContent( + fromDevice, + hashes, + keyAgreementProtocols, + messageAuthenticationCodes, + shortAuthenticationStrings, + VERIFICATION_METHOD_SAS, + RelationDefaultContent( + type = RelationType.REFERENCE, + eventId = transactionId + ), + null + ) + } + + override fun createStartForQrCode(fromDevice: String, + transactionId: String, + sharedSecret: String): VerificationInfoStart { + return MessageVerificationStartContent( + fromDevice, + null, + null, + null, + null, + VERIFICATION_METHOD_RECIPROCATE, + RelationDefaultContent( + type = RelationType.REFERENCE, + eventId = transactionId + ), + sharedSecret + ) + } + + override fun createReady(tid: String, fromDevice: String, methods: List): VerificationInfoReady { + return MessageVerificationReadyContent( + fromDevice = fromDevice, + relatesTo = RelationDefaultContent( + type = RelationType.REFERENCE, + eventId = tid + ), + methods = methods + ) + } + + private fun createEventAndLocalEcho(localId: String = LocalEcho.createLocalEchoId(), type: String, roomId: String, content: Content): Event { + return Event( + roomId = roomId, + originServerTs = System.currentTimeMillis(), + senderId = userId, + eventId = localId, + type = type, + content = content, + unsignedData = UnsignedData(age = null, transactionId = localId) + ).also { + localEchoEventFactory.createLocalEcho(it) + } + } + + override fun sendVerificationReady(keyReq: VerificationInfoReady, + otherUserId: String, + otherDeviceId: String?, + callback: (() -> Unit)?) { + // Not applicable (send event is called directly) + Timber.w("## SAS ignored verification ready with methods: ${keyReq.methods}") + } +} + +internal class VerificationTransportRoomMessageFactory @Inject constructor( + private val workManagerProvider: WorkManagerProvider, + private val stringProvider: StringProvider, + @SessionId + private val sessionId: String, + @UserId + private val userId: String, + @DeviceId + private val deviceId: String?, + private val localEchoEventFactory: LocalEchoEventFactory, + private val taskExecutor: TaskExecutor +) { + + fun createTransport(roomId: String, tx: DefaultVerificationTransaction?): VerificationTransportRoomMessage { + return VerificationTransportRoomMessage(workManagerProvider, + stringProvider, + sessionId, + userId, + deviceId, + roomId, + localEchoEventFactory, + tx, + taskExecutor.executorScope) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDevice.kt new file mode 100644 index 0000000000..1dbcf31c78 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDevice.kt @@ -0,0 +1,261 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.crypto.verification + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoRequest +import org.matrix.android.sdk.api.session.crypto.verification.CancelCode +import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.room.model.message.MessageType +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationAccept +import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationCancel +import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationDone +import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationKey +import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationMac +import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationReady +import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationRequest +import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationStart +import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_RECIPROCATE +import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_SAS +import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask +import org.matrix.android.sdk.internal.di.DeviceId +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import timber.log.Timber +import javax.inject.Inject + +internal class VerificationTransportToDevice( + private var tx: DefaultVerificationTransaction?, + private var sendToDeviceTask: SendToDeviceTask, + private val myDeviceId: String?, + private var taskExecutor: TaskExecutor +) : VerificationTransport { + + override fun sendVerificationRequest(supportedMethods: List, + localId: String, + otherUserId: String, + roomId: String?, + toDevices: List?, + callback: (String?, ValidVerificationInfoRequest?) -> Unit) { + Timber.d("## SAS sending verification request with supported methods: $supportedMethods") + val contentMap = MXUsersDevicesMap() + val validKeyReq = ValidVerificationInfoRequest( + transactionId = localId, + fromDevice = myDeviceId ?: "", + methods = supportedMethods, + timestamp = System.currentTimeMillis() + ) + val keyReq = KeyVerificationRequest( + fromDevice = validKeyReq.fromDevice, + methods = validKeyReq.methods, + timestamp = validKeyReq.timestamp, + transactionId = validKeyReq.transactionId + ) + toDevices?.forEach { + contentMap.setObject(otherUserId, it, keyReq) + } + sendToDeviceTask + .configureWith(SendToDeviceTask.Params(MessageType.MSGTYPE_VERIFICATION_REQUEST, contentMap, localId)) { + this.callback = object : MatrixCallback { + override fun onSuccess(data: Unit) { + Timber.v("## verification [$tx.transactionId] send toDevice request success") + callback.invoke(localId, validKeyReq) + } + + override fun onFailure(failure: Throwable) { + Timber.e("## verification [$tx.transactionId] failed to send toDevice request") + } + } + } + .executeBy(taskExecutor) + } + + override fun sendVerificationReady(keyReq: VerificationInfoReady, + otherUserId: String, + otherDeviceId: String?, + callback: (() -> Unit)?) { + Timber.d("## SAS sending verification ready with methods: ${keyReq.methods}") + val contentMap = MXUsersDevicesMap() + + contentMap.setObject(otherUserId, otherDeviceId, keyReq) + + sendToDeviceTask + .configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_READY, contentMap)) { + this.callback = object : MatrixCallback { + override fun onSuccess(data: Unit) { + Timber.v("## verification [$tx.transactionId] send toDevice request success") + callback?.invoke() + } + + override fun onFailure(failure: Throwable) { + Timber.e("## verification [$tx.transactionId] failed to send toDevice request") + } + } + } + .executeBy(taskExecutor) + } + + override fun sendToOther(type: String, + verificationInfo: VerificationInfo, + nextState: VerificationTxState, + onErrorReason: CancelCode, + onDone: (() -> Unit)?) { + Timber.d("## SAS sending msg type $type") + Timber.v("## SAS sending msg info $verificationInfo") + val stateBeforeCall = tx?.state + val tx = tx ?: return + val contentMap = MXUsersDevicesMap() + val toSendToDeviceObject = verificationInfo.toSendToDeviceObject() + ?: return Unit.also { tx.cancel() } + + contentMap.setObject(tx.otherUserId, tx.otherDeviceId, toSendToDeviceObject) + + sendToDeviceTask + .configureWith(SendToDeviceTask.Params(type, contentMap, tx.transactionId)) { + this.callback = object : MatrixCallback { + override fun onSuccess(data: Unit) { + Timber.v("## SAS verification [$tx.transactionId] toDevice type '$type' success.") + if (onDone != null) { + onDone() + } else { + // we may have received next state (e.g received accept in sending_start) + // We only put next state if the state was what is was before we started + if (tx.state == stateBeforeCall) { + tx.state = nextState + } + } + } + + override fun onFailure(failure: Throwable) { + Timber.e("## SAS verification [$tx.transactionId] failed to send toDevice in state : $tx.state") + tx.cancel(onErrorReason) + } + } + } + .executeBy(taskExecutor) + } + + override fun done(transactionId: String, onDone: (() -> Unit)?) { + val otherUserId = tx?.otherUserId ?: return + val otherUserDeviceId = tx?.otherDeviceId ?: return + val cancelMessage = KeyVerificationDone(transactionId) + val contentMap = MXUsersDevicesMap() + contentMap.setObject(otherUserId, otherUserDeviceId, cancelMessage) + sendToDeviceTask + .configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_DONE, contentMap, transactionId)) { + this.callback = object : MatrixCallback { + override fun onSuccess(data: Unit) { + onDone?.invoke() + Timber.v("## SAS verification [$transactionId] done") + } + + override fun onFailure(failure: Throwable) { + Timber.e(failure, "## SAS verification [$transactionId] failed to done.") + } + } + } + .executeBy(taskExecutor) + } + + override fun cancelTransaction(transactionId: String, otherUserId: String, otherUserDeviceId: String?, code: CancelCode) { + Timber.d("## SAS canceling transaction $transactionId for reason $code") + val cancelMessage = KeyVerificationCancel.create(transactionId, code) + val contentMap = MXUsersDevicesMap() + contentMap.setObject(otherUserId, otherUserDeviceId, cancelMessage) + sendToDeviceTask + .configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_CANCEL, contentMap, transactionId)) { + this.callback = object : MatrixCallback { + override fun onSuccess(data: Unit) { + Timber.v("## SAS verification [$transactionId] canceled for reason ${code.value}") + } + + override fun onFailure(failure: Throwable) { + Timber.e(failure, "## SAS verification [$transactionId] failed to cancel.") + } + } + } + .executeBy(taskExecutor) + } + + override fun createAccept(tid: String, + keyAgreementProtocol: String, + hash: String, + commitment: String, + messageAuthenticationCode: String, + shortAuthenticationStrings: List): VerificationInfoAccept = KeyVerificationAccept.create( + tid, + keyAgreementProtocol, + hash, + commitment, + messageAuthenticationCode, + shortAuthenticationStrings) + + override fun createKey(tid: String, pubKey: String): VerificationInfoKey = KeyVerificationKey.create(tid, pubKey) + + override fun createMac(tid: String, mac: Map, keys: String) = KeyVerificationMac.create(tid, mac, keys) + + override fun createStartForSas(fromDevice: String, + transactionId: String, + keyAgreementProtocols: List, + hashes: List, + messageAuthenticationCodes: List, + shortAuthenticationStrings: List): VerificationInfoStart { + return KeyVerificationStart( + fromDevice, + VERIFICATION_METHOD_SAS, + transactionId, + keyAgreementProtocols, + hashes, + messageAuthenticationCodes, + shortAuthenticationStrings, + null) + } + + override fun createStartForQrCode(fromDevice: String, + transactionId: String, + sharedSecret: String): VerificationInfoStart { + return KeyVerificationStart( + fromDevice, + VERIFICATION_METHOD_RECIPROCATE, + transactionId, + null, + null, + null, + null, + sharedSecret) + } + + override fun createReady(tid: String, fromDevice: String, methods: List): VerificationInfoReady { + return KeyVerificationReady( + transactionId = tid, + fromDevice = fromDevice, + methods = methods + ) + } +} + +internal class VerificationTransportToDeviceFactory @Inject constructor( + private val sendToDeviceTask: SendToDeviceTask, + @DeviceId val myDeviceId: String?, + private val taskExecutor: TaskExecutor) { + + fun createTransport(tx: DefaultVerificationTransaction?): VerificationTransportToDevice { + return VerificationTransportToDevice(tx, sendToDeviceTask, myDeviceId, taskExecutor) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt new file mode 100644 index 0000000000..f0db2e0fee --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt @@ -0,0 +1,284 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.verification.qrcode + +import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService +import org.matrix.android.sdk.api.session.crypto.verification.CancelCode +import org.matrix.android.sdk.api.session.crypto.verification.QrCodeVerificationTransaction +import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction +import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64 +import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64Safe +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationTransaction +import org.matrix.android.sdk.internal.crypto.verification.ValidVerificationInfoStart +import org.matrix.android.sdk.internal.util.exhaustive +import timber.log.Timber + +internal class DefaultQrCodeVerificationTransaction( + setDeviceVerificationAction: SetDeviceVerificationAction, + override val transactionId: String, + override val otherUserId: String, + override var otherDeviceId: String?, + private val crossSigningService: CrossSigningService, + outgoingGossipingRequestManager: OutgoingGossipingRequestManager, + incomingGossipingRequestManager: IncomingGossipingRequestManager, + private val cryptoStore: IMXCryptoStore, + // Not null only if other user is able to scan QR code + private val qrCodeData: QrCodeData?, + val userId: String, + val deviceId: String, + override val isIncoming: Boolean +) : DefaultVerificationTransaction( + setDeviceVerificationAction, + crossSigningService, + outgoingGossipingRequestManager, + incomingGossipingRequestManager, + userId, + transactionId, + otherUserId, + otherDeviceId, + isIncoming), + QrCodeVerificationTransaction { + + override val qrCodeText: String? + get() = qrCodeData?.toEncodedString() + + override var state: VerificationTxState = VerificationTxState.None + set(newState) { + field = newState + + listeners.forEach { + try { + it.transactionUpdated(this) + } catch (e: Throwable) { + Timber.e(e, "## Error while notifying listeners") + } + } + } + + override fun userHasScannedOtherQrCode(otherQrCodeText: String) { + val otherQrCodeData = otherQrCodeText.toQrCodeData() ?: run { + Timber.d("## Verification QR: Invalid QR Code Data") + cancel(CancelCode.QrCodeInvalid) + return + } + + // Perform some checks + if (otherQrCodeData.transactionId != transactionId) { + Timber.d("## Verification QR: Invalid transaction actual ${otherQrCodeData.transactionId} expected:$transactionId") + cancel(CancelCode.QrCodeInvalid) + return + } + + // check master key + val myMasterKey = crossSigningService.getUserCrossSigningKeys(userId)?.masterKey()?.unpaddedBase64PublicKey + var canTrustOtherUserMasterKey = false + + // Check the other device view of my MSK + when (otherQrCodeData) { + is QrCodeData.VerifyingAnotherUser -> { + // key2 (aka otherUserMasterCrossSigningPublicKey) is what the one displaying the QR code (other user) think my MSK is. + // Let's check that it's correct + // If not -> Cancel + if (otherQrCodeData.otherUserMasterCrossSigningPublicKey != myMasterKey) { + Timber.d("## Verification QR: Invalid other master key ${otherQrCodeData.otherUserMasterCrossSigningPublicKey}") + cancel(CancelCode.MismatchedKeys) + return + } else Unit + } + is QrCodeData.SelfVerifyingMasterKeyTrusted -> { + // key1 (aka userMasterCrossSigningPublicKey) is the session displaying the QR code view of our MSK. + // Let's check that I see the same MSK + // If not -> Cancel + if (otherQrCodeData.userMasterCrossSigningPublicKey != myMasterKey) { + Timber.d("## Verification QR: Invalid other master key ${otherQrCodeData.userMasterCrossSigningPublicKey}") + cancel(CancelCode.MismatchedKeys) + return + } else { + // I can trust the MSK then (i see the same one, and other session tell me it's trusted by him) + canTrustOtherUserMasterKey = true + } + } + is QrCodeData.SelfVerifyingMasterKeyNotTrusted -> { + // key2 (aka userMasterCrossSigningPublicKey) is the session displaying the QR code view of our MSK. + // Let's check that it's the good one + // If not -> Cancel + if (otherQrCodeData.userMasterCrossSigningPublicKey != myMasterKey) { + Timber.d("## Verification QR: Invalid other master key ${otherQrCodeData.userMasterCrossSigningPublicKey}") + cancel(CancelCode.MismatchedKeys) + return + } else { + // Nothing special here, we will send a reciprocate start event, and then the other session will trust it's view of the MSK + } + } + }.exhaustive + + val toVerifyDeviceIds = mutableListOf() + + // Let's now check the other user/device key material + when (otherQrCodeData) { + is QrCodeData.VerifyingAnotherUser -> { + // key1(aka userMasterCrossSigningPublicKey) is the MSK of the one displaying the QR code (i.e other user) + // Let's check that it matches what I think it should be + if (otherQrCodeData.userMasterCrossSigningPublicKey + != crossSigningService.getUserCrossSigningKeys(otherUserId)?.masterKey()?.unpaddedBase64PublicKey) { + Timber.d("## Verification QR: Invalid user master key ${otherQrCodeData.userMasterCrossSigningPublicKey}") + cancel(CancelCode.MismatchedKeys) + return + } else { + // It does so i should mark it as trusted + canTrustOtherUserMasterKey = true + Unit + } + } + is QrCodeData.SelfVerifyingMasterKeyTrusted -> { + // key2 (aka otherDeviceKey) is my current device key in POV of the one displaying the QR code (i.e other device) + // Let's check that it's correct + if (otherQrCodeData.otherDeviceKey + != cryptoStore.getUserDevice(userId, deviceId)?.fingerprint()) { + Timber.d("## Verification QR: Invalid other device key ${otherQrCodeData.otherDeviceKey}") + cancel(CancelCode.MismatchedKeys) + return + } else Unit // Nothing special here, we will send a reciprocate start event, and then the other session will trust my device + // and thus allow me to request SSSS secret + } + is QrCodeData.SelfVerifyingMasterKeyNotTrusted -> { + // key1 (aka otherDeviceKey) is the device key of the one displaying the QR code (i.e other device) + // Let's check that it matches what I have locally + if (otherQrCodeData.deviceKey + != cryptoStore.getUserDevice(otherUserId, otherDeviceId ?: "")?.fingerprint()) { + Timber.d("## Verification QR: Invalid device key ${otherQrCodeData.deviceKey}") + cancel(CancelCode.MismatchedKeys) + return + } else { + // Yes it does -> i should trust it and sign then upload the signature + toVerifyDeviceIds.add(otherDeviceId ?: "") + Unit + } + } + }.exhaustive + + if (!canTrustOtherUserMasterKey && toVerifyDeviceIds.isEmpty()) { + // Nothing to verify + cancel(CancelCode.MismatchedKeys) + return + } + + // All checks are correct + // Send the shared secret so that sender can trust me + // qrCodeData.sharedSecret will be used to send the start request + start(otherQrCodeData.sharedSecret) + + trust( + canTrustOtherUserMasterKey = canTrustOtherUserMasterKey, + toVerifyDeviceIds = toVerifyDeviceIds.distinct(), + eventuallyMarkMyMasterKeyAsTrusted = true, + autoDone = false + ) + } + + private fun start(remoteSecret: String, onDone: (() -> Unit)? = null) { + if (state != VerificationTxState.None) { + Timber.e("## Verification QR: start verification from invalid state") + // should I cancel?? + throw IllegalStateException("Interactive Key verification already started") + } + + state = VerificationTxState.Started + val startMessage = transport.createStartForQrCode( + deviceId, + transactionId, + remoteSecret + ) + + transport.sendToOther( + EventType.KEY_VERIFICATION_START, + startMessage, + VerificationTxState.WaitingOtherReciprocateConfirm, + CancelCode.User, + onDone + ) + } + + override fun cancel() { + cancel(CancelCode.User) + } + + override fun cancel(code: CancelCode) { + state = VerificationTxState.Cancelled(code, true) + transport.cancelTransaction(transactionId, otherUserId, otherDeviceId ?: "", code) + } + + override fun isToDeviceTransport() = false + + // Other user has scanned our QR code. check that the secret matched, so we can trust him + fun onStartReceived(startReq: ValidVerificationInfoStart.ReciprocateVerificationInfoStart) { + if (qrCodeData == null) { + // Should not happen + cancel(CancelCode.UnexpectedMessage) + return + } + + if (startReq.sharedSecret.fromBase64Safe()?.contentEquals(qrCodeData.sharedSecret.fromBase64()) == true) { + // Ok, we can trust the other user + // We can only trust the master key in this case + // But first, ask the user for a confirmation + state = VerificationTxState.QrScannedByOther + } else { + // Display a warning + cancel(CancelCode.MismatchedKeys) + } + } + + fun onDoneReceived() { + if (state != VerificationTxState.WaitingOtherReciprocateConfirm) { + cancel(CancelCode.UnexpectedMessage) + return + } + state = VerificationTxState.Verified + transport.done(transactionId) {} + } + + override fun otherUserScannedMyQrCode() { + when (qrCodeData) { + is QrCodeData.VerifyingAnotherUser -> { + // Alice telling Bob that the code was scanned successfully is sufficient for Bob to trust Alice's key, + trust(true, emptyList(), false) + } + is QrCodeData.SelfVerifyingMasterKeyTrusted -> { + // I now know that I have the correct device key for other session, + // and can sign it with the self-signing key and upload the signature + trust(false, listOf(otherDeviceId ?: ""), false) + } + is QrCodeData.SelfVerifyingMasterKeyNotTrusted -> { + // I now know that i can trust my MSK + trust(true, emptyList(), true) + } + } + } + + override fun otherUserDidNotScannedMyQrCode() { + // What can I do then? + // At least remove the transaction... + cancel(CancelCode.MismatchedKeys) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/Extensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/Extensions.kt new file mode 100644 index 0000000000..5e799f63cc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/Extensions.kt @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.verification.qrcode + +import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64 +import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding +import org.matrix.android.sdk.internal.extensions.toUnsignedInt + +// MATRIX +private val prefix = "MATRIX".toByteArray(Charsets.ISO_8859_1) + +fun QrCodeData.toEncodedString(): String { + var result = ByteArray(0) + + // MATRIX + for (i in prefix.indices) { + result += prefix[i] + } + + // Version + result += 2 + + // Mode + result += when (this) { + is QrCodeData.VerifyingAnotherUser -> 0 + is QrCodeData.SelfVerifyingMasterKeyTrusted -> 1 + is QrCodeData.SelfVerifyingMasterKeyNotTrusted -> 2 + }.toByte() + + // TransactionId length + val length = transactionId.length + result += ((length and 0xFF00) shr 8).toByte() + result += length.toByte() + + // TransactionId + transactionId.forEach { + result += it.toByte() + } + + // Keys + firstKey.fromBase64().forEach { + result += it + } + secondKey.fromBase64().forEach { + result += it + } + + // Secret + sharedSecret.fromBase64().forEach { + result += it + } + + return result.toString(Charsets.ISO_8859_1) +} + +fun String.toQrCodeData(): QrCodeData? { + val byteArray = toByteArray(Charsets.ISO_8859_1) + + // Size should be min 6 + 1 + 1 + 2 + ? + 32 + 32 + ? = 74 + transactionLength + secretLength + + // Check header + // MATRIX + if (byteArray.size < 10) return null + + for (i in prefix.indices) { + if (byteArray[i] != prefix[i]) { + return null + } + } + + var cursor = prefix.size // 6 + + // Version + if (byteArray[cursor] != 2.toByte()) { + return null + } + cursor++ + + // Get mode + val mode = byteArray[cursor].toInt() + cursor++ + + // Get transaction length, Big Endian format + val msb = byteArray[cursor].toUnsignedInt() + val lsb = byteArray[cursor + 1].toUnsignedInt() + + val transactionLength = msb.shl(8) + lsb + + cursor++ + cursor++ + + val secretLength = byteArray.size - 74 - transactionLength + + // ensure the secret length is 8 bytes min + if (secretLength < 8) { + return null + } + + val transactionId = byteArray.copyOfRange(cursor, cursor + transactionLength).toString(Charsets.ISO_8859_1) + cursor += transactionLength + val key1 = byteArray.copyOfRange(cursor, cursor + 32).toBase64NoPadding() + cursor += 32 + val key2 = byteArray.copyOfRange(cursor, cursor + 32).toBase64NoPadding() + cursor += 32 + val secret = byteArray.copyOfRange(cursor, byteArray.size).toBase64NoPadding() + + return when (mode) { + 0 -> QrCodeData.VerifyingAnotherUser(transactionId, key1, key2, secret) + 1 -> QrCodeData.SelfVerifyingMasterKeyTrusted(transactionId, key1, key2, secret) + 2 -> QrCodeData.SelfVerifyingMasterKeyNotTrusted(transactionId, key1, key2, secret) + else -> null + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeData.kt new file mode 100644 index 0000000000..9ae0c136f4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeData.kt @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.verification.qrcode + +/** + * Ref: https://github.com/uhoreg/matrix-doc/blob/qr_key_verification/proposals/1543-qr_code_key_verification.md#qr-code-format + */ +sealed class QrCodeData( + /** + * the event ID or transaction_id of the associated verification + */ + open val transactionId: String, + /** + * First key (32 bytes, in base64 no padding) + */ + val firstKey: String, + /** + * Second key (32 bytes, in base64 no padding) + */ + val secondKey: String, + /** + * a random shared secret (in base64 no padding) + */ + open val sharedSecret: String +) { + /** + * verifying another user with cross-signing + * QR code verification mode: 0x00 + */ + data class VerifyingAnotherUser( + override val transactionId: String, + /** + * the user's own master cross-signing public key + */ + val userMasterCrossSigningPublicKey: String, + /** + * what the device thinks the other user's master cross-signing key is + */ + val otherUserMasterCrossSigningPublicKey: String, + override val sharedSecret: String + ) : QrCodeData( + transactionId, + userMasterCrossSigningPublicKey, + otherUserMasterCrossSigningPublicKey, + sharedSecret) + + /** + * self-verifying in which the current device does trust the master key + * QR code verification mode: 0x01 + */ + data class SelfVerifyingMasterKeyTrusted( + override val transactionId: String, + /** + * the user's own master cross-signing public key + */ + val userMasterCrossSigningPublicKey: String, + /** + * what the device thinks the other device's device key is + */ + val otherDeviceKey: String, + override val sharedSecret: String + ) : QrCodeData( + transactionId, + userMasterCrossSigningPublicKey, + otherDeviceKey, + sharedSecret) + + /** + * self-verifying in which the current device does not yet trust the master key + * QR code verification mode: 0x02 + */ + data class SelfVerifyingMasterKeyNotTrusted( + override val transactionId: String, + /** + * the current device's device key + */ + val deviceKey: String, + /** + * what the device thinks the user's master cross-signing key is + */ + val userMasterCrossSigningPublicKey: String, + override val sharedSecret: String + ) : QrCodeData( + transactionId, + deviceKey, + userMasterCrossSigningPublicKey, + sharedSecret) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/SharedSecret.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/SharedSecret.kt new file mode 100644 index 0000000000..edff103820 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/SharedSecret.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.verification.qrcode + +import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding +import java.security.SecureRandom + +fun generateSharedSecretV2(): String { + val secureRandom = SecureRandom() + + // 8 bytes long + val secretBytes = ByteArray(8) + secureRandom.nextBytes(secretBytes) + return secretBytes.toBase64NoPadding() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt new file mode 100644 index 0000000000..c633dc5860 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.database + +import io.realm.Realm +import io.realm.RealmConfiguration +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.isActive +import kotlinx.coroutines.withContext +import timber.log.Timber + +suspend fun awaitTransaction(config: RealmConfiguration, transaction: suspend (realm: Realm) -> T) = withContext(Dispatchers.Default) { + Realm.getInstance(config).use { bgRealm -> + bgRealm.beginTransaction() + val result: T + try { + val start = System.currentTimeMillis() + result = transaction(bgRealm) + if (isActive) { + bgRealm.commitTransaction() + val end = System.currentTimeMillis() + val time = end - start + Timber.v("Execute transaction in $time millis") + } + } finally { + if (bgRealm.isInTransaction) { + bgRealm.cancelTransaction() + } + } + result + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/DBConstants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/DBConstants.kt new file mode 100644 index 0000000000..0f735eb558 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/DBConstants.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database + +internal object DBConstants { + + const val STATE_EVENTS_CHUNK_TOKEN = "STATE_EVENTS_CHUNK_TOKEN" +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/DatabaseCleaner.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/DatabaseCleaner.kt new file mode 100644 index 0000000000..d12f8628b1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/DatabaseCleaner.kt @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database + +import org.matrix.android.sdk.internal.database.helper.nextDisplayIndex +import org.matrix.android.sdk.internal.database.model.ChunkEntity +import org.matrix.android.sdk.internal.database.model.ChunkEntityFields +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.RoomEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.SessionLifecycleObserver +import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection +import org.matrix.android.sdk.internal.task.TaskExecutor +import io.realm.Realm +import io.realm.RealmConfiguration +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +private const val MAX_NUMBER_OF_EVENTS_IN_DB = 35_000L +private const val MIN_NUMBER_OF_EVENTS_BY_CHUNK = 300 + +/** + * This class makes sure to stay under a maximum number of events as it makes Realm to be unusable when listening to events + * when the database is getting too big. This will try incrementally to remove the biggest chunks until we get below the threshold. + * We make sure to still have a minimum number of events so it's not becoming unusable. + * So this won't work for users with a big number of very active rooms. + */ +internal class DatabaseCleaner @Inject constructor(@SessionDatabase private val realmConfiguration: RealmConfiguration, + private val taskExecutor: TaskExecutor) : SessionLifecycleObserver { + + override fun onStart() { + taskExecutor.executorScope.launch(Dispatchers.Default) { + awaitTransaction(realmConfiguration) { realm -> + val allRooms = realm.where(RoomEntity::class.java).findAll() + Timber.v("There are ${allRooms.size} rooms in this session") + cleanUp(realm, MAX_NUMBER_OF_EVENTS_IN_DB / 2L) + } + } + } + + private suspend fun cleanUp(realm: Realm, threshold: Long) { + val numberOfEvents = realm.where(EventEntity::class.java).findAll().size + val numberOfTimelineEvents = realm.where(TimelineEventEntity::class.java).findAll().size + Timber.v("Number of events in db: $numberOfEvents | Number of timeline events in db: $numberOfTimelineEvents") + if (threshold <= MIN_NUMBER_OF_EVENTS_BY_CHUNK || numberOfTimelineEvents < MAX_NUMBER_OF_EVENTS_IN_DB) { + Timber.v("Db is low enough") + } else { + val thresholdChunks = realm.where(ChunkEntity::class.java) + .greaterThan(ChunkEntityFields.NUMBER_OF_TIMELINE_EVENTS, threshold) + .findAll() + + Timber.v("There are ${thresholdChunks.size} chunks to clean with more than $threshold events") + for (chunk in thresholdChunks) { + val maxDisplayIndex = chunk.nextDisplayIndex(PaginationDirection.FORWARDS) + val thresholdDisplayIndex = maxDisplayIndex - threshold + val eventsToRemove = chunk.timelineEvents.where().lessThan(TimelineEventEntityFields.DISPLAY_INDEX, thresholdDisplayIndex).findAll() + Timber.v("There are ${eventsToRemove.size} events to clean in chunk: ${chunk.identifier()} from room ${chunk.room?.first()?.roomId}") + chunk.numberOfTimelineEvents = chunk.numberOfTimelineEvents - eventsToRemove.size + eventsToRemove.forEach { + val canDeleteRoot = it.root?.stateKey == null + if (canDeleteRoot) { + it.root?.deleteFromRealm() + } + it.readReceipts?.readReceipts?.deleteAllFromRealm() + it.readReceipts?.deleteFromRealm() + it.annotations?.apply { + editSummary?.deleteFromRealm() + pollResponseSummary?.deleteFromRealm() + referencesSummaryEntity?.deleteFromRealm() + reactionsSummary.deleteAllFromRealm() + } + it.annotations?.deleteFromRealm() + it.readReceipts?.deleteFromRealm() + it.deleteFromRealm() + } + // We reset the prevToken so we will need to fetch again. + chunk.prevToken = null + } + cleanUp(realm, (threshold / 1.5).toLong()) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt new file mode 100644 index 0000000000..1834961ff2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult +import org.matrix.android.sdk.internal.database.mapper.asDomain +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.EventInsertEntity +import org.matrix.android.sdk.internal.database.model.EventInsertEntityFields +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor +import io.realm.RealmConfiguration +import io.realm.RealmResults +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase realmConfiguration: RealmConfiguration, + private val processors: Set<@JvmSuppressWildcards EventInsertLiveProcessor>, + private val cryptoService: CryptoService) + : RealmLiveEntityObserver(realmConfiguration) { + + override val query = Monarchy.Query { + it.where(EventInsertEntity::class.java) + } + + override fun onChange(results: RealmResults) { + if (!results.isLoaded || results.isEmpty()) { + return + } + val idsToDeleteAfterProcess = ArrayList() + val filteredEvents = ArrayList(results.size) + Timber.v("EventInsertEntity updated with ${results.size} results in db") + results.forEach { + if (shouldProcess(it)) { + // don't use copy from realm over there + val copiedEvent = EventInsertEntity( + eventId = it.eventId, + eventType = it.eventType + ).apply { + insertType = it.insertType + } + filteredEvents.add(copiedEvent) + } + idsToDeleteAfterProcess.add(it.eventId) + } + observerScope.launch { + awaitTransaction(realmConfiguration) { realm -> + Timber.v("##Transaction: There are ${filteredEvents.size} events to process ") + filteredEvents.forEach { eventInsert -> + val eventId = eventInsert.eventId + val event = EventEntity.where(realm, eventId).findFirst() + if (event == null) { + Timber.v("Event $eventId not found") + return@forEach + } + val domainEvent = event.asDomain() + decryptIfNeeded(domainEvent) + processors.filter { + it.shouldProcess(eventId, domainEvent.getClearType(), eventInsert.insertType) + }.forEach { + it.process(realm, domainEvent) + } + } + realm.where(EventInsertEntity::class.java) + .`in`(EventInsertEntityFields.EVENT_ID, idsToDeleteAfterProcess.toTypedArray()) + .findAll() + .deleteAllFromRealm() + } + } + } + + private fun decryptIfNeeded(event: Event) { + if (event.isEncrypted() && event.mxDecryptionResult == null) { + try { + val result = cryptoService.decryptEvent(event, event.roomId ?: "") + event.mxDecryptionResult = OlmDecryptionResult( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) }, + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + ) + } catch (e: MXCryptoError) { + Timber.v("Failed to decrypt event") + // TODO -> we should keep track of this and retry, or some processing will never be handled + } + } + } + + private fun shouldProcess(eventInsertEntity: EventInsertEntity): Boolean { + return processors.any { + it.shouldProcess(eventInsertEntity.eventId, eventInsertEntity.eventType, eventInsertEntity.insertType) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmKeysUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmKeysUtils.kt new file mode 100644 index 0000000000..453cbae325 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmKeysUtils.kt @@ -0,0 +1,128 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.database + +import android.content.Context +import android.util.Base64 +import androidx.core.content.edit +import org.matrix.android.sdk.BuildConfig +import org.matrix.android.sdk.internal.session.securestorage.SecretStoringUtils +import io.realm.RealmConfiguration +import timber.log.Timber +import java.security.SecureRandom +import javax.inject.Inject + +/** + * On creation a random key is generated, this key is then encrypted using the system KeyStore. + * The encrypted key is stored in shared preferences. + * When the database is opened again, the encrypted key is taken from the shared pref, + * then the Keystore is used to decrypt the key. The decrypted key is passed to the RealConfiguration. + * + * On android >=M, the KeyStore generates an AES key to encrypt/decrypt the database key, + * and the encrypted key is stored with the initialization vector in base64 in the shared pref. + * On android (protected val realmConfiguration: RealmConfiguration) + : LiveEntityObserver, RealmChangeListener> { + + private companion object { + val BACKGROUND_HANDLER = createBackgroundHandler("LIVE_ENTITY_BACKGROUND") + } + + protected val observerScope = CoroutineScope(SupervisorJob() + BACKGROUND_HANDLER.asCoroutineDispatcher()) + protected abstract val query: Monarchy.Query + private val isStarted = AtomicBoolean(false) + private val backgroundRealm = AtomicReference() + private lateinit var results: AtomicReference> + + override fun onStart() { + if (isStarted.compareAndSet(false, true)) { + BACKGROUND_HANDLER.post { + val realm = Realm.getInstance(realmConfiguration) + backgroundRealm.set(realm) + val queryResults = query.createQuery(realm).findAll() + queryResults.addChangeListener(this) + results = AtomicReference(queryResults) + } + } + } + + override fun onStop() { + if (isStarted.compareAndSet(true, false)) { + BACKGROUND_HANDLER.post { + results.getAndSet(null).removeAllChangeListeners() + backgroundRealm.getAndSet(null).also { + it.close() + } + observerScope.coroutineContext.cancelChildren() + } + } + } + + override fun onClearCache() { + observerScope.coroutineContext.cancelChildren() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmQueryLatch.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmQueryLatch.kt new file mode 100644 index 0000000000..712b01a69a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmQueryLatch.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database + +import io.realm.Realm +import io.realm.RealmChangeListener +import io.realm.RealmConfiguration +import io.realm.RealmQuery +import io.realm.RealmResults +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout + +internal suspend fun awaitNotEmptyResult(realmConfiguration: RealmConfiguration, + timeoutMillis: Long, + builder: (Realm) -> RealmQuery) { + withTimeout(timeoutMillis) { + // Confine Realm interaction to a single thread with Looper. + withContext(Dispatchers.Main) { + val latch = CompletableDeferred() + + Realm.getInstance(realmConfiguration).use { realm -> + val result = builder(realm).findAllAsync() + + val listener = object : RealmChangeListener> { + override fun onChange(it: RealmResults) { + if (it.isNotEmpty()) { + result.removeChangeListener(this) + latch.complete(Unit) + } + } + } + + result.addChangeListener(listener) + try { + latch.await() + } catch (e: CancellationException) { + result.removeChangeListener(listener) + throw e + } + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt new file mode 100644 index 0000000000..195116cfe6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database + +import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields +import io.realm.DynamicRealm +import io.realm.RealmMigration +import timber.log.Timber +import javax.inject.Inject + +class RealmSessionStoreMigration @Inject constructor() : RealmMigration { + + override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { + Timber.v("Migrating Realm Session from $oldVersion to $newVersion") + + if (oldVersion <= 0) migrateTo1(realm) + if (oldVersion <= 1) migrateTo2(realm) + } + + private fun migrateTo1(realm: DynamicRealm) { + Timber.d("Step 0 -> 1") + // Add hasFailedSending in RoomSummary and a small warning icon on room list + + realm.schema.get("RoomSummaryEntity") + ?.addField(RoomSummaryEntityFields.HAS_FAILED_SENDING, Boolean::class.java) + ?.transform { obj -> + obj.setBoolean(RoomSummaryEntityFields.HAS_FAILED_SENDING, false) + } + } + + private fun migrateTo2(realm: DynamicRealm) { + Timber.d("Step 1 -> 2") + realm.schema.get("HomeServerCapabilitiesEntity") + ?.addField(HomeServerCapabilitiesEntityFields.ADMIN_E2_E_BY_DEFAULT, Boolean::class.java) + ?.transform { obj -> + obj.setBoolean(HomeServerCapabilitiesEntityFields.ADMIN_E2_E_BY_DEFAULT, true) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/SessionRealmConfigurationFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/SessionRealmConfigurationFactory.kt new file mode 100644 index 0000000000..e53240c5b8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/SessionRealmConfigurationFactory.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database + +import android.content.Context +import androidx.core.content.edit +import org.matrix.android.sdk.internal.database.model.SessionRealmModule +import org.matrix.android.sdk.internal.di.SessionFilesDirectory +import org.matrix.android.sdk.internal.di.SessionId +import org.matrix.android.sdk.internal.di.UserMd5 +import org.matrix.android.sdk.internal.session.SessionModule +import io.realm.Realm +import io.realm.RealmConfiguration +import timber.log.Timber +import java.io.File +import javax.inject.Inject + +private const val REALM_SHOULD_CLEAR_FLAG_ = "REALM_SHOULD_CLEAR_FLAG_" +private const val REALM_NAME = "disk_store.realm" + +/** + * This class is handling creation of RealmConfiguration for a session. + * It will handle corrupted realm by clearing the db file. It allows to just clear cache without losing your crypto keys. + * It's clearly not perfect but there is no way to catch the native crash. + */ +internal class SessionRealmConfigurationFactory @Inject constructor( + private val realmKeysUtils: RealmKeysUtils, + @SessionFilesDirectory val directory: File, + @SessionId val sessionId: String, + @UserMd5 val userMd5: String, + val migration: RealmSessionStoreMigration, + context: Context) { + + companion object { + const val SESSION_STORE_SCHEMA_VERSION = 2L + } + + // Keep legacy preferences name for compatibility reason + private val sharedPreferences = context.getSharedPreferences("im.vector.matrix.android.realm", Context.MODE_PRIVATE) + + fun create(): RealmConfiguration { + val shouldClearRealm = sharedPreferences.getBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", false) + if (shouldClearRealm) { + Timber.v("************************************************************") + Timber.v("The realm file session was corrupted and couldn't be loaded.") + Timber.v("The file has been deleted to recover.") + Timber.v("************************************************************") + deleteRealmFiles() + } + sharedPreferences.edit { + putBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", true) + } + + val realmConfiguration = RealmConfiguration.Builder() + .compactOnLaunch() + .directory(directory) + .name(REALM_NAME) + .apply { + realmKeysUtils.configureEncryption(this, SessionModule.getKeyAlias(userMd5)) + } + .modules(SessionRealmModule()) + .schemaVersion(SESSION_STORE_SCHEMA_VERSION) + .migration(migration) + .build() + + // Try creating a realm instance and if it succeeds we can clear the flag + Realm.getInstance(realmConfiguration).use { + Timber.v("Successfully create realm instance") + sharedPreferences.edit { + putBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", false) + } + } + return realmConfiguration + } + + // Delete all the realm files of the session + private fun deleteRealmFiles() { + listOf(REALM_NAME, "$REALM_NAME.lock", "$REALM_NAME.note", "$REALM_NAME.management").forEach { file -> + try { + File(directory, file).deleteRecursively() + } catch (e: Exception) { + Timber.e(e, "Unable to delete files") + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt new file mode 100644 index 0000000000..afe228a240 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt @@ -0,0 +1,199 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.helper + +import org.matrix.android.sdk.api.session.room.model.RoomMemberContent +import org.matrix.android.sdk.internal.database.model.ChunkEntity +import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields +import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.EventEntityFields +import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity +import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntity +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields +import org.matrix.android.sdk.internal.database.query.find +import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.extensions.assertIsManaged +import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection +import io.realm.Realm +import io.realm.Sort +import io.realm.kotlin.createObject +import timber.log.Timber + +internal fun ChunkEntity.deleteOnCascade() { + assertIsManaged() + this.timelineEvents.deleteAllFromRealm() + this.deleteFromRealm() +} + +internal fun ChunkEntity.merge(roomId: String, chunkToMerge: ChunkEntity, direction: PaginationDirection) { + assertIsManaged() + val localRealm = this.realm + val eventsToMerge: List + if (direction == PaginationDirection.FORWARDS) { + this.nextToken = chunkToMerge.nextToken + this.isLastForward = chunkToMerge.isLastForward + eventsToMerge = chunkToMerge.timelineEvents.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING) + } else { + this.prevToken = chunkToMerge.prevToken + this.isLastBackward = chunkToMerge.isLastBackward + eventsToMerge = chunkToMerge.timelineEvents.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) + } + chunkToMerge.stateEvents.forEach { stateEvent -> + addStateEvent(roomId, stateEvent, direction) + } + eventsToMerge.forEach { + addTimelineEventFromMerge(localRealm, it, direction) + } +} + +internal fun ChunkEntity.addStateEvent(roomId: String, stateEvent: EventEntity, direction: PaginationDirection) { + if (direction == PaginationDirection.BACKWARDS) { + Timber.v("We don't keep chunk state events when paginating backward") + } else { + val stateKey = stateEvent.stateKey ?: return + val type = stateEvent.type + val pastStateEvent = stateEvents.where() + .equalTo(EventEntityFields.ROOM_ID, roomId) + .equalTo(EventEntityFields.STATE_KEY, stateKey) + .equalTo(CurrentStateEventEntityFields.TYPE, type) + .findFirst() + + if (pastStateEvent != null) { + stateEvents.remove(pastStateEvent) + } + stateEvents.add(stateEvent) + } +} + +internal fun ChunkEntity.addTimelineEvent(roomId: String, + eventEntity: EventEntity, + direction: PaginationDirection, + roomMemberContentsByUser: Map) { + val eventId = eventEntity.eventId + if (timelineEvents.find(eventId) != null) { + return + } + val displayIndex = nextDisplayIndex(direction) + val localId = TimelineEventEntity.nextId(realm) + val senderId = eventEntity.sender ?: "" + + // Update RR for the sender of a new message with a dummy one + val readReceiptsSummaryEntity = handleReadReceipts(realm, roomId, eventEntity, senderId) + val timelineEventEntity = realm.createObject().apply { + this.localId = localId + this.root = eventEntity + this.eventId = eventId + this.roomId = roomId + this.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() + this.readReceipts = readReceiptsSummaryEntity + this.displayIndex = displayIndex + val roomMemberContent = roomMemberContentsByUser[senderId] + this.senderAvatar = roomMemberContent?.avatarUrl + this.senderName = roomMemberContent?.displayName + isUniqueDisplayName = if (roomMemberContent?.displayName != null) { + computeIsUnique(realm, roomId, isLastForward, roomMemberContent, roomMemberContentsByUser) + } else { + true + } + } + numberOfTimelineEvents++ + timelineEvents.add(timelineEventEntity) +} + +private fun computeIsUnique( + realm: Realm, + roomId: String, + isLastForward: Boolean, + senderRoomMemberContent: RoomMemberContent, + roomMemberContentsByUser: Map +): Boolean { + val isHistoricalUnique = roomMemberContentsByUser.values.find { + it != senderRoomMemberContent && it?.displayName == senderRoomMemberContent.displayName + } == null + return if (isLastForward) { + val isLiveUnique = RoomMemberSummaryEntity + .where(realm, roomId) + .equalTo(RoomMemberSummaryEntityFields.DISPLAY_NAME, senderRoomMemberContent.displayName) + .findAll() + .none { + !roomMemberContentsByUser.containsKey(it.userId) + } + isHistoricalUnique && isLiveUnique + } else { + isHistoricalUnique + } +} + +private fun ChunkEntity.addTimelineEventFromMerge(realm: Realm, timelineEventEntity: TimelineEventEntity, direction: PaginationDirection) { + val eventId = timelineEventEntity.eventId + if (timelineEvents.find(eventId) != null) { + return + } + val displayIndex = nextDisplayIndex(direction) + val localId = TimelineEventEntity.nextId(realm) + val copied = realm.createObject().apply { + this.localId = localId + this.root = timelineEventEntity.root + this.eventId = timelineEventEntity.eventId + this.roomId = timelineEventEntity.roomId + this.annotations = timelineEventEntity.annotations + this.readReceipts = timelineEventEntity.readReceipts + this.displayIndex = displayIndex + this.senderAvatar = timelineEventEntity.senderAvatar + this.senderName = timelineEventEntity.senderName + this.isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName + } + timelineEvents.add(copied) +} + +private fun handleReadReceipts(realm: Realm, roomId: String, eventEntity: EventEntity, senderId: String): ReadReceiptsSummaryEntity { + val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventEntity.eventId).findFirst() + ?: realm.createObject(eventEntity.eventId).apply { + this.roomId = roomId + } + val originServerTs = eventEntity.originServerTs + if (originServerTs != null) { + val timestampOfEvent = originServerTs.toDouble() + val readReceiptOfSender = ReadReceiptEntity.getOrCreate(realm, roomId = roomId, userId = senderId) + // If the synced RR is older, update + if (timestampOfEvent > readReceiptOfSender.originServerTs) { + val previousReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId = readReceiptOfSender.eventId).findFirst() + readReceiptOfSender.eventId = eventEntity.eventId + readReceiptOfSender.originServerTs = timestampOfEvent + previousReceiptsSummary?.readReceipts?.remove(readReceiptOfSender) + readReceiptsSummaryEntity.readReceipts.add(readReceiptOfSender) + } + } + return readReceiptsSummaryEntity +} + +internal fun ChunkEntity.nextDisplayIndex(direction: PaginationDirection): Int { + return when (direction) { + PaginationDirection.FORWARDS -> { + (timelineEvents.where().max(TimelineEventEntityFields.DISPLAY_INDEX)?.toInt() ?: 0) + 1 + } + PaginationDirection.BACKWARDS -> { + (timelineEvents.where().min(TimelineEventEntityFields.DISPLAY_INDEX)?.toInt() ?: 0) - 1 + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/RoomEntityHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/RoomEntityHelper.kt new file mode 100644 index 0000000000..4874a1742b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/RoomEntityHelper.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.helper + +import org.matrix.android.sdk.internal.database.model.ChunkEntity +import org.matrix.android.sdk.internal.database.model.RoomEntity + +internal fun RoomEntity.deleteOnCascade(chunkEntity: ChunkEntity) { + chunks.remove(chunkEntity) + chunkEntity.deleteOnCascade() +} + +internal fun RoomEntity.addOrUpdate(chunkEntity: ChunkEntity) { + if (!chunks.contains(chunkEntity)) { + chunks.add(chunkEntity) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/TimelineEventEntityHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/TimelineEventEntityHelper.kt new file mode 100644 index 0000000000..95ae59f80f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/TimelineEventEntityHelper.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.helper + +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields +import org.matrix.android.sdk.internal.extensions.assertIsManaged +import io.realm.Realm + +internal fun TimelineEventEntity.Companion.nextId(realm: Realm): Long { + val currentIdNum = realm.where(TimelineEventEntity::class.java).max(TimelineEventEntityFields.LOCAL_ID) + return if (currentIdNum == null) { + 1 + } else { + currentIdNum.toLong() + 1 + } +} + +internal fun TimelineEventEntity.deleteOnCascade() { + assertIsManaged() + root?.deleteFromRealm() + annotations?.deleteFromRealm() + readReceipts?.deleteFromRealm() + deleteFromRealm() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/AccountDataMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/AccountDataMapper.kt new file mode 100644 index 0000000000..9811afab8e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/AccountDataMapper.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.mapper + +import com.squareup.moshi.Moshi +import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE +import org.matrix.android.sdk.internal.database.model.UserAccountDataEntity +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent +import javax.inject.Inject + +internal class AccountDataMapper @Inject constructor(moshi: Moshi) { + + private val adapter = moshi.adapter>(JSON_DICT_PARAMETERIZED_TYPE) + + fun map(entity: UserAccountDataEntity): UserAccountDataEvent { + return UserAccountDataEvent( + type = entity.type ?: "", + content = entity.contentStr?.let { adapter.fromJson(it) }.orEmpty() + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ContentMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ContentMapper.kt new file mode 100644 index 0000000000..ab094e94b8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ContentMapper.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.mapper + +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE +import org.matrix.android.sdk.internal.di.MoshiProvider + +internal object ContentMapper { + + private val moshi = MoshiProvider.providesMoshi() + private val adapter = moshi.adapter(JSON_DICT_PARAMETERIZED_TYPE) + + fun map(content: String?): Content? { + return content?.let { + adapter.fromJson(it) + } + } + + fun map(content: Content?): String? { + return content?.let { + adapter.toJson(it) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/DraftMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/DraftMapper.kt new file mode 100644 index 0000000000..bc22c7ed3c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/DraftMapper.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.mapper + +import org.matrix.android.sdk.api.session.room.send.UserDraft +import org.matrix.android.sdk.internal.database.model.DraftEntity + +/** + * DraftEntity <-> UserDraft + */ +internal object DraftMapper { + + fun map(entity: DraftEntity): UserDraft { + return when (entity.draftMode) { + DraftEntity.MODE_REGULAR -> UserDraft.REGULAR(entity.content) + DraftEntity.MODE_EDIT -> UserDraft.EDIT(entity.linkedEventId, entity.content) + DraftEntity.MODE_QUOTE -> UserDraft.QUOTE(entity.linkedEventId, entity.content) + DraftEntity.MODE_REPLY -> UserDraft.REPLY(entity.linkedEventId, entity.content) + else -> null + } ?: UserDraft.REGULAR("") + } + + fun map(domain: UserDraft): DraftEntity { + return when (domain) { + is UserDraft.REGULAR -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_REGULAR, linkedEventId = "") + is UserDraft.EDIT -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_EDIT, linkedEventId = domain.linkedEventId) + is UserDraft.QUOTE -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_QUOTE, linkedEventId = domain.linkedEventId) + is UserDraft.REPLY -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_REPLY, linkedEventId = domain.linkedEventId) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt new file mode 100644 index 0000000000..2f697d53ca --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt @@ -0,0 +1,110 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.mapper + +import org.matrix.android.sdk.api.session.room.model.EditAggregatedSummary +import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary +import org.matrix.android.sdk.api.session.room.model.ReactionAggregatedSummary +import org.matrix.android.sdk.api.session.room.model.ReferencesAggregatedSummary +import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity +import org.matrix.android.sdk.internal.database.model.ReactionAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.model.ReferencesAggregatedSummaryEntity +import io.realm.RealmList + +internal object EventAnnotationsSummaryMapper { + fun map(annotationsSummary: EventAnnotationsSummaryEntity): EventAnnotationsSummary { + return EventAnnotationsSummary( + eventId = annotationsSummary.eventId, + reactionsSummary = annotationsSummary.reactionsSummary.toList().map { + ReactionAggregatedSummary( + it.key, + it.count, + it.addedByMe, + it.firstTimestamp, + it.sourceEvents.toList(), + it.sourceLocalEcho.toList() + ) + }, + editSummary = annotationsSummary.editSummary?.let { + EditAggregatedSummary( + ContentMapper.map(it.aggregatedContent), + it.sourceEvents.toList(), + it.sourceLocalEchoEvents.toList(), + it.lastEditTs + ) + }, + referencesAggregatedSummary = annotationsSummary.referencesSummaryEntity?.let { + ReferencesAggregatedSummary( + it.eventId, + ContentMapper.map(it.content), + it.sourceEvents.toList(), + it.sourceLocalEcho.toList() + ) + }, + pollResponseSummary = annotationsSummary.pollResponseSummary?.let { + PollResponseAggregatedSummaryEntityMapper.map(it) + } + + ) + } + + fun map(annotationsSummary: EventAnnotationsSummary, roomId: String): EventAnnotationsSummaryEntity { + val eventAnnotationsSummaryEntity = EventAnnotationsSummaryEntity() + eventAnnotationsSummaryEntity.eventId = annotationsSummary.eventId + eventAnnotationsSummaryEntity.roomId = roomId + eventAnnotationsSummaryEntity.editSummary = annotationsSummary.editSummary?.let { + EditAggregatedSummaryEntity( + ContentMapper.map(it.aggregatedContent), + RealmList().apply { addAll(it.sourceEvents) }, + RealmList().apply { addAll(it.localEchos) }, + it.lastEditTs + ) + } + eventAnnotationsSummaryEntity.reactionsSummary = annotationsSummary.reactionsSummary.let { + RealmList().apply { + addAll(it.map { + ReactionAggregatedSummaryEntity( + it.key, + it.count, + it.addedByMe, + it.firstTimestamp, + RealmList().apply { addAll(it.sourceEvents) }, + RealmList().apply { addAll(it.localEchoEvents) } + ) + }) + } + } + eventAnnotationsSummaryEntity.referencesSummaryEntity = annotationsSummary.referencesAggregatedSummary?.let { + ReferencesAggregatedSummaryEntity( + it.eventId, + ContentMapper.map(it.content), + RealmList().apply { addAll(it.sourceEvents) }, + RealmList().apply { addAll(it.localEchos) } + ) + } + eventAnnotationsSummaryEntity.pollResponseSummary = annotationsSummary.pollResponseSummary?.let { + PollResponseAggregatedSummaryEntityMapper.map(it) + } + return eventAnnotationsSummaryEntity + } +} + +internal fun EventAnnotationsSummaryEntity.asDomain(): EventAnnotationsSummary { + return EventAnnotationsSummaryMapper.map(this) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt new file mode 100644 index 0000000000..61f09dcece --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt @@ -0,0 +1,108 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.mapper + +import com.squareup.moshi.JsonDataException +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.UnsignedData +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.di.MoshiProvider +import timber.log.Timber + +internal object EventMapper { + + fun map(event: Event, roomId: String): EventEntity { + val uds = if (event.unsignedData == null) null + else MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(event.unsignedData) + val eventEntity = EventEntity() + // TODO change this as we shouldn't use event everywhere + eventEntity.eventId = event.eventId ?: "$$roomId-${System.currentTimeMillis()}-${event.hashCode()}" + eventEntity.roomId = event.roomId ?: roomId + eventEntity.content = ContentMapper.map(event.content) + eventEntity.prevContent = ContentMapper.map(event.resolvedPrevContent()) + eventEntity.isUseless = IsUselessResolver.isUseless(event) + eventEntity.stateKey = event.stateKey + eventEntity.type = event.type + eventEntity.sender = event.senderId + eventEntity.originServerTs = event.originServerTs + eventEntity.redacts = event.redacts + eventEntity.age = event.unsignedData?.age ?: event.originServerTs + eventEntity.unsignedData = uds + eventEntity.decryptionResultJson = event.mxDecryptionResult?.let { + MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).toJson(it) + } + eventEntity.decryptionErrorReason = event.mCryptoErrorReason + eventEntity.decryptionErrorCode = event.mCryptoError?.name + return eventEntity + } + + fun map(eventEntity: EventEntity): Event { + val ud = eventEntity.unsignedData + ?.takeIf { it.isNotBlank() } + ?.let { + try { + MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).fromJson(it) + } catch (t: JsonDataException) { + Timber.e(t, "Failed to parse UnsignedData") + null + } + } + + return Event( + type = eventEntity.type, + eventId = eventEntity.eventId, + content = ContentMapper.map(eventEntity.content), + prevContent = ContentMapper.map(eventEntity.prevContent), + originServerTs = eventEntity.originServerTs, + senderId = eventEntity.sender, + stateKey = eventEntity.stateKey, + roomId = eventEntity.roomId, + unsignedData = ud, + redacts = eventEntity.redacts + ).also { + it.ageLocalTs = eventEntity.ageLocalTs + it.sendState = eventEntity.sendState + eventEntity.decryptionResultJson?.let { json -> + try { + it.mxDecryptionResult = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).fromJson(json) + } catch (t: JsonDataException) { + Timber.e(t, "Failed to parse decryption result") + } + } + // TODO get the full crypto error object + it.mCryptoError = eventEntity.decryptionErrorCode?.let { errorCode -> + MXCryptoError.ErrorType.valueOf(errorCode) + } + it.mCryptoErrorReason = eventEntity.decryptionErrorReason + } + } +} + +internal fun EventEntity.asDomain(): Event { + return EventMapper.map(this) +} + +internal fun Event.toEntity(roomId: String, sendState: SendState, ageLocalTs: Long?): EventEntity { + return EventMapper.map(this, roomId).apply { + this.sendState = sendState + this.ageLocalTs = ageLocalTs + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/GroupSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/GroupSummaryMapper.kt new file mode 100644 index 0000000000..09c96215b4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/GroupSummaryMapper.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.mapper + +import org.matrix.android.sdk.api.session.group.model.GroupSummary +import org.matrix.android.sdk.internal.database.model.GroupSummaryEntity + +internal object GroupSummaryMapper { + + fun map(groupSummaryEntity: GroupSummaryEntity): GroupSummary { + return GroupSummary( + groupSummaryEntity.groupId, + groupSummaryEntity.membership, + groupSummaryEntity.displayName, + groupSummaryEntity.shortDescription, + groupSummaryEntity.avatarUrl, + groupSummaryEntity.roomIds.toList(), + groupSummaryEntity.userIds.toList() + ) + } +} + +internal fun GroupSummaryEntity.asDomain(): GroupSummary { + return GroupSummaryMapper.map(this) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt new file mode 100644 index 0000000000..8d38c3fbe5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.mapper + +import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities +import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntity + +/** + * HomeServerCapabilitiesEntity -> HomeSeverCapabilities + */ +internal object HomeServerCapabilitiesMapper { + + fun map(entity: HomeServerCapabilitiesEntity): HomeServerCapabilities { + return HomeServerCapabilities( + canChangePassword = entity.canChangePassword, + maxUploadFileSize = entity.maxUploadFileSize, + lastVersionIdentityServerSupported = entity.lastVersionIdentityServerSupported, + defaultIdentityServerUrl = entity.defaultIdentityServerUrl, + adminE2EByDefault = entity.adminE2EByDefault + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/IsUselessResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/IsUselessResolver.kt new file mode 100644 index 0000000000..5dde01e15c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/IsUselessResolver.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.mapper + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toContent + +internal object IsUselessResolver { + + /** + * @return true if the event is useless + */ + fun isUseless(event: Event): Boolean { + return when (event.type) { + EventType.STATE_ROOM_MEMBER -> { + // Call toContent(), to filter out null value + event.content != null + && event.content.toContent() == event.resolvedPrevContent()?.toContent() + } + else -> false + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PollResponseAggregatedSummaryEntityMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PollResponseAggregatedSummaryEntityMapper.kt new file mode 100644 index 0000000000..18c774ac40 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PollResponseAggregatedSummaryEntityMapper.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.mapper + +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.PollResponseAggregatedSummary +import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSummaryEntity +import io.realm.RealmList + +internal object PollResponseAggregatedSummaryEntityMapper { + + fun map(entity: PollResponseAggregatedSummaryEntity): PollResponseAggregatedSummary { + return PollResponseAggregatedSummary( + aggregatedContent = ContentMapper.map(entity.aggregatedContent).toModel(), + closedTime = entity.closedTime, + localEchos = entity.sourceLocalEchoEvents.toList(), + sourceEvents = entity.sourceEvents.toList(), + nbOptions = entity.nbOptions + ) + } + + fun map(model: PollResponseAggregatedSummary): PollResponseAggregatedSummaryEntity { + return PollResponseAggregatedSummaryEntity( + aggregatedContent = ContentMapper.map(model.aggregatedContent.toContent()), + nbOptions = model.nbOptions, + closedTime = model.closedTime, + sourceEvents = RealmList().apply { addAll(model.sourceEvents) }, + sourceLocalEchoEvents = RealmList().apply { addAll(model.localEchos) } + ) + } +} + +internal fun PollResponseAggregatedSummaryEntity.asDomain(): PollResponseAggregatedSummary { + return PollResponseAggregatedSummaryEntityMapper.map(this) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PushConditionMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PushConditionMapper.kt new file mode 100644 index 0000000000..cce780bad8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PushConditionMapper.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.mapper + +import org.matrix.android.sdk.api.pushrules.rest.PushCondition +import org.matrix.android.sdk.internal.database.model.PushConditionEntity + +internal object PushConditionMapper { + + fun map(entity: PushConditionEntity): PushCondition { + return PushCondition( + kind = entity.kind, + iz = entity.iz, + key = entity.key, + pattern = entity.pattern + ) + } + + fun map(domain: PushCondition): PushConditionEntity { + return PushConditionEntity( + kind = domain.kind, + iz = domain.iz, + key = domain.key, + pattern = domain.pattern + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PushRulesMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PushRulesMapper.kt new file mode 100644 index 0000000000..90fc62f8f3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PushRulesMapper.kt @@ -0,0 +1,102 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.database.mapper + +import com.squareup.moshi.Types +import org.matrix.android.sdk.api.pushrules.Condition +import org.matrix.android.sdk.api.pushrules.rest.PushCondition +import org.matrix.android.sdk.api.pushrules.rest.PushRule +import org.matrix.android.sdk.internal.database.model.PushRuleEntity +import org.matrix.android.sdk.internal.di.MoshiProvider +import io.realm.RealmList +import timber.log.Timber + +internal object PushRulesMapper { + + private val moshiActionsAdapter = MoshiProvider.providesMoshi().adapter>(Types.newParameterizedType(List::class.java, Any::class.java)) + +// private val listOfAnyAdapter: JsonAdapter> = +// moshi.adapter>(Types.newParameterizedType(List::class.java, Any::class.java), kotlin.collections.emptySet(), "actions") + + fun mapContentRule(pushrule: PushRuleEntity): PushRule { + return PushRule( + actions = fromActionStr(pushrule.actionsStr), + default = pushrule.default, + enabled = pushrule.enabled, + ruleId = pushrule.ruleId, + conditions = listOf( + PushCondition(Condition.Kind.EventMatch.value, "content.body", pushrule.pattern) + ) + ) + } + + private fun fromActionStr(actionsStr: String?): List { + try { + return actionsStr?.let { moshiActionsAdapter.fromJson(it) }.orEmpty() + } catch (e: Throwable) { + Timber.e(e, "## failed to map push rule actions <$actionsStr>") + return emptyList() + } + } + + fun mapRoomRule(pushrule: PushRuleEntity): PushRule { + return PushRule( + actions = fromActionStr(pushrule.actionsStr), + default = pushrule.default, + enabled = pushrule.enabled, + ruleId = pushrule.ruleId, + conditions = listOf( + PushCondition(Condition.Kind.EventMatch.value, "room_id", pushrule.ruleId) + ) + ) + } + + fun mapSenderRule(pushrule: PushRuleEntity): PushRule { + return PushRule( + actions = fromActionStr(pushrule.actionsStr), + default = pushrule.default, + enabled = pushrule.enabled, + ruleId = pushrule.ruleId, + conditions = listOf( + PushCondition(Condition.Kind.EventMatch.value, "user_id", pushrule.ruleId) + ) + ) + } + + fun map(pushrule: PushRuleEntity): PushRule { + return PushRule( + actions = fromActionStr(pushrule.actionsStr), + default = pushrule.default, + enabled = pushrule.enabled, + ruleId = pushrule.ruleId, + conditions = pushrule.conditions?.map { PushConditionMapper.map(it) } + ) + } + + fun map(pushRule: PushRule): PushRuleEntity { + return PushRuleEntity( + actionsStr = moshiActionsAdapter.toJson(pushRule.actions), + default = pushRule.default ?: false, + enabled = pushRule.enabled, + ruleId = pushRule.ruleId, + pattern = pushRule.pattern, + conditions = pushRule.conditions?.let { + RealmList(*pushRule.conditions.map { PushConditionMapper.map(it) }.toTypedArray()) + } ?: RealmList() + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PushersMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PushersMapper.kt new file mode 100644 index 0000000000..9912bcd4f6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PushersMapper.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.database.mapper + +import org.matrix.android.sdk.api.session.pushers.Pusher +import org.matrix.android.sdk.api.session.pushers.PusherData +import org.matrix.android.sdk.internal.database.model.PusherDataEntity +import org.matrix.android.sdk.internal.database.model.PusherEntity +import org.matrix.android.sdk.internal.session.pushers.JsonPusher + +internal object PushersMapper { + + fun map(pushEntity: PusherEntity): Pusher { + return Pusher( + pushKey = pushEntity.pushKey, + kind = pushEntity.kind ?: "", + appId = pushEntity.appId, + appDisplayName = pushEntity.appDisplayName, + deviceDisplayName = pushEntity.deviceDisplayName, + profileTag = pushEntity.profileTag, + lang = pushEntity.lang, + data = PusherData(pushEntity.data?.url, pushEntity.data?.format), + state = pushEntity.state + ) + } + + fun map(pusher: JsonPusher): PusherEntity { + return PusherEntity( + pushKey = pusher.pushKey, + kind = pusher.kind, + appId = pusher.appId, + appDisplayName = pusher.appDisplayName, + deviceDisplayName = pusher.deviceDisplayName, + profileTag = pusher.profileTag, + lang = pusher.lang, + data = PusherDataEntity(pusher.data?.url, pusher.data?.format) + ) + } +} + +internal fun PusherEntity.asDomain(): Pusher { + return PushersMapper.map(this) +} + +internal fun JsonPusher.toEntity(): PusherEntity { + return PushersMapper.map(this) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ReadReceiptsSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ReadReceiptsSummaryMapper.kt new file mode 100644 index 0000000000..188ca4937c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ReadReceiptsSummaryMapper.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.mapper + +import org.matrix.android.sdk.api.session.room.model.ReadReceipt +import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntity +import org.matrix.android.sdk.internal.database.model.UserEntity +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import io.realm.Realm +import io.realm.RealmConfiguration +import javax.inject.Inject + +internal class ReadReceiptsSummaryMapper @Inject constructor(@SessionDatabase private val realmConfiguration: RealmConfiguration) { + + fun map(readReceiptsSummaryEntity: ReadReceiptsSummaryEntity?): List { + if (readReceiptsSummaryEntity == null) { + return emptyList() + } + return Realm.getInstance(realmConfiguration).use { realm -> + val readReceipts = readReceiptsSummaryEntity.readReceipts + readReceipts + .mapNotNull { + val user = UserEntity.where(realm, it.userId).findFirst() + ?: return@mapNotNull null + ReadReceipt(user.asDomain(), it.originServerTs.toLong()) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomMemberSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomMemberSummaryMapper.kt new file mode 100644 index 0000000000..65ea7fa7c6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomMemberSummaryMapper.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.mapper + +import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity + +internal object RoomMemberSummaryMapper { + + fun map(roomMemberSummaryEntity: RoomMemberSummaryEntity): RoomMemberSummary { + return RoomMemberSummary( + userId = roomMemberSummaryEntity.userId, + avatarUrl = roomMemberSummaryEntity.avatarUrl, + displayName = roomMemberSummaryEntity.displayName, + membership = roomMemberSummaryEntity.membership + ) + } +} + +internal fun RoomMemberSummaryEntity.asDomain(): RoomMemberSummary { + return RoomMemberSummaryMapper.map(this) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt new file mode 100644 index 0000000000..bd2aba3e54 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.mapper + +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.tag.RoomTag +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker +import javax.inject.Inject + +internal class RoomSummaryMapper @Inject constructor(private val timelineEventMapper: TimelineEventMapper, + private val typingUsersTracker: DefaultTypingUsersTracker) { + + fun map(roomSummaryEntity: RoomSummaryEntity): RoomSummary { + val tags = roomSummaryEntity.tags.map { + RoomTag(it.tagName, it.tagOrder) + } + + val latestEvent = roomSummaryEntity.latestPreviewableEvent?.let { + timelineEventMapper.map(it, buildReadReceipts = false) + } + // typings are updated through the sync where room summary entity gets updated no matter what, so it's ok get there + val typingUsers = typingUsersTracker.getTypingUsers(roomSummaryEntity.roomId) + + return RoomSummary( + roomId = roomSummaryEntity.roomId, + displayName = roomSummaryEntity.displayName ?: "", + name = roomSummaryEntity.name ?: "", + topic = roomSummaryEntity.topic ?: "", + avatarUrl = roomSummaryEntity.avatarUrl ?: "", + isDirect = roomSummaryEntity.isDirect, + latestPreviewableEvent = latestEvent, + joinedMembersCount = roomSummaryEntity.joinedMembersCount, + invitedMembersCount = roomSummaryEntity.invitedMembersCount, + otherMemberIds = roomSummaryEntity.otherMemberIds.toList(), + highlightCount = roomSummaryEntity.highlightCount, + notificationCount = roomSummaryEntity.notificationCount, + hasUnreadMessages = roomSummaryEntity.hasUnreadMessages, + tags = tags, + typingUsers = typingUsers, + membership = roomSummaryEntity.membership, + versioningState = roomSummaryEntity.versioningState, + readMarkerId = roomSummaryEntity.readMarkerId, + userDrafts = roomSummaryEntity.userDrafts?.userDrafts?.map { DraftMapper.map(it) }.orEmpty(), + canonicalAlias = roomSummaryEntity.canonicalAlias, + aliases = roomSummaryEntity.aliases.toList(), + isEncrypted = roomSummaryEntity.isEncrypted, + encryptionEventTs = roomSummaryEntity.encryptionEventTs, + breadcrumbsIndex = roomSummaryEntity.breadcrumbsIndex, + roomEncryptionTrustLevel = roomSummaryEntity.roomEncryptionTrustLevel, + inviterId = roomSummaryEntity.inviterId, + hasFailedSending = roomSummaryEntity.hasFailedSending + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/TimelineEventMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/TimelineEventMapper.kt new file mode 100644 index 0000000000..71c586cffe --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/TimelineEventMapper.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.mapper + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.model.ReadReceipt +import org.matrix.android.sdk.api.session.room.sender.SenderInfo +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import javax.inject.Inject + +internal class TimelineEventMapper @Inject constructor(private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper) { + + fun map(timelineEventEntity: TimelineEventEntity, buildReadReceipts: Boolean = true, correctedReadReceipts: List? = null): TimelineEvent { + val readReceipts = if (buildReadReceipts) { + correctedReadReceipts ?: timelineEventEntity.readReceipts + ?.let { + readReceiptsSummaryMapper.map(it) + } + } else { + null + } + return TimelineEvent( + root = timelineEventEntity.root?.asDomain() + ?: Event("", timelineEventEntity.eventId), + eventId = timelineEventEntity.eventId, + annotations = timelineEventEntity.annotations?.asDomain(), + localId = timelineEventEntity.localId, + displayIndex = timelineEventEntity.displayIndex, + senderInfo = SenderInfo( + userId = timelineEventEntity.root?.sender ?: "", + displayName = timelineEventEntity.senderName, + isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName, + avatarUrl = timelineEventEntity.senderAvatar + ), + readReceipts = readReceipts + ?.distinctBy { + it.user + }?.sortedByDescending { + it.originServerTs + }.orEmpty() + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/UserMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/UserMapper.kt new file mode 100644 index 0000000000..5f5c541585 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/UserMapper.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.mapper + +import org.matrix.android.sdk.api.session.user.model.User +import org.matrix.android.sdk.internal.database.model.UserEntity + +internal object UserMapper { + + fun map(userEntity: UserEntity): User { + return User( + userEntity.userId, + userEntity.displayName, + userEntity.avatarUrl + ) + } +} + +internal fun UserEntity.asDomain(): User { + return UserMapper.map(this) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/BreadcrumbsEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/BreadcrumbsEntity.kt new file mode 100644 index 0000000000..94306fadc8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/BreadcrumbsEntity.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmList +import io.realm.RealmObject + +internal open class BreadcrumbsEntity( + var recentRoomIds: RealmList = RealmList() +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt new file mode 100644 index 0000000000..a1f7fda7cf --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.RealmResults +import io.realm.annotations.Index +import io.realm.annotations.LinkingObjects + +internal open class ChunkEntity(@Index var prevToken: String? = null, + // Because of gaps we can have several chunks with nextToken == null + @Index var nextToken: String? = null, + var stateEvents: RealmList = RealmList(), + var timelineEvents: RealmList = RealmList(), + var numberOfTimelineEvents: Long = 0, + // Only one chunk will have isLastForward == true + @Index var isLastForward: Boolean = false, + @Index var isLastBackward: Boolean = false +) : RealmObject() { + + fun identifier() = "${prevToken}_$nextToken" + + // If true, then this chunk was previously a last forward chunk + fun hasBeenALastForwardChunk() = nextToken == null && !isLastForward + + @LinkingObjects("chunks") + val room: RealmResults? = null + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/CurrentStateEventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/CurrentStateEventEntity.kt new file mode 100644 index 0000000000..bdd86cec7b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/CurrentStateEventEntity.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + * + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmObject +import io.realm.annotations.Index + +internal open class CurrentStateEventEntity(var eventId: String = "", + var root: EventEntity? = null, + @Index var roomId: String = "", + @Index var type: String = "", + @Index var stateKey: String = "" +) : RealmObject() { + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/DraftEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/DraftEntity.kt new file mode 100644 index 0000000000..7254e6241d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/DraftEntity.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmObject + +internal open class DraftEntity(var content: String = "", + var draftMode: String = MODE_REGULAR, + var linkedEventId: String = "" + +) : RealmObject() { + + companion object { + const val MODE_REGULAR = "REGULAR" + const val MODE_EDIT = "EDIT" + const val MODE_REPLY = "REPLY" + const val MODE_QUOTE = "QUOTE" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EditAggregatedSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EditAggregatedSummaryEntity.kt new file mode 100644 index 0000000000..5f98d2218a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EditAggregatedSummaryEntity.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmList +import io.realm.RealmObject + +/** + * Keep the latest state of edition of a message + */ +internal open class EditAggregatedSummaryEntity( + var aggregatedContent: String? = null, + // The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk) + var sourceEvents: RealmList = RealmList(), + var sourceLocalEchoEvents: RealmList = RealmList(), + var lastEditTs: Long = 0 +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventAnnotationsSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventAnnotationsSummaryEntity.kt new file mode 100644 index 0000000000..140058fbaf --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventAnnotationsSummaryEntity.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class EventAnnotationsSummaryEntity( + @PrimaryKey + var eventId: String = "", + var roomId: String? = null, + var reactionsSummary: RealmList = RealmList(), + var editSummary: EditAggregatedSummaryEntity? = null, + var referencesSummaryEntity: ReferencesAggregatedSummaryEntity? = null, + var pollResponseSummary: PollResponseAggregatedSummaryEntity? = null +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt new file mode 100644 index 0000000000..c76e1402ac --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.model + +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult +import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult +import org.matrix.android.sdk.internal.di.MoshiProvider +import io.realm.RealmObject +import io.realm.annotations.Index + +internal open class EventEntity(@Index var eventId: String = "", + @Index var roomId: String = "", + @Index var type: String = "", + var content: String? = null, + var prevContent: String? = null, + var isUseless: Boolean = false, + @Index var stateKey: String? = null, + var originServerTs: Long? = null, + @Index var sender: String? = null, + var age: Long? = 0, + var unsignedData: String? = null, + var redacts: String? = null, + var decryptionResultJson: String? = null, + var decryptionErrorCode: String? = null, + var decryptionErrorReason: String? = null, + var ageLocalTs: Long? = null +) : RealmObject() { + + private var sendStateStr: String = SendState.UNKNOWN.name + + var sendState: SendState + get() { + return SendState.valueOf(sendStateStr) + } + set(value) { + sendStateStr = value.name + } + + companion object + + fun setDecryptionResult(result: MXEventDecryptionResult) { + val decryptionResult = OlmDecryptionResult( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + ) + val adapter = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java) + decryptionResultJson = adapter.toJson(decryptionResult) + decryptionErrorCode = null + decryptionErrorReason = null + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventInsertEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventInsertEntity.kt new file mode 100644 index 0000000000..16ae051952 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventInsertEntity.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmObject + +/** + * This class is used to get notification on new events being inserted. It's to avoid realm getting slow when listening to insert + * in EventEntity table. + */ +internal open class EventInsertEntity(var eventId: String = "", + var eventType: String = "" +) : RealmObject() { + + private var insertTypeStr: String = EventInsertType.INCREMENTAL_SYNC.name + var insertType: EventInsertType + get() { + return EventInsertType.valueOf(insertTypeStr) + } + set(value) { + insertTypeStr = value.name + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventInsertType.java b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventInsertType.java new file mode 100644 index 0000000000..41ecad003f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventInsertType.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.model; + +public enum EventInsertType { + INITIAL_SYNC, + INCREMENTAL_SYNC, + PAGINATION, + LOCAL_ECHO +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/FilterEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/FilterEntity.kt new file mode 100644 index 0000000000..b7a2f90521 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/FilterEntity.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmObject + +/** + * Contain a map between Json filter string and filterId (from Homeserver) + * Currently there is only one object in this table + */ +internal open class FilterEntity( + // The serialized FilterBody + var filterBodyJson: String = "", + // The serialized room event filter for pagination + var roomEventFilterJson: String = "", + // the id server side of the filterBodyJson, can be used instead of filterBodyJson if not blank + var filterId: String = "" + +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/GroupEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/GroupEntity.kt new file mode 100644 index 0000000000..76ddb31678 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/GroupEntity.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.model + +import org.matrix.android.sdk.api.session.room.model.Membership +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +/** + * This class is used to store group info (groupId and membership) from the sync response. + * Then GetGroupDataTask is called regularly to fetch group information from the homeserver. + */ +internal open class GroupEntity(@PrimaryKey var groupId: String = "") + : RealmObject() { + + private var membershipStr: String = Membership.NONE.name + var membership: Membership + get() { + return Membership.valueOf(membershipStr) + } + set(value) { + membershipStr = value.name + } + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/GroupSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/GroupSummaryEntity.kt new file mode 100644 index 0000000000..00c39d4ee4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/GroupSummaryEntity.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.model + +import org.matrix.android.sdk.api.session.room.model.Membership +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class GroupSummaryEntity(@PrimaryKey var groupId: String = "", + var displayName: String = "", + var shortDescription: String = "", + var avatarUrl: String = "", + var roomIds: RealmList = RealmList(), + var userIds: RealmList = RealmList() +) : RealmObject() { + + private var membershipStr: String = Membership.NONE.name + var membership: Membership + get() { + return Membership.valueOf(membershipStr) + } + set(value) { + membershipStr = value.name + } + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt new file mode 100644 index 0000000000..3b74f053b6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.model + +import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities +import io.realm.RealmObject + +internal open class HomeServerCapabilitiesEntity( + var canChangePassword: Boolean = true, + var maxUploadFileSize: Long = HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN, + var lastVersionIdentityServerSupported: Boolean = false, + var defaultIdentityServerUrl: String? = null, + var adminE2EByDefault: Boolean = true, + var lastUpdatedTimestamp: Long = 0L +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/IgnoredUserEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/IgnoredUserEntity.kt new file mode 100644 index 0000000000..d2b7e0492d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/IgnoredUserEntity.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmObject + +internal open class IgnoredUserEntity(var userId: String = "") : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollResponseAggregatedSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollResponseAggregatedSummaryEntity.kt new file mode 100644 index 0000000000..267675ef8a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollResponseAggregatedSummaryEntity.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmList +import io.realm.RealmObject + +/** + * Keep the latest state of a poll + */ +internal open class PollResponseAggregatedSummaryEntity( + // For now we persist this a JSON for greater flexibility + // #see PollSummaryContent + var aggregatedContent: String? = null, + + // If set the poll is closed (Clients SHOULD NOT consider responses after the close event) + var closedTime: Long? = null, + // Clients SHOULD validate that the option in the relationship is a valid option, and ignore the response if invalid + var nbOptions: Int = 0, + + // The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk) + var sourceEvents: RealmList = RealmList(), + var sourceLocalEchoEvents: RealmList = RealmList() +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PushConditionEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PushConditionEntity.kt new file mode 100644 index 0000000000..2fdcaa250f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PushConditionEntity.kt @@ -0,0 +1,29 @@ +/* + * copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmObject + +internal open class PushConditionEntity( + var kind: String = "", + var key: String? = null, + var pattern: String? = null, + var iz: String? = null +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PushRuleEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PushRuleEntity.kt new file mode 100644 index 0000000000..118d394e06 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PushRuleEntity.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.RealmResults +import io.realm.annotations.LinkingObjects + +internal open class PushRuleEntity( + // Required. The actions to perform when this rule is matched. + var actionsStr: String? = null, + // Required. Whether this is a default rule, or has been set explicitly. + var default: Boolean = false, + // Required. Whether the push rule is enabled or not. + var enabled: Boolean = true, + // Required. The ID of this rule. + var ruleId: String = "", + // The conditions that must hold true for an event in order for a rule to be applied to an event + var conditions: RealmList? = RealmList(), + // The glob-style pattern to match against. Only applicable to content rules. + var pattern: String? = null +) : RealmObject() { + + @LinkingObjects("pushRules") + val parent: RealmResults? = null + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PushRulesEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PushRulesEntity.kt new file mode 100644 index 0000000000..e4a7ef5e0f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PushRulesEntity.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.database.model + +import org.matrix.android.sdk.api.pushrules.RuleKind +import io.realm.RealmList +import io.realm.RealmObject + +internal open class PushRulesEntity( + var scope: String = "", + var pushRules: RealmList = RealmList() +) : RealmObject() { + + private var kindStr: String = RuleKind.CONTENT.name + var kind: RuleKind + get() { + return RuleKind.valueOf(kindStr) + } + set(value) { + kindStr = value.name + } + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PusherDataEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PusherDataEntity.kt new file mode 100644 index 0000000000..9fff183b96 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PusherDataEntity.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmObject + +internal open class PusherDataEntity( + var url: String? = null, + var format: String? = null +) : RealmObject() { + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PusherEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PusherEntity.kt new file mode 100644 index 0000000000..7b299d4f33 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PusherEntity.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.database.model + +import org.matrix.android.sdk.api.session.pushers.PusherState +import io.realm.RealmObject + +// TODO +// at java.lang.Thread.run(Thread.java:764) +// Caused by: java.lang.IllegalArgumentException: 'value' is not a valid managed object. +// at io.realm.ProxyState.checkValidObject(ProxyState.java:213) +// at io.realm.im_vector_matrix_android_internal_database_model_PusherEntityRealmProxy +// .realmSet$data(im_vector_matrix_android_internal_database_model_PusherEntityRealmProxy.java:413) +// at org.matrix.android.sdk.internal.database.model.PusherEntity.setData(PusherEntity.kt:16) +// at org.matrix.android.sdk.internal.session.pushers.AddHttpPusherWorker$doWork$$inlined$fold$lambda$2.execute(AddHttpPusherWorker.kt:70) +// at io.realm.Realm.executeTransaction(Realm.java:1493) +internal open class PusherEntity( + var pushKey: String = "", + var kind: String? = null, + var appId: String = "", + var appDisplayName: String? = null, + var deviceDisplayName: String? = null, + var profileTag: String? = null, + var lang: String? = null, + var data: PusherDataEntity? = null +) : RealmObject() { + private var stateStr: String = PusherState.UNREGISTERED.name + + var state: PusherState + get() { + try { + return PusherState.valueOf(stateStr) + } catch (e: Exception) { + // can this happen? + return PusherState.UNREGISTERED + } + } + set(value) { + stateStr = value.name + } + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReactionAggregatedSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReactionAggregatedSummaryEntity.kt new file mode 100644 index 0000000000..7da933c6e4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReactionAggregatedSummaryEntity.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmList +import io.realm.RealmObject + +/** + * Aggregated Summary of a reaction. + */ +internal open class ReactionAggregatedSummaryEntity( + // The reaction String 😀 + var key: String = "", + // Number of time this reaction was selected + var count: Int = 0, + // Did the current user sent this reaction + var addedByMe: Boolean = false, + // The first time this reaction was added (for ordering purpose) + var firstTimestamp: Long = 0, + // The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk) + var sourceEvents: RealmList = RealmList(), + // List of transaction ids for local echos + var sourceLocalEcho: RealmList = RealmList() +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReadMarkerEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReadMarkerEntity.kt new file mode 100644 index 0000000000..739c0b9e88 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReadMarkerEntity.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class ReadMarkerEntity( + @PrimaryKey + var roomId: String = "", + var eventId: String = "" +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReadReceiptEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReadReceiptEntity.kt new file mode 100644 index 0000000000..f1e3bc4e65 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReadReceiptEntity.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmObject +import io.realm.RealmResults +import io.realm.annotations.LinkingObjects +import io.realm.annotations.PrimaryKey + +internal open class ReadReceiptEntity(@PrimaryKey var primaryKey: String = "", + var eventId: String = "", + var roomId: String = "", + var userId: String = "", + var originServerTs: Double = 0.0 +) : RealmObject() { + companion object + + @LinkingObjects("readReceipts") + val summary: RealmResults? = null +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReadReceiptsSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReadReceiptsSummaryEntity.kt new file mode 100644 index 0000000000..8445abdb4c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReadReceiptsSummaryEntity.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.RealmResults +import io.realm.annotations.LinkingObjects +import io.realm.annotations.PrimaryKey + +internal open class ReadReceiptsSummaryEntity( + @PrimaryKey + var eventId: String = "", + var roomId: String = "", + var readReceipts: RealmList = RealmList() +) : RealmObject() { + + @LinkingObjects("readReceipts") + val timelineEvent: RealmResults? = null + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReferencesAggregatedSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReferencesAggregatedSummaryEntity.kt new file mode 100644 index 0000000000..327648abbc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReferencesAggregatedSummaryEntity.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmList +import io.realm.RealmObject + +internal open class ReferencesAggregatedSummaryEntity( + var eventId: String = "", + var content: String? = null, + // The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk) + var sourceEvents: RealmList = RealmList(), + // List of transaction ids for local echos + var sourceLocalEcho: RealmList = RealmList() +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt new file mode 100644 index 0000000000..ae1e7865d2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.model + +import org.matrix.android.sdk.api.session.room.model.Membership +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class RoomEntity(@PrimaryKey var roomId: String = "", + var chunks: RealmList = RealmList(), + var sendingTimelineEvents: RealmList = RealmList(), + var areAllMembersLoaded: Boolean = false +) : RealmObject() { + + private var membershipStr: String = Membership.NONE.name + var membership: Membership + get() { + return Membership.valueOf(membershipStr) + } + set(value) { + membershipStr = value.name + } + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomMemberSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomMemberSummaryEntity.kt new file mode 100644 index 0000000000..f2ea5a5f16 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomMemberSummaryEntity.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.model + +import org.matrix.android.sdk.api.session.room.model.Membership +import io.realm.RealmObject +import io.realm.annotations.Index +import io.realm.annotations.PrimaryKey + +internal open class RoomMemberSummaryEntity(@PrimaryKey var primaryKey: String = "", + @Index var userId: String = "", + @Index var roomId: String = "", + @Index var displayName: String? = null, + var avatarUrl: String? = null, + var reason: String? = null, + var isDirect: Boolean = false +) : RealmObject() { + + private var membershipStr: String = Membership.NONE.name + var membership: Membership + get() { + return Membership.valueOf(membershipStr) + } + set(value) { + membershipStr = value.name + } + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt new file mode 100644 index 0000000000..d6859f1d3f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt @@ -0,0 +1,92 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.model + +import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.VersioningState +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class RoomSummaryEntity( + @PrimaryKey var roomId: String = "", + var displayName: String? = "", + var avatarUrl: String? = "", + var name: String? = "", + var topic: String? = "", + var latestPreviewableEvent: TimelineEventEntity? = null, + var heroes: RealmList = RealmList(), + var joinedMembersCount: Int? = 0, + var invitedMembersCount: Int? = 0, + var isDirect: Boolean = false, + var directUserId: String? = null, + var otherMemberIds: RealmList = RealmList(), + var notificationCount: Int = 0, + var highlightCount: Int = 0, + var readMarkerId: String? = null, + var hasUnreadMessages: Boolean = false, + var tags: RealmList = RealmList(), + var userDrafts: UserDraftsEntity? = null, + var breadcrumbsIndex: Int = RoomSummary.NOT_IN_BREADCRUMBS, + var canonicalAlias: String? = null, + var aliases: RealmList = RealmList(), + // this is required for querying + var flatAliases: String = "", + var isEncrypted: Boolean = false, + var encryptionEventTs: Long? = 0, + var roomEncryptionTrustLevelStr: String? = null, + var inviterId: String? = null, + var hasFailedSending: Boolean = false +) : RealmObject() { + + private var membershipStr: String = Membership.NONE.name + var membership: Membership + get() { + return Membership.valueOf(membershipStr) + } + set(value) { + membershipStr = value.name + } + + private var versioningStateStr: String = VersioningState.NONE.name + var versioningState: VersioningState + get() { + return VersioningState.valueOf(versioningStateStr) + } + set(value) { + versioningStateStr = value.name + } + + var roomEncryptionTrustLevel: RoomEncryptionTrustLevel? + get() { + return roomEncryptionTrustLevelStr?.let { + try { + RoomEncryptionTrustLevel.valueOf(it) + } catch (failure: Throwable) { + null + } + } + } + set(value) { + roomEncryptionTrustLevelStr = value?.name + } + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomTagEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomTagEntity.kt new file mode 100644 index 0000000000..8fdae3205d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomTagEntity.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmObject + +internal open class RoomTagEntity( + var tagName: String = "", + var tagOrder: Double? = null +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ScalarTokenEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ScalarTokenEntity.kt new file mode 100644 index 0000000000..a8fc454720 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ScalarTokenEntity.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class ScalarTokenEntity( + @PrimaryKey var serverUrl: String = "", + var token: String = "" +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt new file mode 100644 index 0000000000..ea466db352 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.annotations.RealmModule + +/** + * Realm module for Session + */ +@RealmModule(library = true, + classes = [ + ChunkEntity::class, + EventEntity::class, + EventInsertEntity::class, + TimelineEventEntity::class, + FilterEntity::class, + GroupEntity::class, + GroupSummaryEntity::class, + ReadReceiptEntity::class, + RoomEntity::class, + RoomSummaryEntity::class, + RoomTagEntity::class, + SyncEntity::class, + UserEntity::class, + IgnoredUserEntity::class, + BreadcrumbsEntity::class, + UserThreePidEntity::class, + EventAnnotationsSummaryEntity::class, + ReactionAggregatedSummaryEntity::class, + EditAggregatedSummaryEntity::class, + PollResponseAggregatedSummaryEntity::class, + ReferencesAggregatedSummaryEntity::class, + PushRulesEntity::class, + PushRuleEntity::class, + PushConditionEntity::class, + PusherEntity::class, + PusherDataEntity::class, + ReadReceiptsSummaryEntity::class, + ReadMarkerEntity::class, + UserDraftsEntity::class, + DraftEntity::class, + HomeServerCapabilitiesEntity::class, + RoomMemberSummaryEntity::class, + CurrentStateEventEntity::class, + UserAccountDataEntity::class, + ScalarTokenEntity::class, + WellknownIntegrationManagerConfigEntity::class + ]) +internal class SessionRealmModule diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SyncEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SyncEntity.kt new file mode 100644 index 0000000000..5a1bcbc8d0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SyncEntity.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class SyncEntity(var nextBatch: String? = null, + @PrimaryKey var id: Long = 0 +) : RealmObject() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt new file mode 100644 index 0000000000..36f6041fe6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmObject +import io.realm.RealmResults +import io.realm.annotations.Index +import io.realm.annotations.LinkingObjects + +internal open class TimelineEventEntity(var localId: Long = 0, + @Index var eventId: String = "", + @Index var roomId: String = "", + @Index var displayIndex: Int = 0, + var root: EventEntity? = null, + var annotations: EventAnnotationsSummaryEntity? = null, + var senderName: String? = null, + var isUniqueDisplayName: Boolean = false, + var senderAvatar: String? = null, + var senderMembershipEventId: String? = null, + var readReceipts: ReadReceiptsSummaryEntity? = null +) : RealmObject() { + + @LinkingObjects("timelineEvents") + val chunk: RealmResults? = null + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/UserAccountDataEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/UserAccountDataEntity.kt new file mode 100644 index 0000000000..75aacd8dda --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/UserAccountDataEntity.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmObject +import io.realm.annotations.Index + +/** + * Clients can store custom config data for their account on their HomeServer. + * This account data will be synced between different devices and can persist across installations on a particular device. + * Users may only view the account data for their own account. + * The account_data may be either global or scoped to a particular rooms. + */ +internal open class UserAccountDataEntity( + @Index var type: String? = null, + var contentStr: String? = null +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/UserDraftsEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/UserDraftsEntity.kt new file mode 100644 index 0000000000..f84a7b930f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/UserDraftsEntity.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.RealmResults +import io.realm.annotations.LinkingObjects + +/** + * Create a specific table to be able to do direct query on it and keep the draft ordered + */ +internal open class UserDraftsEntity(var userDrafts: RealmList = RealmList() +) : RealmObject() { + + // Link to RoomSummaryEntity + @LinkingObjects("userDrafts") + val roomSummaryEntity: RealmResults? = null + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/UserEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/UserEntity.kt new file mode 100644 index 0000000000..e2150103d9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/UserEntity.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class UserEntity(@PrimaryKey var userId: String = "", + var displayName: String = "", + var avatarUrl: String = "" +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/UserThreePidEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/UserThreePidEntity.kt new file mode 100644 index 0000000000..c7337f6a42 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/UserThreePidEntity.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmObject + +internal open class UserThreePidEntity( + var medium: String = "", + var address: String = "", + var validatedAt: Long = 0, + var addedAt: Long = 0 +) : RealmObject() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/WellknownIntegrationManagerConfigEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/WellknownIntegrationManagerConfigEntity.kt new file mode 100644 index 0000000000..fdabed3c23 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/WellknownIntegrationManagerConfigEntity.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class WellknownIntegrationManagerConfigEntity( + @PrimaryKey var id: Long = 0, + var apiUrl: String = "", + var uiUrl: String = "" +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/BreadcrumbsEntityQuery.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/BreadcrumbsEntityQuery.kt new file mode 100644 index 0000000000..e711e30188 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/BreadcrumbsEntityQuery.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.internal.database.model.BreadcrumbsEntity +import io.realm.Realm +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +internal fun BreadcrumbsEntity.Companion.get(realm: Realm): BreadcrumbsEntity? { + return realm.where().findFirst() +} + +internal fun BreadcrumbsEntity.Companion.getOrCreate(realm: Realm): BreadcrumbsEntity { + return get(realm) ?: realm.createObject() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt new file mode 100644 index 0000000000..79b611115c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.internal.database.model.ChunkEntity +import org.matrix.android.sdk.internal.database.model.ChunkEntityFields +import org.matrix.android.sdk.internal.database.model.RoomEntityFields +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.RealmResults +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +internal fun ChunkEntity.Companion.where(realm: Realm, roomId: String): RealmQuery { + return realm.where() + .equalTo("${ChunkEntityFields.ROOM}.${RoomEntityFields.ROOM_ID}", roomId) +} + +internal fun ChunkEntity.Companion.find(realm: Realm, roomId: String, prevToken: String? = null, nextToken: String? = null): ChunkEntity? { + val query = where(realm, roomId) + if (prevToken != null) { + query.equalTo(ChunkEntityFields.PREV_TOKEN, prevToken) + } + if (nextToken != null) { + query.equalTo(ChunkEntityFields.NEXT_TOKEN, nextToken) + } + return query.findFirst() +} + +internal fun ChunkEntity.Companion.findLastForwardChunkOfRoom(realm: Realm, roomId: String): ChunkEntity? { + return where(realm, roomId) + .equalTo(ChunkEntityFields.IS_LAST_FORWARD, true) + .findFirst() +} + +internal fun ChunkEntity.Companion.findAllIncludingEvents(realm: Realm, eventIds: List): RealmResults { + return realm.where() + .`in`(ChunkEntityFields.TIMELINE_EVENTS.EVENT_ID, eventIds.toTypedArray()) + .findAll() +} + +internal fun ChunkEntity.Companion.findIncludingEvent(realm: Realm, eventId: String): ChunkEntity? { + return findAllIncludingEvents(realm, listOf(eventId)).firstOrNull() +} + +internal fun ChunkEntity.Companion.create( + realm: Realm, + prevToken: String?, + nextToken: String? +): ChunkEntity { + return realm.createObject().apply { + this.prevToken = prevToken + this.nextToken = nextToken + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/CurrentStateEventEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/CurrentStateEventEntityQueries.kt new file mode 100644 index 0000000000..ac00f791b8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/CurrentStateEventEntityQueries.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + * + */ + +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity +import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.createObject + +internal fun CurrentStateEventEntity.Companion.whereType(realm: Realm, roomId: String, type: String): RealmQuery { + return realm.where(CurrentStateEventEntity::class.java) + .equalTo(CurrentStateEventEntityFields.ROOM_ID, roomId) + .equalTo(CurrentStateEventEntityFields.TYPE, type) +} + +internal fun CurrentStateEventEntity.Companion.whereStateKey(realm: Realm, roomId: String, type: String, stateKey: String) + : RealmQuery { + return whereType(realm = realm, roomId = roomId, type = type) + .equalTo(CurrentStateEventEntityFields.STATE_KEY, stateKey) +} + +internal fun CurrentStateEventEntity.Companion.getOrNull(realm: Realm, roomId: String, stateKey: String, type: String): CurrentStateEventEntity? { + return whereStateKey(realm = realm, roomId = roomId, type = type, stateKey = stateKey).findFirst() +} + +internal fun CurrentStateEventEntity.Companion.getOrCreate(realm: Realm, roomId: String, stateKey: String, type: String): CurrentStateEventEntity { + return getOrNull(realm = realm, roomId = roomId, stateKey = stateKey, type = type) ?: create(realm, roomId, stateKey, type) +} + +private fun create(realm: Realm, roomId: String, stateKey: String, type: String): CurrentStateEventEntity { + return realm.createObject().apply { + this.type = type + this.roomId = roomId + this.stateKey = stateKey + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt new file mode 100644 index 0000000000..9fa710a94b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity +import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntityFields +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.where + +internal fun EventAnnotationsSummaryEntity.Companion.where(realm: Realm, eventId: String): RealmQuery { + val query = realm.where() + query.equalTo(EventAnnotationsSummaryEntityFields.EVENT_ID, eventId) + return query +} + +internal fun EventAnnotationsSummaryEntity.Companion.whereInRoom(realm: Realm, roomId: String?): RealmQuery { + val query = realm.where() + if (roomId != null) { + query.equalTo(EventAnnotationsSummaryEntityFields.ROOM_ID, roomId) + } + return query +} + +internal fun EventAnnotationsSummaryEntity.Companion.create(realm: Realm, roomId: String, eventId: String): EventAnnotationsSummaryEntity { + val obj = realm.createObject(EventAnnotationsSummaryEntity::class.java, eventId).apply { + this.roomId = roomId + } + // Denormalization + TimelineEventEntity.where(realm, roomId = roomId, eventId = eventId).findFirst()?.let { + it.annotations = obj + } + return obj +} +internal fun EventAnnotationsSummaryEntity.Companion.getOrCreate(realm: Realm, roomId: String, eventId: String): EventAnnotationsSummaryEntity { + return EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() + ?: EventAnnotationsSummaryEntity.create(realm, roomId, eventId).apply { this.roomId = roomId } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt new file mode 100644 index 0000000000..ee41729e2a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.EventEntityFields +import org.matrix.android.sdk.internal.database.model.EventInsertEntity +import org.matrix.android.sdk.internal.database.model.EventInsertType +import io.realm.Realm +import io.realm.RealmList +import io.realm.RealmQuery +import io.realm.kotlin.where + +internal fun EventEntity.copyToRealmOrIgnore(realm: Realm, insertType: EventInsertType): EventEntity { + val eventEntity = realm.where() + .equalTo(EventEntityFields.EVENT_ID, eventId) + .equalTo(EventEntityFields.ROOM_ID, roomId) + .findFirst() + return if (eventEntity == null) { + val insertEntity = EventInsertEntity(eventId = eventId, eventType = type).apply { + this.insertType = insertType + } + realm.insert(insertEntity) + // copy this event entity and return it + realm.copyToRealm(this) + } else { + eventEntity + } +} + +internal fun EventEntity.Companion.where(realm: Realm, eventId: String): RealmQuery { + return realm.where() + .equalTo(EventEntityFields.EVENT_ID, eventId) +} + +internal fun EventEntity.Companion.where(realm: Realm, eventIds: List): RealmQuery { + return realm.where() + .`in`(EventEntityFields.EVENT_ID, eventIds.toTypedArray()) +} + +internal fun EventEntity.Companion.whereType(realm: Realm, + type: String, + roomId: String? = null +): RealmQuery { + val query = realm.where() + if (roomId != null) { + query.equalTo(EventEntityFields.ROOM_ID, roomId) + } + return query.equalTo(EventEntityFields.TYPE, type) +} + +internal fun EventEntity.Companion.whereTypes(realm: Realm, + typeList: List = emptyList(), + roomId: String? = null): RealmQuery { + val query = realm.where() + query.`in`(EventEntityFields.TYPE, typeList.toTypedArray()) + if (roomId != null) { + query.equalTo(EventEntityFields.ROOM_ID, roomId) + } + return query +} + +internal fun RealmList.find(eventId: String): EventEntity? { + return this.where() + .equalTo(EventEntityFields.EVENT_ID, eventId) + .findFirst() +} + +internal fun RealmList.fastContains(eventId: String): Boolean { + return this.find(eventId) != null +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/FilterEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/FilterEntityQueries.kt new file mode 100644 index 0000000000..33a7bff606 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/FilterEntityQueries.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.internal.database.model.FilterEntity +import org.matrix.android.sdk.internal.session.filter.FilterFactory +import io.realm.Realm +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +/** + * Get the current filter + */ +internal fun FilterEntity.Companion.get(realm: Realm): FilterEntity? { + return realm.where().findFirst() +} + +/** + * Get the current filter, create one if it does not exist + */ +internal fun FilterEntity.Companion.getOrCreate(realm: Realm): FilterEntity { + return get(realm) ?: realm.createObject() + .apply { + filterBodyJson = FilterFactory.createDefaultFilter().toJSONString() + roomEventFilterJson = FilterFactory.createDefaultRoomFilter().toJSONString() + filterId = "" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/GroupEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/GroupEntityQueries.kt new file mode 100644 index 0000000000..1097cce463 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/GroupEntityQueries.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.internal.database.model.GroupEntity +import org.matrix.android.sdk.internal.database.model.GroupEntityFields +import org.matrix.android.sdk.internal.query.process +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.where + +internal fun GroupEntity.Companion.where(realm: Realm, groupId: String): RealmQuery { + return realm.where() + .equalTo(GroupEntityFields.GROUP_ID, groupId) +} + +internal fun GroupEntity.Companion.where(realm: Realm, memberships: List): RealmQuery { + return realm.where().process(GroupEntityFields.MEMBERSHIP_STR, memberships) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/GroupSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/GroupSummaryEntityQueries.kt new file mode 100644 index 0000000000..650558ee2f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/GroupSummaryEntityQueries.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.internal.database.model.GroupSummaryEntity +import org.matrix.android.sdk.internal.database.model.GroupSummaryEntityFields +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +internal fun GroupSummaryEntity.Companion.where(realm: Realm, groupId: String? = null): RealmQuery { + val query = realm.where() + if (groupId != null) { + query.equalTo(GroupSummaryEntityFields.GROUP_ID, groupId) + } + return query +} + +internal fun GroupSummaryEntity.Companion.where(realm: Realm, groupIds: List): RealmQuery { + return realm.where() + .`in`(GroupSummaryEntityFields.GROUP_ID, groupIds.toTypedArray()) +} + +internal fun GroupSummaryEntity.Companion.getOrCreate(realm: Realm, groupId: String): GroupSummaryEntity { + return where(realm, groupId).findFirst() ?: realm.createObject(groupId) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/HomeServerCapabilitiesQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/HomeServerCapabilitiesQueries.kt new file mode 100644 index 0000000000..1ebe276fbe --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/HomeServerCapabilitiesQueries.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntity +import io.realm.Realm +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +/** + * Get the current HomeServerCapabilitiesEntity, return null if it does not exist + */ +internal fun HomeServerCapabilitiesEntity.Companion.get(realm: Realm): HomeServerCapabilitiesEntity? { + return realm.where().findFirst() +} + +/** + * Get the current HomeServerCapabilitiesEntity, create one if it does not exist + */ +internal fun HomeServerCapabilitiesEntity.Companion.getOrCreate(realm: Realm): HomeServerCapabilitiesEntity { + return get(realm) ?: realm.createObject() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/PushersQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/PushersQueries.kt new file mode 100644 index 0000000000..cf34bc0cd1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/PushersQueries.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.api.pushrules.RuleKind +import org.matrix.android.sdk.internal.database.model.PushRuleEntity +import org.matrix.android.sdk.internal.database.model.PushRuleEntityFields +import org.matrix.android.sdk.internal.database.model.PushRulesEntity +import org.matrix.android.sdk.internal.database.model.PushRulesEntityFields +import org.matrix.android.sdk.internal.database.model.PusherEntity +import org.matrix.android.sdk.internal.database.model.PusherEntityFields +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.where + +internal fun PusherEntity.Companion.where(realm: Realm, + pushKey: String? = null): RealmQuery { + return realm.where() + .apply { + if (pushKey != null) { + equalTo(PusherEntityFields.PUSH_KEY, pushKey) + } + } +} + +internal fun PushRulesEntity.Companion.where(realm: Realm, + scope: String, + kind: RuleKind): RealmQuery { + return realm.where() + .equalTo(PushRulesEntityFields.SCOPE, scope) + .equalTo(PushRulesEntityFields.KIND_STR, kind.name) +} + +internal fun PushRuleEntity.Companion.where(realm: Realm, + scope: String, + ruleId: String): RealmQuery { + return realm.where() + .equalTo("${PushRuleEntityFields.PARENT}.${PushRulesEntityFields.SCOPE}", scope) + .equalTo(PushRuleEntityFields.RULE_ID, ruleId) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadMarkerEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadMarkerEntityQueries.kt new file mode 100644 index 0000000000..636fc9ac73 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadMarkerEntityQueries.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.internal.database.model.ReadMarkerEntity +import org.matrix.android.sdk.internal.database.model.ReadMarkerEntityFields +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +internal fun ReadMarkerEntity.Companion.where(realm: Realm, roomId: String): RealmQuery { + return realm.where() + .equalTo(ReadMarkerEntityFields.ROOM_ID, roomId) +} + +internal fun ReadMarkerEntity.Companion.getOrCreate(realm: Realm, roomId: String): ReadMarkerEntity { + return where(realm, roomId).findFirst() ?: realm.createObject(roomId) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt new file mode 100644 index 0000000000..8ccc12a514 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.api.session.events.model.LocalEcho +import org.matrix.android.sdk.internal.database.model.ChunkEntity +import org.matrix.android.sdk.internal.database.model.ReadMarkerEntity +import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import io.realm.Realm +import io.realm.RealmConfiguration + +internal fun isEventRead(realmConfiguration: RealmConfiguration, + userId: String?, + roomId: String?, + eventId: String?): Boolean { + if (userId.isNullOrBlank() || roomId.isNullOrBlank() || eventId.isNullOrBlank()) { + return false + } + if (LocalEcho.isLocalEchoId(eventId)) { + return true + } + var isEventRead = false + + Realm.getInstance(realmConfiguration).use { realm -> + val liveChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId) ?: return@use + val eventToCheck = liveChunk.timelineEvents.find(eventId) + isEventRead = if (eventToCheck == null || eventToCheck.root?.sender == userId) { + true + } else { + val readReceipt = ReadReceiptEntity.where(realm, roomId, userId).findFirst() + ?: return@use + val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.displayIndex + ?: Int.MIN_VALUE + val eventToCheckIndex = eventToCheck.displayIndex + + eventToCheckIndex <= readReceiptIndex + } + } + + return isEventRead +} + +internal fun isReadMarkerMoreRecent(realmConfiguration: RealmConfiguration, + roomId: String?, + eventId: String?): Boolean { + if (roomId.isNullOrBlank() || eventId.isNullOrBlank()) { + return false + } + return Realm.getInstance(realmConfiguration).use { realm -> + val eventToCheck = TimelineEventEntity.where(realm, roomId = roomId, eventId = eventId).findFirst() + val eventToCheckChunk = eventToCheck?.chunk?.firstOrNull() + val readMarker = ReadMarkerEntity.where(realm, roomId).findFirst() ?: return false + val readMarkerEvent = TimelineEventEntity.where(realm, roomId = roomId, eventId = readMarker.eventId).findFirst() + val readMarkerChunk = readMarkerEvent?.chunk?.firstOrNull() + if (eventToCheckChunk == readMarkerChunk) { + val readMarkerIndex = readMarkerEvent?.displayIndex ?: Int.MIN_VALUE + val eventToCheckIndex = eventToCheck?.displayIndex ?: Int.MAX_VALUE + eventToCheckIndex <= readMarkerIndex + } else { + eventToCheckChunk?.isLastForward == false + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt new file mode 100644 index 0000000000..1eb438190a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity +import org.matrix.android.sdk.internal.database.model.ReadReceiptEntityFields +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +internal fun ReadReceiptEntity.Companion.where(realm: Realm, roomId: String, userId: String): RealmQuery { + return realm.where() + .equalTo(ReadReceiptEntityFields.PRIMARY_KEY, buildPrimaryKey(roomId, userId)) +} + +internal fun ReadReceiptEntity.Companion.whereUserId(realm: Realm, userId: String): RealmQuery { + return realm.where() + .equalTo(ReadReceiptEntityFields.USER_ID, userId) +} + +internal fun ReadReceiptEntity.Companion.createUnmanaged(roomId: String, eventId: String, userId: String, originServerTs: Double): ReadReceiptEntity { + return ReadReceiptEntity().apply { + this.primaryKey = "${roomId}_$userId" + this.eventId = eventId + this.roomId = roomId + this.userId = userId + this.originServerTs = originServerTs + } +} + +internal fun ReadReceiptEntity.Companion.getOrCreate(realm: Realm, roomId: String, userId: String): ReadReceiptEntity { + return ReadReceiptEntity.where(realm, roomId, userId).findFirst() + ?: realm.createObject(buildPrimaryKey(roomId, userId)) + .apply { + this.roomId = roomId + this.userId = userId + } +} + +private fun buildPrimaryKey(roomId: String, userId: String) = "${roomId}_$userId" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptsSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptsSummaryEntityQueries.kt new file mode 100644 index 0000000000..1d384a1de6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptsSummaryEntityQueries.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntity +import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntityFields +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.where + +internal fun ReadReceiptsSummaryEntity.Companion.where(realm: Realm, eventId: String): RealmQuery { + return realm.where() + .equalTo(ReadReceiptsSummaryEntityFields.EVENT_ID, eventId) +} + +internal fun ReadReceiptsSummaryEntity.Companion.whereInRoom(realm: Realm, roomId: String): RealmQuery { + return realm.where() + .equalTo(ReadReceiptsSummaryEntityFields.ROOM_ID, roomId) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReferencesAggregatedSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReferencesAggregatedSummaryEntityQueries.kt new file mode 100644 index 0000000000..60f665d460 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReferencesAggregatedSummaryEntityQueries.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.internal.database.model.ReferencesAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.model.ReferencesAggregatedSummaryEntityFields +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.where + +internal fun ReferencesAggregatedSummaryEntity.Companion.where(realm: Realm, eventId: String): RealmQuery { + val query = realm.where() + query.equalTo(ReferencesAggregatedSummaryEntityFields.EVENT_ID, eventId) + return query +} + +internal fun ReferencesAggregatedSummaryEntity.Companion.create(realm: Realm, txID: String): ReferencesAggregatedSummaryEntity { + return realm.createObject(ReferencesAggregatedSummaryEntity::class.java).apply { + this.eventId = txID + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomEntityQueries.kt new file mode 100644 index 0000000000..35d21f8f5f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomEntityQueries.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.RoomEntity +import org.matrix.android.sdk.internal.database.model.RoomEntityFields +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.where + +internal fun RoomEntity.Companion.where(realm: Realm, roomId: String): RealmQuery { + return realm.where() + .equalTo(RoomEntityFields.ROOM_ID, roomId) +} + +internal fun RoomEntity.Companion.where(realm: Realm, membership: Membership? = null): RealmQuery { + val query = realm.where() + if (membership != null) { + query.equalTo(RoomEntityFields.MEMBERSHIP_STR, membership.name) + } + return query +} + +internal fun RoomEntity.fastContains(eventId: String): Boolean { + return EventEntity.where(realm, eventId = eventId).findFirst() != null +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomMemberEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomMemberEntityQueries.kt new file mode 100644 index 0000000000..ae1d42772d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomMemberEntityQueries.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.where + +internal fun RoomMemberSummaryEntity.Companion.where(realm: Realm, roomId: String, userId: String? = null): RealmQuery { + val query = realm + .where() + .equalTo(RoomMemberSummaryEntityFields.ROOM_ID, roomId) + + if (userId != null) { + query.equalTo(RoomMemberSummaryEntityFields.USER_ID, userId) + } + return query +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomSummaryEntityQueries.kt new file mode 100644 index 0000000000..7eee63c7d5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomSummaryEntityQueries.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.RealmResults +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +internal fun RoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = null): RealmQuery { + val query = realm.where() + if (roomId != null) { + query.equalTo(RoomSummaryEntityFields.ROOM_ID, roomId) + } + return query +} + +internal fun RoomSummaryEntity.Companion.findByAlias(realm: Realm, roomAlias: String): RoomSummaryEntity? { + val roomSummary = realm.where() + .equalTo(RoomSummaryEntityFields.CANONICAL_ALIAS, roomAlias) + .findFirst() + if (roomSummary != null) { + return roomSummary + } + return realm.where() + .contains(RoomSummaryEntityFields.FLAT_ALIASES, "|$roomAlias") + .findFirst() +} + +internal fun RoomSummaryEntity.Companion.getOrCreate(realm: Realm, roomId: String): RoomSummaryEntity { + return where(realm, roomId).findFirst() ?: realm.createObject(roomId) +} + +internal fun RoomSummaryEntity.Companion.getDirectRooms(realm: Realm): RealmResults { + return RoomSummaryEntity.where(realm) + .equalTo(RoomSummaryEntityFields.IS_DIRECT, true) + .findAll() +} + +internal fun RoomSummaryEntity.Companion.isDirect(realm: Realm, roomId: String): Boolean { + return RoomSummaryEntity.where(realm) + .equalTo(RoomSummaryEntityFields.ROOM_ID, roomId) + .equalTo(RoomSummaryEntityFields.IS_DIRECT, true) + .findAll() + .isNotEmpty() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ScalarTokenQuery.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ScalarTokenQuery.kt new file mode 100644 index 0000000000..24387856b6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ScalarTokenQuery.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.internal.database.model.ScalarTokenEntity +import org.matrix.android.sdk.internal.database.model.ScalarTokenEntityFields +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.where + +internal fun ScalarTokenEntity.Companion.where(realm: Realm, serverUrl: String): RealmQuery { + return realm + .where() + .equalTo(ScalarTokenEntityFields.SERVER_URL, serverUrl) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt new file mode 100644 index 0000000000..83075a192c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.database.model.ChunkEntity +import org.matrix.android.sdk.internal.database.model.RoomEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields +import io.realm.Realm +import io.realm.RealmList +import io.realm.RealmQuery +import io.realm.RealmResults +import io.realm.Sort +import io.realm.kotlin.where + +internal fun TimelineEventEntity.Companion.where(realm: Realm, roomId: String, eventId: String): RealmQuery { + return realm.where() + .equalTo(TimelineEventEntityFields.ROOM_ID, roomId) + .equalTo(TimelineEventEntityFields.EVENT_ID, eventId) +} + +internal fun TimelineEventEntity.Companion.where(realm: Realm, roomId: String, eventIds: List): RealmQuery { + return realm.where() + .equalTo(TimelineEventEntityFields.ROOM_ID, roomId) + .`in`(TimelineEventEntityFields.EVENT_ID, eventIds.toTypedArray()) +} + +internal fun TimelineEventEntity.Companion.whereRoomId(realm: Realm, + roomId: String): RealmQuery { + return realm.where() + .equalTo(TimelineEventEntityFields.ROOM_ID, roomId) +} + +internal fun TimelineEventEntity.Companion.findWithSenderMembershipEvent(realm: Realm, senderMembershipEventId: String): List { + return realm.where() + .equalTo(TimelineEventEntityFields.SENDER_MEMBERSHIP_EVENT_ID, senderMembershipEventId) + .findAll() +} + +internal fun TimelineEventEntity.Companion.latestEvent(realm: Realm, + roomId: String, + includesSending: Boolean, + filterContentRelation: Boolean = false, + filterTypes: List = emptyList()): TimelineEventEntity? { + val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: return null + val sendingTimelineEvents = roomEntity.sendingTimelineEvents.where().filterTypes(filterTypes) + val liveEvents = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId)?.timelineEvents?.where()?.filterTypes(filterTypes) + if (filterContentRelation) { + liveEvents + ?.not()?.like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.EDIT) + ?.not()?.like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.RESPONSE) + } + val query = if (includesSending && sendingTimelineEvents.findAll().isNotEmpty()) { + sendingTimelineEvents + } else { + liveEvents + } + return query + ?.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) + ?.findFirst() +} + +internal fun RealmQuery.filterTypes(filterTypes: List): RealmQuery { + return if (filterTypes.isEmpty()) { + this + } else { + this.`in`(TimelineEventEntityFields.ROOT.TYPE, filterTypes.toTypedArray()) + } +} + +internal fun RealmList.find(eventId: String): TimelineEventEntity? { + return this.where() + .equalTo(TimelineEventEntityFields.EVENT_ID, eventId) + .findFirst() +} + +internal fun TimelineEventEntity.Companion.findAllInRoomWithSendStates(realm: Realm, + roomId: String, + sendStates: List) + : RealmResults { + return whereRoomId(realm, roomId) + .filterSendStates(sendStates) + .findAll() +} + +internal fun RealmQuery.filterSendStates(sendStates: List): RealmQuery { + val sendStatesStr = sendStates.map { it.name }.toTypedArray() + return `in`(TimelineEventEntityFields.ROOT.SEND_STATE_STR, sendStatesStr) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventFilter.kt new file mode 100644 index 0000000000..068ec0eb8e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventFilter.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.query + +/** + * Query strings used to filter the timeline events regarding the Json raw string of the Event + */ +internal object TimelineEventFilter { + /** + * To apply to Event.content + */ + internal object Content { + internal const val EDIT = """{*"m.relates_to"*"rel_type":*"m.replace"*}""" + internal const val RESPONSE = """{*"m.relates_to"*"rel_type":*"org.matrix.response"*}""" + } + + /** + * To apply to Event.decryptionResultJson + */ + internal object DecryptedContent { + internal const val URL = """{*"file":*"url":*}""" + } + + /** + * To apply to Event.unsigned + */ + internal object Unsigned { + internal const val REDACTED = """{*"redacted_because":*}""" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/UserDraftsEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/UserDraftsEntityQueries.kt new file mode 100644 index 0000000000..6c3bc70787 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/UserDraftsEntityQueries.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields +import org.matrix.android.sdk.internal.database.model.UserDraftsEntity +import org.matrix.android.sdk.internal.database.model.UserDraftsEntityFields +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.where + +internal fun UserDraftsEntity.Companion.where(realm: Realm, roomId: String? = null): RealmQuery { + val query = realm.where() + if (roomId != null) { + query.equalTo(UserDraftsEntityFields.ROOM_SUMMARY_ENTITY + "." + RoomSummaryEntityFields.ROOM_ID, roomId) + } + return query +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/UserEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/UserEntityQueries.kt new file mode 100644 index 0000000000..5566028d60 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/UserEntityQueries.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.database.query + +import org.matrix.android.sdk.internal.database.model.UserEntity +import org.matrix.android.sdk.internal.database.model.UserEntityFields +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.where + +internal fun UserEntity.Companion.where(realm: Realm, userId: String): RealmQuery { + return realm + .where() + .equalTo(UserEntityFields.USER_ID, userId) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/AuthQualifiers.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/AuthQualifiers.kt new file mode 100644 index 0000000000..237eae38ec --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/AuthQualifiers.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.di + +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class Authenticated + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class AuthenticatedIdentity + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class Unauthenticated + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class UnauthenticatedWithCertificate + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class UnauthenticatedWithCertificateWithProgress diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/DbQualifiers.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/DbQualifiers.kt new file mode 100644 index 0000000000..9442dc4865 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/DbQualifiers.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.di + +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class AuthDatabase + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class SessionDatabase + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class CryptoDatabase + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class IdentityDatabase diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/FileQualifiers.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/FileQualifiers.kt new file mode 100644 index 0000000000..5d140232df --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/FileQualifiers.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.di + +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class SessionFilesDirectory + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class SessionDownloadsDirectory + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class CacheDirectory + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class ExternalFilesDirectory diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt new file mode 100644 index 0000000000..816a674d81 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.di + +import android.content.Context +import android.content.res.Resources +import com.squareup.moshi.Moshi +import dagger.BindsInstance +import dagger.Component +import org.matrix.android.sdk.api.Matrix +import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.api.auth.AuthenticationService +import org.matrix.android.sdk.internal.SessionManager +import org.matrix.android.sdk.internal.auth.AuthModule +import org.matrix.android.sdk.internal.auth.SessionParamsStore +import org.matrix.android.sdk.internal.session.MockHttpInterceptor +import org.matrix.android.sdk.internal.session.TestInterceptor +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import okhttp3.OkHttpClient +import org.matrix.olm.OlmManager +import java.io.File + +@Component(modules = [MatrixModule::class, NetworkModule::class, AuthModule::class, NoOpTestModule::class]) +@MatrixScope +internal interface MatrixComponent { + + fun matrixCoroutineDispatchers(): MatrixCoroutineDispatchers + + fun moshi(): Moshi + + @Unauthenticated + fun okHttpClient(): OkHttpClient + + @MockHttpInterceptor + fun testInterceptor(): TestInterceptor? + + fun authenticationService(): AuthenticationService + + fun context(): Context + + fun matrixConfiguration(): MatrixConfiguration + + fun resources(): Resources + + @CacheDirectory + fun cacheDir(): File + + @ExternalFilesDirectory + fun externalFilesDir(): File? + + fun olmManager(): OlmManager + + fun taskExecutor(): TaskExecutor + + fun sessionParamsStore(): SessionParamsStore + + fun backgroundDetectionObserver(): BackgroundDetectionObserver + + fun sessionManager(): SessionManager + + fun inject(matrix: Matrix) + + @Component.Factory + interface Factory { + fun create(@BindsInstance context: Context, + @BindsInstance matrixConfiguration: MatrixConfiguration): MatrixComponent + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixModule.kt new file mode 100644 index 0000000000..be3175c22f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixModule.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.di + +import android.content.Context +import android.content.res.Resources +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import org.matrix.android.sdk.internal.util.createBackgroundHandler +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.android.asCoroutineDispatcher +import kotlinx.coroutines.asCoroutineDispatcher +import org.matrix.olm.OlmManager +import java.io.File +import java.util.concurrent.Executors + +@Module +internal object MatrixModule { + + @JvmStatic + @Provides + @MatrixScope + fun providesMatrixCoroutineDispatchers(): MatrixCoroutineDispatchers { + return MatrixCoroutineDispatchers(io = Dispatchers.IO, + computation = Dispatchers.Default, + main = Dispatchers.Main, + crypto = createBackgroundHandler("Crypto_Thread").asCoroutineDispatcher(), + dmVerif = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + ) + } + + @JvmStatic + @Provides + fun providesResources(context: Context): Resources { + return context.resources + } + + @JvmStatic + @Provides + @CacheDirectory + fun providesCacheDir(context: Context): File { + return context.cacheDir + } + + @JvmStatic + @Provides + @ExternalFilesDirectory + fun providesExternalFilesDir(context: Context): File? { + return context.getExternalFilesDir(null) + } + + @JvmStatic + @Provides + @MatrixScope + fun providesOlmManager(): OlmManager { + return OlmManager() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixScope.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixScope.kt new file mode 100644 index 0000000000..8cfa48f26c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixScope.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.di + +import javax.inject.Scope + +/** + * Use the annotation @MatrixScope to annotate classes we want the SDK to instantiate only once + */ +@Scope +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +internal annotation class MatrixScope diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MoshiProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MoshiProvider.kt new file mode 100644 index 0000000000..5e16d0b455 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MoshiProvider.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.di + +import com.squareup.moshi.Moshi +import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageDefaultContent +import org.matrix.android.sdk.api.session.room.model.message.MessageEmoteContent +import org.matrix.android.sdk.api.session.room.model.message.MessageFileContent +import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageLocationContent +import org.matrix.android.sdk.api.session.room.model.message.MessageNoticeContent +import org.matrix.android.sdk.api.session.room.model.message.MessageOptionsContent +import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent +import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent +import org.matrix.android.sdk.api.session.room.model.message.MessageType +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent +import org.matrix.android.sdk.internal.network.parsing.ForceToBooleanJsonAdapter +import org.matrix.android.sdk.internal.network.parsing.RuntimeJsonAdapterFactory +import org.matrix.android.sdk.internal.network.parsing.UriMoshiAdapter + +object MoshiProvider { + + private val moshi: Moshi = Moshi.Builder() + .add(UriMoshiAdapter()) + .add(ForceToBooleanJsonAdapter()) + .add(RuntimeJsonAdapterFactory.of(MessageContent::class.java, "msgtype", MessageDefaultContent::class.java) + .registerSubtype(MessageTextContent::class.java, MessageType.MSGTYPE_TEXT) + .registerSubtype(MessageNoticeContent::class.java, MessageType.MSGTYPE_NOTICE) + .registerSubtype(MessageEmoteContent::class.java, MessageType.MSGTYPE_EMOTE) + .registerSubtype(MessageAudioContent::class.java, MessageType.MSGTYPE_AUDIO) + .registerSubtype(MessageImageContent::class.java, MessageType.MSGTYPE_IMAGE) + .registerSubtype(MessageVideoContent::class.java, MessageType.MSGTYPE_VIDEO) + .registerSubtype(MessageLocationContent::class.java, MessageType.MSGTYPE_LOCATION) + .registerSubtype(MessageFileContent::class.java, MessageType.MSGTYPE_FILE) + .registerSubtype(MessageVerificationRequestContent::class.java, MessageType.MSGTYPE_VERIFICATION_REQUEST) + .registerSubtype(MessageOptionsContent::class.java, MessageType.MSGTYPE_OPTIONS) + .registerSubtype(MessagePollResponseContent::class.java, MessageType.MSGTYPE_RESPONSE) + ) + .add(SerializeNulls.JSON_ADAPTER_FACTORY) + .build() + + fun providesMoshi(): Moshi { + return moshi + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt new file mode 100644 index 0000000000..71961d02d3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.di + +import com.facebook.stetho.okhttp3.StethoInterceptor +import com.squareup.moshi.Moshi +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.BuildConfig +import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.internal.network.TimeOutInterceptor +import org.matrix.android.sdk.internal.network.UserAgentInterceptor +import org.matrix.android.sdk.internal.network.interceptors.CurlLoggingInterceptor +import org.matrix.android.sdk.internal.network.interceptors.FormattedJsonHttpLogger +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import okreplay.OkReplayInterceptor +import java.util.concurrent.TimeUnit + +@Module +internal object NetworkModule { + + @Provides + @JvmStatic + fun providesHttpLoggingInterceptor(): HttpLoggingInterceptor { + val logger = FormattedJsonHttpLogger() + val interceptor = HttpLoggingInterceptor(logger) + interceptor.level = BuildConfig.OKHTTP_LOGGING_LEVEL + return interceptor + } + + @Provides + @JvmStatic + fun providesOkReplayInterceptor(): OkReplayInterceptor { + return OkReplayInterceptor() + } + + @Provides + @JvmStatic + fun providesStethoInterceptor(): StethoInterceptor { + return StethoInterceptor() + } + + @Provides + @JvmStatic + fun providesCurlLoggingInterceptor(): CurlLoggingInterceptor { + return CurlLoggingInterceptor() + } + + @MatrixScope + @Provides + @JvmStatic + @Unauthenticated + fun providesOkHttpClient(matrixConfiguration: MatrixConfiguration, + stethoInterceptor: StethoInterceptor, + timeoutInterceptor: TimeOutInterceptor, + userAgentInterceptor: UserAgentInterceptor, + httpLoggingInterceptor: HttpLoggingInterceptor, + curlLoggingInterceptor: CurlLoggingInterceptor, + okReplayInterceptor: OkReplayInterceptor): OkHttpClient { + return OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .writeTimeout(60, TimeUnit.SECONDS) + .addNetworkInterceptor(stethoInterceptor) + .addInterceptor(timeoutInterceptor) + .addInterceptor(userAgentInterceptor) + .addInterceptor(httpLoggingInterceptor) + .apply { + if (BuildConfig.LOG_PRIVATE_DATA) { + addInterceptor(curlLoggingInterceptor) + } + matrixConfiguration.proxy?.let { + proxy(it) + } + } + .addInterceptor(okReplayInterceptor) + .build() + } + + @Provides + @JvmStatic + fun providesMoshi(): Moshi { + return MoshiProvider.providesMoshi() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NoOpTestModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NoOpTestModule.kt new file mode 100644 index 0000000000..d74c6055b1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NoOpTestModule.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.di + +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.internal.session.MockHttpInterceptor +import org.matrix.android.sdk.internal.session.TestInterceptor + +@Module +internal object NoOpTestModule { + + @Provides + @JvmStatic + @MockHttpInterceptor + fun providesTestInterceptor(): TestInterceptor? = null +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/SerializeNulls.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/SerializeNulls.kt new file mode 100644 index 0000000000..a66c7ff713 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/SerializeNulls.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.di + +import androidx.annotation.Nullable +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.JsonQualifier +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import java.lang.reflect.Type + +@Retention(AnnotationRetention.RUNTIME) +@JsonQualifier +annotation class SerializeNulls { + companion object { + val JSON_ADAPTER_FACTORY: JsonAdapter.Factory = object : JsonAdapter.Factory { + @Nullable + override fun create(type: Type, annotations: Set, moshi: Moshi): JsonAdapter<*>? { + val nextAnnotations = Types.nextAnnotations(annotations, SerializeNulls::class.java) + ?: return null + return moshi.nextAdapter(this, type, nextAnnotations).serializeNulls() + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/SessionAssistedInjectModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/SessionAssistedInjectModule.kt new file mode 100644 index 0000000000..b9ea7a0ad8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/SessionAssistedInjectModule.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.di + +import com.squareup.inject.assisted.dagger2.AssistedModule +import dagger.Module + +@AssistedModule +@Module(includes = [AssistedInject_SessionAssistedInjectModule::class]) +interface SessionAssistedInjectModule diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/StringQualifiers.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/StringQualifiers.kt new file mode 100644 index 0000000000..10a523bbf7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/StringQualifiers.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.di + +import javax.inject.Qualifier + +/** + * Used to inject the userId + */ +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class UserId + +/** + * Used to inject the deviceId + */ +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class DeviceId + +/** + * Used to inject the md5 of the userId + */ +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class UserMd5 + +/** + * Used to inject the sessionId, which is defined as md5(userId|deviceId) + */ +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class SessionId diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt new file mode 100644 index 0000000000..737b5335d4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.di + +import android.content.Context +import androidx.work.Constraints +import androidx.work.ListenableWorker +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +internal class WorkManagerProvider @Inject constructor( + context: Context, + @SessionId private val sessionId: String +) { + private val tag = MATRIX_SDK_TAG_PREFIX + sessionId + + val workManager = WorkManager.getInstance(context) + + /** + * Create a OneTimeWorkRequestBuilder, with the Matrix SDK tag + */ + inline fun matrixOneTimeWorkRequestBuilder() = + OneTimeWorkRequestBuilder() + .addTag(tag) + + /** + * Create a PeriodicWorkRequestBuilder, with the Matrix SDK tag + */ + inline fun matrixPeriodicWorkRequestBuilder(repeatInterval: Long, + repeatIntervalTimeUnit: TimeUnit) = + PeriodicWorkRequestBuilder(repeatInterval, repeatIntervalTimeUnit) + .addTag(tag) + + /** + * Cancel all works instantiated by the Matrix SDK for the current session, and not those from the SDK client, or for other sessions + */ + fun cancelAllWorks() { + workManager.let { + it.cancelAllWorkByTag(tag) + it.pruneWork() + } + } + + companion object { + private const val MATRIX_SDK_TAG_PREFIX = "MatrixSDK-" + + /** + * Default constraints: connected network + */ + val workConstraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + + const val BACKOFF_DELAY = 10_000L + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/eventbus/EventBusTimberLogger.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/eventbus/EventBusTimberLogger.kt new file mode 100644 index 0000000000..b60d60a61b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/eventbus/EventBusTimberLogger.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.eventbus + +import org.greenrobot.eventbus.Logger +import timber.log.Timber +import java.util.logging.Level + +class EventBusTimberLogger : Logger { + override fun log(level: Level, msg: String) { + Timber.d(msg) + } + + override fun log(level: Level, msg: String, th: Throwable) { + Timber.e(th, msg) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/LiveData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/LiveData.kt new file mode 100644 index 0000000000..64fb72e537 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/LiveData.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.extensions + +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer + +inline fun LiveData.observeK(owner: LifecycleOwner, crossinline observer: (T?) -> Unit) { + this.observe(owner, Observer { observer(it) }) +} + +inline fun LiveData.observeNotNull(owner: LifecycleOwner, crossinline observer: (T) -> Unit) { + this.observe(owner, Observer { it?.run(observer) }) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Primitives.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Primitives.kt new file mode 100644 index 0000000000..ab45f08e42 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Primitives.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.extensions + +/** + * Convert a signed byte to a int value + */ +fun Byte.toUnsignedInt() = toInt() and 0xff diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/RealmExtensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/RealmExtensions.kt new file mode 100644 index 0000000000..ebe9ab7ecb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/RealmExtensions.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.extensions + +import io.realm.RealmObject + +internal fun RealmObject.assertIsManaged() { + check(isManaged) { "${javaClass.simpleName} entity should be managed to use this function" } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Result.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Result.kt new file mode 100644 index 0000000000..0b812736cb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Result.kt @@ -0,0 +1,26 @@ +/* + + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + + */ +package org.matrix.android.sdk.internal.extensions + +import org.matrix.android.sdk.api.MatrixCallback + +fun
Result.foldToCallback(callback: MatrixCallback): Unit = fold( + { callback.onSuccess(it) }, + { callback.onFailure(it) } +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Try.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Try.kt new file mode 100644 index 0000000000..1030d6717b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Try.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.extensions + +import arrow.core.Failure +import arrow.core.Success +import arrow.core.Try +import arrow.core.TryOf +import arrow.core.fix +import org.matrix.android.sdk.api.MatrixCallback + +inline fun TryOf.onError(f: (Throwable) -> Unit): Try = fix() + .fold( + { + f(it) + Failure(it) + }, + { Success(it) } + ) + +fun Try.foldToCallback(callback: MatrixCallback): Unit = fold( + { callback.onFailure(it) }, + { callback.onSuccess(it) }) + +/** + * Same as doOnNext for Observables + */ +inline fun Try.alsoDo(f: (A) -> Unit) = map { + f(it) + it +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/DefaultLegacySessionImporter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/DefaultLegacySessionImporter.kt new file mode 100644 index 0000000000..1741ca4845 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/DefaultLegacySessionImporter.kt @@ -0,0 +1,227 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.legacy + +import android.content.Context +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.auth.data.DiscoveryInformation +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import org.matrix.android.sdk.api.auth.data.SessionParams +import org.matrix.android.sdk.api.auth.data.WellKnownBaseConfig +import org.matrix.android.sdk.api.legacy.LegacySessionImporter +import org.matrix.android.sdk.internal.auth.SessionParamsStore +import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreMigration +import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreModule +import org.matrix.android.sdk.internal.database.RealmKeysUtils +import org.matrix.android.sdk.internal.legacy.riot.LoginStorage +import org.matrix.android.sdk.internal.network.ssl.Fingerprint +import org.matrix.android.sdk.internal.util.md5 +import io.realm.Realm +import io.realm.RealmConfiguration +import kotlinx.coroutines.runBlocking +import timber.log.Timber +import java.io.File +import javax.inject.Inject +import org.matrix.android.sdk.internal.legacy.riot.Fingerprint as LegacyFingerprint +import org.matrix.android.sdk.internal.legacy.riot.HomeServerConnectionConfig as LegacyHomeServerConnectionConfig + +internal class DefaultLegacySessionImporter @Inject constructor( + private val context: Context, + private val sessionParamsStore: SessionParamsStore, + private val realmCryptoStoreMigration: RealmCryptoStoreMigration, + private val realmKeysUtils: RealmKeysUtils +) : LegacySessionImporter { + + private val loginStorage = LoginStorage(context) + + companion object { + // During development, set to false to play several times the migration + private var DELETE_PREVIOUS_DATA = true + } + + override fun process(): Boolean { + Timber.d("Migration: Importing legacy session") + + val list = loginStorage.credentialsList + + Timber.d("Migration: found ${list.size} session(s).") + + val legacyConfig = list.firstOrNull() ?: return false + + runBlocking { + Timber.d("Migration: importing a session") + try { + importCredentials(legacyConfig) + } catch (t: Throwable) { + // It can happen in case of partial migration. To test, do not return + Timber.e(t, "Migration: Error importing credential") + } + + Timber.d("Migration: importing crypto DB") + try { + importCryptoDb(legacyConfig) + } catch (t: Throwable) { + // It can happen in case of partial migration. To test, do not return + Timber.e(t, "Migration: Error importing crypto DB") + } + + if (DELETE_PREVIOUS_DATA) { + try { + Timber.d("Migration: clear file system") + clearFileSystem(legacyConfig) + } catch (t: Throwable) { + Timber.e(t, "Migration: Error clearing filesystem") + } + try { + Timber.d("Migration: clear shared prefs") + clearSharedPrefs() + } catch (t: Throwable) { + Timber.e(t, "Migration: Error clearing shared prefs") + } + } else { + Timber.d("Migration: clear file system - DEACTIVATED") + Timber.d("Migration: clear shared prefs - DEACTIVATED") + } + } + + // A session has been imported + return true + } + + private suspend fun importCredentials(legacyConfig: LegacyHomeServerConnectionConfig) { + @Suppress("DEPRECATION") + val sessionParams = SessionParams( + credentials = Credentials( + userId = legacyConfig.credentials.userId, + accessToken = legacyConfig.credentials.accessToken, + refreshToken = legacyConfig.credentials.refreshToken, + homeServer = legacyConfig.credentials.homeServer, + deviceId = legacyConfig.credentials.deviceId, + discoveryInformation = legacyConfig.credentials.wellKnown?.let { wellKnown -> + // Note credentials.wellKnown is not serialized in the LoginStorage, so this code is a bit useless... + if (wellKnown.homeServer?.baseURL != null || wellKnown.identityServer?.baseURL != null) { + DiscoveryInformation( + homeServer = wellKnown.homeServer?.baseURL?.let { WellKnownBaseConfig(baseURL = it) }, + identityServer = wellKnown.identityServer?.baseURL?.let { WellKnownBaseConfig(baseURL = it) } + ) + } else { + null + } + } + ), + homeServerConnectionConfig = HomeServerConnectionConfig( + homeServerUri = legacyConfig.homeserverUri, + identityServerUri = legacyConfig.identityServerUri, + antiVirusServerUri = legacyConfig.antiVirusServerUri, + allowedFingerprints = legacyConfig.allowedFingerprints.map { + Fingerprint( + bytes = it.bytes, + hashType = when (it.type) { + LegacyFingerprint.HashType.SHA1, + null -> Fingerprint.HashType.SHA1 + LegacyFingerprint.HashType.SHA256 -> Fingerprint.HashType.SHA256 + } + ) + }, + shouldPin = legacyConfig.shouldPin(), + tlsVersions = legacyConfig.acceptedTlsVersions, + tlsCipherSuites = legacyConfig.acceptedTlsCipherSuites, + shouldAcceptTlsExtensions = legacyConfig.shouldAcceptTlsExtensions(), + allowHttpExtension = false, // TODO + forceUsageTlsVersions = legacyConfig.forceUsageOfTlsVersions() + ), + // If token is not valid, this boolean will be updated later + isTokenValid = true + ) + + Timber.d("Migration: save session") + sessionParamsStore.save(sessionParams) + } + + private fun importCryptoDb(legacyConfig: LegacyHomeServerConnectionConfig) { + // Here we migrate the DB, we copy the crypto DB to the location specific to RiotX, and we encrypt it. + val userMd5 = legacyConfig.credentials.userId.md5() + + val sessionId = legacyConfig.credentials.let { (if (it.deviceId.isNullOrBlank()) it.userId else "${it.userId}|${it.deviceId}").md5() } + val newLocation = File(context.filesDir, sessionId) + + val keyAlias = "crypto_module_$userMd5" + + // Ensure newLocation does not exist (can happen in case of partial migration) + newLocation.deleteRecursively() + newLocation.mkdirs() + + Timber.d("Migration: create legacy realm configuration") + + val realmConfiguration = RealmConfiguration.Builder() + .directory(File(context.filesDir, userMd5)) + .name("crypto_store.realm") + .modules(RealmCryptoStoreModule()) + .schemaVersion(RealmCryptoStoreMigration.CRYPTO_STORE_SCHEMA_VERSION) + .migration(realmCryptoStoreMigration) + .build() + + Timber.d("Migration: copy DB to encrypted DB") + Realm.getInstance(realmConfiguration).use { + // Move the DB to the new location, handled by RiotX + it.writeEncryptedCopyTo(File(newLocation, realmConfiguration.realmFileName), realmKeysUtils.getRealmEncryptionKey(keyAlias)) + } + } + + // Delete all the files created by Riot Android which will not be used anymore by RiotX + private fun clearFileSystem(legacyConfig: LegacyHomeServerConnectionConfig) { + val cryptoFolder = legacyConfig.credentials.userId.md5() + + listOf( + // Where session store was saved (we do not care about migrating that, an initial sync will be performed) + File(context.filesDir, "MXFileStore"), + // Previous (and very old) file crypto store + File(context.filesDir, "MXFileCryptoStore"), + // Draft. They will be lost, this is sad but we assume it + File(context.filesDir, "MXLatestMessagesStore"), + // Media storage + File(context.filesDir, "MXMediaStore"), + File(context.filesDir, "MXMediaStore2"), + File(context.filesDir, "MXMediaStore3"), + // Ext folder + File(context.filesDir, "ext_share"), + // Crypto store + File(context.filesDir, cryptoFolder) + ).forEach { file -> + try { + file.deleteRecursively() + } catch (t: Throwable) { + Timber.e(t, "Migration: unable to delete $file") + } + } + } + + private fun clearSharedPrefs() { + // Shared Pref. Note that we do not delete the default preferences, as it should be nearly the same (TODO check that) + listOf( + "Vector.LoginStorage", + "GcmRegistrationManager", + "IntegrationManager.Storage" + ).forEach { prefName -> + context.getSharedPreferences(prefName, Context.MODE_PRIVATE) + .edit() + .clear() + .apply() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/Credentials.java b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/Credentials.java new file mode 100644 index 0000000000..59ad3be4c5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/Credentials.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.legacy.riot; + +import android.text.TextUtils; + +import org.jetbrains.annotations.Nullable; +import org.json.JSONException; +import org.json.JSONObject; + +/* + * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose + */ + +/** + * The user's credentials. + */ +public class Credentials { + public String userId; + + // This is the server name and not a URI, e.g. "matrix.org". Spec says it's now deprecated + @Deprecated + public String homeServer; + + public String accessToken; + + public String refreshToken; + + public String deviceId; + + // Optional data that may contain info to override home server and/or identity server + public WellKnown wellKnown; + + public JSONObject toJson() throws JSONException { + JSONObject json = new JSONObject(); + + json.put("user_id", userId); + json.put("home_server", homeServer); + json.put("access_token", accessToken); + json.put("refresh_token", TextUtils.isEmpty(refreshToken) ? JSONObject.NULL : refreshToken); + json.put("device_id", deviceId); + + return json; + } + + public static Credentials fromJson(JSONObject obj) throws JSONException { + Credentials creds = new Credentials(); + creds.userId = obj.getString("user_id"); + creds.homeServer = obj.getString("home_server"); + creds.accessToken = obj.getString("access_token"); + + if (obj.has("device_id")) { + creds.deviceId = obj.getString("device_id"); + } + + // refresh_token is mandatory + if (obj.has("refresh_token")) { + try { + creds.refreshToken = obj.getString("refresh_token"); + } catch (Exception e) { + creds.refreshToken = null; + } + } else { + throw new RuntimeException("refresh_token is required."); + } + + return creds; + } + + @Override + public String toString() { + return "Credentials{" + + "userId='" + userId + '\'' + + ", homeServer='" + homeServer + '\'' + + ", refreshToken.length='" + (refreshToken != null ? refreshToken.length() : "null") + '\'' + + ", accessToken.length='" + (accessToken != null ? accessToken.length() : "null") + '\'' + + '}'; + } + + @Nullable + public String getUserId() { + return userId; + } + + @Nullable + public String getHomeServer() { + return homeServer; + } + + @Nullable + public String getAccessToken() { + return accessToken; + } + + @Nullable + public String getDeviceId() { + return deviceId; + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/Fingerprint.java b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/Fingerprint.java new file mode 100644 index 0000000000..3975618f39 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/Fingerprint.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.legacy.riot; + +import android.util.Base64; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Arrays; + +/* + * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose + */ + +/** + * Represents a X509 Certificate fingerprint. + */ +public class Fingerprint { + public enum HashType { + SHA1, + SHA256 + } + + private final HashType mHashType; + private final byte[] mBytes; + + public Fingerprint(HashType hashType, byte[] bytes) { + mHashType = hashType; + mBytes = bytes; + } + + public HashType getType() { + return mHashType; + } + + public byte[] getBytes() { + return mBytes; + } + + public JSONObject toJson() throws JSONException { + JSONObject obj = new JSONObject(); + obj.put("bytes", Base64.encodeToString(getBytes(), Base64.DEFAULT)); + obj.put("hash_type", mHashType.toString()); + return obj; + } + + public static Fingerprint fromJson(JSONObject obj) throws JSONException { + String hashTypeStr = obj.getString("hash_type"); + byte[] fingerprintBytes = Base64.decode(obj.getString("bytes"), Base64.DEFAULT); + + final HashType hashType; + if ("SHA256".equalsIgnoreCase(hashTypeStr)) { + hashType = HashType.SHA256; + } else if ("SHA1".equalsIgnoreCase(hashTypeStr)) { + hashType = HashType.SHA1; + } else { + throw new JSONException("Unrecognized hash type: " + hashTypeStr); + } + + return new Fingerprint(hashType, fingerprintBytes); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Fingerprint that = (Fingerprint) o; + + if (!Arrays.equals(mBytes, that.mBytes)) return false; + return mHashType == that.mHashType; + + } + + @Override + public int hashCode() { + int result = mBytes != null ? Arrays.hashCode(mBytes) : 0; + result = 31 * result + (mHashType != null ? mHashType.hashCode() : 0); + return result; + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/HomeServerConnectionConfig.java b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/HomeServerConnectionConfig.java new file mode 100644 index 0000000000..6732a2cd92 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/HomeServerConnectionConfig.java @@ -0,0 +1,677 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.legacy.riot; + +import android.net.Uri; +import android.text.TextUtils; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.util.ArrayList; +import java.util.List; + +import okhttp3.CipherSuite; +import okhttp3.TlsVersion; +import timber.log.Timber; + +/* + * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose + */ + +/** + * Represents how to connect to a specific Homeserver, may include credentials to use. + */ +public class HomeServerConnectionConfig { + + // the home server URI + private Uri mHomeServerUri; + // the jitsi server URI. Can be null + @Nullable + private Uri mJitsiServerUri; + // the identity server URI. Can be null + @Nullable + private Uri mIdentityServerUri; + // the anti-virus server URI + private Uri mAntiVirusServerUri; + // allowed fingerprints + private List mAllowedFingerprints = new ArrayList<>(); + // the credentials + private Credentials mCredentials; + // tell whether we should reject X509 certs that were issued by trusts CAs and only trustcerts with matching fingerprints. + private boolean mPin; + // the accepted TLS versions + private List mTlsVersions; + // the accepted TLS cipher suites + private List mTlsCipherSuites; + // should accept TLS extensions + private boolean mShouldAcceptTlsExtensions = true; + // Force usage of TLS versions + private boolean mForceUsageTlsVersions; + // the proxy hostname + private String mProxyHostname; + // the proxy port + private int mProxyPort = -1; + + + /** + * Private constructor. Please use the Builder + */ + private HomeServerConnectionConfig() { + // Private constructor + } + + /** + * Update the home server URI. + * + * @param uri the new HS uri + */ + public void setHomeserverUri(Uri uri) { + mHomeServerUri = uri; + } + + /** + * @return the home server uri + */ + public Uri getHomeserverUri() { + return mHomeServerUri; + } + + /** + * @return the jitsi server uri + */ + public Uri getJitsiServerUri() { + return mJitsiServerUri; + } + + /** + * @return the identity server uri, or null if not defined + */ + @Nullable + public Uri getIdentityServerUri() { + return mIdentityServerUri; + } + + /** + * @return the anti-virus server uri + */ + public Uri getAntiVirusServerUri() { + if (null != mAntiVirusServerUri) { + return mAntiVirusServerUri; + } + // Else consider the HS uri by default. + return mHomeServerUri; + } + + /** + * @return the allowed fingerprints. + */ + public List getAllowedFingerprints() { + return mAllowedFingerprints; + } + + /** + * @return the credentials + */ + public Credentials getCredentials() { + return mCredentials; + } + + /** + * Update the credentials. + * + * @param credentials the new credentials + */ + public void setCredentials(Credentials credentials) { + mCredentials = credentials; + + // Override home server url and/or identity server url if provided + if (credentials.wellKnown != null) { + if (credentials.wellKnown.homeServer != null) { + String homeServerUrl = credentials.wellKnown.homeServer.baseURL; + + if (!TextUtils.isEmpty(homeServerUrl)) { + // remove trailing "/" + if (homeServerUrl.endsWith("/")) { + homeServerUrl = homeServerUrl.substring(0, homeServerUrl.length() - 1); + } + + Timber.d("Overriding homeserver url to " + homeServerUrl); + mHomeServerUri = Uri.parse(homeServerUrl); + } + } + + if (credentials.wellKnown.identityServer != null) { + String identityServerUrl = credentials.wellKnown.identityServer.baseURL; + + if (!TextUtils.isEmpty(identityServerUrl)) { + // remove trailing "/" + if (identityServerUrl.endsWith("/")) { + identityServerUrl = identityServerUrl.substring(0, identityServerUrl.length() - 1); + } + + Timber.d("Overriding identity server url to " + identityServerUrl); + mIdentityServerUri = Uri.parse(identityServerUrl); + } + } + + if (credentials.wellKnown.jitsiServer != null) { + String jitsiServerUrl = credentials.wellKnown.jitsiServer.preferredDomain; + + if (!TextUtils.isEmpty(jitsiServerUrl)) { + // add trailing "/" + if (!jitsiServerUrl.endsWith("/")) { + jitsiServerUrl =jitsiServerUrl + "/"; + } + + Timber.d("Overriding jitsi server url to " + jitsiServerUrl); + mJitsiServerUri = Uri.parse(jitsiServerUrl); + } + } + } + } + + /** + * @return whether we should reject X509 certs that were issued by trusts CAs and only trust + * certs with matching fingerprints. + */ + public boolean shouldPin() { + return mPin; + } + + /** + * TLS versions accepted for TLS connections with the home server. + */ + @Nullable + public List getAcceptedTlsVersions() { + return mTlsVersions; + } + + /** + * TLS cipher suites accepted for TLS connections with the home server. + */ + @Nullable + public List getAcceptedTlsCipherSuites() { + return mTlsCipherSuites; + } + + /** + * @return whether we should accept TLS extensions. + */ + public boolean shouldAcceptTlsExtensions() { + return mShouldAcceptTlsExtensions; + } + + /** + * @return true if the usage of TlsVersions has to be forced + */ + public boolean forceUsageOfTlsVersions() { + return mForceUsageTlsVersions; + } + + + /** + * @return proxy config if available + */ + @Nullable + public Proxy getProxyConfig() { + if (mProxyHostname == null || mProxyHostname.length() == 0 || mProxyPort == -1) { + return null; + } + + return new Proxy(Proxy.Type.HTTP, + new InetSocketAddress(mProxyHostname, mProxyPort)); + } + + + @Override + public String toString() { + return "HomeserverConnectionConfig{" + + "mHomeServerUri=" + mHomeServerUri + + ", mJitsiServerUri=" + mJitsiServerUri + + ", mIdentityServerUri=" + mIdentityServerUri + + ", mAntiVirusServerUri=" + mAntiVirusServerUri + + ", mAllowedFingerprints size=" + mAllowedFingerprints.size() + + ", mCredentials=" + mCredentials + + ", mPin=" + mPin + + ", mShouldAcceptTlsExtensions=" + mShouldAcceptTlsExtensions + + ", mProxyHostname=" + (null == mProxyHostname ? "" : mProxyHostname) + + ", mProxyPort=" + (-1 == mProxyPort ? "" : mProxyPort) + + ", mTlsVersions=" + (null == mTlsVersions ? "" : mTlsVersions.size()) + + ", mTlsCipherSuites=" + (null == mTlsCipherSuites ? "" : mTlsCipherSuites.size()) + + '}'; + } + + /** + * Convert the object instance into a JSon object + * + * @return the JSon representation + * @throws JSONException the JSON conversion failure reason + */ + public JSONObject toJson() throws JSONException { + JSONObject json = new JSONObject(); + + json.put("home_server_url", mHomeServerUri.toString()); + Uri jitsiServerUri = getJitsiServerUri(); + if (jitsiServerUri != null) { + json.put("jitsi_server_url", jitsiServerUri.toString()); + } + Uri identityServerUri = getIdentityServerUri(); + if (identityServerUri != null) { + json.put("identity_server_url", identityServerUri.toString()); + } + + if (mAntiVirusServerUri != null) { + json.put("antivirus_server_url", mAntiVirusServerUri.toString()); + } + + json.put("pin", mPin); + + if (mCredentials != null) json.put("credentials", mCredentials.toJson()); + if (mAllowedFingerprints != null) { + List fingerprints = new ArrayList<>(mAllowedFingerprints.size()); + + for (Fingerprint fingerprint : mAllowedFingerprints) { + fingerprints.add(fingerprint.toJson()); + } + + json.put("fingerprints", new JSONArray(fingerprints)); + } + + json.put("tls_extensions", mShouldAcceptTlsExtensions); + + if (mTlsVersions != null) { + List tlsVersions = new ArrayList<>(mTlsVersions.size()); + + for (TlsVersion tlsVersion : mTlsVersions) { + tlsVersions.add(tlsVersion.javaName()); + } + + json.put("tls_versions", new JSONArray(tlsVersions)); + } + + json.put("force_usage_of_tls_versions", mForceUsageTlsVersions); + + if (mTlsCipherSuites != null) { + List tlsCipherSuites = new ArrayList<>(mTlsCipherSuites.size()); + + for (CipherSuite tlsCipherSuite : mTlsCipherSuites) { + tlsCipherSuites.add(tlsCipherSuite.javaName()); + } + + json.put("tls_cipher_suites", new JSONArray(tlsCipherSuites)); + } + + if (mProxyPort != -1) { + json.put("proxy_port", mProxyPort); + } + + if (mProxyHostname != null && mProxyHostname.length() > 0) { + json.put("proxy_hostname", mProxyHostname); + } + + return json; + } + + /** + * Create an object instance from the json object. + * + * @param jsonObject the json object + * @return a HomeServerConnectionConfig instance + * @throws JSONException the conversion failure reason + */ + public static HomeServerConnectionConfig fromJson(JSONObject jsonObject) throws JSONException { + JSONObject credentialsObj = jsonObject.optJSONObject("credentials"); + Credentials creds = credentialsObj != null ? Credentials.fromJson(credentialsObj) : null; + + Builder builder = new Builder() + .withHomeServerUri(Uri.parse(jsonObject.getString("home_server_url"))) + .withJitsiServerUri(jsonObject.has("jitsi_server_url") ? Uri.parse(jsonObject.getString("jitsi_server_url")) : null) + .withIdentityServerUri(jsonObject.has("identity_server_url") ? Uri.parse(jsonObject.getString("identity_server_url")) : null) + .withCredentials(creds) + .withPin(jsonObject.optBoolean("pin", false)); + + JSONArray fingerprintArray = jsonObject.optJSONArray("fingerprints"); + if (fingerprintArray != null) { + for (int i = 0; i < fingerprintArray.length(); i++) { + builder.addAllowedFingerPrint(Fingerprint.fromJson(fingerprintArray.getJSONObject(i))); + } + } + + // Set the anti-virus server uri if any + if (jsonObject.has("antivirus_server_url")) { + builder.withAntiVirusServerUri(Uri.parse(jsonObject.getString("antivirus_server_url"))); + } + + builder.withShouldAcceptTlsExtensions(jsonObject.optBoolean("tls_extensions", true)); + + // Set the TLS versions if any + if (jsonObject.has("tls_versions")) { + JSONArray tlsVersionsArray = jsonObject.optJSONArray("tls_versions"); + if (tlsVersionsArray != null) { + for (int i = 0; i < tlsVersionsArray.length(); i++) { + builder.addAcceptedTlsVersion(TlsVersion.forJavaName(tlsVersionsArray.getString(i))); + } + } + } + + builder.forceUsageOfTlsVersions(jsonObject.optBoolean("force_usage_of_tls_versions", false)); + + // Set the TLS cipher suites if any + if (jsonObject.has("tls_cipher_suites")) { + JSONArray tlsCipherSuitesArray = jsonObject.optJSONArray("tls_cipher_suites"); + if (tlsCipherSuitesArray != null) { + for (int i = 0; i < tlsCipherSuitesArray.length(); i++) { + builder.addAcceptedTlsCipherSuite(CipherSuite.forJavaName(tlsCipherSuitesArray.getString(i))); + } + } + } + + // Set the proxy options right if any + if (jsonObject.has("proxy_hostname") && jsonObject.has("proxy_port")) { + builder.withProxy(jsonObject.getString("proxy_hostname"), jsonObject.getInt("proxy_port")); + } + + return builder.build(); + } + + /** + * Builder + */ + public static class Builder { + private HomeServerConnectionConfig mHomeServerConnectionConfig; + + /** + * Builder constructor + */ + public Builder() { + mHomeServerConnectionConfig = new HomeServerConnectionConfig(); + } + + /** + * create a Builder from an existing HomeServerConnectionConfig + */ + public Builder(HomeServerConnectionConfig from) { + try { + mHomeServerConnectionConfig = HomeServerConnectionConfig.fromJson(from.toJson()); + } catch (JSONException e) { + // Should not happen + throw new RuntimeException("Unable to create a HomeServerConnectionConfig", e); + } + } + + /** + * @param homeServerUri The URI to use to connect to the homeserver. Cannot be null + * @return this builder + */ + public Builder withHomeServerUri(final Uri homeServerUri) { + if (homeServerUri == null || (!"http".equals(homeServerUri.getScheme()) && !"https".equals(homeServerUri.getScheme()))) { + throw new RuntimeException("Invalid home server URI: " + homeServerUri); + } + + // remove trailing / + if (homeServerUri.toString().endsWith("/")) { + try { + String url = homeServerUri.toString(); + mHomeServerConnectionConfig.mHomeServerUri = Uri.parse(url.substring(0, url.length() - 1)); + } catch (Exception e) { + throw new RuntimeException("Invalid home server URI: " + homeServerUri); + } + } else { + mHomeServerConnectionConfig.mHomeServerUri = homeServerUri; + } + + return this; + } + + /** + * @param jitsiServerUri The URI to use to manage identity. Can be null + * @return this builder + */ + public Builder withJitsiServerUri(@Nullable final Uri jitsiServerUri) { + if (jitsiServerUri != null + && !jitsiServerUri.toString().isEmpty() + && !"http".equals(jitsiServerUri.getScheme()) + && !"https".equals(jitsiServerUri.getScheme())) { + throw new RuntimeException("Invalid jitsi server URI: " + jitsiServerUri); + } + + // add trailing / + if ((null != jitsiServerUri) && !jitsiServerUri.toString().endsWith("/")) { + try { + String url = jitsiServerUri.toString(); + mHomeServerConnectionConfig.mJitsiServerUri = Uri.parse(url + "/"); + } catch (Exception e) { + throw new RuntimeException("Invalid jitsi server URI: " + jitsiServerUri); + } + } else { + if (jitsiServerUri != null && jitsiServerUri.toString().isEmpty()) { + mHomeServerConnectionConfig.mJitsiServerUri = null; + } else { + mHomeServerConnectionConfig.mJitsiServerUri = jitsiServerUri; + } + } + + return this; + } + + /** + * @param identityServerUri The URI to use to manage identity. Can be null + * @return this builder + */ + public Builder withIdentityServerUri(@Nullable final Uri identityServerUri) { + if (identityServerUri != null + && !identityServerUri.toString().isEmpty() + && !"http".equals(identityServerUri.getScheme()) + && !"https".equals(identityServerUri.getScheme())) { + throw new RuntimeException("Invalid identity server URI: " + identityServerUri); + } + + // remove trailing / + if ((null != identityServerUri) && identityServerUri.toString().endsWith("/")) { + try { + String url = identityServerUri.toString(); + mHomeServerConnectionConfig.mIdentityServerUri = Uri.parse(url.substring(0, url.length() - 1)); + } catch (Exception e) { + throw new RuntimeException("Invalid identity server URI: " + identityServerUri); + } + } else { + if (identityServerUri != null && identityServerUri.toString().isEmpty()) { + mHomeServerConnectionConfig.mIdentityServerUri = null; + } else { + mHomeServerConnectionConfig.mIdentityServerUri = identityServerUri; + } + } + + return this; + } + + /** + * @param credentials The credentials to use, if needed. Can be null. + * @return this builder + */ + public Builder withCredentials(@Nullable Credentials credentials) { + mHomeServerConnectionConfig.mCredentials = credentials; + return this; + } + + /** + * @param allowedFingerprint If using SSL, allow server certs that match this fingerprint. + * @return this builder + */ + public Builder addAllowedFingerPrint(@Nullable Fingerprint allowedFingerprint) { + if (allowedFingerprint != null) { + mHomeServerConnectionConfig.mAllowedFingerprints.add(allowedFingerprint); + } + + return this; + } + + /** + * @param pin If true only allow certs matching given fingerprints, otherwise fallback to + * standard X509 checks. + * @return this builder + */ + public Builder withPin(boolean pin) { + mHomeServerConnectionConfig.mPin = pin; + + return this; + } + + /** + * @param shouldAcceptTlsExtension + * @return this builder + */ + public Builder withShouldAcceptTlsExtensions(boolean shouldAcceptTlsExtension) { + mHomeServerConnectionConfig.mShouldAcceptTlsExtensions = shouldAcceptTlsExtension; + + return this; + } + + /** + * Add an accepted TLS version for TLS connections with the home server. + * + * @param tlsVersion the tls version to add to the set of TLS versions accepted. + * @return this builder + */ + public Builder addAcceptedTlsVersion(@NonNull TlsVersion tlsVersion) { + if (mHomeServerConnectionConfig.mTlsVersions == null) { + mHomeServerConnectionConfig.mTlsVersions = new ArrayList<>(); + } + + mHomeServerConnectionConfig.mTlsVersions.add(tlsVersion); + + return this; + } + + /** + * Force the usage of TlsVersion. This can be usefull for device on Android version < 20 + * + * @param forceUsageOfTlsVersions set to true to force the usage of specified TlsVersions (with {@link #addAcceptedTlsVersion(TlsVersion)} + * @return this builder + */ + public Builder forceUsageOfTlsVersions(boolean forceUsageOfTlsVersions) { + mHomeServerConnectionConfig.mForceUsageTlsVersions = forceUsageOfTlsVersions; + + return this; + } + + /** + * Add a TLS cipher suite to the list of accepted TLS connections with the home server. + * + * @param tlsCipherSuite the tls cipher suite to add. + * @return this builder + */ + public Builder addAcceptedTlsCipherSuite(@NonNull CipherSuite tlsCipherSuite) { + if (mHomeServerConnectionConfig.mTlsCipherSuites == null) { + mHomeServerConnectionConfig.mTlsCipherSuites = new ArrayList<>(); + } + + mHomeServerConnectionConfig.mTlsCipherSuites.add(tlsCipherSuite); + + return this; + } + + /** + * Update the anti-virus server URI. + * + * @param antivirusServerUri the new anti-virus uri. Can be null + * @return this builder + */ + public Builder withAntiVirusServerUri(@Nullable Uri antivirusServerUri) { + if ((null != antivirusServerUri) && (!"http".equals(antivirusServerUri.getScheme()) && !"https".equals(antivirusServerUri.getScheme()))) { + throw new RuntimeException("Invalid antivirus server URI: " + antivirusServerUri); + } + + mHomeServerConnectionConfig.mAntiVirusServerUri = antivirusServerUri; + + return this; + } + + /** + * Convenient method to limit the TLS versions and cipher suites for this Builder + * Ref: + * - https://www.ssi.gouv.fr/uploads/2017/02/security-recommendations-for-tls_v1.1.pdf + * - https://developer.android.com/reference/javax/net/ssl/SSLEngine + * + * @param tlsLimitations true to use Tls limitations + * @param enableCompatibilityMode set to true for Android < 20 + * @return this builder + */ + public Builder withTlsLimitations(boolean tlsLimitations, boolean enableCompatibilityMode) { + if (tlsLimitations) { + withShouldAcceptTlsExtensions(false); + + // Tls versions + addAcceptedTlsVersion(TlsVersion.TLS_1_2); + addAcceptedTlsVersion(TlsVersion.TLS_1_3); + + forceUsageOfTlsVersions(enableCompatibilityMode); + + // Cipher suites + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256); + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256); + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256); + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256); + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384); + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384); + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256); + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256); + + if (enableCompatibilityMode) { + // Adopt some preceding cipher suites for Android < 20 to be able to negotiate + // a TLS session. + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA); + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA); + } + } + + return this; + } + + /** + * @param proxyHostname Proxy Hostname + * @param proxyPort Proxy Port + * @return this builder + */ + public Builder withProxy(@Nullable String proxyHostname, int proxyPort) { + mHomeServerConnectionConfig.mProxyHostname = proxyHostname; + mHomeServerConnectionConfig.mProxyPort = proxyPort; + return this; + } + + /** + * @return the {@link HomeServerConnectionConfig} + */ + public HomeServerConnectionConfig build() { + // Check mandatory parameters + if (mHomeServerConnectionConfig.mHomeServerUri == null) { + throw new RuntimeException("Home server URI not set"); + } + + return mHomeServerConnectionConfig; + } + + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/LoginStorage.java b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/LoginStorage.java new file mode 100755 index 0000000000..672053d4cc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/LoginStorage.java @@ -0,0 +1,207 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.legacy.riot; + +import android.content.Context; +import android.content.SharedPreferences; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; + +import timber.log.Timber; + +/* + * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose + */ + +/** + * Stores login credentials in SharedPreferences. + */ +public class LoginStorage { + private static final String PREFS_LOGIN = "Vector.LoginStorage"; + + // multi accounts + home server config + private static final String PREFS_KEY_CONNECTION_CONFIGS = "PREFS_KEY_CONNECTION_CONFIGS"; + + private final Context mContext; + + public LoginStorage(Context appContext) { + mContext = appContext.getApplicationContext(); + + } + + /** + * @return the list of home server configurations. + */ + public List getCredentialsList() { + SharedPreferences prefs = mContext.getSharedPreferences(PREFS_LOGIN, Context.MODE_PRIVATE); + + String connectionConfigsString = prefs.getString(PREFS_KEY_CONNECTION_CONFIGS, null); + + Timber.d("Got connection json: "); + + if (connectionConfigsString == null) { + return new ArrayList<>(); + } + + try { + JSONArray connectionConfigsStrings = new JSONArray(connectionConfigsString); + + List configList = new ArrayList<>( + connectionConfigsStrings.length() + ); + + for (int i = 0; i < connectionConfigsStrings.length(); i++) { + configList.add( + HomeServerConnectionConfig.fromJson(connectionConfigsStrings.getJSONObject(i)) + ); + } + + return configList; + } catch (JSONException e) { + Timber.e(e, "Failed to deserialize accounts"); + throw new RuntimeException("Failed to deserialize accounts"); + } + } + + /** + * Add a credentials to the credentials list + * + * @param config the home server config to add. + */ + public void addCredentials(HomeServerConnectionConfig config) { + if (null != config && config.getCredentials() != null) { + SharedPreferences prefs = mContext.getSharedPreferences(PREFS_LOGIN, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + + List configs = getCredentialsList(); + + configs.add(config); + + List serialized = new ArrayList<>(configs.size()); + + try { + for (HomeServerConnectionConfig c : configs) { + serialized.add(c.toJson()); + } + } catch (JSONException e) { + throw new RuntimeException("Failed to serialize connection config"); + } + + String ser = new JSONArray(serialized).toString(); + + Timber.d("Storing " + serialized.size() + " credentials"); + + editor.putString(PREFS_KEY_CONNECTION_CONFIGS, ser); + editor.apply(); + } + } + + /** + * Remove the credentials from credentials list + * + * @param config the credentials to remove + */ + public void removeCredentials(HomeServerConnectionConfig config) { + if (null != config && config.getCredentials() != null) { + Timber.d("Removing account: " + config.getCredentials().userId); + + SharedPreferences prefs = mContext.getSharedPreferences(PREFS_LOGIN, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + + List configs = getCredentialsList(); + List serialized = new ArrayList<>(configs.size()); + + boolean found = false; + try { + for (HomeServerConnectionConfig c : configs) { + if (c.getCredentials().userId.equals(config.getCredentials().userId)) { + found = true; + } else { + serialized.add(c.toJson()); + } + } + } catch (JSONException e) { + throw new RuntimeException("Failed to serialize connection config"); + } + + if (!found) return; + + String ser = new JSONArray(serialized).toString(); + + Timber.d("Storing " + serialized.size() + " credentials"); + + editor.putString(PREFS_KEY_CONNECTION_CONFIGS, ser); + editor.apply(); + } + } + + /** + * Replace the credential from credentials list, based on credentials.userId. + * If it does not match an existing credential it does *not* insert the new credentials. + * + * @param config the credentials to insert + */ + public void replaceCredentials(HomeServerConnectionConfig config) { + if (null != config && config.getCredentials() != null) { + SharedPreferences prefs = mContext.getSharedPreferences(PREFS_LOGIN, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + + List configs = getCredentialsList(); + List serialized = new ArrayList<>(configs.size()); + + boolean found = false; + try { + for (HomeServerConnectionConfig c : configs) { + if (c.getCredentials().userId.equals(config.getCredentials().userId)) { + serialized.add(config.toJson()); + found = true; + } else { + serialized.add(c.toJson()); + } + } + } catch (JSONException e) { + throw new RuntimeException("Failed to serialize connection config"); + } + + if (!found) return; + + String ser = new JSONArray(serialized).toString(); + + Timber.d("Storing " + serialized.size() + " credentials"); + + editor.putString(PREFS_KEY_CONNECTION_CONFIGS, ser); + editor.apply(); + } + } + + /** + * Clear the stored values + */ + public void clear() { + SharedPreferences prefs = mContext.getSharedPreferences(PREFS_LOGIN, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + editor.remove(PREFS_KEY_CONNECTION_CONFIGS); + //Need to commit now because called before forcing an app restart + editor.commit(); + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnown.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnown.kt new file mode 100644 index 0000000000..234cd72689 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnown.kt @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.legacy.riot + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/* + * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose + */ + +/** + * https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery + *
+ * {
+ *     "m.homeserver": {
+ *         "base_url": "https://matrix.org"
+ *     },
+ *     "m.identity_server": {
+ *         "base_url": "https://vector.im"
+ *     }
+ *     "m.integrations": {
+ *          "managers": [
+ *              {
+ *                  "api_url": "https://integrations.example.org",
+ *                  "ui_url": "https://integrations.example.org/ui"
+ *              },
+ *              {
+ *                  "api_url": "https://bots.example.org"
+ *              }
+ *          ]
+ *    }
+ *     "im.vector.riot.jitsi": {
+ *         "preferredDomain": "https://jitsi.riot.im/"
+ *     }
+ * }
+ * 
+ */ +@JsonClass(generateAdapter = true) +class WellKnown { + + @JvmField + @Json(name = "m.homeserver") + var homeServer: WellKnownBaseConfig? = null + + @JvmField + @Json(name = "m.identity_server") + var identityServer: WellKnownBaseConfig? = null + + @JvmField + @Json(name = "m.integrations") + var integrations: Map? = null + + /** + * Returns the list of integration managers proposed + */ + fun getIntegrationManagers(): List { + val managers = ArrayList() + integrations?.get("managers")?.let { + (it as? ArrayList<*>)?.let { configs -> + configs.forEach { config -> + (config as? Map<*, *>)?.let { map -> + val apiUrl = map["api_url"] as? String + val uiUrl = map["ui_url"] as? String ?: apiUrl + if (apiUrl != null + && apiUrl.startsWith("https://") + && uiUrl!!.startsWith("https://")) { + managers.add(WellKnownManagerConfig( + apiUrl = apiUrl, + uiUrl = uiUrl + )) + } + } + } + } + } + return managers + } + + @JvmField + @Json(name = "im.vector.riot.jitsi") + var jitsiServer: WellKnownPreferredConfig? = null +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnownBaseConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnownBaseConfig.kt new file mode 100644 index 0000000000..e9efccce3c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnownBaseConfig.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.legacy.riot + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/* + * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose + */ + +/** + * https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery + *
+ * {
+ *     "base_url": "https://vector.im"
+ * }
+ * 
+ */ +@JsonClass(generateAdapter = true) +class WellKnownBaseConfig { + + @JvmField + @Json(name = "base_url") + var baseURL: String? = null +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnownManagerConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnownManagerConfig.kt new file mode 100644 index 0000000000..f93d90af26 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnownManagerConfig.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.legacy.riot + +/* + * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose + */ + +data class WellKnownManagerConfig( + val apiUrl : String, + val uiUrl: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnownPreferredConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnownPreferredConfig.kt new file mode 100644 index 0000000000..5a918d6f0d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnownPreferredConfig.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.legacy.riot + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/* + * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose + */ + +/** + * https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery + *
+ * {
+ *     "preferredDomain": "https://jitsi.riot.im/"
+ * }
+ * 
+ */ +@JsonClass(generateAdapter = true) +class WellKnownPreferredConfig { + + @JvmField + @Json(name = "preferredDomain") + var preferredDomain: String? = null +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/AccessTokenInterceptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/AccessTokenInterceptor.kt new file mode 100644 index 0000000000..6f5b07b229 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/AccessTokenInterceptor.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.network + +import org.matrix.android.sdk.internal.network.token.AccessTokenProvider +import okhttp3.Interceptor +import okhttp3.Response + +internal class AccessTokenInterceptor(private val accessTokenProvider: AccessTokenProvider) : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + var request = chain.request() + + // Add the access token to all requests if it is set + accessTokenProvider.getToken()?.let { token -> + val newRequestBuilder = request.newBuilder() + newRequestBuilder.header(HttpHeaders.Authorization, "Bearer $token") + request = newRequestBuilder.build() + } + + return chain.proceed(request) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/HttpHeaders.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/HttpHeaders.kt new file mode 100644 index 0000000000..f15b0353e2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/HttpHeaders.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.network + +object HttpHeaders { + + const val Authorization = "Authorization" + const val UserAgent = "User-Agent" +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkCallbackStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkCallbackStrategy.kt new file mode 100644 index 0000000000..caf5090ad6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkCallbackStrategy.kt @@ -0,0 +1,88 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.network + +import android.annotation.TargetApi +import android.content.Context +import android.content.IntentFilter +import android.net.ConnectivityManager +import android.net.Network +import android.os.Build +import androidx.core.content.getSystemService +import timber.log.Timber +import javax.inject.Inject + +internal interface NetworkCallbackStrategy { + fun register(hasChanged: () -> Unit) + fun unregister() +} + +internal class FallbackNetworkCallbackStrategy @Inject constructor(private val context: Context, + private val networkInfoReceiver: NetworkInfoReceiver) : NetworkCallbackStrategy { + + @Suppress("DEPRECATION") + val filter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION) + + override fun register(hasChanged: () -> Unit) { + networkInfoReceiver.isConnectedCallback = { + hasChanged() + } + context.registerReceiver(networkInfoReceiver, filter) + } + + override fun unregister() { + networkInfoReceiver.isConnectedCallback = null + context.unregisterReceiver(networkInfoReceiver) + } +} + +@TargetApi(Build.VERSION_CODES.N) +internal class PreferredNetworkCallbackStrategy @Inject constructor(context: Context) : NetworkCallbackStrategy { + + private var hasChangedCallback: (() -> Unit)? = null + private val conn = context.getSystemService()!! + private val networkCallback = object : ConnectivityManager.NetworkCallback() { + + override fun onLost(network: Network) { + hasChangedCallback?.invoke() + } + + override fun onAvailable(network: Network) { + hasChangedCallback?.invoke() + } + } + + override fun register(hasChanged: () -> Unit) { + hasChangedCallback = hasChanged + conn.registerDefaultNetworkCallback(networkCallback) + } + + override fun unregister() { + // It can crash after an application update, if not registered + val doUnregister = hasChangedCallback != null + hasChangedCallback = null + if (doUnregister) { + // Add a try catch for safety + try { + conn.unregisterNetworkCallback(networkCallback) + } catch (t: Throwable) { + Timber.e(t, "Unable to unregister network callback") + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConnectivityChecker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConnectivityChecker.kt new file mode 100644 index 0000000000..1f3e77d800 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConnectivityChecker.kt @@ -0,0 +1,110 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.network + +import androidx.annotation.WorkerThread +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.homeserver.HomeServerPinger +import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver +import kotlinx.coroutines.runBlocking +import java.util.Collections +import java.util.concurrent.atomic.AtomicBoolean +import javax.inject.Inject + +interface NetworkConnectivityChecker { + /** + * Returns true when internet is available + */ + @WorkerThread + fun hasInternetAccess(forcePing: Boolean): Boolean + + fun register(listener: Listener) + fun unregister(listener: Listener) + + interface Listener { + fun onConnectivityChanged() + } +} + +@SessionScope +internal class DefaultNetworkConnectivityChecker @Inject constructor(private val homeServerPinger: HomeServerPinger, + private val backgroundDetectionObserver: BackgroundDetectionObserver, + private val networkCallbackStrategy: NetworkCallbackStrategy) + : NetworkConnectivityChecker { + + private val hasInternetAccess = AtomicBoolean(true) + private val listeners = Collections.synchronizedSet(LinkedHashSet()) + private val backgroundDetectionObserverListener = object : BackgroundDetectionObserver.Listener { + override fun onMoveToForeground() { + bind() + } + + override fun onMoveToBackground() { + unbind() + } + } + + /** + * Returns true when internet is available + */ + @WorkerThread + override fun hasInternetAccess(forcePing: Boolean): Boolean { + return if (forcePing) { + runBlocking { + homeServerPinger.canReachHomeServer() + } + } else { + hasInternetAccess.get() + } + } + + override fun register(listener: NetworkConnectivityChecker.Listener) { + if (listeners.isEmpty()) { + if (backgroundDetectionObserver.isInBackground) { + unbind() + } else { + bind() + } + backgroundDetectionObserver.register(backgroundDetectionObserverListener) + } + listeners.add(listener) + } + + override fun unregister(listener: NetworkConnectivityChecker.Listener) { + listeners.remove(listener) + if (listeners.isEmpty()) { + backgroundDetectionObserver.unregister(backgroundDetectionObserverListener) + } + } + + private fun bind() { + networkCallbackStrategy.register { + val localListeners = listeners.toList() + localListeners.forEach { + it.onConnectivityChanged() + } + } + homeServerPinger.canReachHomeServer { + hasInternetAccess.set(it) + } + } + + private fun unbind() { + networkCallbackStrategy.unregister() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt new file mode 100644 index 0000000000..eb8ea2dc68 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.network + +internal object NetworkConstants { + + private const val URI_API_PREFIX_PATH = "_matrix/client" + const val URI_API_PREFIX_PATH_ = "$URI_API_PREFIX_PATH/" + const val URI_API_PREFIX_PATH_R0 = "$URI_API_PREFIX_PATH/r0/" + const val URI_API_PREFIX_PATH_UNSTABLE = "$URI_API_PREFIX_PATH/unstable/" + + // Media + private const val URI_API_MEDIA_PREFIX_PATH = "_matrix/media" + const val URI_API_MEDIA_PREFIX_PATH_R0 = "$URI_API_MEDIA_PREFIX_PATH/r0/" + + // Identity server + const val URI_IDENTITY_PREFIX_PATH = "_matrix/identity/v2" + const val URI_IDENTITY_PATH_V2 = "$URI_IDENTITY_PREFIX_PATH/" + + const val URI_INTEGRATION_MANAGER_PATH = "_matrix/integrations/v1/" +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkInfoReceiver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkInfoReceiver.kt new file mode 100644 index 0000000000..85a69c853e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkInfoReceiver.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +// This BroadcastReceiver is used only if the build code is below 24. +@file:Suppress("DEPRECATION") + +package org.matrix.android.sdk.internal.network + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.net.ConnectivityManager +import android.net.NetworkInfo +import androidx.core.content.getSystemService +import javax.inject.Inject + +internal class NetworkInfoReceiver @Inject constructor() : BroadcastReceiver() { + + var isConnectedCallback: ((Boolean) -> Unit)? = null + + override fun onReceive(context: Context, intent: Intent) { + val conn = context.getSystemService()!! + val networkInfo: NetworkInfo? = conn.activeNetworkInfo + isConnectedCallback?.invoke(networkInfo?.isConnected ?: false) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ProgressRequestBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ProgressRequestBody.kt new file mode 100644 index 0000000000..7ce260e54e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ProgressRequestBody.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.network + +import okhttp3.MediaType +import okhttp3.RequestBody +import okio.Buffer +import okio.BufferedSink +import okio.ForwardingSink +import okio.Sink +import okio.buffer +import java.io.IOException + +internal class ProgressRequestBody(private val delegate: RequestBody, + private val listener: Listener) : RequestBody() { + + private lateinit var countingSink: CountingSink + + override fun contentType(): MediaType? { + return delegate.contentType() + } + + override fun contentLength(): Long { + try { + return delegate.contentLength() + } catch (e: IOException) { + e.printStackTrace() + } + + return -1 + } + + @Throws(IOException::class) + override fun writeTo(sink: BufferedSink) { + countingSink = CountingSink(sink) + val bufferedSink = countingSink.buffer() + delegate.writeTo(bufferedSink) + bufferedSink.flush() + } + + private inner class CountingSink(delegate: Sink) : ForwardingSink(delegate) { + + private var bytesWritten: Long = 0 + + @Throws(IOException::class) + override fun write(source: Buffer, byteCount: Long) { + super.write(source, byteCount) + bytesWritten += byteCount + listener.onProgress(bytesWritten, contentLength()) + } + } + + interface Listener { + fun onProgress(current: Long, total: Long) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt new file mode 100644 index 0000000000..52556e4c2d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.network + +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.shouldBeRetried +import org.matrix.android.sdk.internal.network.ssl.CertUtil +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.delay +import org.greenrobot.eventbus.EventBus +import retrofit2.Call +import retrofit2.awaitResponse +import java.io.IOException + +internal suspend inline fun executeRequest(eventBus: EventBus?, + block: Request.() -> Unit) = Request(eventBus).apply(block).execute() + +internal class Request(private val eventBus: EventBus?) { + + var isRetryable = false + var initialDelay: Long = 100L + var maxDelay: Long = 10_000L + var maxRetryCount = Int.MAX_VALUE + private var currentRetryCount = 0 + private var currentDelay = initialDelay + lateinit var apiCall: Call + + suspend fun execute(): DATA { + return try { + val response = apiCall.clone().awaitResponse() + if (response.isSuccessful) { + response.body() + ?: throw IllegalStateException("The request returned a null body") + } else { + throw response.toFailure(eventBus) + } + } catch (exception: Throwable) { + // Check if this is a certificateException + CertUtil.getCertificateException(exception) + // TODO Support certificate error once logged + // ?.also { unrecognizedCertificateException -> + // // Send the error to the bus, for a global management + // eventBus?.post(GlobalError.CertificateError(unrecognizedCertificateException)) + // } + ?.also { unrecognizedCertificateException -> throw unrecognizedCertificateException } + + if (isRetryable && currentRetryCount++ < maxRetryCount && exception.shouldBeRetried()) { + delay(currentDelay) + currentDelay = (currentDelay * 2L).coerceAtMost(maxDelay) + return execute() + } else { + throw when (exception) { + is IOException -> Failure.NetworkConnection(exception) + is Failure.ServerError, + is Failure.OtherServerError -> exception + is CancellationException -> Failure.Cancelled(exception) + else -> Failure.Unknown(exception) + } + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitExtensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitExtensions.kt new file mode 100644 index 0000000000..b7eacfb4f5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitExtensions.kt @@ -0,0 +1,102 @@ +/* + * + * * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * * + * * 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. + * + */ + +package org.matrix.android.sdk.internal.network + +import com.squareup.moshi.JsonEncodingException +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.GlobalError +import org.matrix.android.sdk.api.failure.MatrixError +import org.matrix.android.sdk.internal.di.MoshiProvider +import kotlinx.coroutines.suspendCancellableCoroutine +import okhttp3.ResponseBody +import org.greenrobot.eventbus.EventBus +import retrofit2.Response +import timber.log.Timber +import java.io.IOException +import java.net.HttpURLConnection +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +internal suspend fun okhttp3.Call.awaitResponse(): okhttp3.Response { + return suspendCancellableCoroutine { continuation -> + continuation.invokeOnCancellation { + cancel() + } + + enqueue(object : okhttp3.Callback { + override fun onResponse(call: okhttp3.Call, response: okhttp3.Response) { + continuation.resume(response) + } + + override fun onFailure(call: okhttp3.Call, e: IOException) { + continuation.resumeWithException(e) + } + }) + } +} + +/** + * Convert a retrofit Response to a Failure, and eventually parse errorBody to convert it to a MatrixError + */ +internal fun Response.toFailure(eventBus: EventBus?): Failure { + return toFailure(errorBody(), code(), eventBus) +} + +/** + * Convert a okhttp3 Response to a Failure, and eventually parse errorBody to convert it to a MatrixError + */ +internal fun okhttp3.Response.toFailure(eventBus: EventBus?): Failure { + return toFailure(body, code, eventBus) +} + +private fun toFailure(errorBody: ResponseBody?, httpCode: Int, eventBus: EventBus?): Failure { + if (errorBody == null) { + return Failure.Unknown(RuntimeException("errorBody should not be null")) + } + + val errorBodyStr = errorBody.string() + + val matrixErrorAdapter = MoshiProvider.providesMoshi().adapter(MatrixError::class.java) + + try { + val matrixError = matrixErrorAdapter.fromJson(errorBodyStr) + + if (matrixError != null) { + if (matrixError.code == MatrixError.M_CONSENT_NOT_GIVEN && !matrixError.consentUri.isNullOrBlank()) { + // Also send this error to the bus, for a global management + eventBus?.post(GlobalError.ConsentNotGivenError(matrixError.consentUri)) + } else if (httpCode == HttpURLConnection.HTTP_UNAUTHORIZED /* 401 */ + && matrixError.code == MatrixError.M_UNKNOWN_TOKEN) { + // Also send this error to the bus, for a global management + eventBus?.post(GlobalError.InvalidToken(matrixError.isSoftLogout)) + } + + return Failure.ServerError(matrixError, httpCode) + } + } catch (ex: Exception) { + // This is not a MatrixError + Timber.w("The error returned by the server is not a MatrixError") + } catch (ex: JsonEncodingException) { + // This is not a MatrixError, HTML code? + Timber.w("The error returned by the server is not a MatrixError, probably HTML string") + } + + return Failure.OtherServerError(errorBodyStr, httpCode) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitFactory.kt new file mode 100644 index 0000000000..368611dd7d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitFactory.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.network + +import com.squareup.moshi.Moshi +import dagger.Lazy +import org.matrix.android.sdk.internal.util.ensureTrailingSlash +import okhttp3.Call +import okhttp3.OkHttpClient +import okhttp3.Request +import retrofit2.Retrofit +import retrofit2.converter.moshi.MoshiConverterFactory +import retrofit2.converter.scalars.ScalarsConverterFactory +import javax.inject.Inject + +internal class RetrofitFactory @Inject constructor(private val moshi: Moshi) { + + /** + * Use only for authentication service + */ + fun create(okHttpClient: OkHttpClient, baseUrl: String): Retrofit { + return Retrofit.Builder() + .baseUrl(baseUrl.ensureTrailingSlash()) + .client(okHttpClient) + .addConverterFactory(UnitConverterFactory) + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .build() + } + + fun create(okHttpClient: Lazy, baseUrl: String): Retrofit { + return Retrofit.Builder() + .baseUrl(baseUrl.ensureTrailingSlash()) + .callFactory(object : Call.Factory { + override fun newCall(request: Request): Call { + return okHttpClient.get().newCall(request) + } + }) + .addConverterFactory(ScalarsConverterFactory.create()) + .addConverterFactory(UnitConverterFactory) + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .build() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/TimeOutInterceptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/TimeOutInterceptor.kt new file mode 100644 index 0000000000..4fb8f513d0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/TimeOutInterceptor.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.network + +import okhttp3.Interceptor +import okhttp3.Response +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +/** + * Get the specific headers to apply specific timeout + * Inspired from https://github.com/square/retrofit/issues/2561 + */ +internal class TimeOutInterceptor @Inject constructor() : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + var request = chain.request() + + val connectTimeout = request.header(CONNECT_TIMEOUT)?.let { Integer.valueOf(it) } ?: chain.connectTimeoutMillis() + val readTimeout = request.header(READ_TIMEOUT)?.let { Integer.valueOf(it) } ?: chain.readTimeoutMillis() + val writeTimeout = request.header(WRITE_TIMEOUT)?.let { Integer.valueOf(it) } ?: chain.writeTimeoutMillis() + + val newRequestBuilder = request.newBuilder() + .removeHeader(CONNECT_TIMEOUT) + .removeHeader(READ_TIMEOUT) + .removeHeader(WRITE_TIMEOUT) + + request = newRequestBuilder.build() + + return chain + .withConnectTimeout(connectTimeout, TimeUnit.MILLISECONDS) + .withReadTimeout(readTimeout, TimeUnit.MILLISECONDS) + .withWriteTimeout(writeTimeout, TimeUnit.MILLISECONDS) + .proceed(request) + } + + companion object { + // Custom header name + const val CONNECT_TIMEOUT = "CONNECT_TIMEOUT" + const val READ_TIMEOUT = "READ_TIMEOUT" + const val WRITE_TIMEOUT = "WRITE_TIMEOUT" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UnitConverterFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UnitConverterFactory.kt new file mode 100644 index 0000000000..dfcf041261 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UnitConverterFactory.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.network + +import okhttp3.ResponseBody +import retrofit2.Converter +import retrofit2.Retrofit +import java.lang.reflect.Type + +object UnitConverterFactory : Converter.Factory() { + override fun responseBodyConverter(type: Type, annotations: Array, + retrofit: Retrofit): Converter? { + return if (type == Unit::class.java) UnitConverter else null + } + + private object UnitConverter : Converter { + override fun convert(value: ResponseBody) { + value.close() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UserAgentHolder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UserAgentHolder.kt new file mode 100644 index 0000000000..9a5451bdde --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UserAgentHolder.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.network + +import android.content.Context +import org.matrix.android.sdk.BuildConfig +import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.internal.di.MatrixScope +import timber.log.Timber +import javax.inject.Inject + +@MatrixScope +internal class UserAgentHolder @Inject constructor(private val context: Context, + matrixConfiguration: MatrixConfiguration) { + + var userAgent: String = "" + private set + + init { + setApplicationFlavor(matrixConfiguration.applicationFlavor) + } + + /** + * Create an user agent with the application version. + * Ex: RiotX/1.0.0 (Linux; U; Android 6.0.1; SM-A510F Build/MMB29; Flavour GPlay; MatrixAndroidSDK_X 1.0) + * + * @param flavorDescription the flavor description + */ + private fun setApplicationFlavor(flavorDescription: String) { + var appName = "" + var appVersion = "" + + try { + val appPackageName = context.applicationContext.packageName + val pm = context.packageManager + val appInfo = pm.getApplicationInfo(appPackageName, 0) + appName = pm.getApplicationLabel(appInfo).toString() + + val pkgInfo = pm.getPackageInfo(context.applicationContext.packageName, 0) + appVersion = pkgInfo.versionName ?: "" + + // Use appPackageName instead of appName if appName contains any non-ASCII character + if (!appName.matches("\\A\\p{ASCII}*\\z".toRegex())) { + appName = appPackageName + } + } catch (e: Exception) { + Timber.e(e, "## initUserAgent() : failed") + } + + val systemUserAgent = System.getProperty("http.agent") + + // cannot retrieve the application version + if (appName.isEmpty() || appVersion.isEmpty()) { + if (null == systemUserAgent) { + userAgent = "Java" + System.getProperty("java.version") + } + return + } + + // if there is no user agent or cannot parse it + if (null == systemUserAgent || systemUserAgent.lastIndexOf(")") == -1 || !systemUserAgent.contains("(")) { + userAgent = (appName + "/" + appVersion + " ( Flavour " + flavorDescription + + "; MatrixAndroidSDK_X " + BuildConfig.VERSION_NAME + ")") + } else { + // update + userAgent = appName + "/" + appVersion + " " + + systemUserAgent.substring(systemUserAgent.indexOf("("), systemUserAgent.lastIndexOf(")") - 1) + + "; Flavour " + flavorDescription + + "; MatrixAndroidSDK_X " + BuildConfig.VERSION_NAME + ")" + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UserAgentInterceptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UserAgentInterceptor.kt new file mode 100644 index 0000000000..1099751112 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UserAgentInterceptor.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.network + +import okhttp3.Interceptor +import okhttp3.Response +import javax.inject.Inject + +internal class UserAgentInterceptor @Inject constructor(private val userAgentHolder: UserAgentHolder) : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + var request = chain.request() + val newRequestBuilder = request.newBuilder() + // Add the user agent to all requests if it is set + userAgentHolder.userAgent + .takeIf { it.isNotBlank() } + ?.let { + newRequestBuilder.header(HttpHeaders.UserAgent, it) + } + request = newRequestBuilder.build() + return chain.proceed(request) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/httpclient/OkHttpClientUtil.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/httpclient/OkHttpClientUtil.kt new file mode 100644 index 0000000000..94d1d95857 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/httpclient/OkHttpClientUtil.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.network.httpclient + +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import org.matrix.android.sdk.internal.network.AccessTokenInterceptor +import org.matrix.android.sdk.internal.network.interceptors.CurlLoggingInterceptor +import org.matrix.android.sdk.internal.network.ssl.CertUtil +import org.matrix.android.sdk.internal.network.token.AccessTokenProvider +import okhttp3.OkHttpClient +import timber.log.Timber + +internal fun OkHttpClient.Builder.addAccessTokenInterceptor(accessTokenProvider: AccessTokenProvider): OkHttpClient.Builder { + // Remove the previous CurlLoggingInterceptor, to add it after the accessTokenInterceptor + val existingCurlInterceptors = interceptors().filterIsInstance() + interceptors().removeAll(existingCurlInterceptors) + + addInterceptor(AccessTokenInterceptor(accessTokenProvider)) + + // Re add eventually the curl logging interceptors + existingCurlInterceptors.forEach { + addInterceptor(it) + } + + return this +} + +internal fun OkHttpClient.Builder.addSocketFactory(homeServerConnectionConfig: HomeServerConnectionConfig): OkHttpClient.Builder { + try { + val pair = CertUtil.newPinnedSSLSocketFactory(homeServerConnectionConfig) + sslSocketFactory(pair.sslSocketFactory, pair.x509TrustManager) + hostnameVerifier(CertUtil.newHostnameVerifier(homeServerConnectionConfig)) + connectionSpecs(CertUtil.newConnectionSpecs(homeServerConnectionConfig)) + } catch (e: Exception) { + Timber.e(e, "addSocketFactory failed") + } + + return this +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/ForceToBoolean.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/ForceToBoolean.kt new file mode 100644 index 0000000000..42a63e0c56 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/ForceToBoolean.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.network.parsing + +import com.squareup.moshi.FromJson +import com.squareup.moshi.JsonQualifier +import com.squareup.moshi.JsonReader +import com.squareup.moshi.ToJson +import timber.log.Timber + +@JsonQualifier +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.FUNCTION) +annotation class ForceToBoolean + +internal class ForceToBooleanJsonAdapter { + @ToJson + fun toJson(@ForceToBoolean b: Boolean): Boolean { + return b + } + + @FromJson + @ForceToBoolean + fun fromJson(reader: JsonReader): Boolean { + return when (val token = reader.peek()) { + JsonReader.Token.NUMBER -> reader.nextInt() != 0 + JsonReader.Token.BOOLEAN -> reader.nextBoolean() + else -> { + Timber.e("Expecting a boolean or a int but get: $token") + reader.skipValue() + false + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/RuntimeJsonAdapterFactory.java b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/RuntimeJsonAdapterFactory.java new file mode 100644 index 0000000000..a49660523e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/RuntimeJsonAdapterFactory.java @@ -0,0 +1,170 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.network.parsing; + +import com.squareup.moshi.JsonAdapter; +import com.squareup.moshi.JsonDataException; +import com.squareup.moshi.JsonReader; +import com.squareup.moshi.JsonWriter; +import com.squareup.moshi.Moshi; +import com.squareup.moshi.Types; + +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import javax.annotation.CheckReturnValue; + +/** + * A JsonAdapter factory for polymorphic types. This is useful when the type is not known before + * decoding the JSON. This factory's adapters expect JSON in the format of a JSON object with a + * key whose value is a label that determines the type to which to map the JSON object. + */ +public final class RuntimeJsonAdapterFactory implements JsonAdapter.Factory { + final Class baseType; + final String labelKey; + final Class fallbackType; + final Map labelToType = new LinkedHashMap<>(); + + /** + * @param baseType The base type for which this factory will create adapters. Cannot be Object. + * @param labelKey The key in the JSON object whose value determines the type to which to map the + * JSON object. + */ + @CheckReturnValue + public static RuntimeJsonAdapterFactory of(Class baseType, String labelKey, Class fallbackType) { + if (baseType == null) throw new NullPointerException("baseType == null"); + if (labelKey == null) throw new NullPointerException("labelKey == null"); + if (baseType == Object.class) { + throw new IllegalArgumentException( + "The base type must not be Object. Consider using a marker interface."); + } + return new RuntimeJsonAdapterFactory<>(baseType, labelKey, fallbackType); + } + + RuntimeJsonAdapterFactory(Class baseType, String labelKey, Class fallbackType) { + this.baseType = baseType; + this.labelKey = labelKey; + this.fallbackType = fallbackType; + } + + /** + * Register the subtype that can be created based on the label. When an unknown type is found + * during encoding an {@linkplain IllegalArgumentException} will be thrown. When an unknown label + * is found during decoding a {@linkplain JsonDataException} will be thrown. + */ + public RuntimeJsonAdapterFactory registerSubtype(Class subtype, String label) { + if (subtype == null) throw new NullPointerException("subtype == null"); + if (label == null) throw new NullPointerException("label == null"); + if (labelToType.containsKey(label) || labelToType.containsValue(subtype)) { + throw new IllegalArgumentException("Subtypes and labels must be unique."); + } + labelToType.put(label, subtype); + return this; + } + + @Override + public JsonAdapter create(Type type, Set annotations, Moshi moshi) { + if (Types.getRawType(type) != baseType || !annotations.isEmpty()) { + return null; + } + int size = labelToType.size(); + Map> labelToAdapter = new LinkedHashMap<>(size); + Map typeToLabel = new LinkedHashMap<>(size); + for (Map.Entry entry : labelToType.entrySet()) { + String label = entry.getKey(); + Type typeValue = entry.getValue(); + typeToLabel.put(typeValue, label); + labelToAdapter.put(label, moshi.adapter(typeValue)); + } + + final JsonAdapter fallbackAdapter = moshi.adapter(fallbackType); + JsonAdapter objectJsonAdapter = moshi.adapter(Object.class); + + return new RuntimeJsonAdapter(labelKey, labelToAdapter, typeToLabel, + objectJsonAdapter, fallbackAdapter).nullSafe(); + } + + static final class RuntimeJsonAdapter extends JsonAdapter { + final String labelKey; + final Map> labelToAdapter; + final Map typeToLabel; + final JsonAdapter objectJsonAdapter; + final JsonAdapter fallbackAdapter; + + RuntimeJsonAdapter(String labelKey, Map> labelToAdapter, + Map typeToLabel, JsonAdapter objectJsonAdapter, + JsonAdapter fallbackAdapter) { + this.labelKey = labelKey; + this.labelToAdapter = labelToAdapter; + this.typeToLabel = typeToLabel; + this.objectJsonAdapter = objectJsonAdapter; + this.fallbackAdapter = fallbackAdapter; + } + + @Override + public Object fromJson(JsonReader reader) throws IOException { + JsonReader.Token peekedToken = reader.peek(); + if (peekedToken != JsonReader.Token.BEGIN_OBJECT) { + throw new JsonDataException("Expected BEGIN_OBJECT but was " + peekedToken + + " at path " + reader.getPath()); + } + Object jsonValue = reader.readJsonValue(); + Map jsonObject = (Map) jsonValue; + Object label = jsonObject.get(labelKey); + if (!(label instanceof String)) { + return null; + } + JsonAdapter adapter = labelToAdapter.get(label); + if (adapter == null) { + return fallbackAdapter.fromJsonValue(jsonValue); + } + return adapter.fromJsonValue(jsonValue); + } + + @Override + public void toJson(JsonWriter writer, Object value) throws IOException { + Class type = value.getClass(); + String label = typeToLabel.get(type); + if (label == null) { + throw new IllegalArgumentException("Expected one of " + + typeToLabel.keySet() + + " but found " + + value + + ", a " + + value.getClass() + + ". Register this subtype."); + } + JsonAdapter adapter = labelToAdapter.get(label); + Map jsonValue = (Map) adapter.toJsonValue(value); + + Map valueWithLabel = new LinkedHashMap<>(1 + jsonValue.size()); + valueWithLabel.put(labelKey, label); + valueWithLabel.putAll(jsonValue); + objectJsonAdapter.toJson(writer, valueWithLabel); + } + + @Override + public String toString() { + return "RuntimeJsonAdapter(" + labelKey + ")"; + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/UriMoshiAdapter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/UriMoshiAdapter.kt new file mode 100644 index 0000000000..f45c89304b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/UriMoshiAdapter.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.network.parsing + +import android.net.Uri +import com.squareup.moshi.FromJson +import com.squareup.moshi.ToJson + +internal class UriMoshiAdapter { + + @ToJson + fun toJson(uri: Uri): String { + return uri.toString() + } + + @FromJson + fun fromJson(uriString: String): Uri { + return Uri.parse(uriString) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/CertUtil.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/CertUtil.kt new file mode 100644 index 0000000000..40a8e29829 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/CertUtil.kt @@ -0,0 +1,262 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.network.ssl + +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import okhttp3.ConnectionSpec +import okhttp3.internal.tls.OkHostnameVerifier +import timber.log.Timber +import java.security.KeyStore +import java.security.MessageDigest +import java.security.cert.CertificateException +import java.security.cert.X509Certificate +import javax.net.ssl.HostnameVerifier +import javax.net.ssl.SSLContext +import javax.net.ssl.SSLPeerUnverifiedException +import javax.net.ssl.SSLSocketFactory +import javax.net.ssl.TrustManager +import javax.net.ssl.TrustManagerFactory +import javax.net.ssl.X509TrustManager + +/** + * Various utility classes for dealing with X509Certificates + */ +internal object CertUtil { + + // Set to false to do some test + private const val USE_DEFAULT_HOSTNAME_VERIFIER = true + + private val hexArray = "0123456789ABCDEF".toCharArray() + + /** + * Generates the SHA-256 fingerprint of the given certificate + * + * @param cert the certificate. + * @return the finger print + * @throws CertificateException the certificate exception + */ + @Throws(CertificateException::class) + fun generateSha256Fingerprint(cert: X509Certificate): ByteArray { + return generateFingerprint(cert, "SHA-256") + } + + /** + * Generates the SHA-1 fingerprint of the given certificate + * + * @param cert the certificated + * @return the SHA1 fingerprint + * @throws CertificateException the certificate exception + */ + @Throws(CertificateException::class) + fun generateSha1Fingerprint(cert: X509Certificate): ByteArray { + return generateFingerprint(cert, "SHA-1") + } + + /** + * Generate the fingerprint for a dedicated type. + * + * @param cert the certificate + * @param type the type + * @return the fingerprint + * @throws CertificateException certificate exception + */ + @Throws(CertificateException::class) + private fun generateFingerprint(cert: X509Certificate, type: String): ByteArray { + val fingerprint: ByteArray + val md: MessageDigest + try { + md = MessageDigest.getInstance(type) + } catch (e: Exception) { + // This really *really* shouldn't throw, as java should always have a SHA-256 and SHA-1 impl. + throw CertificateException(e) + } + + fingerprint = md.digest(cert.encoded) + + return fingerprint + } + + /** + * Convert the fingerprint to an hexa string. + * + * @param fingerprint the fingerprint + * @return the hexa string. + */ + fun fingerprintToHexString(fingerprint: ByteArray, sep: Char = ' '): String { + val hexChars = CharArray(fingerprint.size * 3) + for (j in fingerprint.indices) { + val v = (fingerprint[j].toInt() and 0xFF) + hexChars[j * 3] = hexArray[v.ushr(4)] + hexChars[j * 3 + 1] = hexArray[v and 0x0F] + hexChars[j * 3 + 2] = sep + } + return String(hexChars, 0, hexChars.size - 1) + } + + /** + * Recursively checks the exception to see if it was caused by an + * UnrecognizedCertificateException + * + * @param root the throwable. + * @return The UnrecognizedCertificateException if exists, else null. + */ + fun getCertificateException(root: Throwable?): UnrecognizedCertificateException? { + var e = root + var i = 0 // Just in case there is a getCause loop + while (e != null && i < 10) { + if (e is UnrecognizedCertificateException) { + return e + } + e = e.cause + i++ + } + + return null + } + + internal data class PinnedSSLSocketFactory( + val sslSocketFactory: SSLSocketFactory, + val x509TrustManager: X509TrustManager + ) + + /** + * Create a SSLSocket factory for a HS config. + * + * @param hsConfig the HS config. + * @return SSLSocket factory + */ + fun newPinnedSSLSocketFactory(hsConfig: HomeServerConnectionConfig): PinnedSSLSocketFactory { + try { + var defaultTrustManager: X509TrustManager? = null + + // If we haven't specified that we wanted to shouldPin the certs, fallback to standard + // X509 checks if fingerprints don't match. + if (!hsConfig.shouldPin) { + var tf: TrustManagerFactory? = null + + // get the PKIX instance + try { + tf = TrustManagerFactory.getInstance("PKIX") + } catch (e: Exception) { + Timber.e(e, "## newPinnedSSLSocketFactory() : TrustManagerFactory.getInstance failed") + } + + // it doesn't exist, use the default one. + if (null == tf) { + try { + tf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) + } catch (e: Exception) { + Timber.e(e, "## newPinnedSSLSocketFactory() : TrustManagerFactory.getInstance of default failed") + } + } + + tf!!.init(null as KeyStore?) + val trustManagers = tf.trustManagers + + for (i in trustManagers.indices) { + if (trustManagers[i] is X509TrustManager) { + defaultTrustManager = trustManagers[i] as X509TrustManager + break + } + } + } + + val trustPinned = arrayOf(PinnedTrustManagerProvider.provide(hsConfig.allowedFingerprints, defaultTrustManager)) + + val sslSocketFactory: SSLSocketFactory + + if (hsConfig.forceUsageTlsVersions && hsConfig.tlsVersions != null) { + // Force usage of accepted Tls Versions for Android < 20 + sslSocketFactory = TLSSocketFactory(trustPinned, hsConfig.tlsVersions) + } else { + val sslContext = SSLContext.getInstance("TLS") + sslContext.init(null, trustPinned, java.security.SecureRandom()) + sslSocketFactory = sslContext.socketFactory + } + + return PinnedSSLSocketFactory(sslSocketFactory, defaultTrustManager!!) + } catch (e: Exception) { + throw RuntimeException(e) + } + } + + /** + * Create a Host name verifier for a hs config. + * + * @param hsConfig the hs config. + * @return a new HostnameVerifier. + */ + fun newHostnameVerifier(hsConfig: HomeServerConnectionConfig): HostnameVerifier { + val defaultVerifier: HostnameVerifier = OkHostnameVerifier // HttpsURLConnection.getDefaultHostnameVerifier() + val trustedFingerprints = hsConfig.allowedFingerprints + + return HostnameVerifier { hostname, session -> + if (USE_DEFAULT_HOSTNAME_VERIFIER) { + if (defaultVerifier.verify(hostname, session)) return@HostnameVerifier true + } + // TODO How to recover from this error? + if (trustedFingerprints.isEmpty()) return@HostnameVerifier false + + // If remote cert matches an allowed fingerprint, just accept it. + try { + for (cert in session.peerCertificates) { + for (allowedFingerprint in trustedFingerprints) { + if (cert is X509Certificate && allowedFingerprint.matchesCert(cert)) { + return@HostnameVerifier true + } + } + } + } catch (e: SSLPeerUnverifiedException) { + return@HostnameVerifier false + } catch (e: CertificateException) { + return@HostnameVerifier false + } + + false + } + } + + /** + * Create a list of accepted TLS specifications for a hs config. + * + * @param hsConfig the hs config. + * @return a list of accepted TLS specifications. + */ + fun newConnectionSpecs(hsConfig: HomeServerConnectionConfig): List { + val builder = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) + val tlsVersions = hsConfig.tlsVersions + if (null != tlsVersions && tlsVersions.isNotEmpty()) { + builder.tlsVersions(*tlsVersions.toTypedArray()) + } + + val tlsCipherSuites = hsConfig.tlsCipherSuites + if (null != tlsCipherSuites && tlsCipherSuites.isNotEmpty()) { + builder.cipherSuites(*tlsCipherSuites.toTypedArray()) + } + + @Suppress("DEPRECATION") + builder.supportsTlsExtensions(hsConfig.shouldAcceptTlsExtensions) + val list = ArrayList() + list.add(builder.build()) + // TODO: we should display a warning if user enter an http url + if (hsConfig.allowHttpExtension || hsConfig.homeServerUri.toString().startsWith("http://")) { + list.add(ConnectionSpec.CLEARTEXT) + } + return list + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/Fingerprint.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/Fingerprint.kt new file mode 100644 index 0000000000..f1280b879b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/Fingerprint.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.network.ssl + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.security.cert.CertificateException +import java.security.cert.X509Certificate + +@JsonClass(generateAdapter = true) +data class Fingerprint( + val bytes: ByteArray, + val hashType: HashType +) { + + val displayableHexRepr: String by lazy { + CertUtil.fingerprintToHexString(bytes) + } + + @Throws(CertificateException::class) + internal fun matchesCert(cert: X509Certificate): Boolean { + val o: Fingerprint? = when (hashType) { + HashType.SHA256 -> newSha256Fingerprint(cert) + HashType.SHA1 -> newSha1Fingerprint(cert) + } + return equals(o) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Fingerprint + if (!bytes.contentEquals(other.bytes)) return false + if (hashType != other.hashType) return false + + return true + } + + override fun hashCode(): Int { + var result = bytes.contentHashCode() + result = 31 * result + hashType.hashCode() + return result + } + + internal companion object { + + @Throws(CertificateException::class) + fun newSha256Fingerprint(cert: X509Certificate): Fingerprint { + return Fingerprint( + CertUtil.generateSha256Fingerprint(cert), + HashType.SHA256 + ) + } + + @Throws(CertificateException::class) + fun newSha1Fingerprint(cert: X509Certificate): Fingerprint { + return Fingerprint( + CertUtil.generateSha1Fingerprint(cert), + HashType.SHA1 + ) + } + } + + @JsonClass(generateAdapter = false) + enum class HashType { + @Json(name = "sha-1") SHA1, + @Json(name = "sha-256") SHA256 + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManager.kt new file mode 100644 index 0000000000..289a4ee04f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManager.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.network.ssl + +import java.security.cert.CertificateException +import java.security.cert.X509Certificate + +import javax.net.ssl.X509TrustManager + +/** + * Implements a TrustManager that checks Certificates against an explicit list of known + * fingerprints. + */ + +/** + * @param fingerprints Not empty array of SHA256 cert fingerprints + * @param defaultTrustManager Optional trust manager to fall back on if cert does not match + * any of the fingerprints. Can be null. + */ +internal class PinnedTrustManager(private val fingerprints: List, + private val defaultTrustManager: X509TrustManager?) : X509TrustManager { + + @Throws(CertificateException::class) + override fun checkClientTrusted(chain: Array, s: String) { + try { + if (defaultTrustManager != null) { + defaultTrustManager.checkClientTrusted(chain, s) + return + } + } catch (e: CertificateException) { + // If there is an exception we fall back to checking fingerprints + if (fingerprints.isEmpty()) { + throw UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.cause) + } + } + + checkTrusted(chain) + } + + @Throws(CertificateException::class) + override fun checkServerTrusted(chain: Array, s: String) { + try { + if (defaultTrustManager != null) { + defaultTrustManager.checkServerTrusted(chain, s) + return + } + } catch (e: CertificateException) { + // If there is an exception we fall back to checking fingerprints + if (fingerprints.isEmpty()) { + throw UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.cause /* BMA: Shouldn't be `e` ? */) + } + } + + checkTrusted(chain) + } + + @Throws(CertificateException::class) + private fun checkTrusted(chain: Array) { + val cert = chain[0] + + if (!fingerprints.any { it.matchesCert(cert) }) { + throw UnrecognizedCertificateException(cert, Fingerprint.newSha256Fingerprint(cert), null) + } + } + + override fun getAcceptedIssuers(): Array { + return defaultTrustManager?.acceptedIssuers ?: emptyArray() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManagerApi24.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManagerApi24.kt new file mode 100644 index 0000000000..ed5a099ee4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManagerApi24.kt @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.network.ssl + +import android.os.Build +import androidx.annotation.RequiresApi +import java.net.Socket +import java.security.cert.CertificateException +import java.security.cert.X509Certificate +import javax.net.ssl.SSLEngine +import javax.net.ssl.X509ExtendedTrustManager + +/** + * Implements a TrustManager that checks Certificates against an explicit list of known + * fingerprints. + */ + +/** + * @param fingerprints An array of SHA256 cert fingerprints + * @param defaultTrustManager Optional trust manager to fall back on if cert does not match + * any of the fingerprints. Can be null. + */ +@RequiresApi(Build.VERSION_CODES.N) +internal class PinnedTrustManagerApi24(private val fingerprints: List, + private val defaultTrustManager: X509ExtendedTrustManager?) : X509ExtendedTrustManager() { + + @Throws(CertificateException::class) + override fun checkClientTrusted(chain: Array, authType: String, engine: SSLEngine?) { + try { + if (defaultTrustManager != null) { + defaultTrustManager.checkClientTrusted(chain, authType, engine) + return + } + } catch (e: CertificateException) { + // If there is an exception we fall back to checking fingerprints + if (fingerprints.isEmpty()) { + throw UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.cause) + } + } + + checkTrusted(chain) + } + + @Throws(CertificateException::class) + override fun checkClientTrusted(chain: Array, authType: String, socket: Socket?) { + try { + if (defaultTrustManager != null) { + defaultTrustManager.checkClientTrusted(chain, authType, socket) + return + } + } catch (e: CertificateException) { + // If there is an exception we fall back to checking fingerprints + if (fingerprints.isEmpty()) { + throw UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.cause) + } + } + + checkTrusted(chain) + } + + @Throws(CertificateException::class) + override fun checkClientTrusted(chain: Array, authType: String) { + try { + if (defaultTrustManager != null) { + defaultTrustManager.checkClientTrusted(chain, authType) + return + } + } catch (e: CertificateException) { + // If there is an exception we fall back to checking fingerprints + if (fingerprints.isEmpty()) { + throw UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.cause) + } + } + + checkTrusted(chain) + } + + @Throws(CertificateException::class) + override fun checkServerTrusted(chain: Array, authType: String, socket: Socket?) { + try { + if (defaultTrustManager != null) { + defaultTrustManager.checkServerTrusted(chain, authType, socket) + return + } + } catch (e: CertificateException) { + // If there is an exception we fall back to checking fingerprints + if (fingerprints.isEmpty()) { + throw UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.cause /* BMA: Shouldn't be `e` ? */) + } + } + + checkTrusted(chain) + } + + @Throws(CertificateException::class) + override fun checkServerTrusted(chain: Array, authType: String, engine: SSLEngine?) { + try { + if (defaultTrustManager != null) { + defaultTrustManager.checkServerTrusted(chain, authType, engine) + return + } + } catch (e: CertificateException) { + // If there is an exception we fall back to checking fingerprints + if (fingerprints.isEmpty()) { + throw UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.cause /* BMA: Shouldn't be `e` ? */) + } + } + + checkTrusted(chain) + } + + @Throws(CertificateException::class) + override fun checkServerTrusted(chain: Array, s: String) { + try { + if (defaultTrustManager != null) { + defaultTrustManager.checkServerTrusted(chain, s) + return + } + } catch (e: CertificateException) { + // If there is an exception we fall back to checking fingerprints + if (fingerprints.isEmpty()) { + throw UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.cause /* BMA: Shouldn't be `e` ? */) + } + } + + checkTrusted(chain) + } + + @Throws(CertificateException::class) + private fun checkTrusted(chain: Array) { + val cert = chain[0] + + if (!fingerprints.any { it.matchesCert(cert) }) { + throw UnrecognizedCertificateException(cert, Fingerprint.newSha256Fingerprint(cert), null) + } + } + + override fun getAcceptedIssuers(): Array { + return defaultTrustManager?.acceptedIssuers ?: emptyArray() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManagerProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManagerProvider.kt new file mode 100644 index 0000000000..7dcff0294f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManagerProvider.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.network.ssl + +import android.os.Build +import javax.net.ssl.X509ExtendedTrustManager +import javax.net.ssl.X509TrustManager + +internal object PinnedTrustManagerProvider { + // Set to false to perform some tests + private const val USE_DEFAULT_TRUST_MANAGER = true + + fun provide(fingerprints: List?, + defaultTrustManager: X509TrustManager?): X509TrustManager { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && defaultTrustManager is X509ExtendedTrustManager) { + PinnedTrustManagerApi24( + fingerprints.orEmpty(), + defaultTrustManager.takeIf { USE_DEFAULT_TRUST_MANAGER } + ) + } else { + PinnedTrustManager( + fingerprints.orEmpty(), + defaultTrustManager.takeIf { USE_DEFAULT_TRUST_MANAGER } + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/TLSSocketFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/TLSSocketFactory.kt new file mode 100644 index 0000000000..4ebeafd0c3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/TLSSocketFactory.kt @@ -0,0 +1,121 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.network.ssl + +import okhttp3.TlsVersion +import timber.log.Timber +import java.io.IOException +import java.net.InetAddress +import java.net.Socket +import java.net.UnknownHostException +import java.security.KeyManagementException +import java.security.NoSuchAlgorithmException +import java.security.SecureRandom +import javax.net.ssl.SSLContext +import javax.net.ssl.SSLSocket +import javax.net.ssl.SSLSocketFactory +import javax.net.ssl.TrustManager + +/** + * Force the usage of Tls versions on every created socket + * Inspired from https://blog.dev-area.net/2015/08/13/android-4-1-enable-tls-1-1-and-tls-1-2/ + */ + +internal class TLSSocketFactory + +/** + * Constructor + * + * @param trustPinned + * @param acceptedTlsVersions + * @throws KeyManagementException + * @throws NoSuchAlgorithmException + */ +@Throws(KeyManagementException::class, NoSuchAlgorithmException::class) +constructor(trustPinned: Array, acceptedTlsVersions: List) : SSLSocketFactory() { + + private val internalSSLSocketFactory: SSLSocketFactory + private val enabledProtocols: Array + + init { + val context = SSLContext.getInstance("TLS") + context.init(null, trustPinned, SecureRandom()) + internalSSLSocketFactory = context.socketFactory + enabledProtocols = Array(acceptedTlsVersions.size) { + acceptedTlsVersions[it].javaName + } + } + + override fun getDefaultCipherSuites(): Array { + return internalSSLSocketFactory.defaultCipherSuites + } + + override fun getSupportedCipherSuites(): Array { + return internalSSLSocketFactory.supportedCipherSuites + } + + @Throws(IOException::class) + override fun createSocket(): Socket? { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket()) + } + + @Throws(IOException::class) + override fun createSocket(s: Socket, host: String, port: Int, autoClose: Boolean): Socket? { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket(s, host, port, autoClose)) + } + + @Throws(IOException::class, UnknownHostException::class) + override fun createSocket(host: String, port: Int): Socket? { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port)) + } + + @Throws(IOException::class, UnknownHostException::class) + override fun createSocket(host: String, port: Int, localHost: InetAddress, localPort: Int): Socket? { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port, localHost, localPort)) + } + + @Throws(IOException::class) + override fun createSocket(host: InetAddress, port: Int): Socket? { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port)) + } + + @Throws(IOException::class) + override fun createSocket(address: InetAddress, port: Int, localAddress: InetAddress, localPort: Int): Socket? { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort)) + } + + private fun enableTLSOnSocket(socket: Socket?): Socket? { + if (socket is SSLSocket) { + val supportedProtocols = socket.supportedProtocols.toSet() + val filteredEnabledProtocols = enabledProtocols.filter { it in supportedProtocols } + + if (filteredEnabledProtocols.isNotEmpty()) { + try { + socket.enabledProtocols = filteredEnabledProtocols.toTypedArray() + } catch (e: Exception) { + Timber.e(e) + } + } + } + return socket + } + + companion object { + private val LOG_TAG = TLSSocketFactory::class.java.simpleName + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/UnrecognizedCertificateException.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/UnrecognizedCertificateException.kt new file mode 100644 index 0000000000..ba68a51344 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/UnrecognizedCertificateException.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.network.ssl + +import java.security.cert.CertificateException +import java.security.cert.X509Certificate + +/** + * Thrown when we are given a certificate that does match the certificate we were told to + * expect. + */ +internal data class UnrecognizedCertificateException( + val certificate: X509Certificate, + val fingerprint: Fingerprint, + override val cause: Throwable? +) : CertificateException("Unrecognized certificate with unknown fingerprint: " + certificate.subjectDN, cause) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/token/AccessTokenProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/token/AccessTokenProvider.kt new file mode 100644 index 0000000000..c1e20aaa58 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/token/AccessTokenProvider.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.network.token + +internal interface AccessTokenProvider { + fun getToken(): String? +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/token/HomeserverAccessTokenProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/token/HomeserverAccessTokenProvider.kt new file mode 100644 index 0000000000..1bb0f34222 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/token/HomeserverAccessTokenProvider.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.network.token + +import org.matrix.android.sdk.internal.auth.SessionParamsStore +import org.matrix.android.sdk.internal.di.SessionId +import javax.inject.Inject + +internal class HomeserverAccessTokenProvider @Inject constructor( + @SessionId private val sessionId: String, + private val sessionParamsStore: SessionParamsStore +) : AccessTokenProvider { + override fun getToken() = sessionParamsStore.get(sessionId)?.credentials?.accessToken +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryEnumListProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryEnumListProcessor.kt new file mode 100644 index 0000000000..6e0cb3e677 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryEnumListProcessor.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.query + +import io.realm.RealmObject +import io.realm.RealmQuery + +fun > RealmQuery.process(field: String, enums: List>): RealmQuery { + val lastEnumValue = enums.lastOrNull() + beginGroup() + for (enumValue in enums) { + equalTo(field, enumValue.name) + if (enumValue != lastEnumValue) { + or() + } + } + endGroup() + return this +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryStringValueProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryStringValueProcessor.kt new file mode 100644 index 0000000000..fadea5ada7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryStringValueProcessor.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.query + +import org.matrix.android.sdk.api.query.QueryStringValue +import io.realm.Case +import io.realm.RealmObject +import io.realm.RealmQuery +import timber.log.Timber + +fun RealmQuery.process(field: String, queryStringValue: QueryStringValue): RealmQuery { + when (queryStringValue) { + is QueryStringValue.NoCondition -> Timber.v("No condition to process") + is QueryStringValue.IsNotNull -> isNotNull(field) + is QueryStringValue.IsNull -> isNull(field) + is QueryStringValue.IsEmpty -> isEmpty(field) + is QueryStringValue.IsNotEmpty -> isNotEmpty(field) + is QueryStringValue.Equals -> equalTo(field, queryStringValue.string, queryStringValue.case.toRealmCase()) + is QueryStringValue.Contains -> contains(field, queryStringValue.string, queryStringValue.case.toRealmCase()) + } + return this +} + +private fun QueryStringValue.Case.toRealmCase(): Case { + return when (this) { + QueryStringValue.Case.INSENSITIVE -> Case.INSENSITIVE + QueryStringValue.Case.SENSITIVE -> Case.SENSITIVE + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt new file mode 100644 index 0000000000..97ebe943ec --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt @@ -0,0 +1,256 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session + +import android.content.Context +import android.net.Uri +import android.webkit.MimeTypeMap +import androidx.core.content.FileProvider +import arrow.core.Try +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.session.content.ContentUrlResolver +import org.matrix.android.sdk.api.session.file.FileService +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.NoOpCancellable +import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt +import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments +import org.matrix.android.sdk.internal.di.CacheDirectory +import org.matrix.android.sdk.internal.di.ExternalFilesDirectory +import org.matrix.android.sdk.internal.di.SessionDownloadsDirectory +import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificateWithProgress +import org.matrix.android.sdk.internal.session.download.DownloadProgressInterceptor.Companion.DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import org.matrix.android.sdk.internal.util.toCancelable +import org.matrix.android.sdk.internal.util.writeToFile +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import okhttp3.Request +import okio.buffer +import okio.sink +import okio.source +import timber.log.Timber +import java.io.File +import java.io.IOException +import java.io.InputStream +import java.net.URLEncoder +import javax.inject.Inject + +internal class DefaultFileService @Inject constructor( + private val context: Context, + @CacheDirectory + private val cacheDirectory: File, + @ExternalFilesDirectory + private val externalFilesDirectory: File?, + @SessionDownloadsDirectory + private val sessionCacheDirectory: File, + private val contentUrlResolver: ContentUrlResolver, + @UnauthenticatedWithCertificateWithProgress + private val okHttpClient: OkHttpClient, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val taskExecutor: TaskExecutor +) : FileService { + + private fun String.safeFileName() = URLEncoder.encode(this, Charsets.US_ASCII.displayName()) + + private val downloadFolder = File(sessionCacheDirectory, "MF") + + /** + * Retain ongoing downloads to avoid re-downloading and already downloading file + * map of mxCurl to callbacks + */ + private val ongoing = mutableMapOf>>() + + /** + * Download file in the cache folder, and eventually decrypt it + * TODO looks like files are copied 3 times + */ + override fun downloadFile(downloadMode: FileService.DownloadMode, + id: String, + fileName: String, + mimeType: String?, + url: String?, + elementToDecrypt: ElementToDecrypt?, + callback: MatrixCallback): Cancelable { + val unwrappedUrl = url ?: return NoOpCancellable.also { + callback.onFailure(IllegalArgumentException("url is null")) + } + + Timber.v("## FileService downloadFile $unwrappedUrl") + + synchronized(ongoing) { + val existing = ongoing[unwrappedUrl] + if (existing != null) { + Timber.v("## FileService downloadFile is already downloading.. ") + existing.add(callback) + return NoOpCancellable + } else { + // mark as tracked + ongoing[unwrappedUrl] = ArrayList() + // and proceed to download + } + } + + return taskExecutor.executorScope.launch(coroutineDispatchers.main) { + withContext(coroutineDispatchers.io) { + Try { + if (!downloadFolder.exists()) { + downloadFolder.mkdirs() + } + // ensure we use unique file name by using URL (mapped to suitable file name) + // Also we need to add extension for the FileProvider, if not it lot's of app that it's + // shared with will not function well (even if mime type is passed in the intent) + File(downloadFolder, fileForUrl(unwrappedUrl, mimeType)) + }.flatMap { destFile -> + if (!destFile.exists()) { + val resolvedUrl = contentUrlResolver.resolveFullSize(url) ?: return@flatMap Try.Failure(IllegalArgumentException("url is null")) + + val request = Request.Builder() + .url(resolvedUrl) + .header(DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER, url) + .build() + + val response = try { + okHttpClient.newCall(request).execute() + } catch (e: Throwable) { + return@flatMap Try.Failure(e) + } + + if (!response.isSuccessful) { + return@flatMap Try.Failure(IOException()) + } + + val source = response.body?.source() + ?: return@flatMap Try.Failure(IOException()) + + Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${!source.exhausted()}") + + if (elementToDecrypt != null) { + Timber.v("## decrypt file") + val decryptedStream = MXEncryptedAttachments.decryptAttachment(source.inputStream(), elementToDecrypt) + response.close() + if (decryptedStream == null) { + return@flatMap Try.Failure(IllegalStateException("Decryption error")) + } else { + decryptedStream.use { + writeToFile(decryptedStream, destFile) + } + } + } else { + writeToFile(source.inputStream(), destFile) + response.close() + } + } + + Try.just(copyFile(destFile, downloadMode)) + } + }.fold({ + callback.onFailure(it) + // notify concurrent requests + val toNotify = synchronized(ongoing) { + ongoing[unwrappedUrl]?.also { + ongoing.remove(unwrappedUrl) + } + } + toNotify?.forEach { otherCallbacks -> + tryThis { otherCallbacks.onFailure(it) } + } + }, { file -> + callback.onSuccess(file) + // notify concurrent requests + val toNotify = synchronized(ongoing) { + ongoing[unwrappedUrl]?.also { + ongoing.remove(unwrappedUrl) + } + } + Timber.v("## FileService additional to notify ${toNotify?.size ?: 0} ") + toNotify?.forEach { otherCallbacks -> + tryThis { otherCallbacks.onSuccess(file) } + } + }) + }.toCancelable() + } + + fun storeDataFor(url: String, mimeType: String?, inputStream: InputStream) { + val file = File(downloadFolder, fileForUrl(url, mimeType)) + val source = inputStream.source().buffer() + file.sink().buffer().let { sink -> + source.use { input -> + sink.use { output -> + output.writeAll(input) + } + } + } + } + + private fun fileForUrl(url: String, mimeType: String?): String { + val extension = mimeType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) } + return if (extension != null) "${url.safeFileName()}.$extension" else url.safeFileName() + } + + override fun isFileInCache(mxcUrl: String, mimeType: String?): Boolean { + return File(downloadFolder, fileForUrl(mxcUrl, mimeType)).exists() + } + + override fun fileState(mxcUrl: String, mimeType: String?): FileService.FileState { + if (isFileInCache(mxcUrl, mimeType)) return FileService.FileState.IN_CACHE + val isDownloading = synchronized(ongoing) { + ongoing[mxcUrl] != null + } + return if (isDownloading) FileService.FileState.DOWNLOADING else FileService.FileState.UNKNOWN + } + + /** + * Use this URI and pass it to intent using flag Intent.FLAG_GRANT_READ_URI_PERMISSION + * (if not other app won't be able to access it) + */ + override fun getTemporarySharableURI(mxcUrl: String, mimeType: String?): Uri? { + // this string could be extracted no? + val authority = "${context.packageName}.mx-sdk.fileprovider" + val targetFile = File(downloadFolder, fileForUrl(mxcUrl, mimeType)) + if (!targetFile.exists()) return null + return FileProvider.getUriForFile(context, authority, targetFile) + } + + private fun copyFile(file: File, downloadMode: FileService.DownloadMode): File { + // TODO some of this seems outdated, will need to be re-worked + return when (downloadMode) { + FileService.DownloadMode.TO_EXPORT -> + file.copyTo(File(externalFilesDirectory, file.name), true) + FileService.DownloadMode.FOR_EXTERNAL_SHARE -> + file.copyTo(File(File(cacheDirectory, "ext_share"), file.name), true) + FileService.DownloadMode.FOR_INTERNAL_USE -> + file + } + } + + override fun getCacheSize(): Int { + return downloadFolder.walkTopDown() + .onEnter { + Timber.v("Get size of ${it.absolutePath}") + true + } + .sumBy { it.length().toInt() } + } + + override fun clearCache() { + downloadFolder.deleteRecursively() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultInitialSyncProgressService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultInitialSyncProgressService.kt new file mode 100644 index 0000000000..ff1f44341b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultInitialSyncProgressService.kt @@ -0,0 +1,137 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session + +import androidx.annotation.StringRes +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import org.matrix.android.sdk.api.session.InitialSyncProgressService +import timber.log.Timber +import javax.inject.Inject + +@SessionScope +class DefaultInitialSyncProgressService @Inject constructor() : InitialSyncProgressService { + + private val status = MutableLiveData() + + private var rootTask: TaskInfo? = null + + override fun getInitialSyncProgressStatus(): LiveData { + return status + } + + fun startTask(@StringRes nameRes: Int, totalProgress: Int, parentWeight: Float = 1f) { + // Create a rootTask, or add a child to the leaf + if (rootTask == null) { + rootTask = TaskInfo(nameRes, totalProgress) + } else { + val currentLeaf = rootTask!!.leaf() + + val newTask = TaskInfo(nameRes, + totalProgress, + currentLeaf, + parentWeight) + + currentLeaf.child = newTask + } + reportProgress(0) + } + + fun reportProgress(progress: Int) { + rootTask?.leaf()?.setProgress(progress) + } + + fun endTask(nameRes: Int) { + val endedTask = rootTask?.leaf() + if (endedTask?.nameRes == nameRes) { + // close it + val parent = endedTask.parent + parent?.child = null + parent?.setProgress(endedTask.offset + (endedTask.totalProgress * endedTask.parentWeight).toInt()) + } + if (endedTask?.parent == null) { + status.postValue(InitialSyncProgressService.Status.Idle) + } + } + + fun endAll() { + rootTask = null + status.postValue(InitialSyncProgressService.Status.Idle) + } + + private inner class TaskInfo(@StringRes var nameRes: Int, + var totalProgress: Int, + var parent: TaskInfo? = null, + var parentWeight: Float = 1f, + var offset: Int = parent?.currentProgress ?: 0) { + var child: TaskInfo? = null + var currentProgress: Int = 0 + + /** + * Get the further child + */ + fun leaf(): TaskInfo { + var last = this + while (last.child != null) { + last = last.child!! + } + return last + } + + /** + * Set progress of the parent if any (which will post value), or post the value + */ + fun setProgress(progress: Int) { + currentProgress = progress +// val newProgress = Math.min(currentProgress + progress, totalProgress) + parent?.let { + val parentProgress = (currentProgress * parentWeight).toInt() + it.setProgress(offset + parentProgress) + } ?: run { + Timber.v("--- ${leaf().nameRes}: $currentProgress") + status.postValue(InitialSyncProgressService.Status.Progressing(leaf().nameRes, currentProgress)) + } + } + } +} + +inline fun reportSubtask(reporter: DefaultInitialSyncProgressService?, + @StringRes nameRes: Int, + totalProgress: Int, + parentWeight: Float = 1f, + block: () -> T): T { + reporter?.startTask(nameRes, totalProgress, parentWeight) + return block().also { + reporter?.endTask(nameRes) + } +} + +inline fun Map.mapWithProgress(reporter: DefaultInitialSyncProgressService?, + taskId: Int, + weight: Float, + transform: (Map.Entry) -> R): List { + val total = count().toFloat() + var current = 0 + reporter?.startTask(taskId, 100, weight) + return map { + reporter?.reportProgress((current / total * 100).toInt()) + current++ + transform.invoke(it) + }.also { + reporter?.endTask(taskId) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt new file mode 100644 index 0000000000..eb4b475b4d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt @@ -0,0 +1,279 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session + +import androidx.annotation.MainThread +import dagger.Lazy +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.auth.data.SessionParams +import org.matrix.android.sdk.api.failure.GlobalError +import org.matrix.android.sdk.api.pushrules.PushRuleService +import org.matrix.android.sdk.api.session.InitialSyncProgressService +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.account.AccountService +import org.matrix.android.sdk.api.session.accountdata.AccountDataService +import org.matrix.android.sdk.api.session.cache.CacheService +import org.matrix.android.sdk.api.session.call.CallSignalingService +import org.matrix.android.sdk.api.session.content.ContentUploadStateTracker +import org.matrix.android.sdk.api.session.content.ContentUrlResolver +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.file.ContentDownloadStateTracker +import org.matrix.android.sdk.api.session.file.FileService +import org.matrix.android.sdk.api.session.group.GroupService +import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService +import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService +import org.matrix.android.sdk.api.session.profile.ProfileService +import org.matrix.android.sdk.api.session.pushers.PushersService +import org.matrix.android.sdk.api.session.room.RoomDirectoryService +import org.matrix.android.sdk.api.session.room.RoomService +import org.matrix.android.sdk.api.session.securestorage.SecureStorageService +import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService +import org.matrix.android.sdk.api.session.signout.SignOutService +import org.matrix.android.sdk.api.session.sync.FilterService +import org.matrix.android.sdk.api.session.terms.TermsService +import org.matrix.android.sdk.api.session.typing.TypingUsersTracker +import org.matrix.android.sdk.api.session.user.UserService +import org.matrix.android.sdk.api.session.widgets.WidgetService +import org.matrix.android.sdk.internal.auth.SessionParamsStore +import org.matrix.android.sdk.internal.crypto.DefaultCryptoService +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.SessionId +import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificate +import org.matrix.android.sdk.internal.di.WorkManagerProvider +import org.matrix.android.sdk.internal.session.identity.DefaultIdentityService +import org.matrix.android.sdk.internal.session.room.timeline.TimelineEventDecryptor +import org.matrix.android.sdk.internal.session.sync.SyncTokenStore +import org.matrix.android.sdk.internal.session.sync.job.SyncThread +import org.matrix.android.sdk.internal.session.sync.job.SyncWorker +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import org.matrix.android.sdk.internal.util.createUIHandler +import io.realm.RealmConfiguration +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import okhttp3.OkHttpClient +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Provider + +@SessionScope +internal class DefaultSession @Inject constructor( + override val sessionParams: SessionParams, + private val workManagerProvider: WorkManagerProvider, + private val eventBus: EventBus, + @SessionId + override val sessionId: String, + @SessionDatabase private val realmConfiguration: RealmConfiguration, + private val lifecycleObservers: Set<@JvmSuppressWildcards SessionLifecycleObserver>, + private val sessionListeners: SessionListeners, + private val roomService: Lazy, + private val roomDirectoryService: Lazy, + private val groupService: Lazy, + private val userService: Lazy, + private val filterService: Lazy, + private val cacheService: Lazy, + private val signOutService: Lazy, + private val pushRuleService: Lazy, + private val pushersService: Lazy, + private val termsService: Lazy, + private val cryptoService: Lazy, + private val defaultFileService: Lazy, + private val secureStorageService: Lazy, + private val profileService: Lazy, + private val widgetService: Lazy, + private val syncThreadProvider: Provider, + private val contentUrlResolver: ContentUrlResolver, + private val syncTokenStore: SyncTokenStore, + private val sessionParamsStore: SessionParamsStore, + private val contentUploadProgressTracker: ContentUploadStateTracker, + private val typingUsersTracker: TypingUsersTracker, + private val contentDownloadStateTracker: ContentDownloadStateTracker, + private val initialSyncProgressService: Lazy, + private val homeServerCapabilitiesService: Lazy, + private val accountDataService: Lazy, + private val _sharedSecretStorageService: Lazy, + private val accountService: Lazy, + private val timelineEventDecryptor: TimelineEventDecryptor, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val defaultIdentityService: DefaultIdentityService, + private val integrationManagerService: IntegrationManagerService, + private val taskExecutor: TaskExecutor, + private val callSignalingService: Lazy, + @UnauthenticatedWithCertificate + private val unauthenticatedWithCertificateOkHttpClient: Lazy +) : Session, + RoomService by roomService.get(), + RoomDirectoryService by roomDirectoryService.get(), + GroupService by groupService.get(), + UserService by userService.get(), + SignOutService by signOutService.get(), + FilterService by filterService.get(), + PushRuleService by pushRuleService.get(), + PushersService by pushersService.get(), + TermsService by termsService.get(), + InitialSyncProgressService by initialSyncProgressService.get(), + SecureStorageService by secureStorageService.get(), + HomeServerCapabilitiesService by homeServerCapabilitiesService.get(), + ProfileService by profileService.get(), + AccountDataService by accountDataService.get(), + AccountService by accountService.get() { + + override val sharedSecretStorageService: SharedSecretStorageService + get() = _sharedSecretStorageService.get() + + private var isOpen = false + + private var syncThread: SyncThread? = null + + private val uiHandler = createUIHandler() + + override val isOpenable: Boolean + get() = sessionParamsStore.get(sessionId)?.isTokenValid ?: false + + @MainThread + override fun open() { + assert(!isOpen) + isOpen = true + cryptoService.get().ensureDevice() + uiHandler.post { + lifecycleObservers.forEach { it.onStart() } + } + eventBus.register(this) + timelineEventDecryptor.start() + } + + override fun requireBackgroundSync() { + SyncWorker.requireBackgroundSync(workManagerProvider, sessionId) + } + + override fun startAutomaticBackgroundSync(repeatDelay: Long) { + SyncWorker.automaticallyBackgroundSync(workManagerProvider, sessionId, 0, repeatDelay) + } + + override fun stopAnyBackgroundSync() { + SyncWorker.stopAnyBackgroundSync(workManagerProvider) + } + + override fun startSync(fromForeground: Boolean) { + Timber.i("Starting sync thread") + assert(isOpen) + val localSyncThread = getSyncThread() + localSyncThread.setInitialForeground(fromForeground) + if (!localSyncThread.isAlive) { + localSyncThread.start() + } else { + localSyncThread.restart() + Timber.w("Attempt to start an already started thread") + } + } + + override fun stopSync() { + assert(isOpen) + syncThread?.kill() + syncThread = null + } + + override fun close() { + assert(isOpen) + stopSync() + timelineEventDecryptor.destroy() + uiHandler.post { + lifecycleObservers.forEach { it.onStop() } + } + cryptoService.get().close() + isOpen = false + eventBus.unregister(this) + } + + override fun getSyncStateLive() = getSyncThread().liveState() + + override fun getSyncState() = getSyncThread().currentState() + + override fun hasAlreadySynced(): Boolean { + return syncTokenStore.getLastToken() != null + } + + private fun getSyncThread(): SyncThread { + return syncThread ?: syncThreadProvider.get().also { + syncThread = it + } + } + + override fun clearCache(callback: MatrixCallback) { + stopSync() + stopAnyBackgroundSync() + uiHandler.post { + lifecycleObservers.forEach { it.onClearCache() } + } + cacheService.get().clearCache(callback) + workManagerProvider.cancelAllWorks() + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onGlobalError(globalError: GlobalError) { + if (globalError is GlobalError.InvalidToken + && globalError.softLogout) { + // Mark the token has invalid + taskExecutor.executorScope.launch(Dispatchers.IO) { + sessionParamsStore.setTokenInvalid(sessionId) + } + } + + sessionListeners.dispatchGlobalError(globalError) + } + + override fun contentUrlResolver() = contentUrlResolver + + override fun contentUploadProgressTracker() = contentUploadProgressTracker + + override fun typingUsersTracker() = typingUsersTracker + + override fun contentDownloadProgressTracker(): ContentDownloadStateTracker = contentDownloadStateTracker + + override fun cryptoService(): CryptoService = cryptoService.get() + + override fun identityService() = defaultIdentityService + + override fun fileService(): FileService = defaultFileService.get() + + override fun widgetService(): WidgetService = widgetService.get() + + override fun integrationManagerService() = integrationManagerService + + override fun callSignalingService(): CallSignalingService = callSignalingService.get() + + override fun getOkHttpClient(): OkHttpClient { + return unauthenticatedWithCertificateOkHttpClient.get() + } + + override fun addListener(listener: Session.Listener) { + sessionListeners.addListener(listener) + } + + override fun removeListener(listener: Session.Listener) { + sessionListeners.removeListener(listener) + } + + // For easy debugging + override fun toString(): String { + return "$myUserId - ${sessionParams.deviceId}" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/EventInsertLiveProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/EventInsertLiveProcessor.kt new file mode 100644 index 0000000000..85d714698f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/EventInsertLiveProcessor.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.internal.database.model.EventInsertType +import io.realm.Realm + +internal interface EventInsertLiveProcessor { + + fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean + + suspend fun process(realm: Realm, event: Event) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt new file mode 100644 index 0000000000..475450837e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt @@ -0,0 +1,139 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session + +import dagger.BindsInstance +import dagger.Component +import org.matrix.android.sdk.api.auth.data.SessionParams +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.internal.crypto.CancelGossipRequestWorker +import org.matrix.android.sdk.internal.crypto.CryptoModule +import org.matrix.android.sdk.internal.crypto.SendGossipRequestWorker +import org.matrix.android.sdk.internal.crypto.SendGossipWorker +import org.matrix.android.sdk.internal.crypto.verification.SendVerificationMessageWorker +import org.matrix.android.sdk.internal.di.MatrixComponent +import org.matrix.android.sdk.internal.di.SessionAssistedInjectModule +import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker +import org.matrix.android.sdk.internal.session.account.AccountModule +import org.matrix.android.sdk.internal.session.cache.CacheModule +import org.matrix.android.sdk.internal.session.call.CallModule +import org.matrix.android.sdk.internal.session.content.ContentModule +import org.matrix.android.sdk.internal.session.content.UploadContentWorker +import org.matrix.android.sdk.internal.session.filter.FilterModule +import org.matrix.android.sdk.internal.session.group.GetGroupDataWorker +import org.matrix.android.sdk.internal.session.group.GroupModule +import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesModule +import org.matrix.android.sdk.internal.session.identity.IdentityModule +import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManagerModule +import org.matrix.android.sdk.internal.session.openid.OpenIdModule +import org.matrix.android.sdk.internal.session.profile.ProfileModule +import org.matrix.android.sdk.internal.session.pushers.AddHttpPusherWorker +import org.matrix.android.sdk.internal.session.pushers.PushersModule +import org.matrix.android.sdk.internal.session.room.RoomModule +import org.matrix.android.sdk.internal.session.room.relation.SendRelationWorker +import org.matrix.android.sdk.internal.session.room.send.EncryptEventWorker +import org.matrix.android.sdk.internal.session.room.send.MultipleEventSendingDispatcherWorker +import org.matrix.android.sdk.internal.session.room.send.RedactEventWorker +import org.matrix.android.sdk.internal.session.room.send.SendEventWorker +import org.matrix.android.sdk.internal.session.signout.SignOutModule +import org.matrix.android.sdk.internal.session.sync.SyncModule +import org.matrix.android.sdk.internal.session.sync.SyncTask +import org.matrix.android.sdk.internal.session.sync.SyncTokenStore +import org.matrix.android.sdk.internal.session.sync.job.SyncWorker +import org.matrix.android.sdk.internal.session.terms.TermsModule +import org.matrix.android.sdk.internal.session.user.UserModule +import org.matrix.android.sdk.internal.session.user.accountdata.AccountDataModule +import org.matrix.android.sdk.internal.session.widgets.WidgetModule +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers + +@Component(dependencies = [MatrixComponent::class], + modules = [ + SessionModule::class, + RoomModule::class, + SyncModule::class, + HomeServerCapabilitiesModule::class, + SignOutModule::class, + GroupModule::class, + UserModule::class, + FilterModule::class, + GroupModule::class, + ContentModule::class, + CacheModule::class, + CryptoModule::class, + PushersModule::class, + OpenIdModule::class, + WidgetModule::class, + IntegrationManagerModule::class, + IdentityModule::class, + TermsModule::class, + AccountDataModule::class, + ProfileModule::class, + SessionAssistedInjectModule::class, + AccountModule::class, + CallModule::class + ] +) +@SessionScope +internal interface SessionComponent { + + fun coroutineDispatchers(): MatrixCoroutineDispatchers + + fun session(): Session + + fun syncTask(): SyncTask + + fun syncTokenStore(): SyncTokenStore + + fun networkConnectivityChecker(): NetworkConnectivityChecker + + fun taskExecutor(): TaskExecutor + + fun inject(worker: SendEventWorker) + + fun inject(worker: SendRelationWorker) + + fun inject(worker: EncryptEventWorker) + + fun inject(worker: MultipleEventSendingDispatcherWorker) + + fun inject(worker: RedactEventWorker) + + fun inject(worker: GetGroupDataWorker) + + fun inject(worker: UploadContentWorker) + + fun inject(worker: SyncWorker) + + fun inject(worker: AddHttpPusherWorker) + + fun inject(worker: SendVerificationMessageWorker) + + fun inject(worker: SendGossipRequestWorker) + + fun inject(worker: CancelGossipRequestWorker) + + fun inject(worker: SendGossipWorker) + + @Component.Factory + interface Factory { + fun create( + matrixComponent: MatrixComponent, + @BindsInstance sessionParams: SessionParams): SessionComponent + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionLifecycleObserver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionLifecycleObserver.kt new file mode 100644 index 0000000000..3cc73599ff --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionLifecycleObserver.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session + +import androidx.annotation.MainThread + +/** + * This defines methods associated with some lifecycle events of a session. + * A list of SessionLifecycle will be injected into [DefaultSession] + */ +internal interface SessionLifecycleObserver { + /* + Called when the session is opened + */ + @MainThread + fun onStart() { + // noop + } + + /* + Called when the session is cleared + */ + @MainThread + fun onClearCache() { + // noop + } + + /* + Called when the session is closed + */ + @MainThread + fun onStop() { + // noop + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionListeners.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionListeners.kt new file mode 100644 index 0000000000..36242616ff --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionListeners.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session + +import org.matrix.android.sdk.api.failure.GlobalError +import org.matrix.android.sdk.api.session.Session +import javax.inject.Inject + +internal class SessionListeners @Inject constructor() { + + private val listeners = mutableSetOf() + + fun addListener(listener: Session.Listener) { + synchronized(listeners) { + listeners.add(listener) + } + } + + fun removeListener(listener: Session.Listener) { + synchronized(listeners) { + listeners.remove(listener) + } + } + + fun dispatchGlobalError(globalError: GlobalError) { + synchronized(listeners) { + listeners.forEach { + it.onGlobalError(globalError) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt new file mode 100644 index 0000000000..1c1804ba5f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt @@ -0,0 +1,361 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session + +import android.content.Context +import android.os.Build +import com.zhuinden.monarchy.Monarchy +import dagger.Binds +import dagger.Lazy +import dagger.Module +import dagger.Provides +import dagger.multibindings.IntoSet +import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import org.matrix.android.sdk.api.auth.data.SessionParams +import org.matrix.android.sdk.api.auth.data.sessionId +import org.matrix.android.sdk.api.crypto.MXCryptoConfig +import org.matrix.android.sdk.api.session.InitialSyncProgressService +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.accountdata.AccountDataService +import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService +import org.matrix.android.sdk.api.session.securestorage.SecureStorageService +import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService +import org.matrix.android.sdk.api.session.typing.TypingUsersTracker +import org.matrix.android.sdk.internal.crypto.crosssigning.ShieldTrustUpdater +import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorageService +import org.matrix.android.sdk.internal.crypto.verification.VerificationMessageProcessor +import org.matrix.android.sdk.internal.database.DatabaseCleaner +import org.matrix.android.sdk.internal.database.EventInsertLiveObserver +import org.matrix.android.sdk.internal.database.SessionRealmConfigurationFactory +import org.matrix.android.sdk.internal.di.Authenticated +import org.matrix.android.sdk.internal.di.DeviceId +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.SessionDownloadsDirectory +import org.matrix.android.sdk.internal.di.SessionFilesDirectory +import org.matrix.android.sdk.internal.di.SessionId +import org.matrix.android.sdk.internal.di.Unauthenticated +import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificate +import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificateWithProgress +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.di.UserMd5 +import org.matrix.android.sdk.internal.eventbus.EventBusTimberLogger +import org.matrix.android.sdk.internal.network.DefaultNetworkConnectivityChecker +import org.matrix.android.sdk.internal.network.FallbackNetworkCallbackStrategy +import org.matrix.android.sdk.internal.network.NetworkCallbackStrategy +import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker +import org.matrix.android.sdk.internal.network.PreferredNetworkCallbackStrategy +import org.matrix.android.sdk.internal.network.RetrofitFactory +import org.matrix.android.sdk.internal.network.httpclient.addAccessTokenInterceptor +import org.matrix.android.sdk.internal.network.httpclient.addSocketFactory +import org.matrix.android.sdk.internal.network.interceptors.CurlLoggingInterceptor +import org.matrix.android.sdk.internal.network.token.AccessTokenProvider +import org.matrix.android.sdk.internal.network.token.HomeserverAccessTokenProvider +import org.matrix.android.sdk.internal.session.call.CallEventProcessor +import org.matrix.android.sdk.internal.session.download.DownloadProgressInterceptor +import org.matrix.android.sdk.internal.session.homeserver.DefaultHomeServerCapabilitiesService +import org.matrix.android.sdk.internal.session.identity.DefaultIdentityService +import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManager +import org.matrix.android.sdk.internal.session.room.EventRelationsAggregationProcessor +import org.matrix.android.sdk.internal.session.room.create.RoomCreateEventProcessor +import org.matrix.android.sdk.internal.session.room.prune.RedactionEventProcessor +import org.matrix.android.sdk.internal.session.room.tombstone.RoomTombstoneEventProcessor +import org.matrix.android.sdk.internal.session.securestorage.DefaultSecureStorageService +import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker +import org.matrix.android.sdk.internal.session.user.accountdata.DefaultAccountDataService +import org.matrix.android.sdk.internal.session.widgets.DefaultWidgetURLFormatter +import org.matrix.android.sdk.internal.util.md5 +import io.realm.RealmConfiguration +import okhttp3.OkHttpClient +import org.greenrobot.eventbus.EventBus +import retrofit2.Retrofit +import java.io.File +import javax.inject.Provider +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +annotation class MockHttpInterceptor + +@Module +internal abstract class SessionModule { + + @Module + companion object { + internal fun getKeyAlias(userMd5: String) = "session_db_$userMd5" + + /** + * Rules: + * Annotate methods with @SessionScope only the @Provides annotated methods with computation and logic. + */ + + @JvmStatic + @Provides + fun providesHomeServerConnectionConfig(sessionParams: SessionParams): HomeServerConnectionConfig { + return sessionParams.homeServerConnectionConfig + } + + @JvmStatic + @Provides + fun providesCredentials(sessionParams: SessionParams): Credentials { + return sessionParams.credentials + } + + @JvmStatic + @UserId + @Provides + @SessionScope + fun providesUserId(credentials: Credentials): String { + return credentials.userId + } + + @JvmStatic + @DeviceId + @Provides + fun providesDeviceId(credentials: Credentials): String? { + return credentials.deviceId + } + + @JvmStatic + @UserMd5 + @Provides + @SessionScope + fun providesUserMd5(@UserId userId: String): String { + return userId.md5() + } + + @JvmStatic + @SessionId + @Provides + @SessionScope + fun providesSessionId(credentials: Credentials): String { + return credentials.sessionId() + } + + @JvmStatic + @Provides + @SessionFilesDirectory + fun providesFilesDir(@UserMd5 userMd5: String, + @SessionId sessionId: String, + context: Context): File { + // Temporary code for migration + val old = File(context.filesDir, userMd5) + if (old.exists()) { + old.renameTo(File(context.filesDir, sessionId)) + } + + return File(context.filesDir, sessionId) + } + + @JvmStatic + @Provides + @SessionDownloadsDirectory + fun providesCacheDir(@SessionId sessionId: String, + context: Context): File { + return File(context.cacheDir, "downloads/$sessionId") + } + + @JvmStatic + @Provides + @SessionDatabase + @SessionScope + fun providesRealmConfiguration(realmConfigurationFactory: SessionRealmConfigurationFactory): RealmConfiguration { + return realmConfigurationFactory.create() + } + + @JvmStatic + @Provides + @SessionDatabase + @SessionScope + fun providesMonarchy(@SessionDatabase realmConfiguration: RealmConfiguration): Monarchy { + return Monarchy.Builder() + .setRealmConfiguration(realmConfiguration) + .build() + } + + @JvmStatic + @Provides + @SessionScope + @UnauthenticatedWithCertificate + fun providesOkHttpClientWithCertificate(@Unauthenticated okHttpClient: OkHttpClient, + homeServerConnectionConfig: HomeServerConnectionConfig): OkHttpClient { + return okHttpClient + .newBuilder() + .addSocketFactory(homeServerConnectionConfig) + .build() + } + + @JvmStatic + @Provides + @SessionScope + @Authenticated + fun providesOkHttpClient(@UnauthenticatedWithCertificate okHttpClient: OkHttpClient, + @Authenticated accessTokenProvider: AccessTokenProvider, + @SessionId sessionId: String, + @MockHttpInterceptor testInterceptor: TestInterceptor?): OkHttpClient { + return okHttpClient + .newBuilder() + .addAccessTokenInterceptor(accessTokenProvider) + .apply { + if (testInterceptor != null) { + testInterceptor.sessionId = sessionId + addInterceptor(testInterceptor) + } + } + .build() + } + + @JvmStatic + @Provides + @SessionScope + @UnauthenticatedWithCertificateWithProgress + fun providesProgressOkHttpClient(@UnauthenticatedWithCertificate okHttpClient: OkHttpClient, + downloadProgressInterceptor: DownloadProgressInterceptor): OkHttpClient { + return okHttpClient.newBuilder() + .apply { + // Remove the previous CurlLoggingInterceptor, to add it after the accessTokenInterceptor + val existingCurlInterceptors = interceptors().filterIsInstance() + interceptors().removeAll(existingCurlInterceptors) + + addInterceptor(downloadProgressInterceptor) + + // Re add eventually the curl logging interceptors + existingCurlInterceptors.forEach { + addInterceptor(it) + } + }.build() + } + + @JvmStatic + @Provides + @SessionScope + fun providesRetrofit(@Authenticated okHttpClient: Lazy, + sessionParams: SessionParams, + retrofitFactory: RetrofitFactory): Retrofit { + return retrofitFactory + .create(okHttpClient, sessionParams.homeServerConnectionConfig.homeServerUri.toString()) + } + + @JvmStatic + @Provides + @SessionScope + fun providesEventBus(): EventBus { + return EventBus + .builder() + .logger(EventBusTimberLogger()) + .build() + } + + @JvmStatic + @Provides + @SessionScope + fun providesNetworkCallbackStrategy(fallbackNetworkCallbackStrategy: Provider, + preferredNetworkCallbackStrategy: Provider + ): NetworkCallbackStrategy { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + preferredNetworkCallbackStrategy.get() + } else { + fallbackNetworkCallbackStrategy.get() + } + } + + @JvmStatic + @Provides + @SessionScope + fun providesMxCryptoConfig(matrixConfiguration: MatrixConfiguration): MXCryptoConfig { + return matrixConfiguration.cryptoConfig + } + } + + @Binds + @Authenticated + abstract fun bindAccessTokenProvider(provider: HomeserverAccessTokenProvider): AccessTokenProvider + + @Binds + abstract fun bindSession(session: DefaultSession): Session + + @Binds + abstract fun bindNetworkConnectivityChecker(checker: DefaultNetworkConnectivityChecker): NetworkConnectivityChecker + + @Binds + @IntoSet + abstract fun bindEventRedactionProcessor(processor: RedactionEventProcessor): EventInsertLiveProcessor + + @Binds + @IntoSet + abstract fun bindEventRelationsAggregationProcessor(processor: EventRelationsAggregationProcessor): EventInsertLiveProcessor + + @Binds + @IntoSet + abstract fun bindRoomTombstoneEventProcessor(processor: RoomTombstoneEventProcessor): EventInsertLiveProcessor + + @Binds + @IntoSet + abstract fun bindRoomCreateEventProcessor(processor: RoomCreateEventProcessor): EventInsertLiveProcessor + + @Binds + @IntoSet + abstract fun bindVerificationMessageProcessor(processor: VerificationMessageProcessor): EventInsertLiveProcessor + + @Binds + @IntoSet + abstract fun bindCallEventProcessor(processor: CallEventProcessor): EventInsertLiveProcessor + + @Binds + @IntoSet + abstract fun bindEventInsertObserver(observer: EventInsertLiveObserver): SessionLifecycleObserver + + @Binds + @IntoSet + abstract fun bindIntegrationManager(observer: IntegrationManager): SessionLifecycleObserver + + @Binds + @IntoSet + abstract fun bindWidgetUrlFormatter(observer: DefaultWidgetURLFormatter): SessionLifecycleObserver + + @Binds + @IntoSet + abstract fun bindShieldTrustUpdated(observer: ShieldTrustUpdater): SessionLifecycleObserver + + @Binds + @IntoSet + abstract fun bindIdentityService(observer: DefaultIdentityService): SessionLifecycleObserver + + @Binds + @IntoSet + abstract fun bindDatabaseCleaner(observer: DatabaseCleaner): SessionLifecycleObserver + + @Binds + abstract fun bindInitialSyncProgressService(service: DefaultInitialSyncProgressService): InitialSyncProgressService + + @Binds + abstract fun bindSecureStorageService(service: DefaultSecureStorageService): SecureStorageService + + @Binds + abstract fun bindHomeServerCapabilitiesService(service: DefaultHomeServerCapabilitiesService): HomeServerCapabilitiesService + + @Binds + abstract fun bindAccountDataService(service: DefaultAccountDataService): AccountDataService + + @Binds + abstract fun bindSharedSecretStorageService(service: DefaultSharedSecretStorageService): SharedSecretStorageService + + @Binds + abstract fun bindTypingUsersTracker(tracker: DefaultTypingUsersTracker): TypingUsersTracker +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionScope.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionScope.kt new file mode 100644 index 0000000000..1fb950bad4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionScope.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session + +import javax.inject.Scope + +@Scope +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +internal annotation class SessionScope diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/TestInterceptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/TestInterceptor.kt new file mode 100644 index 0000000000..cf8701ab86 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/TestInterceptor.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session + +import okhttp3.Interceptor + +interface TestInterceptor : Interceptor { + var sessionId: String? +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/AccountAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/AccountAPI.kt new file mode 100644 index 0000000000..be25c680a5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/AccountAPI.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.account + +import org.matrix.android.sdk.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.POST + +internal interface AccountAPI { + + /** + * Ask the homeserver to change the password with the provided new password. + * @param params parameters to change password. + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/password") + fun changePassword(@Body params: ChangePasswordParams): Call + + /** + * Deactivate the user account + * + * @param params the deactivate account params + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/deactivate") + fun deactivate(@Body params: DeactivateAccountParams): Call +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/AccountModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/AccountModule.kt new file mode 100644 index 0000000000..50469f99b0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/AccountModule.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.account + +import dagger.Binds +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.api.session.account.AccountService +import org.matrix.android.sdk.internal.session.SessionScope +import retrofit2.Retrofit + +@Module +internal abstract class AccountModule { + + @Module + companion object { + @Provides + @JvmStatic + @SessionScope + fun providesAccountAPI(retrofit: Retrofit): AccountAPI { + return retrofit.create(AccountAPI::class.java) + } + } + + @Binds + abstract fun bindChangePasswordTask(task: DefaultChangePasswordTask): ChangePasswordTask + + @Binds + abstract fun bindDeactivateAccountTask(task: DefaultDeactivateAccountTask): DeactivateAccountTask + + @Binds + abstract fun bindAccountService(service: DefaultAccountService): AccountService +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/ChangePasswordParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/ChangePasswordParams.kt new file mode 100644 index 0000000000..347e39ae39 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/ChangePasswordParams.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.account + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth + +/** + * Class to pass request parameters to update the password. + */ +@JsonClass(generateAdapter = true) +internal data class ChangePasswordParams( + @Json(name = "auth") + val auth: UserPasswordAuth? = null, + + @Json(name = "new_password") + val newPassword: String? = null +) { + companion object { + fun create(userId: String, oldPassword: String, newPassword: String): ChangePasswordParams { + return ChangePasswordParams( + auth = UserPasswordAuth(user = userId, password = oldPassword), + newPassword = newPassword + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/ChangePasswordTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/ChangePasswordTask.kt new file mode 100644 index 0000000000..9338f58e6f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/ChangePasswordTask.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.account + +import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface ChangePasswordTask : Task { + data class Params( + val password: String, + val newPassword: String + ) +} + +internal class DefaultChangePasswordTask @Inject constructor( + private val accountAPI: AccountAPI, + private val eventBus: EventBus, + @UserId private val userId: String +) : ChangePasswordTask { + + override suspend fun execute(params: ChangePasswordTask.Params) { + val changePasswordParams = ChangePasswordParams.create(userId, params.password, params.newPassword) + try { + executeRequest(eventBus) { + apiCall = accountAPI.changePassword(changePasswordParams) + } + } catch (throwable: Throwable) { + val registrationFlowResponse = throwable.toRegistrationFlowResponse() + + if (registrationFlowResponse != null + /* Avoid infinite loop */ + && changePasswordParams.auth?.session == null) { + // Retry with authentication + executeRequest(eventBus) { + apiCall = accountAPI.changePassword( + changePasswordParams.copy(auth = changePasswordParams.auth?.copy(session = registrationFlowResponse.session)) + ) + } + } else { + throw throwable + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountParams.kt new file mode 100644 index 0000000000..a8fa999c98 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountParams.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.account + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth + +@JsonClass(generateAdapter = true) +internal data class DeactivateAccountParams( + @Json(name = "auth") + val auth: UserPasswordAuth? = null, + + // Set to true to erase all data of the account + @Json(name = "erase") + val erase: Boolean +) { + companion object { + fun create(userId: String, password: String, erase: Boolean): DeactivateAccountParams { + return DeactivateAccountParams( + auth = UserPasswordAuth(user = userId, password = password), + erase = erase + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountTask.kt new file mode 100644 index 0000000000..b31a0a4c97 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountTask.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.account + +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.cleanup.CleanupSession +import org.matrix.android.sdk.internal.session.identity.IdentityDisconnectTask +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import javax.inject.Inject + +internal interface DeactivateAccountTask : Task { + data class Params( + val password: String, + val eraseAllData: Boolean + ) +} + +internal class DefaultDeactivateAccountTask @Inject constructor( + private val accountAPI: AccountAPI, + private val eventBus: EventBus, + @UserId private val userId: String, + private val identityDisconnectTask: IdentityDisconnectTask, + private val cleanupSession: CleanupSession +) : DeactivateAccountTask { + + override suspend fun execute(params: DeactivateAccountTask.Params) { + val deactivateAccountParams = DeactivateAccountParams.create(userId, params.password, params.eraseAllData) + + executeRequest(eventBus) { + apiCall = accountAPI.deactivate(deactivateAccountParams) + } + + // Logout from identity server if any, ignoring errors + runCatching { identityDisconnectTask.execute(Unit) } + .onFailure { Timber.w(it, "Unable to disconnect identity server") } + + cleanupSession.handle() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DefaultAccountService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DefaultAccountService.kt new file mode 100644 index 0000000000..892d91fe34 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DefaultAccountService.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.account + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.account.AccountService +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import javax.inject.Inject + +internal class DefaultAccountService @Inject constructor(private val changePasswordTask: ChangePasswordTask, + private val deactivateAccountTask: DeactivateAccountTask, + private val taskExecutor: TaskExecutor) : AccountService { + + override fun changePassword(password: String, newPassword: String, callback: MatrixCallback): Cancelable { + return changePasswordTask + .configureWith(ChangePasswordTask.Params(password, newPassword)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun deactivateAccount(password: String, eraseAllData: Boolean, callback: MatrixCallback): Cancelable { + return deactivateAccountTask + .configureWith(DeactivateAccountTask.Params(password, eraseAllData)) { + this.callback = callback + } + .executeBy(taskExecutor) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/CacheModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/CacheModule.kt new file mode 100644 index 0000000000..b6e2a8d0c4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/CacheModule.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.cache + +import dagger.Binds +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.api.session.cache.CacheService +import org.matrix.android.sdk.internal.di.SessionDatabase +import io.realm.RealmConfiguration + +@Module +internal abstract class CacheModule { + + @Module + companion object { + @JvmStatic + @Provides + @SessionDatabase + fun providesClearCacheTask(@SessionDatabase realmConfiguration: RealmConfiguration): ClearCacheTask { + return RealmClearCacheTask(realmConfiguration) + } + } + + @Binds + abstract fun bindCacheService(service: DefaultCacheService): CacheService +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/ClearCacheTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/ClearCacheTask.kt new file mode 100644 index 0000000000..9b968f3d03 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/ClearCacheTask.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.cache + +import org.matrix.android.sdk.internal.database.awaitTransaction +import org.matrix.android.sdk.internal.task.Task +import io.realm.RealmConfiguration +import javax.inject.Inject + +internal interface ClearCacheTask : Task + +internal class RealmClearCacheTask @Inject constructor(private val realmConfiguration: RealmConfiguration) : ClearCacheTask { + + override suspend fun execute(params: Unit) { + awaitTransaction(realmConfiguration) { + it.deleteAll() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/DefaultCacheService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/DefaultCacheService.kt new file mode 100644 index 0000000000..ab53e87067 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/DefaultCacheService.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.cache + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.cache.CacheService +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import javax.inject.Inject + +internal class DefaultCacheService @Inject constructor(@SessionDatabase + private val clearCacheTask: ClearCacheTask, + private val taskExecutor: TaskExecutor) : CacheService { + + override fun clearCache(callback: MatrixCallback) { + taskExecutor.cancelAll() + clearCacheTask + .configureWith { + this.callback = callback + } + .executeBy(taskExecutor) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt new file mode 100644 index 0000000000..5d1d5808e3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.call + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.internal.database.model.EventInsertType +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor +import io.realm.Realm +import timber.log.Timber +import javax.inject.Inject + +internal class CallEventProcessor @Inject constructor( + @UserId private val userId: String, + private val callService: DefaultCallSignalingService +) : EventInsertLiveProcessor { + + private val allowedTypes = listOf( + EventType.CALL_ANSWER, + EventType.CALL_CANDIDATES, + EventType.CALL_INVITE, + EventType.CALL_HANGUP, + EventType.ENCRYPTED + ) + + override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean { + if (insertType != EventInsertType.INCREMENTAL_SYNC) { + return false + } + return allowedTypes.contains(eventType) + } + + override suspend fun process(realm: Realm, event: Event) { + update(realm, event) + } + + private fun update(realm: Realm, event: Event) { + val now = System.currentTimeMillis() + // TODO might check if an invite is not closed (hangup/answsered) in the same event batch? + event.roomId ?: return Unit.also { + Timber.w("Event with no room id ${event.eventId}") + } + val age = now - (event.ageLocalTs ?: now) + if (age > 40_000) { + // To old to ring? + return + } + event.ageLocalTs + if (EventType.isCallEvent(event.getClearType())) { + callService.onCallEvent(event) + } + Timber.v("$realm : $userId") + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallModule.kt new file mode 100644 index 0000000000..60887db497 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallModule.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.call + +import dagger.Binds +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.api.session.call.CallSignalingService +import org.matrix.android.sdk.internal.session.SessionScope +import retrofit2.Retrofit + +@Module +internal abstract class CallModule { + + @Module + companion object { + @Provides + @JvmStatic + @SessionScope + fun providesVoipApi(retrofit: Retrofit): VoipApi { + return retrofit.create(VoipApi::class.java) + } + } + + @Binds + abstract fun bindCallSignalingService(service: DefaultCallSignalingService): CallSignalingService + + @Binds + abstract fun bindGetTurnServerTask(task: DefaultGetTurnServerTask): GetTurnServerTask +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/DefaultCallSignalingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/DefaultCallSignalingService.kt new file mode 100644 index 0000000000..0c1a129733 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/DefaultCallSignalingService.kt @@ -0,0 +1,237 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.call + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.session.call.CallSignalingService +import org.matrix.android.sdk.api.session.call.CallState +import org.matrix.android.sdk.api.session.call.CallsListener +import org.matrix.android.sdk.api.session.call.MxCall +import org.matrix.android.sdk.api.session.call.TurnServerResponse +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent +import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent +import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent +import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.NoOpCancellable +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.call.model.MxCallImpl +import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory +import org.matrix.android.sdk.internal.session.room.send.RoomEventSender +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import timber.log.Timber +import java.util.UUID +import javax.inject.Inject + +@SessionScope +internal class DefaultCallSignalingService @Inject constructor( + @UserId + private val userId: String, + private val localEchoEventFactory: LocalEchoEventFactory, + private val roomEventSender: RoomEventSender, + private val taskExecutor: TaskExecutor, + private val turnServerTask: GetTurnServerTask +) : CallSignalingService { + + private val callListeners = mutableSetOf() + + private val activeCalls = mutableListOf() + + private var cachedTurnServerResponse: TurnServerResponse? = null + + override fun getTurnServer(callback: MatrixCallback): Cancelable { + if (cachedTurnServerResponse != null) { + cachedTurnServerResponse?.let { callback.onSuccess(it) } + return NoOpCancellable + } + return turnServerTask + .configureWith(GetTurnServerTask.Params) { + this.callback = object : MatrixCallback { + override fun onSuccess(data: TurnServerResponse) { + cachedTurnServerResponse = data + callback.onSuccess(data) + } + + override fun onFailure(failure: Throwable) { + callback.onFailure(failure) + } + } + } + .executeBy(taskExecutor) + } + + override fun createOutgoingCall(roomId: String, otherUserId: String, isVideoCall: Boolean): MxCall { + return MxCallImpl( + callId = UUID.randomUUID().toString(), + isOutgoing = true, + roomId = roomId, + userId = userId, + otherUserId = otherUserId, + isVideoCall = isVideoCall, + localEchoEventFactory = localEchoEventFactory, + roomEventSender = roomEventSender + ).also { + activeCalls.add(it) + } + } + + override fun addCallListener(listener: CallsListener) { + callListeners.add(listener) + } + + override fun removeCallListener(listener: CallsListener) { + callListeners.remove(listener) + } + + override fun getCallWithId(callId: String): MxCall? { + Timber.v("## VOIP getCallWithId $callId all calls ${activeCalls.map { it.callId }}") + return activeCalls.find { it.callId == callId } + } + + internal fun onCallEvent(event: Event) { + when (event.getClearType()) { + EventType.CALL_ANSWER -> { + event.getClearContent().toModel()?.let { + if (event.senderId == userId) { + // ok it's an answer from me.. is it remote echo or other session + val knownCall = getCallWithId(it.callId) + if (knownCall == null) { + Timber.d("## VOIP onCallEvent ${event.getClearType()} id ${it.callId} send by me") + } else if (!knownCall.isOutgoing) { + // incoming call + // if it was anwsered by this session, the call state would be in Answering(or connected) state + if (knownCall.state == CallState.LocalRinging) { + // discard current call, it's answered by another of my session + onCallManageByOtherSession(it.callId) + } + } + return + } + + onCallAnswer(it) + } + } + EventType.CALL_INVITE -> { + if (event.senderId == userId) { + // Always ignore local echos of invite + return + } + event.getClearContent().toModel()?.let { content -> + val incomingCall = MxCallImpl( + callId = content.callId ?: return@let, + isOutgoing = false, + roomId = event.roomId ?: return@let, + userId = userId, + otherUserId = event.senderId ?: return@let, + isVideoCall = content.isVideo(), + localEchoEventFactory = localEchoEventFactory, + roomEventSender = roomEventSender + ) + activeCalls.add(incomingCall) + onCallInvite(incomingCall, content) + } + } + EventType.CALL_HANGUP -> { + event.getClearContent().toModel()?.let { content -> + + if (event.senderId == userId) { + // ok it's an answer from me.. is it remote echo or other session + val knownCall = getCallWithId(content.callId) + if (knownCall == null) { + Timber.d("## VOIP onCallEvent ${event.getClearType()} id ${content.callId} send by me") + } else if (!knownCall.isOutgoing) { + // incoming call + if (knownCall.state == CallState.LocalRinging) { + // discard current call, it's answered by another of my session + onCallManageByOtherSession(content.callId) + } + } + return + } + + onCallHangup(content) + activeCalls.removeAll { it.callId == content.callId } + } + } + EventType.CALL_CANDIDATES -> { + if (event.senderId == userId) { + // Always ignore local echos of invite + return + } + event.getClearContent().toModel()?.let { content -> + activeCalls.firstOrNull { it.callId == content.callId }?.let { + onCallIceCandidate(it, content) + } + } + } + } + } + + private fun onCallHangup(hangup: CallHangupContent) { + callListeners.toList().forEach { + tryThis { + it.onCallHangupReceived(hangup) + } + } + } + + private fun onCallAnswer(answer: CallAnswerContent) { + callListeners.toList().forEach { + tryThis { + it.onCallAnswerReceived(answer) + } + } + } + + private fun onCallManageByOtherSession(callId: String) { + callListeners.toList().forEach { + tryThis { + it.onCallManagedByOtherSession(callId) + } + } + } + + private fun onCallInvite(incomingCall: MxCall, invite: CallInviteContent) { + // Ignore the invitation from current user + if (incomingCall.otherUserId == userId) return + + callListeners.toList().forEach { + tryThis { + it.onCallInviteReceived(incomingCall, invite) + } + } + } + + private fun onCallIceCandidate(incomingCall: MxCall, candidates: CallCandidatesContent) { + callListeners.toList().forEach { + tryThis { + it.onCallIceCandidateReceived(incomingCall, candidates) + } + } + } + + companion object { + const val CALL_TIMEOUT_MS = 120_000 + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/GetTurnServerTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/GetTurnServerTask.kt new file mode 100644 index 0000000000..c38a00d1bd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/GetTurnServerTask.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.call + +import org.matrix.android.sdk.api.session.call.TurnServerResponse +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal abstract class GetTurnServerTask : Task { + object Params +} + +internal class DefaultGetTurnServerTask @Inject constructor(private val voipAPI: VoipApi, + private val eventBus: EventBus) : GetTurnServerTask() { + + override suspend fun execute(params: Params): TurnServerResponse { + return executeRequest(eventBus) { + apiCall = voipAPI.getTurnServer() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/VoipApi.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/VoipApi.kt new file mode 100644 index 0000000000..ea2f55cf67 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/VoipApi.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.call + +import org.matrix.android.sdk.api.session.call.TurnServerResponse +import org.matrix.android.sdk.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.GET + +internal interface VoipApi { + + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "voip/turnServer") + fun getTurnServer(): Call +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt new file mode 100644 index 0000000000..1e724706f3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.call.model + +import org.matrix.android.sdk.api.session.call.CallState +import org.matrix.android.sdk.api.session.call.MxCall +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.LocalEcho +import org.matrix.android.sdk.api.session.events.model.UnsignedData +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent +import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent +import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent +import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent +import org.matrix.android.sdk.internal.session.call.DefaultCallSignalingService +import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory +import org.matrix.android.sdk.internal.session.room.send.RoomEventSender +import org.webrtc.IceCandidate +import org.webrtc.SessionDescription +import timber.log.Timber + +internal class MxCallImpl( + override val callId: String, + override val isOutgoing: Boolean, + override val roomId: String, + private val userId: String, + override val otherUserId: String, + override val isVideoCall: Boolean, + private val localEchoEventFactory: LocalEchoEventFactory, + private val roomEventSender: RoomEventSender +) : MxCall { + + override var state: CallState = CallState.Idle + set(value) { + field = value + dispatchStateChange() + } + + private val listeners = mutableListOf() + + override fun addListener(listener: MxCall.StateListener) { + listeners.add(listener) + } + + override fun removeListener(listener: MxCall.StateListener) { + listeners.remove(listener) + } + + private fun dispatchStateChange() { + listeners.forEach { + try { + it.onStateUpdate(this) + } catch (failure: Throwable) { + Timber.d("dispatchStateChange failed for call $callId : ${failure.localizedMessage}") + } + } + } + + init { + if (isOutgoing) { + state = CallState.Idle + } else { + // because it's created on reception of an offer + state = CallState.LocalRinging + } + } + + override fun offerSdp(sdp: SessionDescription) { + if (!isOutgoing) return + Timber.v("## VOIP offerSdp $callId") + state = CallState.Dialing + CallInviteContent( + callId = callId, + lifetime = DefaultCallSignalingService.CALL_TIMEOUT_MS, + offer = CallInviteContent.Offer(sdp = sdp.description) + ) + .let { createEventAndLocalEcho(type = EventType.CALL_INVITE, roomId = roomId, content = it.toContent()) } + .also { roomEventSender.sendEvent(it) } + } + + override fun sendLocalIceCandidates(candidates: List) { + CallCandidatesContent( + callId = callId, + candidates = candidates.map { + CallCandidatesContent.Candidate( + sdpMid = it.sdpMid, + sdpMLineIndex = it.sdpMLineIndex, + candidate = it.sdp + ) + } + ) + .let { createEventAndLocalEcho(type = EventType.CALL_CANDIDATES, roomId = roomId, content = it.toContent()) } + .also { roomEventSender.sendEvent(it) } + } + + override fun sendLocalIceCandidateRemovals(candidates: List) { + // For now we don't support this flow + } + + override fun hangUp() { + Timber.v("## VOIP hangup $callId") + CallHangupContent( + callId = callId + ) + .let { createEventAndLocalEcho(type = EventType.CALL_HANGUP, roomId = roomId, content = it.toContent()) } + .also { roomEventSender.sendEvent(it) } + state = CallState.Terminated + } + + override fun accept(sdp: SessionDescription) { + Timber.v("## VOIP accept $callId") + if (isOutgoing) return + state = CallState.Answering + CallAnswerContent( + callId = callId, + answer = CallAnswerContent.Answer(sdp = sdp.description) + ) + .let { createEventAndLocalEcho(type = EventType.CALL_ANSWER, roomId = roomId, content = it.toContent()) } + .also { roomEventSender.sendEvent(it) } + } + + private fun createEventAndLocalEcho(localId: String = LocalEcho.createLocalEchoId(), type: String, roomId: String, content: Content): Event { + return Event( + roomId = roomId, + originServerTs = System.currentTimeMillis(), + senderId = userId, + eventId = localId, + type = type, + content = content, + unsignedData = UnsignedData(age = null, transactionId = localId) + ) + .also { localEchoEventFactory.createLocalEcho(it) } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cleanup/CleanupSession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cleanup/CleanupSession.kt new file mode 100644 index 0000000000..427fb59898 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cleanup/CleanupSession.kt @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.cleanup + +import org.matrix.android.sdk.BuildConfig +import org.matrix.android.sdk.internal.SessionManager +import org.matrix.android.sdk.internal.auth.SessionParamsStore +import org.matrix.android.sdk.internal.crypto.CryptoModule +import org.matrix.android.sdk.internal.database.RealmKeysUtils +import org.matrix.android.sdk.internal.di.CryptoDatabase +import org.matrix.android.sdk.internal.di.SessionDownloadsDirectory +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.SessionFilesDirectory +import org.matrix.android.sdk.internal.di.SessionId +import org.matrix.android.sdk.internal.di.UserMd5 +import org.matrix.android.sdk.internal.di.WorkManagerProvider +import org.matrix.android.sdk.internal.session.SessionModule +import org.matrix.android.sdk.internal.session.cache.ClearCacheTask +import io.realm.Realm +import io.realm.RealmConfiguration +import timber.log.Timber +import java.io.File +import javax.inject.Inject + +internal class CleanupSession @Inject constructor( + private val workManagerProvider: WorkManagerProvider, + @SessionId private val sessionId: String, + private val sessionManager: SessionManager, + private val sessionParamsStore: SessionParamsStore, + @SessionDatabase private val clearSessionDataTask: ClearCacheTask, + @CryptoDatabase private val clearCryptoDataTask: ClearCacheTask, + @SessionFilesDirectory private val sessionFiles: File, + @SessionDownloadsDirectory private val sessionCache: File, + private val realmKeysUtils: RealmKeysUtils, + @SessionDatabase private val realmSessionConfiguration: RealmConfiguration, + @CryptoDatabase private val realmCryptoConfiguration: RealmConfiguration, + @UserMd5 private val userMd5: String +) { + suspend fun handle() { + Timber.d("Cleanup: release session...") + sessionManager.releaseSession(sessionId) + + Timber.d("Cleanup: cancel pending works...") + workManagerProvider.cancelAllWorks() + + Timber.d("Cleanup: delete session params...") + sessionParamsStore.delete(sessionId) + + Timber.d("Cleanup: clear session data...") + clearSessionDataTask.execute(Unit) + + Timber.d("Cleanup: clear crypto data...") + clearCryptoDataTask.execute(Unit) + + Timber.d("Cleanup: clear file system") + sessionFiles.deleteRecursively() + sessionCache.deleteRecursively() + + Timber.d("Cleanup: clear the database keys") + realmKeysUtils.clear(SessionModule.getKeyAlias(userMd5)) + realmKeysUtils.clear(CryptoModule.getKeyAlias(userMd5)) + + // Sanity check + if (BuildConfig.DEBUG) { + Realm.getGlobalInstanceCount(realmSessionConfiguration) + .takeIf { it > 0 } + ?.let { Timber.e("All realm instance for session has not been closed ($it)") } + Realm.getGlobalInstanceCount(realmCryptoConfiguration) + .takeIf { it > 0 } + ?.let { Timber.e("All realm instance for crypto has not been closed ($it)") } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ContentModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ContentModule.kt new file mode 100644 index 0000000000..ba87d06097 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ContentModule.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.content + +import dagger.Binds +import dagger.Module +import org.matrix.android.sdk.api.session.content.ContentUploadStateTracker +import org.matrix.android.sdk.api.session.content.ContentUrlResolver +import org.matrix.android.sdk.api.session.file.ContentDownloadStateTracker +import org.matrix.android.sdk.internal.session.download.DefaultContentDownloadStateTracker + +@Module +internal abstract class ContentModule { + + @Binds + abstract fun bindContentUploadStateTracker(tracker: DefaultContentUploadStateTracker): ContentUploadStateTracker + + @Binds + abstract fun bindContentDownloadStateTracker(tracker: DefaultContentDownloadStateTracker): ContentDownloadStateTracker + + @Binds + abstract fun bindContentUrlResolver(resolver: DefaultContentUrlResolver): ContentUrlResolver +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ContentUploadResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ContentUploadResponse.kt new file mode 100644 index 0000000000..cb0c11d1b8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ContentUploadResponse.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.content + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ContentUploadResponse( + @Json(name = "content_uri") val contentUri: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUploadStateTracker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUploadStateTracker.kt new file mode 100644 index 0000000000..aa8b98ae62 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUploadStateTracker.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.content + +import android.os.Handler +import android.os.Looper +import org.matrix.android.sdk.api.session.content.ContentUploadStateTracker +import org.matrix.android.sdk.internal.session.SessionScope +import timber.log.Timber +import javax.inject.Inject + +@SessionScope +internal class DefaultContentUploadStateTracker @Inject constructor() : ContentUploadStateTracker { + + private val mainHandler = Handler(Looper.getMainLooper()) + private val states = mutableMapOf() + private val listeners = mutableMapOf>() + + override fun track(key: String, updateListener: ContentUploadStateTracker.UpdateListener) { + val listeners = listeners.getOrPut(key) { ArrayList() } + listeners.add(updateListener) + val currentState = states[key] ?: ContentUploadStateTracker.State.Idle + mainHandler.post { + try { + updateListener.onUpdate(currentState) + } catch (e: Exception) { + Timber.e(e, "## ContentUploadStateTracker.onUpdate() failed") + } + } + } + + override fun untrack(key: String, updateListener: ContentUploadStateTracker.UpdateListener) { + listeners[key]?.apply { + remove(updateListener) + } + } + + override fun clear() { + listeners.clear() + } + + internal fun setFailure(key: String, throwable: Throwable) { + val failure = ContentUploadStateTracker.State.Failure(throwable) + updateState(key, failure) + } + + internal fun setSuccess(key: String) { + val success = ContentUploadStateTracker.State.Success + updateState(key, success) + } + + internal fun setEncryptingThumbnail(key: String) { + val progressData = ContentUploadStateTracker.State.EncryptingThumbnail + updateState(key, progressData) + } + + internal fun setProgressThumbnail(key: String, current: Long, total: Long) { + val progressData = ContentUploadStateTracker.State.UploadingThumbnail(current, total) + updateState(key, progressData) + } + + internal fun setEncrypting(key: String) { + val progressData = ContentUploadStateTracker.State.Encrypting + updateState(key, progressData) + } + + internal fun setProgress(key: String, current: Long, total: Long) { + val progressData = ContentUploadStateTracker.State.Uploading(current, total) + updateState(key, progressData) + } + + private fun updateState(key: String, state: ContentUploadStateTracker.State) { + states[key] = state + mainHandler.post { + listeners[key]?.forEach { + try { + it.onUpdate(state) + } catch (e: Exception) { + Timber.e(e, "## ContentUploadStateTracker.onUpdate() failed") + } + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUrlResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUrlResolver.kt new file mode 100644 index 0000000000..80f69f8890 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUrlResolver.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.content + +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import org.matrix.android.sdk.api.session.content.ContentUrlResolver +import org.matrix.android.sdk.internal.network.NetworkConstants +import org.matrix.android.sdk.internal.util.ensureTrailingSlash +import javax.inject.Inject + +private const val MATRIX_CONTENT_URI_SCHEME = "mxc://" + +internal class DefaultContentUrlResolver @Inject constructor(homeServerConnectionConfig: HomeServerConnectionConfig) : ContentUrlResolver { + + private val baseUrl = homeServerConnectionConfig.homeServerUri.toString().ensureTrailingSlash() + + override val uploadUrl = baseUrl + NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "upload" + + override fun resolveFullSize(contentUrl: String?): String? { + return contentUrl + // do not allow non-mxc content URLs + ?.takeIf { it.isValidMatrixContentUrl() } + ?.let { + resolve( + contentUrl = it, + prefix = NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "download/" + ) + } + } + + override fun resolveThumbnail(contentUrl: String?, width: Int, height: Int, method: ContentUrlResolver.ThumbnailMethod): String? { + return contentUrl + // do not allow non-mxc content URLs + ?.takeIf { it.isValidMatrixContentUrl() } + ?.let { + resolve( + contentUrl = it, + prefix = NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "thumbnail/", + params = "?width=$width&height=$height&method=${method.value}" + ) + } + } + + private fun resolve(contentUrl: String, + prefix: String, + params: String = ""): String? { + var serverAndMediaId = contentUrl.removePrefix(MATRIX_CONTENT_URI_SCHEME) + val fragmentOffset = serverAndMediaId.indexOf("#") + var fragment = "" + if (fragmentOffset >= 0) { + fragment = serverAndMediaId.substring(fragmentOffset) + serverAndMediaId = serverAndMediaId.substring(0, fragmentOffset) + } + + return baseUrl + prefix + serverAndMediaId + params + fragment + } + + private fun String.isValidMatrixContentUrl(): Boolean { + return startsWith(MATRIX_CONTENT_URI_SCHEME) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt new file mode 100644 index 0000000000..798d7ceee0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.content + +import android.content.Context +import android.net.Uri +import com.squareup.moshi.Moshi +import org.matrix.android.sdk.api.session.content.ContentUrlResolver +import org.matrix.android.sdk.internal.di.Authenticated +import org.matrix.android.sdk.internal.network.ProgressRequestBody +import org.matrix.android.sdk.internal.network.awaitResponse +import org.matrix.android.sdk.internal.network.toFailure +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.asRequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import org.greenrobot.eventbus.EventBus +import java.io.File +import java.io.FileNotFoundException +import java.io.IOException +import javax.inject.Inject + +internal class FileUploader @Inject constructor(@Authenticated + private val okHttpClient: OkHttpClient, + private val eventBus: EventBus, + private val context: Context, + contentUrlResolver: ContentUrlResolver, + moshi: Moshi) { + + private val uploadUrl = contentUrlResolver.uploadUrl + private val responseAdapter = moshi.adapter(ContentUploadResponse::class.java) + + suspend fun uploadFile(file: File, + filename: String?, + mimeType: String?, + progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse { + val uploadBody = file.asRequestBody(mimeType?.toMediaTypeOrNull()) + return upload(uploadBody, filename, progressListener) + } + + suspend fun uploadByteArray(byteArray: ByteArray, + filename: String?, + mimeType: String?, + progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse { + val uploadBody = byteArray.toRequestBody(mimeType?.toMediaTypeOrNull()) + return upload(uploadBody, filename, progressListener) + } + + suspend fun uploadFromUri(uri: Uri, + filename: String?, + mimeType: String?, + progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse { + val inputStream = withContext(Dispatchers.IO) { + context.contentResolver.openInputStream(uri) + } ?: throw FileNotFoundException() + + inputStream.use { + return uploadByteArray(it.readBytes(), filename, mimeType, progressListener) + } + } + + private suspend fun upload(uploadBody: RequestBody, filename: String?, progressListener: ProgressRequestBody.Listener?): ContentUploadResponse { + val urlBuilder = uploadUrl.toHttpUrlOrNull()?.newBuilder() ?: throw RuntimeException() + + val httpUrl = urlBuilder + .addQueryParameter("filename", filename) + .build() + + val requestBody = if (progressListener != null) ProgressRequestBody(uploadBody, progressListener) else uploadBody + + val request = Request.Builder() + .url(httpUrl) + .post(requestBody) + .build() + + return okHttpClient.newCall(request).awaitResponse().use { response -> + if (!response.isSuccessful) { + throw response.toFailure(eventBus) + } else { + response.body?.source()?.let { + responseAdapter.fromJson(it) + } + ?: throw IOException() + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ThumbnailExtractor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ThumbnailExtractor.kt new file mode 100644 index 0000000000..05558fcf4c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ThumbnailExtractor.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.content + +import android.content.Context +import android.graphics.Bitmap +import android.media.MediaMetadataRetriever +import org.matrix.android.sdk.api.session.content.ContentAttachmentData +import timber.log.Timber +import java.io.ByteArrayOutputStream + +internal object ThumbnailExtractor { + + class ThumbnailData( + val width: Int, + val height: Int, + val size: Long, + val bytes: ByteArray, + val mimeType: String + ) + + fun extractThumbnail(context: Context, attachment: ContentAttachmentData): ThumbnailData? { + return if (attachment.type == ContentAttachmentData.Type.VIDEO) { + extractVideoThumbnail(context, attachment) + } else { + null + } + } + + private fun extractVideoThumbnail(context: Context, attachment: ContentAttachmentData): ThumbnailData? { + var thumbnailData: ThumbnailData? = null + val mediaMetadataRetriever = MediaMetadataRetriever() + try { + mediaMetadataRetriever.setDataSource(context, attachment.queryUri) + val thumbnail = mediaMetadataRetriever.frameAtTime + + val outputStream = ByteArrayOutputStream() + thumbnail.compress(Bitmap.CompressFormat.JPEG, 100, outputStream) + val thumbnailWidth = thumbnail.width + val thumbnailHeight = thumbnail.height + val thumbnailSize = outputStream.size() + thumbnailData = ThumbnailData( + width = thumbnailWidth, + height = thumbnailHeight, + size = thumbnailSize.toLong(), + bytes = outputStream.toByteArray(), + mimeType = "image/jpeg" + ) + thumbnail.recycle() + outputStream.reset() + } catch (e: Exception) { + Timber.e(e, "Cannot extract video thumbnail") + } finally { + mediaMetadataRetriever.release() + } + return thumbnailData + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt new file mode 100644 index 0000000000..6d354cdcbe --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt @@ -0,0 +1,347 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.content + +import android.content.Context +import android.graphics.BitmapFactory +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.squareup.moshi.JsonClass +import id.zelory.compressor.Compressor +import id.zelory.compressor.constraint.default +import org.matrix.android.sdk.api.session.content.ContentAttachmentData +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageFileContent +import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent +import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments +import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo +import org.matrix.android.sdk.internal.network.ProgressRequestBody +import org.matrix.android.sdk.internal.session.DefaultFileService +import org.matrix.android.sdk.internal.session.room.send.MultipleEventSendingDispatcherWorker +import org.matrix.android.sdk.internal.worker.SessionWorkerParams +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import org.matrix.android.sdk.internal.worker.getSessionComponent +import timber.log.Timber +import java.io.ByteArrayInputStream +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.util.UUID +import javax.inject.Inject + +private data class NewImageAttributes( + val newWidth: Int?, + val newHeight: Int?, + val newFileSize: Int +) + +/** + * Possible previous worker: None + * Possible next worker : Always [MultipleEventSendingDispatcherWorker] + */ +internal class UploadContentWorker(val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { + + @JsonClass(generateAdapter = true) + internal data class Params( + override val sessionId: String, + val events: List, + val attachment: ContentAttachmentData, + val isEncrypted: Boolean, + val compressBeforeSending: Boolean, + override val lastFailureMessage: String? = null + ) : SessionWorkerParams + + @Inject lateinit var fileUploader: FileUploader + @Inject lateinit var contentUploadStateTracker: DefaultContentUploadStateTracker + @Inject lateinit var fileService: DefaultFileService + + override suspend fun doWork(): Result { + val params = WorkerParamsFactory.fromData(inputData) + ?: return Result.success() + .also { Timber.e("Unable to parse work parameters") } + + Timber.v("Starting upload media work with params $params") + + if (params.lastFailureMessage != null) { + // Transmit the error + return Result.success(inputData) + .also { Timber.e("Work cancelled due to input error from parent") } + } + + // Just defensive code to ensure that we never have an uncaught exception that could break the queue + return try { + internalDoWork(params) + } catch (failure: Throwable) { + Timber.e(failure) + handleFailure(params, failure) + } + } + + private suspend fun internalDoWork(params: Params): Result { + val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() + sessionComponent.inject(this) + + val attachment = params.attachment + + var newImageAttributes: NewImageAttributes? = null + + try { + val inputStream = context.contentResolver.openInputStream(attachment.queryUri) + ?: return Result.success( + WorkerParamsFactory.toData( + params.copy( + lastFailureMessage = "Cannot openInputStream for file: " + attachment.queryUri.toString() + ) + ) + ) + + inputStream.use { + var uploadedThumbnailUrl: String? = null + var uploadedThumbnailEncryptedFileInfo: EncryptedFileInfo? = null + + ThumbnailExtractor.extractThumbnail(context, params.attachment)?.let { thumbnailData -> + val thumbnailProgressListener = object : ProgressRequestBody.Listener { + override fun onProgress(current: Long, total: Long) { + notifyTracker(params) { contentUploadStateTracker.setProgressThumbnail(it, current, total) } + } + } + + try { + val contentUploadResponse = if (params.isEncrypted) { + Timber.v("Encrypt thumbnail") + notifyTracker(params) { contentUploadStateTracker.setEncryptingThumbnail(it) } + val encryptionResult = MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType) + uploadedThumbnailEncryptedFileInfo = encryptionResult.encryptedFileInfo + fileUploader.uploadByteArray(encryptionResult.encryptedByteArray, + "thumb_${attachment.name}", + "application/octet-stream", + thumbnailProgressListener) + } else { + fileUploader.uploadByteArray(thumbnailData.bytes, + "thumb_${attachment.name}", + thumbnailData.mimeType, + thumbnailProgressListener) + } + + uploadedThumbnailUrl = contentUploadResponse.contentUri + } catch (t: Throwable) { + Timber.e(t, "Thumbnail update failed") + } + } + + val progressListener = object : ProgressRequestBody.Listener { + override fun onProgress(current: Long, total: Long) { + notifyTracker(params) { + if (isStopped) { + contentUploadStateTracker.setFailure(it, Throwable("Cancelled")) + } else { + contentUploadStateTracker.setProgress(it, current, total) + } + } + } + } + + var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null + + return try { + // Compressor library works with File instead of Uri for now. Since Scoped Storage doesn't allow us to access files directly, we should + // copy it to a cache folder by using InputStream and OutputStream. + // https://github.com/zetbaitsu/Compressor/pull/150 + // As soon as the above PR is merged, we can use attachment.queryUri instead of creating a cacheFile. + var cacheFile = File.createTempFile(attachment.name ?: UUID.randomUUID().toString(), ".jpg", context.cacheDir) + cacheFile.parentFile?.mkdirs() + if (cacheFile.exists()) { + cacheFile.delete() + } + cacheFile.createNewFile() + cacheFile.deleteOnExit() + + val outputStream = FileOutputStream(cacheFile) + outputStream.use { + inputStream.copyTo(outputStream) + } + + if (attachment.type == ContentAttachmentData.Type.IMAGE && params.compressBeforeSending) { + cacheFile = Compressor.compress(context, cacheFile) { + default( + width = MAX_IMAGE_SIZE, + height = MAX_IMAGE_SIZE + ) + }.also { compressedFile -> + val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } + BitmapFactory.decodeFile(compressedFile.absolutePath, options) + val fileSize = compressedFile.length().toInt() + newImageAttributes = NewImageAttributes( + options.outWidth, + options.outHeight, + fileSize + ) + } + } + + val contentUploadResponse = if (params.isEncrypted) { + Timber.v("Encrypt file") + notifyTracker(params) { contentUploadStateTracker.setEncrypting(it) } + + val encryptionResult = MXEncryptedAttachments.encryptAttachment(FileInputStream(cacheFile), attachment.getSafeMimeType()) + uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo + + fileUploader + .uploadByteArray(encryptionResult.encryptedByteArray, attachment.name, "application/octet-stream", progressListener) + } else { + fileUploader + .uploadFile(cacheFile, attachment.name, attachment.getSafeMimeType(), progressListener) + } + + // If it's a file update the file service so that it does not redownload? + if (params.attachment.type == ContentAttachmentData.Type.FILE) { + context.contentResolver.openInputStream(attachment.queryUri)?.let { + fileService.storeDataFor(contentUploadResponse.contentUri, params.attachment.getSafeMimeType(), it) + } + } + + handleSuccess(params, + contentUploadResponse.contentUri, + uploadedFileEncryptedFileInfo, + uploadedThumbnailUrl, + uploadedThumbnailEncryptedFileInfo, + newImageAttributes) + } catch (t: Throwable) { + Timber.e(t) + handleFailure(params, t) + } + } + } catch (e: Exception) { + Timber.e(e) + notifyTracker(params) { contentUploadStateTracker.setFailure(it, e) } + return Result.success( + WorkerParamsFactory.toData( + params.copy( + lastFailureMessage = e.localizedMessage + ) + ) + ) + } + } + + private fun handleFailure(params: Params, failure: Throwable): Result { + notifyTracker(params) { contentUploadStateTracker.setFailure(it, failure) } + + return Result.success( + WorkerParamsFactory.toData( + params.copy( + lastFailureMessage = failure.localizedMessage + ) + ) + ) + } + + private fun handleSuccess(params: Params, + attachmentUrl: String, + encryptedFileInfo: EncryptedFileInfo?, + thumbnailUrl: String?, + thumbnailEncryptedFileInfo: EncryptedFileInfo?, + newImageAttributes: NewImageAttributes?): Result { + Timber.v("handleSuccess $attachmentUrl, work is stopped $isStopped") + notifyTracker(params) { contentUploadStateTracker.setSuccess(it) } + + val updatedEvents = params.events + .map { + updateEvent(it, attachmentUrl, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo, newImageAttributes) + } + + val sendParams = MultipleEventSendingDispatcherWorker.Params(params.sessionId, updatedEvents, params.isEncrypted) + return Result.success(WorkerParamsFactory.toData(sendParams)) + } + + private fun updateEvent(event: Event, + url: String, + encryptedFileInfo: EncryptedFileInfo?, + thumbnailUrl: String? = null, + thumbnailEncryptedFileInfo: EncryptedFileInfo?, + newImageAttributes: NewImageAttributes?): Event { + val messageContent: MessageContent = event.content.toModel() ?: return event + val updatedContent = when (messageContent) { + is MessageImageContent -> messageContent.update(url, encryptedFileInfo, newImageAttributes) + is MessageVideoContent -> messageContent.update(url, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo) + is MessageFileContent -> messageContent.update(url, encryptedFileInfo) + is MessageAudioContent -> messageContent.update(url, encryptedFileInfo) + else -> messageContent + } + return event.copy(content = updatedContent.toContent()) + } + + private fun notifyTracker(params: Params, function: (String) -> Unit) { + params.events + .mapNotNull { it.eventId } + .forEach { eventId -> function.invoke(eventId) } + } + + private fun MessageImageContent.update(url: String, + encryptedFileInfo: EncryptedFileInfo?, + newImageAttributes: NewImageAttributes?): MessageImageContent { + return copy( + url = if (encryptedFileInfo == null) url else null, + encryptedFileInfo = encryptedFileInfo?.copy(url = url), + info = info?.copy( + width = newImageAttributes?.newWidth ?: info.width, + height = newImageAttributes?.newHeight ?: info.height, + size = newImageAttributes?.newFileSize ?: info.size + ) + ) + } + + private fun MessageVideoContent.update(url: String, + encryptedFileInfo: EncryptedFileInfo?, + thumbnailUrl: String?, + thumbnailEncryptedFileInfo: EncryptedFileInfo?): MessageVideoContent { + return copy( + url = if (encryptedFileInfo == null) url else null, + encryptedFileInfo = encryptedFileInfo?.copy(url = url), + videoInfo = videoInfo?.copy( + thumbnailUrl = if (thumbnailEncryptedFileInfo == null) thumbnailUrl else null, + thumbnailFile = thumbnailEncryptedFileInfo?.copy(url = thumbnailUrl) + ) + ) + } + + private fun MessageFileContent.update(url: String, + encryptedFileInfo: EncryptedFileInfo?): MessageFileContent { + return copy( + url = if (encryptedFileInfo == null) url else null, + encryptedFileInfo = encryptedFileInfo?.copy(url = url) + ) + } + + private fun MessageAudioContent.update(url: String, + encryptedFileInfo: EncryptedFileInfo?): MessageAudioContent { + return copy( + url = if (encryptedFileInfo == null) url else null, + encryptedFileInfo = encryptedFileInfo?.copy(url = url) + ) + } + + companion object { + private const val MAX_IMAGE_SIZE = 640 + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/download/DefaultContentDownloadStateTracker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/download/DefaultContentDownloadStateTracker.kt new file mode 100644 index 0000000000..295a829b08 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/download/DefaultContentDownloadStateTracker.kt @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.download + +import android.os.Handler +import android.os.Looper +import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.session.file.ContentDownloadStateTracker +import org.matrix.android.sdk.internal.session.SessionScope +import timber.log.Timber +import javax.inject.Inject + +@SessionScope +internal class DefaultContentDownloadStateTracker @Inject constructor() : ProgressListener, ContentDownloadStateTracker { + + private val mainHandler = Handler(Looper.getMainLooper()) + private val states = mutableMapOf() + private val listeners = mutableMapOf>() + + override fun track(key: String, updateListener: ContentDownloadStateTracker.UpdateListener) { + val listeners = listeners.getOrPut(key) { ArrayList() } + if (!listeners.contains(updateListener)) { + listeners.add(updateListener) + } + val currentState = states[key] ?: ContentDownloadStateTracker.State.Idle + mainHandler.post { + try { + updateListener.onDownloadStateUpdate(currentState) + } catch (e: Exception) { + Timber.e(e, "## ContentUploadStateTracker.onUpdate() failed") + } + } + } + + override fun unTrack(key: String, updateListener: ContentDownloadStateTracker.UpdateListener) { + listeners[key]?.apply { + remove(updateListener) + } + } + + override fun clear() { + states.clear() + listeners.clear() + } + +// private fun URL.toKey() = toString() + + override fun update(url: String, bytesRead: Long, contentLength: Long, done: Boolean) { + Timber.v("## DL Progress url:$url read:$bytesRead total:$contentLength done:$done") + if (done) { + updateState(url, ContentDownloadStateTracker.State.Success) + } else { + updateState(url, ContentDownloadStateTracker.State.Downloading(bytesRead, contentLength, contentLength == -1L)) + } + } + + override fun error(url: String, errorCode: Int) { + Timber.v("## DL Progress Error code:$errorCode") + updateState(url, ContentDownloadStateTracker.State.Failure(errorCode)) + listeners[url]?.forEach { + tryThis { it.onDownloadStateUpdate(ContentDownloadStateTracker.State.Failure(errorCode)) } + } + } + + private fun updateState(url: String, state: ContentDownloadStateTracker.State) { + states[url] = state + listeners[url]?.forEach { + tryThis { it.onDownloadStateUpdate(state) } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/download/DownloadProgressInterceptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/download/DownloadProgressInterceptor.kt new file mode 100644 index 0000000000..d8ef0c3323 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/download/DownloadProgressInterceptor.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.download + +import okhttp3.Interceptor +import okhttp3.Response +import javax.inject.Inject + +internal class DownloadProgressInterceptor @Inject constructor( + private val downloadStateTracker: DefaultContentDownloadStateTracker +) : Interceptor { + + companion object { + const val DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER = "matrix-sdk:mxc_URL" + } + + override fun intercept(chain: Interceptor.Chain): Response { + val url = chain.request().url.toUrl() + val mxcURl = chain.request().header(DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER) + + val request = chain.request().newBuilder() + .removeHeader(DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER) + .build() + + val originalResponse = chain.proceed(request) + if (!originalResponse.isSuccessful) { + downloadStateTracker.error(mxcURl ?: url.toExternalForm(), originalResponse.code) + return originalResponse + } + val responseBody = originalResponse.body ?: return originalResponse + return originalResponse.newBuilder() + .body(ProgressResponseBody(responseBody, mxcURl ?: url.toExternalForm(), downloadStateTracker)) + .build() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/download/ProgressResponseBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/download/ProgressResponseBody.kt new file mode 100644 index 0000000000..8bfe74862d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/download/ProgressResponseBody.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.download + +import okhttp3.MediaType +import okhttp3.ResponseBody +import okio.Buffer +import okio.BufferedSource +import okio.ForwardingSource +import okio.Source +import okio.buffer + +class ProgressResponseBody( + private val responseBody: ResponseBody, + private val chainUrl: String, + private val progressListener: ProgressListener) : ResponseBody() { + + private var bufferedSource: BufferedSource? = null + + override fun contentType(): MediaType? = responseBody.contentType() + override fun contentLength(): Long = responseBody.contentLength() + + override fun source(): BufferedSource { + if (bufferedSource == null) { + bufferedSource = source(responseBody.source()).buffer() + } + return bufferedSource!! + } + + fun source(source: Source): Source { + return object : ForwardingSource(source) { + var totalBytesRead = 0L + + override fun read(sink: Buffer, byteCount: Long): Long { + val bytesRead = super.read(sink, byteCount) + // read() returns the number of bytes read, or -1 if this source is exhausted. + totalBytesRead += if (bytesRead != -1L) bytesRead else 0L + progressListener.update(chainUrl, totalBytesRead, responseBody.contentLength(), bytesRead == -1L) + return bytesRead + } + } + } +} + +interface ProgressListener { + fun update(url: String, bytesRead: Long, contentLength: Long, done: Boolean) + fun error(url: String, errorCode: Int) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterRepository.kt new file mode 100644 index 0000000000..e07778b536 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterRepository.kt @@ -0,0 +1,91 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.filter + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.internal.database.model.FilterEntity +import org.matrix.android.sdk.internal.database.model.FilterEntityFields +import org.matrix.android.sdk.internal.database.query.get +import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.util.awaitTransaction +import io.realm.Realm +import io.realm.kotlin.where +import javax.inject.Inject + +internal class DefaultFilterRepository @Inject constructor(@SessionDatabase private val monarchy: Monarchy) : FilterRepository { + + override suspend fun storeFilter(filter: Filter, roomEventFilter: RoomEventFilter): Boolean { + return Realm.getInstance(monarchy.realmConfiguration).use { realm -> + val filterEntity = FilterEntity.get(realm) + // Filter has changed, or no filter Id yet + filterEntity == null + || filterEntity.filterBodyJson != filter.toJSONString() + || filterEntity.filterId.isBlank() + }.also { hasChanged -> + if (hasChanged) { + // Filter is new or has changed, store it and reset the filter Id. + // This has to be done outside of the Realm.use(), because awaitTransaction change the current thread + monarchy.awaitTransaction { realm -> + // We manage only one filter for now + val filterJson = filter.toJSONString() + val roomEventFilterJson = roomEventFilter.toJSONString() + + val filterEntity = FilterEntity.getOrCreate(realm) + + filterEntity.filterBodyJson = filterJson + filterEntity.roomEventFilterJson = roomEventFilterJson + // Reset filterId + filterEntity.filterId = "" + } + } + } + } + + override suspend fun storeFilterId(filter: Filter, filterId: String) { + monarchy.awaitTransaction { + // We manage only one filter for now + val filterJson = filter.toJSONString() + + // Update the filter id, only if the filter body matches + it.where() + .equalTo(FilterEntityFields.FILTER_BODY_JSON, filterJson) + ?.findFirst() + ?.filterId = filterId + } + } + + override suspend fun getFilter(): String { + return monarchy.awaitTransaction { + val filter = FilterEntity.getOrCreate(it) + if (filter.filterId.isBlank()) { + // Use the Json format + filter.filterBodyJson + } else { + // Use FilterId + filter.filterId + } + } + } + + override suspend fun getRoomFilter(): String { + return monarchy.awaitTransaction { + FilterEntity.getOrCreate(it).roomEventFilterJson + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterService.kt new file mode 100644 index 0000000000..d120312a1f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterService.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.filter + +import org.matrix.android.sdk.api.session.sync.FilterService +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import javax.inject.Inject + +internal class DefaultFilterService @Inject constructor(private val saveFilterTask: SaveFilterTask, + private val taskExecutor: TaskExecutor) : FilterService { + + // TODO Pass a list of support events instead + override fun setFilter(filterPreset: FilterService.FilterPreset) { + saveFilterTask + .configureWith(SaveFilterTask.Params(filterPreset)) + .executeBy(taskExecutor) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultSaveFilterTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultSaveFilterTask.kt new file mode 100644 index 0000000000..b5c214dbaa --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultSaveFilterTask.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.filter + +import org.matrix.android.sdk.api.session.sync.FilterService +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +/** + * Save a filter, in db and if any changes, upload to the server + */ +internal interface SaveFilterTask : Task { + + data class Params( + val filterPreset: FilterService.FilterPreset + ) +} + +internal class DefaultSaveFilterTask @Inject constructor( + @UserId private val userId: String, + private val filterAPI: FilterApi, + private val filterRepository: FilterRepository, + private val eventBus: EventBus +) : SaveFilterTask { + + override suspend fun execute(params: SaveFilterTask.Params) { + val filterBody = when (params.filterPreset) { + FilterService.FilterPreset.RiotFilter -> { + FilterFactory.createRiotFilter() + } + FilterService.FilterPreset.NoFilter -> { + FilterFactory.createDefaultFilter() + } + } + val roomFilter = when (params.filterPreset) { + FilterService.FilterPreset.RiotFilter -> { + FilterFactory.createRiotRoomFilter() + } + FilterService.FilterPreset.NoFilter -> { + FilterFactory.createDefaultRoomFilter() + } + } + val updated = filterRepository.storeFilter(filterBody, roomFilter) + if (updated) { + val filterResponse = executeRequest(eventBus) { + // TODO auto retry + apiCall = filterAPI.uploadFilter(userId, filterBody) + } + filterRepository.storeFilterId(filterBody, filterResponse.filterId) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/EventFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/EventFilter.kt new file mode 100644 index 0000000000..fdfda09633 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/EventFilter.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.filter + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Represents "Filter" as mentioned in the SPEC + * https://matrix.org/docs/spec/client_server/r0.3.0.html#post-matrix-client-r0-user-userid-filter + */ +@JsonClass(generateAdapter = true) +data class EventFilter( + /** + * The maximum number of events to return. + */ + @Json(name = "limit") val limit: Int? = null, + /** + * A list of senders IDs to include. If this list is absent then all senders are included. + */ + @Json(name = "senders") val senders: List? = null, + /** + * A list of sender IDs to exclude. If this list is absent then no senders are excluded. + * A matching sender will be excluded even if it is listed in the 'senders' filter. + */ + @Json(name = "not_senders") val notSenders: List? = null, + /** + * A list of event types to include. If this list is absent then all event types are included. + * A '*' can be used as a wildcard to match any sequence of characters. + */ + @Json(name = "types") val types: List? = null, + /** + * A list of event types to exclude. If this list is absent then no event types are excluded. + * A matching type will be excluded even if it is listed in the 'types' filter. + * A '*' can be used as a wildcard to match any sequence of characters. + */ + @Json(name = "not_types") val notTypes: List? = null +) { + fun hasData(): Boolean { + return limit != null + || senders != null + || notSenders != null + || types != null + || notTypes != null + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/Filter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/Filter.kt new file mode 100644 index 0000000000..4c4a4d6181 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/Filter.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.filter + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.di.MoshiProvider + +/** + * Class which can be parsed to a filter json string. Used for POST and GET + * Have a look here for further information: + * https://matrix.org/docs/spec/client_server/r0.3.0.html#post-matrix-client-r0-user-userid-filter + */ +@JsonClass(generateAdapter = true) +internal data class Filter( + /** + * List of event fields to include. If this list is absent then all fields are included. The entries may + * include '.' characters to indicate sub-fields. So ['content.body'] will include the 'body' field of the + * 'content' object. A literal '.' character in a field name may be escaped using a '\'. A server may + * include more fields than were requested. + */ + @Json(name = "event_fields") val eventFields: List? = null, + /** + * The format to use for events. 'client' will return the events in a format suitable for clients. + * 'federation' will return the raw event as received over federation. The default is 'client'. One of: ["client", "federation"] + */ + @Json(name = "event_format") val eventFormat: String? = null, + /** + * The presence updates to include. + */ + @Json(name = "presence") val presence: EventFilter? = null, + /** + * The user account data that isn't associated with rooms to include. + */ + @Json(name = "account_data") val accountData: EventFilter? = null, + /** + * Filters to be applied to room data. + */ + @Json(name = "room") val room: RoomFilter? = null +) { + + fun toJSONString(): String { + return MoshiProvider.providesMoshi().adapter(Filter::class.java).toJson(this) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterApi.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterApi.kt new file mode 100644 index 0000000000..8a45a6d66f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterApi.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2018 Matthias Kesler + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.filter + +import org.matrix.android.sdk.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Path + +internal interface FilterApi { + + /** + * Upload FilterBody to get a filter_id which can be used for /sync requests + * + * @param userId the user id + * @param body the Json representation of a FilterBody object + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/filter") + fun uploadFilter(@Path("userId") userId: String, + @Body body: Filter): Call + + /** + * Gets a filter with a given filterId from the homeserver + * + * @param userId the user id + * @param filterId the filterID + * @return Filter + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/filter/{filterId}") + fun getFilterById(@Path("userId") userId: String, + @Path("filterId") filterId: String): Call +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt new file mode 100644 index 0000000000..12764248ef --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.filter + +import org.matrix.android.sdk.api.session.events.model.EventType + +internal object FilterFactory { + + fun createUploadsFilter(numberOfEvents: Int): RoomEventFilter { + return RoomEventFilter( + limit = numberOfEvents, + containsUrl = true, + types = listOf(EventType.MESSAGE), + lazyLoadMembers = true + ) + } + + fun createDefaultFilter(): Filter { + return FilterUtil.enableLazyLoading(Filter(), true) + } + + fun createRiotFilter(): Filter { + return Filter( + room = RoomFilter( + timeline = createRiotTimelineFilter(), + state = createRiotStateFilter() + ) + ) + } + + fun createDefaultRoomFilter(): RoomEventFilter { + return RoomEventFilter( + lazyLoadMembers = true + ) + } + + fun createRiotRoomFilter(): RoomEventFilter { + return RoomEventFilter( + lazyLoadMembers = true + // TODO Enable this for optimization + // types = (listOfSupportedEventTypes + listOfSupportedStateEventTypes).toMutableList() + ) + } + + private fun createRiotTimelineFilter(): RoomEventFilter { + return RoomEventFilter().apply { + // TODO Enable this for optimization + // types = listOfSupportedEventTypes.toMutableList() + } + } + + private fun createRiotStateFilter(): RoomEventFilter { + return RoomEventFilter( + lazyLoadMembers = true + ) + } + + // Get only managed types by Riot + private val listOfSupportedEventTypes = listOf( + // TODO Complete the list + EventType.MESSAGE + ) + + // Get only managed types by Riot + private val listOfSupportedStateEventTypes = listOf( + // TODO Complete the list + EventType.STATE_ROOM_MEMBER + ) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterModule.kt new file mode 100644 index 0000000000..f5052d57ac --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterModule.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.filter + +import dagger.Binds +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.api.session.sync.FilterService +import org.matrix.android.sdk.internal.session.SessionScope +import retrofit2.Retrofit + +@Module +internal abstract class FilterModule { + + @Module + companion object { + @Provides + @JvmStatic + @SessionScope + fun providesFilterApi(retrofit: Retrofit): FilterApi { + return retrofit.create(FilterApi::class.java) + } + } + + @Binds + abstract fun bindFilterRepository(repository: DefaultFilterRepository): FilterRepository + + @Binds + abstract fun bindFilterService(service: DefaultFilterService): FilterService + + @Binds + abstract fun bindSaveFilterTask(task: DefaultSaveFilterTask): SaveFilterTask +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterRepository.kt new file mode 100644 index 0000000000..b19478c42f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterRepository.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.filter + +internal interface FilterRepository { + + /** + * Return true if the filterBody has changed, or need to be sent to the server + */ + suspend fun storeFilter(filter: Filter, roomEventFilter: RoomEventFilter): Boolean + + /** + * Set the filterId of this filter + */ + suspend fun storeFilterId(filter: Filter, filterId: String) + + /** + * Return filter json or filter id + */ + suspend fun getFilter(): String + + /** + * Return the room filter + */ + suspend fun getRoomFilter(): String +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterResponse.kt new file mode 100644 index 0000000000..951b7e8ca2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterResponse.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.filter + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Represents the body which is the response when creating a filter on the server + * https://matrix.org/docs/spec/client_server/r0.3.0.html#post-matrix-client-r0-user-userid-filter + */ +@JsonClass(generateAdapter = true) +data class FilterResponse( + /** + * Required. The ID of the filter that was created. Cannot start with a { as this character + * is used to determine if the filter provided is inline JSON or a previously declared + * filter by homeservers on some APIs. + */ + @Json(name = "filter_id") val filterId: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterUtil.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterUtil.kt new file mode 100644 index 0000000000..3a030cc470 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterUtil.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.filter + +internal object FilterUtil { + + /** + * Patch the filterBody to enable or disable the data save mode + * + * If data save mode is on, FilterBody will contains + * FIXME New expected filter: + * "{\"room\": {\"ephemeral\": {\"notTypes\": [\"m.typing\"]}}, \"presence\":{\"notTypes\": [\"*\"]}}" + * + * @param filterBody filterBody to patch + * @param useDataSaveMode true to enable data save mode + */ + /* + fun enableDataSaveMode(filterBody: FilterBody, useDataSaveMode: Boolean) { + if (useDataSaveMode) { + // Enable data save mode + if (filterBody.room == null) { + filterBody.room = RoomFilter() + } + filterBody.room?.let { room -> + if (room.ephemeral == null) { + room.ephemeral = RoomEventFilter() + } + room.ephemeral?.types?.let { types -> + if (!types.contains("m.receipt")) { + types.add("m.receipt") + } + } + } + + if (filterBody.presence == null) { + filterBody.presence = Filter() + } + filterBody.presence?.notTypes?.let { notTypes -> + if (!notTypes.contains("*")) { + notTypes.add("*") + } + } + } else { + filterBody.room?.let { room -> + room.ephemeral?.types?.remove("m.receipt") + if (room.ephemeral?.types?.isEmpty() == true) { + room.ephemeral?.types = null + } + if (room.ephemeral?.hasData() == false) { + room.ephemeral = null + } + } + if (filterBody.room?.hasData() == false) { + filterBody.room = null + } + + filterBody.presence?.let { presence -> + presence.notTypes?.remove("*") + if (presence.notTypes?.isEmpty() == true) { + presence.notTypes = null + } + } + if (filterBody.presence?.hasData() == false) { + filterBody.presence = null + } + } + } */ + + /** + * Compute a new filter to enable or disable the lazy loading + * + * + * If lazy loading is on, the filter will looks like + * {"room":{"state":{"lazy_load_members":true})} + * + * @param filter filter to patch + * @param useLazyLoading true to enable lazy loading + */ + fun enableLazyLoading(filter: Filter, useLazyLoading: Boolean): Filter { + if (useLazyLoading) { + // Enable lazy loading + return filter.copy( + room = filter.room?.copy( + state = filter.room.state?.copy(lazyLoadMembers = true) + ?: RoomEventFilter(lazyLoadMembers = true) + ) + ?: RoomFilter(state = RoomEventFilter(lazyLoadMembers = true)) + ) + } else { + val newRoomEventFilter = filter.room?.state?.copy(lazyLoadMembers = null)?.takeIf { it.hasData() } + val newRoomFilter = filter.room?.copy(state = newRoomEventFilter)?.takeIf { it.hasData() } + + return filter.copy( + room = newRoomFilter + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt new file mode 100644 index 0000000000..cefa9e8ece --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.filter + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.di.MoshiProvider + +/** + * Represents "RoomEventFilter" as mentioned in the SPEC + * https://matrix.org/docs/spec/client_server/r0.3.0.html#post-matrix-client-r0-user-userid-filter + */ +@JsonClass(generateAdapter = true) +data class RoomEventFilter( + /** + * The maximum number of events to return. + */ + @Json(name = "limit") val limit: Int? = null, + /** + * A list of sender IDs to exclude. If this list is absent then no senders are excluded. A matching sender will + * be excluded even if it is listed in the 'senders' filter. + */ + @Json(name = "not_senders") val notSenders: List? = null, + /** + * A list of event types to exclude. If this list is absent then no event types are excluded. A matching type will + * be excluded even if it is listed in the 'types' filter. A '*' can be used as a wildcard to match any sequence of characters. + */ + @Json(name = "not_types") val notTypes: List? = null, + /** + * A list of senders IDs to include. If this list is absent then all senders are included. + */ + @Json(name = "senders") val senders: List? = null, + /** + * A list of event types to include. If this list is absent then all event types are included. A '*' can be used as + * a wildcard to match any sequence of characters. + */ + @Json(name = "types") val types: List? = null, + /** + * A list of room IDs to include. If this list is absent then all rooms are included. + */ + @Json(name = "rooms") val rooms: List? = null, + /** + * A list of room IDs to exclude. If this list is absent then no rooms are excluded. A matching room will be excluded + * even if it is listed in the 'rooms' filter. + */ + @Json(name = "not_rooms") val notRooms: List? = null, + /** + * If true, includes only events with a url key in their content. If false, excludes those events. If omitted, url + * key is not considered for filtering. + */ + @Json(name = "contains_url") val containsUrl: Boolean? = null, + /** + * If true, enables lazy-loading of membership events. See Lazy-loading room members for more information. Defaults to false. + */ + @Json(name = "lazy_load_members") val lazyLoadMembers: Boolean? = null +) { + + fun toJSONString(): String { + return MoshiProvider.providesMoshi().adapter(RoomEventFilter::class.java).toJson(this) + } + + fun hasData(): Boolean { + return (limit != null + || notSenders != null + || notTypes != null + || senders != null + || types != null + || rooms != null + || notRooms != null + || containsUrl != null + || lazyLoadMembers != null) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomFilter.kt new file mode 100644 index 0000000000..c0694aee51 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomFilter.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.filter + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Represents "RoomFilter" as mentioned in the SPEC + * https://matrix.org/docs/spec/client_server/r0.3.0.html#post-matrix-client-r0-user-userid-filter + */ +@JsonClass(generateAdapter = true) +data class RoomFilter( + /** + * A list of room IDs to exclude. If this list is absent then no rooms are excluded. + * A matching room will be excluded even if it is listed in the 'rooms' filter. + * This filter is applied before the filters in ephemeral, state, timeline or account_data + */ + @Json(name = "not_rooms") val notRooms: List? = null, + /** + * A list of room IDs to include. If this list is absent then all rooms are included. + * This filter is applied before the filters in ephemeral, state, timeline or account_data + */ + @Json(name = "rooms") val rooms: List? = null, + /** + * The events that aren't recorded in the room history, e.g. typing and receipts, to include for rooms. + */ + @Json(name = "ephemeral") val ephemeral: RoomEventFilter? = null, + /** + * Include rooms that the user has left in the sync, default false + */ + @Json(name = "include_leave") val includeLeave: Boolean? = null, + /** + * The state events to include for rooms. + * Developer remark: StateFilter is exactly the same than RoomEventFilter + */ + @Json(name = "state") val state: RoomEventFilter? = null, + /** + * The message and state update events to include for rooms. + */ + @Json(name = "timeline") val timeline: RoomEventFilter? = null, + /** + * The per user account data to include for rooms. + */ + @Json(name = "account_data") val accountData: RoomEventFilter? = null +) { + + fun hasData(): Boolean { + return (notRooms != null + || rooms != null + || ephemeral != null + || includeLeave != null + || state != null + || timeline != null + || accountData != null) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGetGroupDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGetGroupDataTask.kt new file mode 100644 index 0000000000..c91bd381a4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGetGroupDataTask.kt @@ -0,0 +1,111 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.group + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.internal.database.model.GroupEntity +import org.matrix.android.sdk.internal.database.model.GroupSummaryEntity +import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.group.model.GroupRooms +import org.matrix.android.sdk.internal.session.group.model.GroupSummaryResponse +import org.matrix.android.sdk.internal.session.group.model.GroupUsers +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import javax.inject.Inject + +internal interface GetGroupDataTask : Task { + sealed class Params { + object FetchAllActive : Params() + data class FetchWithIds(val groupIds: List) : Params() + } +} + +internal class DefaultGetGroupDataTask @Inject constructor( + private val groupAPI: GroupAPI, + @SessionDatabase private val monarchy: Monarchy, + private val eventBus: EventBus +) : GetGroupDataTask { + + private data class GroupData( + val groupId: String, + val groupSummary: GroupSummaryResponse, + val groupRooms: GroupRooms, + val groupUsers: GroupUsers + ) + + override suspend fun execute(params: GetGroupDataTask.Params) { + val groupIds = when (params) { + is GetGroupDataTask.Params.FetchAllActive -> { + getActiveGroupIds() + } + is GetGroupDataTask.Params.FetchWithIds -> { + params.groupIds + } + } + Timber.v("Fetch data for group with ids: ${groupIds.joinToString(";")}") + val data = groupIds.map { groupId -> + val groupSummary = executeRequest(eventBus) { + apiCall = groupAPI.getSummary(groupId) + } + val groupRooms = executeRequest(eventBus) { + apiCall = groupAPI.getRooms(groupId) + } + val groupUsers = executeRequest(eventBus) { + apiCall = groupAPI.getUsers(groupId) + } + GroupData(groupId, groupSummary, groupRooms, groupUsers) + } + insertInDb(data) + } + + private fun getActiveGroupIds(): List { + return monarchy.fetchAllMappedSync( + { realm -> + GroupEntity.where(realm, Membership.activeMemberships()) + }, + { it.groupId } + ) + } + + private suspend fun insertInDb(groupDataList: List) { + monarchy + .awaitTransaction { realm -> + groupDataList.forEach { groupData -> + + val groupSummaryEntity = GroupSummaryEntity.getOrCreate(realm, groupData.groupId) + + groupSummaryEntity.avatarUrl = groupData.groupSummary.profile?.avatarUrl ?: "" + val name = groupData.groupSummary.profile?.name + groupSummaryEntity.displayName = if (name.isNullOrEmpty()) groupData.groupId else name + groupSummaryEntity.shortDescription = groupData.groupSummary.profile?.shortDescription ?: "" + + groupSummaryEntity.roomIds.clear() + groupData.groupRooms.rooms.mapTo(groupSummaryEntity.roomIds) { it.roomId } + + groupSummaryEntity.userIds.clear() + groupData.groupUsers.users.mapTo(groupSummaryEntity.userIds) { it.userId } + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGroup.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGroup.kt new file mode 100644 index 0000000000..3fbed5d992 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGroup.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.group + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.group.Group +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith + +internal class DefaultGroup(override val groupId: String, + private val taskExecutor: TaskExecutor, + private val getGroupDataTask: GetGroupDataTask) : Group { + + override fun fetchGroupData(callback: MatrixCallback): Cancelable { + val params = GetGroupDataTask.Params.FetchWithIds(listOf(groupId)) + return getGroupDataTask.configureWith(params) { + this.callback = callback + }.executeBy(taskExecutor) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGroupService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGroupService.kt new file mode 100644 index 0000000000..25c9d1dff7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGroupService.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.group + +import androidx.lifecycle.LiveData +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.group.Group +import org.matrix.android.sdk.api.session.group.GroupService +import org.matrix.android.sdk.api.session.group.GroupSummaryQueryParams +import org.matrix.android.sdk.api.session.group.model.GroupSummary +import org.matrix.android.sdk.internal.database.mapper.asDomain +import org.matrix.android.sdk.internal.database.model.GroupEntity +import org.matrix.android.sdk.internal.database.model.GroupSummaryEntity +import org.matrix.android.sdk.internal.database.model.GroupSummaryEntityFields +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.query.process +import org.matrix.android.sdk.internal.util.fetchCopyMap +import io.realm.Realm +import io.realm.RealmQuery +import javax.inject.Inject + +internal class DefaultGroupService @Inject constructor(@SessionDatabase private val monarchy: Monarchy, + private val groupFactory: GroupFactory) : GroupService { + + override fun getGroup(groupId: String): Group? { + return Realm.getInstance(monarchy.realmConfiguration).use { realm -> + GroupEntity.where(realm, groupId).findFirst()?.let { + groupFactory.create(groupId) + } + } + } + + override fun getGroupSummary(groupId: String): GroupSummary? { + return monarchy.fetchCopyMap( + { realm -> GroupSummaryEntity.where(realm, groupId).findFirst() }, + { it, _ -> it.asDomain() } + ) + } + + override fun getGroupSummaries(groupSummaryQueryParams: GroupSummaryQueryParams): List { + return monarchy.fetchAllMappedSync( + { groupSummariesQuery(it, groupSummaryQueryParams) }, + { it.asDomain() } + ) + } + + override fun getGroupSummariesLive(groupSummaryQueryParams: GroupSummaryQueryParams): LiveData> { + return monarchy.findAllMappedWithChanges( + { groupSummariesQuery(it, groupSummaryQueryParams) }, + { it.asDomain() } + ) + } + + private fun groupSummariesQuery(realm: Realm, queryParams: GroupSummaryQueryParams): RealmQuery { + return GroupSummaryEntity.where(realm) + .process(GroupSummaryEntityFields.DISPLAY_NAME, queryParams.displayName) + .process(GroupSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GetGroupDataWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GetGroupDataWorker.kt new file mode 100644 index 0000000000..7a04f076e9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GetGroupDataWorker.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.group + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.worker.SessionWorkerParams +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import org.matrix.android.sdk.internal.worker.getSessionComponent +import timber.log.Timber +import javax.inject.Inject + +/** + * Possible previous worker: None + * Possible next worker : None + */ +internal class GetGroupDataWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { + + @JsonClass(generateAdapter = true) + internal data class Params( + override val sessionId: String, + override val lastFailureMessage: String? = null + ) : SessionWorkerParams + + @Inject lateinit var getGroupDataTask: GetGroupDataTask + + override suspend fun doWork(): Result { + val params = WorkerParamsFactory.fromData(inputData) + ?: return Result.failure() + .also { Timber.e("Unable to parse work parameters") } + + val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() + sessionComponent.inject(this) + return runCatching { + getGroupDataTask.execute(GetGroupDataTask.Params.FetchAllActive) + }.fold( + { Result.success() }, + { Result.retry() } + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GroupAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GroupAPI.kt new file mode 100644 index 0000000000..f5156017ea --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GroupAPI.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.group + +import org.matrix.android.sdk.internal.network.NetworkConstants +import org.matrix.android.sdk.internal.session.group.model.GroupRooms +import org.matrix.android.sdk.internal.session.group.model.GroupSummaryResponse +import org.matrix.android.sdk.internal.session.group.model.GroupUsers +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Path + +internal interface GroupAPI { + + /** + * Request a group summary + * + * @param groupId the group id + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "groups/{groupId}/summary") + fun getSummary(@Path("groupId") groupId: String): Call + + /** + * Request the rooms list. + * + * @param groupId the group id + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "groups/{groupId}/rooms") + fun getRooms(@Path("groupId") groupId: String): Call + + /** + * Request the users list. + * + * @param groupId the group id + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "groups/{groupId}/users") + fun getUsers(@Path("groupId") groupId: String): Call +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GroupFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GroupFactory.kt new file mode 100644 index 0000000000..d9566fe5f1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GroupFactory.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.group + +import org.matrix.android.sdk.api.session.group.Group +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.task.TaskExecutor +import javax.inject.Inject + +internal interface GroupFactory { + fun create(groupId: String): Group +} + +@SessionScope +internal class DefaultGroupFactory @Inject constructor(private val getGroupDataTask: GetGroupDataTask, + private val taskExecutor: TaskExecutor) : + GroupFactory { + + override fun create(groupId: String): Group { + return DefaultGroup( + groupId = groupId, + taskExecutor = taskExecutor, + getGroupDataTask = getGroupDataTask + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GroupModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GroupModule.kt new file mode 100644 index 0000000000..b47bb0a5ad --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GroupModule.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.group + +import dagger.Binds +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.api.session.group.GroupService +import org.matrix.android.sdk.internal.session.SessionScope +import retrofit2.Retrofit + +@Module +internal abstract class GroupModule { + + @Module + companion object { + @Provides + @JvmStatic + @SessionScope + fun providesGroupAPI(retrofit: Retrofit): GroupAPI { + return retrofit.create(GroupAPI::class.java) + } + } + + @Binds + abstract fun bindGroupFactory(factory: DefaultGroupFactory): GroupFactory + + @Binds + abstract fun bindGetGroupDataTask(task: DefaultGetGroupDataTask): GetGroupDataTask + + @Binds + abstract fun bindGroupService(service: DefaultGroupService): GroupService +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupProfile.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupProfile.kt new file mode 100644 index 0000000000..9990e3d821 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupProfile.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.group.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This class represents a community profile in the server responses. + */ +@JsonClass(generateAdapter = true) +internal data class GroupProfile( + + @Json(name = "short_description") val shortDescription: String? = null, + + /** + * Tell whether the group is public. + */ + @Json(name = "is_public") val isPublic: Boolean? = null, + + /** + * The URL for the group's avatar. May be nil. + */ + @Json(name = "avatar_url") val avatarUrl: String? = null, + + /** + * The group's name. + */ + @Json(name = "name") val name: String? = null, + + /** + * The optional HTML formatted string used to described the group. + */ + @Json(name = "long_description") val longDescription: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupRoom.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupRoom.kt new file mode 100644 index 0000000000..c93878a0d4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupRoom.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.group.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class GroupRoom( + + @Json(name = "aliases") val aliases: List = emptyList(), + @Json(name = "canonical_alias") val canonicalAlias: String? = null, + @Json(name = "name") val name: String? = null, + @Json(name = "num_joined_members") val numJoinedMembers: Int = 0, + @Json(name = "room_id") val roomId: String, + @Json(name = "topic") val topic: String? = null, + @Json(name = "world_readable") val worldReadable: Boolean = false, + @Json(name = "guest_can_join") val guestCanJoin: Boolean = false, + @Json(name = "avatar_url") val avatarUrl: String? = null + +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupRooms.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupRooms.kt new file mode 100644 index 0000000000..f7e36ad8bc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupRooms.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.group.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class GroupRooms( + + @Json(name = "total_room_count_estimate") val totalRoomCountEstimate: Int? = null, + @Json(name = "chunk") val rooms: List = emptyList() + +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryResponse.kt new file mode 100644 index 0000000000..a11ba1ecdc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryResponse.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.group.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This class represents the summary of a community in the server response. + */ +@JsonClass(generateAdapter = true) +internal data class GroupSummaryResponse( + /** + * The group profile. + */ + @Json(name = "profile") val profile: GroupProfile? = null, + + /** + * The group users. + */ + @Json(name = "users_section") val usersSection: GroupSummaryUsersSection? = null, + + /** + * The current user status. + */ + @Json(name = "user") val user: GroupSummaryUser? = null, + + /** + * The rooms linked to the community. + */ + @Json(name = "rooms_section") val roomsSection: GroupSummaryRoomsSection? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryRoomsSection.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryRoomsSection.kt new file mode 100644 index 0000000000..428caaa209 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryRoomsSection.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.group.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This class represents the community rooms in a group summary response. + */ +@JsonClass(generateAdapter = true) +internal data class GroupSummaryRoomsSection( + + @Json(name = "total_room_count_estimate") val totalRoomCountEstimate: Int? = null, + + @Json(name = "rooms") val rooms: List = emptyList() + + // @TODO: Check the meaning and the usage of these categories. This dictionary is empty FTM. + // public Map categories; +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryUser.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryUser.kt new file mode 100644 index 0000000000..f61160fb1a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryUser.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.group.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This class represents the current user status in a group summary response. + */ +@JsonClass(generateAdapter = true) +internal data class GroupSummaryUser( + + /** + * The current user membership in this community. + */ + @Json(name = "membership") val membership: String? = null, + + /** + * Tell whether the user published this community on his profile. + */ + @Json(name = "is_publicised") val isPublicised: Boolean? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryUsersSection.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryUsersSection.kt new file mode 100644 index 0000000000..a8ade1ab5e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryUsersSection.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.group.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This class represents the community members in a group summary response. + */ + +@JsonClass(generateAdapter = true) +internal data class GroupSummaryUsersSection( + + @Json(name = "total_user_count_estimate") val totalUserCountEstimate: Int, + + @Json(name = "users") val users: List = emptyList() + + // @TODO: Check the meaning and the usage of these roles. This dictionary is empty FTM. + // public Map roles; +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupUser.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupUser.kt new file mode 100644 index 0000000000..d9a9631ef7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupUser.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.group.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class GroupUser( + @Json(name = "display_name") val displayName: String = "", + @Json(name = "user_id") val userId: String, + @Json(name = "is_privileged") val isPrivileged: Boolean = false, + @Json(name = "avatar_url") val avatarUrl: String? = "", + @Json(name = "is_public") val isPublic: Boolean = false +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupUsers.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupUsers.kt new file mode 100644 index 0000000000..1ce283756d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupUsers.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.group.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class GroupUsers( + @Json(name = "total_user_count_estimate") val totalUserCountEstimate: Int, + @Json(name = "chunk") val users: List = emptyList() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/CapabilitiesAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/CapabilitiesAPI.kt new file mode 100644 index 0000000000..33ebf3e548 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/CapabilitiesAPI.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.homeserver + +import org.matrix.android.sdk.internal.auth.version.Versions +import org.matrix.android.sdk.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.GET + +internal interface CapabilitiesAPI { + + /** + * Request the homeserver capabilities + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "capabilities") + fun getCapabilities(): Call + + /** + * Request the upload capabilities + */ + @GET(NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "config") + fun getUploadCapabilities(): Call + + /** + * Request the versions + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_ + "versions") + fun getVersions(): Call + + /** + * Ping the homeserver. We do not care about the returned data, so there is no use to parse them + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_ + "versions") + fun ping(): Call +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt new file mode 100644 index 0000000000..0f9d9548d2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt @@ -0,0 +1,130 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.homeserver + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import org.matrix.android.sdk.api.auth.wellknown.WellknownResult +import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities +import org.matrix.android.sdk.internal.auth.version.Versions +import org.matrix.android.sdk.internal.auth.version.isLoginAndRegistrationSupportedBySdk +import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntity +import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManagerConfigExtractor +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import org.matrix.android.sdk.internal.wellknown.GetWellknownTask +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import java.util.Date +import javax.inject.Inject + +internal interface GetHomeServerCapabilitiesTask : Task + +internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( + private val capabilitiesAPI: CapabilitiesAPI, + @SessionDatabase private val monarchy: Monarchy, + private val eventBus: EventBus, + private val getWellknownTask: GetWellknownTask, + private val configExtractor: IntegrationManagerConfigExtractor, + private val homeServerConnectionConfig: HomeServerConnectionConfig, + @UserId + private val userId: String +) : GetHomeServerCapabilitiesTask { + + override suspend fun execute(params: Unit) { + var doRequest = false + monarchy.awaitTransaction { realm -> + val homeServerCapabilitiesEntity = HomeServerCapabilitiesEntity.getOrCreate(realm) + + doRequest = homeServerCapabilitiesEntity.lastUpdatedTimestamp + MIN_DELAY_BETWEEN_TWO_REQUEST_MILLIS < Date().time + } + + if (!doRequest) { + return + } + + val capabilities = runCatching { + executeRequest(eventBus) { + apiCall = capabilitiesAPI.getCapabilities() + } + }.getOrNull() + + val uploadCapabilities = runCatching { + executeRequest(eventBus) { + apiCall = capabilitiesAPI.getUploadCapabilities() + } + }.getOrNull() + + val versions = runCatching { + executeRequest(null) { + apiCall = capabilitiesAPI.getVersions() + } + }.getOrNull() + + val wellknownResult = runCatching { + getWellknownTask.execute(GetWellknownTask.Params(userId, homeServerConnectionConfig)) + }.getOrNull() + + insertInDb(capabilities, uploadCapabilities, versions, wellknownResult) + } + + private suspend fun insertInDb(getCapabilitiesResult: GetCapabilitiesResult?, + getUploadCapabilitiesResult: GetUploadCapabilitiesResult?, + getVersionResult: Versions?, + getWellknownResult: WellknownResult?) { + monarchy.awaitTransaction { realm -> + val homeServerCapabilitiesEntity = HomeServerCapabilitiesEntity.getOrCreate(realm) + + if (getCapabilitiesResult != null) { + homeServerCapabilitiesEntity.canChangePassword = getCapabilitiesResult.canChangePassword() + } + + if (getUploadCapabilitiesResult != null) { + homeServerCapabilitiesEntity.maxUploadFileSize = getUploadCapabilitiesResult.maxUploadSize + ?: HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN + } + + if (getVersionResult != null) { + homeServerCapabilitiesEntity.lastVersionIdentityServerSupported = getVersionResult.isLoginAndRegistrationSupportedBySdk() + } + + if (getWellknownResult != null && getWellknownResult is WellknownResult.Prompt) { + homeServerCapabilitiesEntity.defaultIdentityServerUrl = getWellknownResult.identityServerUrl + homeServerCapabilitiesEntity.adminE2EByDefault = getWellknownResult.wellKnown.e2eAdminSetting?.e2eDefault ?: true + // We are also checking for integration manager configurations + val config = configExtractor.extract(getWellknownResult.wellKnown) + if (config != null) { + Timber.v("Extracted integration config : $config") + realm.insertOrUpdate(config) + } + } else { + homeServerCapabilitiesEntity.adminE2EByDefault = true + } + homeServerCapabilitiesEntity.lastUpdatedTimestamp = Date().time + } + } + + companion object { + // 8 hours like on Riot Web + private const val MIN_DELAY_BETWEEN_TWO_REQUEST_MILLIS = 8 * 60 * 60 * 1000 + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultHomeServerCapabilitiesService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultHomeServerCapabilitiesService.kt new file mode 100644 index 0000000000..0989d6c191 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultHomeServerCapabilitiesService.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.homeserver + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities +import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService +import org.matrix.android.sdk.internal.database.mapper.HomeServerCapabilitiesMapper +import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntity +import org.matrix.android.sdk.internal.database.query.get +import org.matrix.android.sdk.internal.di.SessionDatabase +import io.realm.Realm +import javax.inject.Inject + +internal class DefaultHomeServerCapabilitiesService @Inject constructor(@SessionDatabase private val monarchy: Monarchy) : HomeServerCapabilitiesService { + + override fun getHomeServerCapabilities(): HomeServerCapabilities { + return Realm.getInstance(monarchy.realmConfiguration).use { realm -> + HomeServerCapabilitiesEntity.get(realm)?.let { + HomeServerCapabilitiesMapper.map(it) + } + } + ?: HomeServerCapabilities() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt new file mode 100644 index 0000000000..54d8cc7839 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.homeserver + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.extensions.orTrue + +/** + * Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-capabilities + */ +@JsonClass(generateAdapter = true) +internal data class GetCapabilitiesResult( + /** + * Required. The custom capabilities the server supports, using the Java package naming convention. + */ + @Json(name = "capabilities") + val capabilities: Capabilities? = null +) + +@JsonClass(generateAdapter = true) +internal data class Capabilities( + /** + * Capability to indicate if the user can change their password. + */ + @Json(name = "m.change_password") + val changePassword: ChangePassword? = null + + // No need for m.room_versions for the moment +) + +@JsonClass(generateAdapter = true) +internal data class ChangePassword( + /** + * Required. True if the user can change their password, false otherwise. + */ + @Json(name = "enabled") + val enabled: Boolean? +) + +// The spec says: If not present, the client should assume that password changes are possible via the API +internal fun GetCapabilitiesResult.canChangePassword(): Boolean { + return capabilities?.changePassword?.enabled.orTrue() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetUploadCapabilitiesResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetUploadCapabilitiesResult.kt new file mode 100644 index 0000000000..43395aae3e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetUploadCapabilitiesResult.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.homeserver + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class GetUploadCapabilitiesResult( + /** + * The maximum size an upload can be in bytes. Clients SHOULD use this as a guide when uploading content. + * If not listed or null, the size limit should be treated as unknown. + */ + @Json(name = "m.upload.size") + val maxUploadSize: Long? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/HomeServerCapabilitiesModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/HomeServerCapabilitiesModule.kt new file mode 100644 index 0000000000..240839cf7b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/HomeServerCapabilitiesModule.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.homeserver + +import dagger.Binds +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.wellknown.WellknownModule +import retrofit2.Retrofit + +@Module(includes = [WellknownModule::class]) +internal abstract class HomeServerCapabilitiesModule { + + @Module + companion object { + @Provides + @JvmStatic + @SessionScope + fun providesCapabilitiesAPI(retrofit: Retrofit): CapabilitiesAPI { + return retrofit.create(CapabilitiesAPI::class.java) + } + } + + @Binds + abstract fun bindGetHomeServerCapabilitiesTask(task: DefaultGetHomeServerCapabilitiesTask): GetHomeServerCapabilitiesTask +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/HomeServerPinger.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/HomeServerPinger.kt new file mode 100644 index 0000000000..dee73f08f1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/HomeServerPinger.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.homeserver + +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.TaskExecutor +import kotlinx.coroutines.launch +import javax.inject.Inject + +internal class HomeServerPinger @Inject constructor(private val taskExecutor: TaskExecutor, + private val capabilitiesAPI: CapabilitiesAPI) { + + fun canReachHomeServer(callback: (Boolean) -> Unit) { + taskExecutor.executorScope.launch { + val canReach = canReachHomeServer() + callback(canReach) + } + } + + suspend fun canReachHomeServer(): Boolean { + return try { + executeRequest(null) { + apiCall = capabilitiesAPI.ping() + } + true + } catch (throwable: Throwable) { + if (throwable is Failure.OtherServerError) { + (throwable.httpCode == 404 || throwable.httpCode == 400) + } else { + false + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt new file mode 100644 index 0000000000..d3a10764d3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt @@ -0,0 +1,333 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.identity + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import dagger.Lazy +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.auth.data.SessionParams +import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.MatrixError +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService +import org.matrix.android.sdk.api.session.identity.FoundThreePid +import org.matrix.android.sdk.api.session.identity.IdentityService +import org.matrix.android.sdk.api.session.identity.IdentityServiceError +import org.matrix.android.sdk.api.session.identity.IdentityServiceListener +import org.matrix.android.sdk.api.session.identity.SharedState +import org.matrix.android.sdk.api.session.identity.ThreePid +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.NoOpCancellable +import org.matrix.android.sdk.internal.di.AuthenticatedIdentity +import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificate +import org.matrix.android.sdk.internal.extensions.observeNotNull +import org.matrix.android.sdk.internal.network.RetrofitFactory +import org.matrix.android.sdk.internal.session.SessionLifecycleObserver +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.identity.data.IdentityStore +import org.matrix.android.sdk.internal.session.openid.GetOpenIdTokenTask +import org.matrix.android.sdk.internal.session.profile.BindThreePidsTask +import org.matrix.android.sdk.internal.session.profile.UnbindThreePidsTask +import org.matrix.android.sdk.internal.session.sync.model.accountdata.IdentityServerContent +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes +import org.matrix.android.sdk.internal.session.user.accountdata.AccountDataDataSource +import org.matrix.android.sdk.internal.session.user.accountdata.UpdateUserAccountDataTask +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.launchToCallback +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import org.matrix.android.sdk.internal.util.ensureProtocol +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import timber.log.Timber +import javax.inject.Inject +import javax.net.ssl.HttpsURLConnection + +@SessionScope +internal class DefaultIdentityService @Inject constructor( + private val identityStore: IdentityStore, + private val ensureIdentityTokenTask: EnsureIdentityTokenTask, + private val getOpenIdTokenTask: GetOpenIdTokenTask, + private val identityBulkLookupTask: IdentityBulkLookupTask, + private val identityRegisterTask: IdentityRegisterTask, + private val identityPingTask: IdentityPingTask, + private val identityDisconnectTask: IdentityDisconnectTask, + private val identityRequestTokenForBindingTask: IdentityRequestTokenForBindingTask, + @UnauthenticatedWithCertificate + private val unauthenticatedOkHttpClient: Lazy, + @AuthenticatedIdentity + private val okHttpClient: Lazy, + private val retrofitFactory: RetrofitFactory, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val updateUserAccountDataTask: UpdateUserAccountDataTask, + private val bindThreePidsTask: BindThreePidsTask, + private val submitTokenForBindingTask: IdentitySubmitTokenForBindingTask, + private val unbindThreePidsTask: UnbindThreePidsTask, + private val identityApiProvider: IdentityApiProvider, + private val accountDataDataSource: AccountDataDataSource, + private val homeServerCapabilitiesService: HomeServerCapabilitiesService, + private val sessionParams: SessionParams, + private val taskExecutor: TaskExecutor +) : IdentityService, SessionLifecycleObserver { + + private val lifecycleOwner: LifecycleOwner = LifecycleOwner { lifecycleRegistry } + private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(lifecycleOwner) + + private val listeners = mutableSetOf() + + override fun onStart() { + lifecycleRegistry.currentState = Lifecycle.State.STARTED + // Observe the account data change + accountDataDataSource + .getLiveAccountDataEvent(UserAccountDataTypes.TYPE_IDENTITY_SERVER) + .observeNotNull(lifecycleOwner) { + notifyIdentityServerUrlChange(it.getOrNull()?.content?.toModel()?.baseUrl) + } + + // Init identityApi + updateIdentityAPI(identityStore.getIdentityData()?.identityServerUrl) + } + + private fun notifyIdentityServerUrlChange(baseUrl: String?) { + // This is maybe not a real change (echo of account data we are just setting) + if (identityStore.getIdentityData()?.identityServerUrl == baseUrl) { + Timber.d("Echo of local identity server url change, or no change") + } else { + // Url has changed, we have to reset our store, update internal configuration and notify listeners + identityStore.setUrl(baseUrl) + updateIdentityAPI(baseUrl) + listeners.toList().forEach { tryThis { it.onIdentityServerChange() } } + } + } + + override fun onStop() { + lifecycleRegistry.currentState = Lifecycle.State.DESTROYED + } + + /** + * First return the identity server provided during login phase. + * If null, provide the one in wellknown configuration of the homeserver + * Else return null + */ + override fun getDefaultIdentityServer(): String? { + return sessionParams.defaultIdentityServerUrl + ?.takeIf { it.isNotEmpty() } + ?: homeServerCapabilitiesService.getHomeServerCapabilities().defaultIdentityServerUrl + } + + override fun getCurrentIdentityServerUrl(): String? { + return identityStore.getIdentityData()?.identityServerUrl + } + + override fun startBindThreePid(threePid: ThreePid, callback: MatrixCallback): Cancelable { + if (homeServerCapabilitiesService.getHomeServerCapabilities().lastVersionIdentityServerSupported.not()) { + callback.onFailure(IdentityServiceError.OutdatedHomeServer) + return NoOpCancellable + } + + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + identityRequestTokenForBindingTask.execute(IdentityRequestTokenForBindingTask.Params(threePid, false)) + } + } + + override fun cancelBindThreePid(threePid: ThreePid, callback: MatrixCallback): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + identityStore.deletePendingBinding(threePid) + } + } + + override fun sendAgainValidationCode(threePid: ThreePid, callback: MatrixCallback): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + identityRequestTokenForBindingTask.execute(IdentityRequestTokenForBindingTask.Params(threePid, true)) + } + } + + override fun finalizeBindThreePid(threePid: ThreePid, callback: MatrixCallback): Cancelable { + if (homeServerCapabilitiesService.getHomeServerCapabilities().lastVersionIdentityServerSupported.not()) { + callback.onFailure(IdentityServiceError.OutdatedHomeServer) + return NoOpCancellable + } + + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + bindThreePidsTask.execute(BindThreePidsTask.Params(threePid)) + } + } + + override fun submitValidationToken(threePid: ThreePid, code: String, callback: MatrixCallback): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + submitTokenForBindingTask.execute(IdentitySubmitTokenForBindingTask.Params(threePid, code)) + } + } + + override fun unbindThreePid(threePid: ThreePid, callback: MatrixCallback): Cancelable { + if (homeServerCapabilitiesService.getHomeServerCapabilities().lastVersionIdentityServerSupported.not()) { + callback.onFailure(IdentityServiceError.OutdatedHomeServer) + return NoOpCancellable + } + + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + unbindThreePidsTask.execute(UnbindThreePidsTask.Params(threePid)) + } + } + + override fun isValidIdentityServer(url: String, callback: MatrixCallback): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + val api = retrofitFactory.create(unauthenticatedOkHttpClient, url).create(IdentityAuthAPI::class.java) + + identityPingTask.execute(IdentityPingTask.Params(api)) + } + } + + override fun disconnect(callback: MatrixCallback): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + identityDisconnectTask.execute(Unit) + + identityStore.setUrl(null) + updateIdentityAPI(null) + updateAccountData(null) + } + } + + override fun setNewIdentityServer(url: String, callback: MatrixCallback): Cancelable { + val urlCandidate = url.ensureProtocol() + + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + val current = getCurrentIdentityServerUrl() + if (urlCandidate == current) { + // Nothing to do + Timber.d("Same URL, nothing to do") + } else { + // Disconnect previous one if any, first, because the token will change. + // In case of error when configuring the new identity server, this is not a big deal, + // we will ask for a new token on the previous Identity server + runCatching { identityDisconnectTask.execute(Unit) } + .onFailure { Timber.w(it, "Unable to disconnect identity server") } + + // Try to get a token + val token = getNewIdentityServerToken(urlCandidate) + + identityStore.setUrl(urlCandidate) + identityStore.setToken(token) + updateIdentityAPI(urlCandidate) + + updateAccountData(urlCandidate) + } + urlCandidate + } + } + + private suspend fun updateAccountData(url: String?) { + // Also notify the listener + withContext(coroutineDispatchers.main) { + listeners.toList().forEach { tryThis { it.onIdentityServerChange() } } + } + + updateUserAccountDataTask.execute(UpdateUserAccountDataTask.IdentityParams( + identityContent = IdentityServerContent(baseUrl = url) + )) + } + + override fun lookUp(threePids: List, callback: MatrixCallback>): Cancelable { + if (threePids.isEmpty()) { + callback.onSuccess(emptyList()) + return NoOpCancellable + } + + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + lookUpInternal(true, threePids) + } + } + + override fun getShareStatus(threePids: List, callback: MatrixCallback>): Cancelable { + if (threePids.isEmpty()) { + callback.onSuccess(emptyMap()) + return NoOpCancellable + } + + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + val lookupResult = lookUpInternal(true, threePids) + + threePids.associateWith { threePid -> + // If not in lookup result, check if there is a pending binding + if (lookupResult.firstOrNull { it.threePid == threePid } == null) { + if (identityStore.getPendingBinding(threePid) == null) { + SharedState.NOT_SHARED + } else { + SharedState.BINDING_IN_PROGRESS + } + } else { + SharedState.SHARED + } + } + } + } + + private suspend fun lookUpInternal(canRetry: Boolean, threePids: List): List { + ensureIdentityTokenTask.execute(Unit) + + return try { + identityBulkLookupTask.execute(IdentityBulkLookupTask.Params(threePids)) + } catch (throwable: Throwable) { + // Refresh token? + when { + throwable.isInvalidToken() && canRetry -> { + identityStore.setToken(null) + lookUpInternal(false, threePids) + } + throwable.isTermsNotSigned() -> throw IdentityServiceError.TermsNotSignedException + else -> throw throwable + } + } + } + + private suspend fun getNewIdentityServerToken(url: String): String { + val api = retrofitFactory.create(unauthenticatedOkHttpClient, url).create(IdentityAuthAPI::class.java) + + val openIdToken = getOpenIdTokenTask.execute(Unit) + val token = identityRegisterTask.execute(IdentityRegisterTask.Params(api, openIdToken)) + + return token.token + } + + override fun addListener(listener: IdentityServiceListener) { + listeners.add(listener) + } + + override fun removeListener(listener: IdentityServiceListener) { + listeners.remove(listener) + } + + private fun updateIdentityAPI(url: String?) { + identityApiProvider.identityApi = url + ?.let { retrofitFactory.create(okHttpClient, it) } + ?.create(IdentityAPI::class.java) + } +} + +private fun Throwable.isInvalidToken(): Boolean { + return this is Failure.ServerError + && httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED /* 401 */ +} + +private fun Throwable.isTermsNotSigned(): Boolean { + return this is Failure.ServerError + && httpCode == HttpsURLConnection.HTTP_FORBIDDEN /* 403 */ + && error.code == MatrixError.M_TERMS_NOT_SIGNED +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/EnsureIdentityToken.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/EnsureIdentityToken.kt new file mode 100644 index 0000000000..838b9975b7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/EnsureIdentityToken.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.identity + +import dagger.Lazy +import org.matrix.android.sdk.api.session.identity.IdentityServiceError +import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificate +import org.matrix.android.sdk.internal.network.RetrofitFactory +import org.matrix.android.sdk.internal.session.identity.data.IdentityStore +import org.matrix.android.sdk.internal.session.openid.GetOpenIdTokenTask +import org.matrix.android.sdk.internal.task.Task +import okhttp3.OkHttpClient +import javax.inject.Inject + +internal interface EnsureIdentityTokenTask : Task + +internal class DefaultEnsureIdentityTokenTask @Inject constructor( + private val identityStore: IdentityStore, + private val retrofitFactory: RetrofitFactory, + @UnauthenticatedWithCertificate + private val unauthenticatedOkHttpClient: Lazy, + private val getOpenIdTokenTask: GetOpenIdTokenTask, + private val identityRegisterTask: IdentityRegisterTask +) : EnsureIdentityTokenTask { + + override suspend fun execute(params: Unit) { + val identityData = identityStore.getIdentityData() ?: throw IdentityServiceError.NoIdentityServerConfigured + val url = identityData.identityServerUrl ?: throw IdentityServiceError.NoIdentityServerConfigured + + if (identityData.token == null) { + // Try to get a token + val token = getNewIdentityServerToken(url) + identityStore.setToken(token) + } + } + + private suspend fun getNewIdentityServerToken(url: String): String { + val api = retrofitFactory.create(unauthenticatedOkHttpClient, url).create(IdentityAuthAPI::class.java) + + val openIdToken = getOpenIdTokenTask.execute(Unit) + val token = identityRegisterTask.execute(IdentityRegisterTask.Params(api, openIdToken)) + + return token.token + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAPI.kt new file mode 100644 index 0000000000..f2c889f024 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAPI.kt @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.identity + +import org.matrix.android.sdk.internal.auth.registration.SuccessResult +import org.matrix.android.sdk.internal.network.NetworkConstants +import org.matrix.android.sdk.internal.session.identity.model.IdentityAccountResponse +import org.matrix.android.sdk.internal.session.identity.model.IdentityHashDetailResponse +import org.matrix.android.sdk.internal.session.identity.model.IdentityLookUpParams +import org.matrix.android.sdk.internal.session.identity.model.IdentityLookUpResponse +import org.matrix.android.sdk.internal.session.identity.model.IdentityRequestOwnershipParams +import org.matrix.android.sdk.internal.session.identity.model.IdentityRequestTokenForEmailBody +import org.matrix.android.sdk.internal.session.identity.model.IdentityRequestTokenForMsisdnBody +import org.matrix.android.sdk.internal.session.identity.model.IdentityRequestTokenResponse +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Path + +/** + * Ref: https://matrix.org/docs/spec/identity_service/latest + * This contain the requests which need an identity server token + */ +internal interface IdentityAPI { + /** + * Gets information about what user owns the access token used in the request. + * Will return a 403 for when terms are not signed + * Ref: https://matrix.org/docs/spec/identity_service/latest#get-matrix-identity-v2-account + */ + @GET(NetworkConstants.URI_IDENTITY_PATH_V2 + "account") + fun getAccount(): Call + + /** + * Logs out the access token, preventing it from being used to authenticate future requests to the server. + */ + @POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "account/logout") + fun logout(): Call + + /** + * Request the hash detail to request a bunch of 3PIDs + * Ref: https://matrix.org/docs/spec/identity_service/latest#get-matrix-identity-v2-hash-details + */ + @GET(NetworkConstants.URI_IDENTITY_PATH_V2 + "hash_details") + fun hashDetails(): Call + + /** + * Request a bunch of 3PIDs + * Ref: https://matrix.org/docs/spec/identity_service/latest#post-matrix-identity-v2-lookup + * + * @param body the body request + */ + @POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "lookup") + fun lookup(@Body body: IdentityLookUpParams): Call + + /** + * Create a session to change the bind status of an email to an identity server + * The identity server will also send an email + * + * @param body + * @return the sid + */ + @POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "validate/email/requestToken") + fun requestTokenToBindEmail(@Body body: IdentityRequestTokenForEmailBody): Call + + /** + * Create a session to change the bind status of an phone number to an identity server + * The identity server will also send an SMS on the ThreePid provided + * + * @param body + * @return the sid + */ + @POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "validate/msisdn/requestToken") + fun requestTokenToBindMsisdn(@Body body: IdentityRequestTokenForMsisdnBody): Call + + /** + * Validate ownership of an email address, or a phone number. + * Ref: + * - https://matrix.org/docs/spec/identity_service/latest#post-matrix-identity-v2-validate-msisdn-submittoken + * - https://matrix.org/docs/spec/identity_service/latest#post-matrix-identity-v2-validate-email-submittoken + */ + @POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "validate/{medium}/submitToken") + fun submitToken(@Path("medium") medium: String, + @Body body: IdentityRequestOwnershipParams): Call +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAccessTokenProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAccessTokenProvider.kt new file mode 100644 index 0000000000..cf9ba0ee89 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAccessTokenProvider.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.identity + +import org.matrix.android.sdk.internal.network.token.AccessTokenProvider +import org.matrix.android.sdk.internal.session.identity.data.IdentityStore +import javax.inject.Inject + +internal class IdentityAccessTokenProvider @Inject constructor( + private val identityStore: IdentityStore +) : AccessTokenProvider { + override fun getToken() = identityStore.getIdentityData()?.token +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityApiProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityApiProvider.kt new file mode 100644 index 0000000000..09922cb475 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityApiProvider.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.identity + +import org.matrix.android.sdk.internal.session.SessionScope +import javax.inject.Inject + +@SessionScope +internal class IdentityApiProvider @Inject constructor() { + + var identityApi: IdentityAPI? = null +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAuthAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAuthAPI.kt new file mode 100644 index 0000000000..7ebe775ce5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAuthAPI.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.identity + +import org.matrix.android.sdk.internal.network.NetworkConstants +import org.matrix.android.sdk.internal.session.identity.model.IdentityRegisterResponse +import org.matrix.android.sdk.internal.session.openid.RequestOpenIdTokenResponse +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST + +/** + * Ref: https://matrix.org/docs/spec/identity_service/latest + * This contain the requests which do not need an identity server token + */ +internal interface IdentityAuthAPI { + + /** + * https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery + * Simple ping call to check if server exists and is alive + * + * Ref: https://matrix.org/docs/spec/identity_service/unstable#status-check + * https://matrix.org/docs/spec/identity_service/latest#get-matrix-identity-v2 + * + * @return 200 in case of success + */ + @GET(NetworkConstants.URI_IDENTITY_PREFIX_PATH) + fun ping(): Call + + /** + * Ping v1 will be used to check outdated Identity server + */ + @GET("_matrix/identity/api/v1") + fun pingV1(): Call + + /** + * Exchanges an OpenID token from the homeserver for an access token to access the identity server. + * The request body is the same as the values returned by /openid/request_token in the Client-Server API. + */ + @POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "account/register") + fun register(@Body openIdToken: RequestOpenIdTokenResponse): Call +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt new file mode 100644 index 0000000000..ac33c2666f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.identity + +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.MatrixError +import org.matrix.android.sdk.api.session.identity.FoundThreePid +import org.matrix.android.sdk.api.session.identity.IdentityServiceError +import org.matrix.android.sdk.api.session.identity.ThreePid +import org.matrix.android.sdk.api.session.identity.toMedium +import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments.base64ToBase64Url +import org.matrix.android.sdk.internal.crypto.tools.withOlmUtility +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.identity.data.IdentityStore +import org.matrix.android.sdk.internal.session.identity.model.IdentityHashDetailResponse +import org.matrix.android.sdk.internal.session.identity.model.IdentityLookUpParams +import org.matrix.android.sdk.internal.session.identity.model.IdentityLookUpResponse +import org.matrix.android.sdk.internal.task.Task +import java.util.Locale +import javax.inject.Inject + +internal interface IdentityBulkLookupTask : Task> { + data class Params( + val threePids: List + ) +} + +internal class DefaultIdentityBulkLookupTask @Inject constructor( + private val identityApiProvider: IdentityApiProvider, + private val identityStore: IdentityStore, + @UserId private val userId: String +) : IdentityBulkLookupTask { + + override suspend fun execute(params: IdentityBulkLookupTask.Params): List { + val identityAPI = getIdentityApiAndEnsureTerms(identityApiProvider, userId) + val identityData = identityStore.getIdentityData() ?: throw IdentityServiceError.NoIdentityServerConfigured + val pepper = identityData.hashLookupPepper + val hashDetailResponse = if (pepper == null) { + // We need to fetch the hash details first + fetchAndStoreHashDetails(identityAPI) + } else { + IdentityHashDetailResponse(pepper, identityData.hashLookupAlgorithm) + } + + if (hashDetailResponse.algorithms.contains("sha256").not()) { + // TODO We should ask the user if he is ok to send their 3Pid in clear, but for the moment we do not do it + // Also, what we have in cache could be outdated, the identity server maybe now supports sha256 + throw IdentityServiceError.BulkLookupSha256NotSupported + } + + val hashedAddresses = withOlmUtility { olmUtility -> + params.threePids.map { threePid -> + base64ToBase64Url( + olmUtility.sha256(threePid.value.toLowerCase(Locale.ROOT) + + " " + threePid.toMedium() + " " + hashDetailResponse.pepper) + ) + } + } + + val identityLookUpV2Response = lookUpInternal(identityAPI, hashedAddresses, hashDetailResponse, true) + + // Convert back to List + return handleSuccess(params.threePids, hashedAddresses, identityLookUpV2Response) + } + + private suspend fun lookUpInternal(identityAPI: IdentityAPI, + hashedAddresses: List, + hashDetailResponse: IdentityHashDetailResponse, + canRetry: Boolean): IdentityLookUpResponse { + return try { + executeRequest(null) { + apiCall = identityAPI.lookup(IdentityLookUpParams( + hashedAddresses, + IdentityHashDetailResponse.ALGORITHM_SHA256, + hashDetailResponse.pepper + )) + } + } catch (failure: Throwable) { + // Catch invalid hash pepper and retry + if (canRetry && failure is Failure.ServerError && failure.error.code == MatrixError.M_INVALID_PEPPER) { + // This is not documented, by the error can contain the new pepper! + if (!failure.error.newLookupPepper.isNullOrEmpty()) { + // Store it and use it right now + hashDetailResponse.copy(pepper = failure.error.newLookupPepper) + .also { identityStore.setHashDetails(it) } + .let { lookUpInternal(identityAPI, hashedAddresses, it, false /* Avoid infinite loop */) } + } else { + // Retrieve the new hash details + val newHashDetailResponse = fetchAndStoreHashDetails(identityAPI) + + if (hashDetailResponse.algorithms.contains(IdentityHashDetailResponse.ALGORITHM_SHA256).not()) { + // TODO We should ask the user if he is ok to send their 3Pid in clear, but for the moment we do not do it + // Also, what we have in cache is maybe outdated, the identity server maybe now support sha256 + throw IdentityServiceError.BulkLookupSha256NotSupported + } + + lookUpInternal(identityAPI, hashedAddresses, newHashDetailResponse, false /* Avoid infinite loop */) + } + } else { + // Other error + throw failure + } + } + } + + private suspend fun fetchAndStoreHashDetails(identityAPI: IdentityAPI): IdentityHashDetailResponse { + return executeRequest(null) { + apiCall = identityAPI.hashDetails() + } + .also { identityStore.setHashDetails(it) } + } + + private fun handleSuccess(threePids: List, hashedAddresses: List, identityLookUpResponse: IdentityLookUpResponse): List { + return identityLookUpResponse.mappings.keys.map { hashedAddress -> + FoundThreePid(threePids[hashedAddresses.indexOf(hashedAddress)], identityLookUpResponse.mappings[hashedAddress] ?: error("")) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityDisconnectTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityDisconnectTask.kt new file mode 100644 index 0000000000..0e68689ce7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityDisconnectTask.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.identity + +import org.matrix.android.sdk.api.session.identity.IdentityServiceError +import org.matrix.android.sdk.internal.di.AuthenticatedIdentity +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.network.token.AccessTokenProvider +import org.matrix.android.sdk.internal.task.Task +import timber.log.Timber +import javax.inject.Inject + +internal interface IdentityDisconnectTask : Task + +internal class DefaultIdentityDisconnectTask @Inject constructor( + private val identityApiProvider: IdentityApiProvider, + @AuthenticatedIdentity + private val accessTokenProvider: AccessTokenProvider +) : IdentityDisconnectTask { + + override suspend fun execute(params: Unit) { + val identityAPI = identityApiProvider.identityApi ?: throw IdentityServiceError.NoIdentityServerConfigured + + // Ensure we have a token. + // We can have an identity server configured, but no token yet. + if (accessTokenProvider.getToken() == null) { + Timber.d("No token to disconnect identity server.") + return + } + + executeRequest(null) { + apiCall = identityAPI.logout() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityModule.kt new file mode 100644 index 0000000000..0c35eef642 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityModule.kt @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.identity + +import dagger.Binds +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.internal.database.RealmKeysUtils +import org.matrix.android.sdk.internal.di.AuthenticatedIdentity +import org.matrix.android.sdk.internal.di.IdentityDatabase +import org.matrix.android.sdk.internal.di.SessionFilesDirectory +import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificate +import org.matrix.android.sdk.internal.di.UserMd5 +import org.matrix.android.sdk.internal.network.httpclient.addAccessTokenInterceptor +import org.matrix.android.sdk.internal.network.token.AccessTokenProvider +import org.matrix.android.sdk.internal.session.SessionModule +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.identity.data.IdentityStore +import org.matrix.android.sdk.internal.session.identity.db.IdentityRealmModule +import org.matrix.android.sdk.internal.session.identity.db.RealmIdentityStore +import io.realm.RealmConfiguration +import okhttp3.OkHttpClient +import java.io.File + +@Module +internal abstract class IdentityModule { + + @Module + companion object { + @JvmStatic + @Provides + @SessionScope + @AuthenticatedIdentity + fun providesOkHttpClient(@UnauthenticatedWithCertificate okHttpClient: OkHttpClient, + @AuthenticatedIdentity accessTokenProvider: AccessTokenProvider): OkHttpClient { + return okHttpClient + .newBuilder() + .addAccessTokenInterceptor(accessTokenProvider) + .build() + } + + @JvmStatic + @Provides + @IdentityDatabase + @SessionScope + fun providesIdentityRealmConfiguration(realmKeysUtils: RealmKeysUtils, + @SessionFilesDirectory directory: File, + @UserMd5 userMd5: String): RealmConfiguration { + return RealmConfiguration.Builder() + .directory(directory) + .name("matrix-sdk-identity.realm") + .apply { + realmKeysUtils.configureEncryption(this, SessionModule.getKeyAlias(userMd5)) + } + .modules(IdentityRealmModule()) + .build() + } + } + + @Binds + @AuthenticatedIdentity + abstract fun bindAccessTokenProvider(provider: IdentityAccessTokenProvider): AccessTokenProvider + + @Binds + abstract fun bindIdentityStore(store: RealmIdentityStore): IdentityStore + + @Binds + abstract fun bindEnsureIdentityTokenTask(task: DefaultEnsureIdentityTokenTask): EnsureIdentityTokenTask + + @Binds + abstract fun bindIdentityPingTask(task: DefaultIdentityPingTask): IdentityPingTask + + @Binds + abstract fun bindIdentityRegisterTask(task: DefaultIdentityRegisterTask): IdentityRegisterTask + + @Binds + abstract fun bindIdentityRequestTokenForBindingTask(task: DefaultIdentityRequestTokenForBindingTask): IdentityRequestTokenForBindingTask + + @Binds + abstract fun bindIdentitySubmitTokenForBindingTask(task: DefaultIdentitySubmitTokenForBindingTask): IdentitySubmitTokenForBindingTask + + @Binds + abstract fun bindIdentityBulkLookupTask(task: DefaultIdentityBulkLookupTask): IdentityBulkLookupTask + + @Binds + abstract fun bindIdentityDisconnectTask(task: DefaultIdentityDisconnectTask): IdentityDisconnectTask +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityPingTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityPingTask.kt new file mode 100644 index 0000000000..6994ef1bce --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityPingTask.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.identity + +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.session.identity.IdentityServiceError +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject +import javax.net.ssl.HttpsURLConnection + +internal interface IdentityPingTask : Task { + data class Params( + val identityAuthAPI: IdentityAuthAPI + ) +} + +internal class DefaultIdentityPingTask @Inject constructor() : IdentityPingTask { + + override suspend fun execute(params: IdentityPingTask.Params) { + try { + executeRequest(null) { + apiCall = params.identityAuthAPI.ping() + } + } catch (throwable: Throwable) { + if (throwable is Failure.ServerError && throwable.httpCode == HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) { + // Check if API v1 is available + executeRequest(null) { + apiCall = params.identityAuthAPI.pingV1() + } + // API V1 is responding, but not V2 -> Outdated + throw IdentityServiceError.OutdatedIdentityServer + } else { + throw throwable + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityRegisterTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityRegisterTask.kt new file mode 100644 index 0000000000..bba6f99178 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityRegisterTask.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.identity + +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.identity.model.IdentityRegisterResponse +import org.matrix.android.sdk.internal.session.openid.RequestOpenIdTokenResponse +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal interface IdentityRegisterTask : Task { + data class Params( + val identityAuthAPI: IdentityAuthAPI, + val openIdTokenResponse: RequestOpenIdTokenResponse + ) +} + +internal class DefaultIdentityRegisterTask @Inject constructor() : IdentityRegisterTask { + + override suspend fun execute(params: IdentityRegisterTask.Params): IdentityRegisterResponse { + return executeRequest(null) { + apiCall = params.identityAuthAPI.register(params.openIdTokenResponse) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityRequestTokenForBindingTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityRequestTokenForBindingTask.kt new file mode 100644 index 0000000000..3155e19943 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityRequestTokenForBindingTask.kt @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.identity + +import org.matrix.android.sdk.api.session.identity.IdentityServiceError +import org.matrix.android.sdk.api.session.identity.ThreePid +import org.matrix.android.sdk.api.session.identity.getCountryCode +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.identity.data.IdentityPendingBinding +import org.matrix.android.sdk.internal.session.identity.data.IdentityStore +import org.matrix.android.sdk.internal.session.identity.model.IdentityRequestTokenForEmailBody +import org.matrix.android.sdk.internal.session.identity.model.IdentityRequestTokenForMsisdnBody +import org.matrix.android.sdk.internal.session.identity.model.IdentityRequestTokenResponse +import org.matrix.android.sdk.internal.task.Task +import java.util.UUID +import javax.inject.Inject + +internal interface IdentityRequestTokenForBindingTask : Task { + data class Params( + val threePid: ThreePid, + // True to request the identity server to send again the email or the SMS + val sendAgain: Boolean + ) +} + +internal class DefaultIdentityRequestTokenForBindingTask @Inject constructor( + private val identityApiProvider: IdentityApiProvider, + private val identityStore: IdentityStore, + @UserId private val userId: String +) : IdentityRequestTokenForBindingTask { + + override suspend fun execute(params: IdentityRequestTokenForBindingTask.Params) { + val identityAPI = getIdentityApiAndEnsureTerms(identityApiProvider, userId) + + val identityPendingBinding = identityStore.getPendingBinding(params.threePid) + + if (params.sendAgain && identityPendingBinding == null) { + throw IdentityServiceError.NoCurrentBindingError + } + + val clientSecret = identityPendingBinding?.clientSecret ?: UUID.randomUUID().toString() + val sendAttempt = identityPendingBinding?.sendAttempt?.inc() ?: 1 + + val tokenResponse = executeRequest(null) { + apiCall = when (params.threePid) { + is ThreePid.Email -> identityAPI.requestTokenToBindEmail(IdentityRequestTokenForEmailBody( + clientSecret = clientSecret, + sendAttempt = sendAttempt, + email = params.threePid.email + )) + is ThreePid.Msisdn -> { + identityAPI.requestTokenToBindMsisdn(IdentityRequestTokenForMsisdnBody( + clientSecret = clientSecret, + sendAttempt = sendAttempt, + phoneNumber = params.threePid.msisdn, + countryCode = params.threePid.getCountryCode() + )) + } + } + } + + // Store client secret, send attempt and sid + identityStore.storePendingBinding( + params.threePid, + IdentityPendingBinding( + clientSecret = clientSecret, + sendAttempt = sendAttempt, + sid = tokenResponse.sid + ) + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentitySubmitTokenForBindingTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentitySubmitTokenForBindingTask.kt new file mode 100644 index 0000000000..7b25986ae3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentitySubmitTokenForBindingTask.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.identity + +import org.matrix.android.sdk.api.session.identity.IdentityServiceError +import org.matrix.android.sdk.api.session.identity.ThreePid +import org.matrix.android.sdk.api.session.identity.toMedium +import org.matrix.android.sdk.internal.auth.registration.SuccessResult +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.identity.data.IdentityStore +import org.matrix.android.sdk.internal.session.identity.model.IdentityRequestOwnershipParams +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal interface IdentitySubmitTokenForBindingTask : Task { + data class Params( + val threePid: ThreePid, + val token: String + ) +} + +internal class DefaultIdentitySubmitTokenForBindingTask @Inject constructor( + private val identityApiProvider: IdentityApiProvider, + private val identityStore: IdentityStore, + @UserId private val userId: String +) : IdentitySubmitTokenForBindingTask { + + override suspend fun execute(params: IdentitySubmitTokenForBindingTask.Params) { + val identityAPI = getIdentityApiAndEnsureTerms(identityApiProvider, userId) + val identityPendingBinding = identityStore.getPendingBinding(params.threePid) ?: throw IdentityServiceError.NoCurrentBindingError + + val tokenResponse = executeRequest(null) { + apiCall = identityAPI.submitToken( + params.threePid.toMedium(), + IdentityRequestOwnershipParams( + clientSecret = identityPendingBinding.clientSecret, + sid = identityPendingBinding.sid, + token = params.token + )) + } + + if (!tokenResponse.isSuccess()) { + throw IdentityServiceError.BindingError + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityTaskHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityTaskHelper.kt new file mode 100644 index 0000000000..52f29c857b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityTaskHelper.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.identity + +import org.matrix.android.sdk.api.session.identity.IdentityServiceError +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.identity.model.IdentityAccountResponse + +internal suspend fun getIdentityApiAndEnsureTerms(identityApiProvider: IdentityApiProvider, userId: String): IdentityAPI { + val identityAPI = identityApiProvider.identityApi ?: throw IdentityServiceError.NoIdentityServerConfigured + + // Always check that we have access to the service (regarding terms) + val identityAccountResponse = executeRequest(null) { + apiCall = identityAPI.getAccount() + } + + assert(userId == identityAccountResponse.userId) + + return identityAPI +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/data/IdentityData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/data/IdentityData.kt new file mode 100644 index 0000000000..4574d9d598 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/data/IdentityData.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.identity.data + +internal data class IdentityData( + val identityServerUrl: String?, + val token: String?, + val hashLookupPepper: String?, + val hashLookupAlgorithm: List +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/data/IdentityPendingBinding.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/data/IdentityPendingBinding.kt new file mode 100644 index 0000000000..85bf65d741 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/data/IdentityPendingBinding.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.identity.data + +internal data class IdentityPendingBinding( + /* Managed by Riot */ + val clientSecret: String, + /* Managed by Riot */ + val sendAttempt: Int, + /* Provided by the identity server */ + val sid: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/data/IdentityStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/data/IdentityStore.kt new file mode 100644 index 0000000000..61334f188d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/data/IdentityStore.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.identity.data + +import org.matrix.android.sdk.api.session.identity.ThreePid +import org.matrix.android.sdk.internal.session.identity.model.IdentityHashDetailResponse + +internal interface IdentityStore { + + fun getIdentityData(): IdentityData? + + fun setUrl(url: String?) + + fun setToken(token: String?) + + fun setHashDetails(hashDetailResponse: IdentityHashDetailResponse) + + /** + * Store details about a current binding + */ + fun storePendingBinding(threePid: ThreePid, data: IdentityPendingBinding) + + fun getPendingBinding(threePid: ThreePid): IdentityPendingBinding? + + fun deletePendingBinding(threePid: ThreePid) +} + +internal fun IdentityStore.getIdentityServerUrlWithoutProtocol(): String? { + return getIdentityData()?.identityServerUrl?.substringAfter("://") +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityDataEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityDataEntity.kt new file mode 100644 index 0000000000..41645bc07b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityDataEntity.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.identity.db + +import io.realm.RealmList +import io.realm.RealmObject + +internal open class IdentityDataEntity( + var identityServerUrl: String? = null, + var token: String? = null, + var hashLookupPepper: String? = null, + var hashLookupAlgorithm: RealmList = RealmList() +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityDataEntityQuery.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityDataEntityQuery.kt new file mode 100644 index 0000000000..1c612d6bf4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityDataEntityQuery.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.identity.db + +import io.realm.Realm +import io.realm.RealmList +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +/** + * Only one object can be stored at a time + */ +internal fun IdentityDataEntity.Companion.get(realm: Realm): IdentityDataEntity? { + return realm.where().findFirst() +} + +private fun IdentityDataEntity.Companion.getOrCreate(realm: Realm): IdentityDataEntity { + return get(realm) ?: realm.createObject() +} + +internal fun IdentityDataEntity.Companion.setUrl(realm: Realm, + url: String?) { + realm.where().findAll().deleteAllFromRealm() + // Delete all pending binding if any + IdentityPendingBindingEntity.deleteAll(realm) + + if (url != null) { + getOrCreate(realm).apply { + identityServerUrl = url + } + } +} + +internal fun IdentityDataEntity.Companion.setToken(realm: Realm, + newToken: String?) { + get(realm)?.apply { + token = newToken + } +} + +internal fun IdentityDataEntity.Companion.setHashDetails(realm: Realm, + pepper: String, + algorithms: List) { + get(realm)?.apply { + hashLookupPepper = pepper + hashLookupAlgorithm = RealmList().apply { addAll(algorithms) } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityMapper.kt new file mode 100644 index 0000000000..4b99ba17d3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityMapper.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.identity.db + +import org.matrix.android.sdk.internal.session.identity.data.IdentityData +import org.matrix.android.sdk.internal.session.identity.data.IdentityPendingBinding + +internal object IdentityMapper { + + fun map(entity: IdentityDataEntity): IdentityData { + return IdentityData( + identityServerUrl = entity.identityServerUrl, + token = entity.token, + hashLookupPepper = entity.hashLookupPepper, + hashLookupAlgorithm = entity.hashLookupAlgorithm.toList() + ) + } + + fun map(entity: IdentityPendingBindingEntity): IdentityPendingBinding { + return IdentityPendingBinding( + clientSecret = entity.clientSecret, + sendAttempt = entity.sendAttempt, + sid = entity.sid + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityPendingBindingEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityPendingBindingEntity.kt new file mode 100644 index 0000000000..94a359f4d1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityPendingBindingEntity.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.identity.db + +import org.matrix.android.sdk.api.session.identity.ThreePid +import org.matrix.android.sdk.api.session.identity.toMedium +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class IdentityPendingBindingEntity( + @PrimaryKey var threePid: String = "", + /* Managed by Riot */ + var clientSecret: String = "", + /* Managed by Riot */ + var sendAttempt: Int = 0, + /* Provided by the identity server */ + var sid: String = "" +) : RealmObject() { + + companion object { + fun ThreePid.toPrimaryKey() = "${toMedium()}_$value" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityPendingBindingEntityQuery.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityPendingBindingEntityQuery.kt new file mode 100644 index 0000000000..30de96e557 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityPendingBindingEntityQuery.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.identity.db + +import org.matrix.android.sdk.api.session.identity.ThreePid +import io.realm.Realm +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +internal fun IdentityPendingBindingEntity.Companion.get(realm: Realm, threePid: ThreePid): IdentityPendingBindingEntity? { + return realm.where() + .equalTo(IdentityPendingBindingEntityFields.THREE_PID, threePid.toPrimaryKey()) + .findFirst() +} + +internal fun IdentityPendingBindingEntity.Companion.getOrCreate(realm: Realm, threePid: ThreePid): IdentityPendingBindingEntity { + return get(realm, threePid) ?: realm.createObject(threePid.toPrimaryKey()) +} + +internal fun IdentityPendingBindingEntity.Companion.delete(realm: Realm, threePid: ThreePid) { + get(realm, threePid)?.deleteFromRealm() +} + +internal fun IdentityPendingBindingEntity.Companion.deleteAll(realm: Realm) { + realm.where() + .findAll() + .deleteAllFromRealm() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityRealmModule.kt new file mode 100644 index 0000000000..85ea903ab6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityRealmModule.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.identity.db + +import io.realm.annotations.RealmModule + +/** + * Realm module for identity server classes + */ +@RealmModule(library = true, + classes = [ + IdentityDataEntity::class, + IdentityPendingBindingEntity::class + ]) +internal class IdentityRealmModule diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/RealmIdentityStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/RealmIdentityStore.kt new file mode 100644 index 0000000000..244f09f06a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/RealmIdentityStore.kt @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.identity.db + +import org.matrix.android.sdk.api.session.identity.ThreePid +import org.matrix.android.sdk.internal.di.IdentityDatabase +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.identity.data.IdentityPendingBinding +import org.matrix.android.sdk.internal.session.identity.data.IdentityData +import org.matrix.android.sdk.internal.session.identity.data.IdentityStore +import org.matrix.android.sdk.internal.session.identity.model.IdentityHashDetailResponse +import io.realm.Realm +import io.realm.RealmConfiguration +import javax.inject.Inject + +@SessionScope +internal class RealmIdentityStore @Inject constructor( + @IdentityDatabase + private val realmConfiguration: RealmConfiguration +) : IdentityStore { + + override fun getIdentityData(): IdentityData? { + return Realm.getInstance(realmConfiguration).use { realm -> + IdentityDataEntity.get(realm)?.let { IdentityMapper.map(it) } + } + } + + override fun setUrl(url: String?) { + Realm.getInstance(realmConfiguration).use { + it.executeTransaction { realm -> + IdentityDataEntity.setUrl(realm, url) + } + } + } + + override fun setToken(token: String?) { + Realm.getInstance(realmConfiguration).use { + it.executeTransaction { realm -> + IdentityDataEntity.setToken(realm, token) + } + } + } + + override fun setHashDetails(hashDetailResponse: IdentityHashDetailResponse) { + Realm.getInstance(realmConfiguration).use { + it.executeTransaction { realm -> + IdentityDataEntity.setHashDetails(realm, hashDetailResponse.pepper, hashDetailResponse.algorithms) + } + } + } + + override fun storePendingBinding(threePid: ThreePid, data: IdentityPendingBinding) { + Realm.getInstance(realmConfiguration).use { + it.executeTransaction { realm -> + IdentityPendingBindingEntity.getOrCreate(realm, threePid).let { entity -> + entity.clientSecret = data.clientSecret + entity.sendAttempt = data.sendAttempt + entity.sid = data.sid + } + } + } + } + + override fun getPendingBinding(threePid: ThreePid): IdentityPendingBinding? { + return Realm.getInstance(realmConfiguration).use { realm -> + IdentityPendingBindingEntity.get(realm, threePid)?.let { IdentityMapper.map(it) } + } + } + + override fun deletePendingBinding(threePid: ThreePid) { + Realm.getInstance(realmConfiguration).use { + it.executeTransaction { realm -> + IdentityPendingBindingEntity.delete(realm, threePid) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityAccountResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityAccountResponse.kt new file mode 100644 index 0000000000..f23c493206 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityAccountResponse.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.identity.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class IdentityAccountResponse( + /** + * Required. The user ID which registered the token. + */ + @Json(name = "user_id") + val userId: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityHashDetailResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityHashDetailResponse.kt new file mode 100644 index 0000000000..04ed62bddd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityHashDetailResponse.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.identity.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Ref: https://github.com/matrix-org/matrix-doc/blob/hs/hash-identity/proposals/2134-identity-hash-lookup.md + */ +@JsonClass(generateAdapter = true) +internal data class IdentityHashDetailResponse( + /** + * Required. The pepper the client MUST use in hashing identifiers, and MUST supply to the /lookup endpoint when performing lookups. + * Servers SHOULD rotate this string often. + */ + @Json(name = "lookup_pepper") + val pepper: String, + + /** + * Required. The algorithms the server supports. Must contain at least "sha256". + * "none" can be another possible value. + */ + @Json(name = "algorithms") + val algorithms: List +) { + companion object { + const val ALGORITHM_SHA256 = "sha256" + const val ALGORITHM_NONE = "none" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityLookUpParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityLookUpParams.kt new file mode 100644 index 0000000000..f737e4742c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityLookUpParams.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.identity.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Ref: https://github.com/matrix-org/matrix-doc/blob/hs/hash-identity/proposals/2134-identity-hash-lookup.md + */ +@JsonClass(generateAdapter = true) +internal data class IdentityLookUpParams( + /** + * Required. The addresses to look up. The format of the entries here depend on the algorithm used. + * Note that queries which have been incorrectly hashed or formatted will lead to no matches. + */ + @Json(name = "addresses") + val hashedAddresses: List, + + /** + * Required. The algorithm the client is using to encode the addresses. This should be one of the available options from /hash_details. + */ + @Json(name = "algorithm") + val algorithm: String, + + /** + * Required. The pepper from /hash_details. This is required even when the algorithm does not make use of it. + */ + @Json(name = "pepper") + val pepper: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityLookUpResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityLookUpResponse.kt new file mode 100644 index 0000000000..0a274ebe44 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityLookUpResponse.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.identity.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Ref: https://github.com/matrix-org/matrix-doc/blob/hs/hash-identity/proposals/2134-identity-hash-lookup.md + */ +@JsonClass(generateAdapter = true) +internal data class IdentityLookUpResponse( + /** + * Required. Any applicable mappings of addresses to Matrix User IDs. Addresses which do not have associations will + * not be included, which can make this property be an empty object. + */ + @Json(name = "mappings") + val mappings: Map +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityRegisterResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityRegisterResponse.kt new file mode 100644 index 0000000000..1769b3654e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityRegisterResponse.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.identity.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class IdentityRegisterResponse( + /** + * Required. An opaque string representing the token to authenticate future requests to the identity server with. + */ + @Json(name = "token") + val token: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityRequestOwnershipParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityRequestOwnershipParams.kt new file mode 100644 index 0000000000..bd263e1dc6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityRequestOwnershipParams.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.identity.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class IdentityRequestOwnershipParams( + /** + * Required. The client secret that was supplied to the requestToken call. + */ + @Json(name = "client_secret") + val clientSecret: String, + + /** + * Required. The session ID, generated by the requestToken call. + */ + @Json(name = "sid") + val sid: String, + + /** + * Required. The token generated by the requestToken call and sent to the user. + */ + @Json(name = "token") + val token: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityRequestTokenBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityRequestTokenBody.kt new file mode 100644 index 0000000000..b93a3f43ae --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityRequestTokenBody.kt @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.identity.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +// Just to consider common parameters +private interface IdentityRequestTokenBody { + /** + * Required. A unique string generated by the client, and used to identify the validation attempt. + * It must be a string consisting of the characters [0-9a-zA-Z.=_-]. + * Its length must not exceed 255 characters and it must not be empty. + */ + val clientSecret: String + + val sendAttempt: Int +} + +@JsonClass(generateAdapter = true) +internal data class IdentityRequestTokenForEmailBody( + @Json(name = "client_secret") + override val clientSecret: String, + + /** + * Required. The server will only send an email if the send_attempt is a number greater than the most + * recent one which it has seen, scoped to that email + client_secret pair. This is to avoid repeatedly + * sending the same email in the case of request retries between the POSTing user and the identity server. + * The client should increment this value if they desire a new email (e.g. a reminder) to be sent. + * If they do not, the server should respond with success but not resend the email. + */ + @Json(name = "send_attempt") + override val sendAttempt: Int, + + /** + * Required. The email address to validate. + */ + @Json(name = "email") + val email: String +) : IdentityRequestTokenBody + +@JsonClass(generateAdapter = true) +internal data class IdentityRequestTokenForMsisdnBody( + @Json(name = "client_secret") + override val clientSecret: String, + + /** + * Required. The server will only send an SMS if the send_attempt is a number greater than the most recent one + * which it has seen, scoped to that country + phone_number + client_secret triple. This is to avoid repeatedly + * sending the same SMS in the case of request retries between the POSTing user and the identity server. + * The client should increment this value if they desire a new SMS (e.g. a reminder) to be sent. + */ + @Json(name = "send_attempt") + override val sendAttempt: Int, + + /** + * Required. The phone number to validate. + */ + @Json(name = "phone_number") + val phoneNumber: String, + + /** + * Required. The two-letter uppercase ISO-3166-1 alpha-2 country code that the number in phone_number + * should be parsed as if it were dialled from. + */ + @Json(name = "country") + val countryCode: String +) : IdentityRequestTokenBody diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityRequestTokenResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityRequestTokenResponse.kt new file mode 100644 index 0000000000..5f4209cac8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/IdentityRequestTokenResponse.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.identity.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class IdentityRequestTokenResponse( + /** + * Required. The session ID. Session IDs are opaque strings generated by the identity server. + * They must consist entirely of the characters [0-9a-zA-Z.=_-]. + * Their length must not exceed 255 characters and they must not be empty. + */ + @Json(name = "sid") + val sid: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/AllowedWidgetsContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/AllowedWidgetsContent.kt new file mode 100644 index 0000000000..5ea6e4d4d7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/AllowedWidgetsContent.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.integrationmanager + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class AllowedWidgetsContent( + /** + * Map of stateEventId to Allowed + */ + @Json(name = "widgets") val widgets: Map = emptyMap(), + + /** + * Map of native widgetType to a map of domain to Allowed + * { + * "jitsi" : { + * "jitsi.domain.org" : true, + * "jitsi.other.org" : false + * } + * } + */ + @Json(name = "native_widgets") val native: Map> = emptyMap() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/DefaultIntegrationManagerService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/DefaultIntegrationManagerService.kt new file mode 100644 index 0000000000..4f1185929f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/DefaultIntegrationManagerService.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.integrationmanager + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerConfig +import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService +import org.matrix.android.sdk.api.util.Cancelable +import javax.inject.Inject + +internal class DefaultIntegrationManagerService @Inject constructor(private val integrationManager: IntegrationManager) : IntegrationManagerService { + + override fun addListener(listener: IntegrationManagerService.Listener) { + integrationManager.addListener(listener) + } + + override fun removeListener(listener: IntegrationManagerService.Listener) { + integrationManager.removeListener(listener) + } + + override fun getOrderedConfigs(): List { + return integrationManager.getOrderedConfigs() + } + + override fun getPreferredConfig(): IntegrationManagerConfig { + return integrationManager.getPreferredConfig() + } + + override fun isIntegrationEnabled(): Boolean { + return integrationManager.isIntegrationEnabled() + } + + override fun setIntegrationEnabled(enable: Boolean, callback: MatrixCallback): Cancelable { + return integrationManager.setIntegrationEnabled(enable, callback) + } + + override fun setWidgetAllowed(stateEventId: String, allowed: Boolean, callback: MatrixCallback): Cancelable { + return integrationManager.setWidgetAllowed(stateEventId, allowed, callback) + } + + override fun isWidgetAllowed(stateEventId: String): Boolean { + return integrationManager.isWidgetAllowed(stateEventId) + } + + override fun setNativeWidgetDomainAllowed(widgetType: String, domain: String, allowed: Boolean, callback: MatrixCallback): Cancelable { + return integrationManager.setNativeWidgetDomainAllowed(widgetType, domain, allowed, callback) + } + + override fun isNativeWidgetDomainAllowed(widgetType: String, domain: String): Boolean { + return integrationManager.isNativeWidgetDomainAllowed(widgetType, domain) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManager.kt new file mode 100644 index 0000000000..0f7b3f85c7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManager.kt @@ -0,0 +1,292 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.integrationmanager + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerConfig +import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService +import org.matrix.android.sdk.api.session.widgets.model.WidgetContent +import org.matrix.android.sdk.api.session.widgets.model.WidgetType +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.NoOpCancellable +import org.matrix.android.sdk.internal.database.model.WellknownIntegrationManagerConfigEntity +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.extensions.observeNotNull +import org.matrix.android.sdk.internal.session.SessionLifecycleObserver +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent +import org.matrix.android.sdk.internal.session.user.accountdata.AccountDataDataSource +import org.matrix.android.sdk.internal.session.user.accountdata.UpdateUserAccountDataTask +import org.matrix.android.sdk.internal.session.widgets.helper.WidgetFactory +import org.matrix.android.sdk.internal.session.widgets.helper.extractWidgetSequence +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import timber.log.Timber +import javax.inject.Inject + +/** + * The integration manager allows to + * - Get the Integration Manager that a user has explicitly set for its account (via account data) + * - Get the recommended/preferred Integration Manager list as defined by the HomeServer (via wellknown) + * - Check if the user has disabled the integration manager feature + * - Allow / Disallow Integration manager (propagated to other riot clients) + * + * The integration manager listen to account data, and can notify observer for changes. + * + * The wellknown is refreshed at each application fresh start + * + */ +@SessionScope +internal class IntegrationManager @Inject constructor(matrixConfiguration: MatrixConfiguration, + private val taskExecutor: TaskExecutor, + @SessionDatabase private val monarchy: Monarchy, + private val updateUserAccountDataTask: UpdateUserAccountDataTask, + private val accountDataDataSource: AccountDataDataSource, + private val widgetFactory: WidgetFactory) + : SessionLifecycleObserver { + + private val currentConfigs = ArrayList() + private val lifecycleOwner: LifecycleOwner = LifecycleOwner { lifecycleRegistry } + private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(lifecycleOwner) + + private val listeners = HashSet() + fun addListener(listener: IntegrationManagerService.Listener) = synchronized(listeners) { listeners.add(listener) } + fun removeListener(listener: IntegrationManagerService.Listener) = synchronized(listeners) { listeners.remove(listener) } + + init { + val defaultConfig = IntegrationManagerConfig( + uiUrl = matrixConfiguration.integrationUIUrl, + restUrl = matrixConfiguration.integrationRestUrl, + kind = IntegrationManagerConfig.Kind.DEFAULT + ) + currentConfigs.add(defaultConfig) + } + + override fun onStart() { + lifecycleRegistry.currentState = Lifecycle.State.STARTED + observeWellknownConfig() + accountDataDataSource + .getLiveAccountDataEvent(UserAccountDataTypes.TYPE_ALLOWED_WIDGETS) + .observeNotNull(lifecycleOwner) { + val allowedWidgetsContent = it.getOrNull()?.content?.toModel() + if (allowedWidgetsContent != null) { + notifyWidgetPermissionsChanged(allowedWidgetsContent) + } + } + accountDataDataSource + .getLiveAccountDataEvent(UserAccountDataTypes.TYPE_INTEGRATION_PROVISIONING) + .observeNotNull(lifecycleOwner) { + val integrationProvisioningContent = it.getOrNull()?.content?.toModel() + if (integrationProvisioningContent != null) { + notifyIsEnabledChanged(integrationProvisioningContent) + } + } + accountDataDataSource + .getLiveAccountDataEvent(UserAccountDataTypes.TYPE_WIDGETS) + .observeNotNull(lifecycleOwner) { + val integrationManagerContent = it.getOrNull()?.asIntegrationManagerWidgetContent() + val config = integrationManagerContent?.extractIntegrationManagerConfig() + updateCurrentConfigs(IntegrationManagerConfig.Kind.ACCOUNT, config) + } + } + + override fun onStop() { + lifecycleRegistry.currentState = Lifecycle.State.DESTROYED + } + + fun hasConfig() = currentConfigs.isNotEmpty() + + fun getOrderedConfigs(): List { + return currentConfigs.sortedBy { + it.kind + } + } + + fun getPreferredConfig(): IntegrationManagerConfig { + // This can't be null as we should have at least the default one registered + return getOrderedConfigs().first() + } + + /** + * Returns false if the user as disabled integration manager feature + */ + fun isIntegrationEnabled(): Boolean { + val integrationProvisioningData = accountDataDataSource.getAccountDataEvent(UserAccountDataTypes.TYPE_INTEGRATION_PROVISIONING) + val integrationProvisioningContent = integrationProvisioningData?.content?.toModel() + return integrationProvisioningContent?.enabled ?: false + } + + fun setIntegrationEnabled(enable: Boolean, callback: MatrixCallback): Cancelable { + val isIntegrationEnabled = isIntegrationEnabled() + if (enable == isIntegrationEnabled) { + callback.onSuccess(Unit) + return NoOpCancellable + } + val integrationProvisioningContent = IntegrationProvisioningContent(enabled = enable) + val params = UpdateUserAccountDataTask.IntegrationProvisioning(integrationProvisioningContent = integrationProvisioningContent) + return updateUserAccountDataTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + fun setWidgetAllowed(stateEventId: String, allowed: Boolean, callback: MatrixCallback): Cancelable { + val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountDataTypes.TYPE_ALLOWED_WIDGETS) + val currentContent = currentAllowedWidgets?.content?.toModel() + val newContent = if (currentContent == null) { + val allowedWidget = mapOf(stateEventId to allowed) + AllowedWidgetsContent(widgets = allowedWidget, native = emptyMap()) + } else { + val allowedWidgets = currentContent.widgets.toMutableMap().apply { + put(stateEventId, allowed) + } + currentContent.copy(widgets = allowedWidgets) + } + val params = UpdateUserAccountDataTask.AllowedWidgets(allowedWidgetsContent = newContent) + return updateUserAccountDataTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + fun isWidgetAllowed(stateEventId: String): Boolean { + val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountDataTypes.TYPE_ALLOWED_WIDGETS) + val currentContent = currentAllowedWidgets?.content?.toModel() + return currentContent?.widgets?.get(stateEventId) ?: false + } + + fun setNativeWidgetDomainAllowed(widgetType: String, domain: String, allowed: Boolean, callback: MatrixCallback): Cancelable { + val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountDataTypes.TYPE_ALLOWED_WIDGETS) + val currentContent = currentAllowedWidgets?.content?.toModel() + val newContent = if (currentContent == null) { + val nativeAllowedWidgets = mapOf(widgetType to mapOf(domain to allowed)) + AllowedWidgetsContent(widgets = emptyMap(), native = nativeAllowedWidgets) + } else { + val nativeAllowedWidgets = currentContent.native.toMutableMap().apply { + (get(widgetType))?.let { + set(widgetType, it.toMutableMap().apply { set(domain, allowed) }) + } ?: run { + set(widgetType, mapOf(domain to allowed)) + } + } + currentContent.copy(native = nativeAllowedWidgets) + } + val params = UpdateUserAccountDataTask.AllowedWidgets(allowedWidgetsContent = newContent) + return updateUserAccountDataTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + fun isNativeWidgetDomainAllowed(widgetType: String, domain: String?): Boolean { + val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountDataTypes.TYPE_ALLOWED_WIDGETS) + val currentContent = currentAllowedWidgets?.content?.toModel() + return currentContent?.native?.get(widgetType)?.get(domain) ?: false + } + + private fun notifyConfigurationChanged() { + synchronized(listeners) { + listeners.forEach { + try { + it.onConfigurationChanged(currentConfigs) + } catch (t: Throwable) { + Timber.e(t, "Failed to notify listener") + } + } + } + } + + private fun notifyWidgetPermissionsChanged(allowedWidgets: AllowedWidgetsContent) { + Timber.v("On widget permissions changed: $allowedWidgets") + synchronized(listeners) { + listeners.forEach { + try { + it.onWidgetPermissionsChanged(allowedWidgets.widgets) + } catch (t: Throwable) { + Timber.e(t, "Failed to notify listener") + } + } + } + } + + private fun notifyIsEnabledChanged(provisioningContent: IntegrationProvisioningContent) { + Timber.v("On provisioningContent changed : $provisioningContent") + synchronized(listeners) { + listeners.forEach { + try { + it.onIsEnabledChanged(provisioningContent.enabled) + } catch (t: Throwable) { + Timber.e(t, "Failed to notify listener") + } + } + } + } + + private fun WidgetContent.extractIntegrationManagerConfig(): IntegrationManagerConfig? { + if (url.isNullOrBlank()) { + return null + } + val integrationManagerData = data.toModel() + return IntegrationManagerConfig( + uiUrl = url, + restUrl = integrationManagerData?.apiUrl ?: url, + kind = IntegrationManagerConfig.Kind.ACCOUNT + ) + } + + private fun UserAccountDataEvent.asIntegrationManagerWidgetContent(): WidgetContent? { + return extractWidgetSequence(widgetFactory) + .filter { + WidgetType.IntegrationManager == it.type + } + .firstOrNull()?.widgetContent + } + + private fun observeWellknownConfig() { + val liveData = monarchy.findAllMappedWithChanges( + { it.where(WellknownIntegrationManagerConfigEntity::class.java) }, + { IntegrationManagerConfig(it.uiUrl, it.apiUrl, IntegrationManagerConfig.Kind.HOMESERVER) } + ) + liveData.observeNotNull(lifecycleOwner) { + val config = it.firstOrNull() + updateCurrentConfigs(IntegrationManagerConfig.Kind.HOMESERVER, config) + } + } + + private fun updateCurrentConfigs(kind: IntegrationManagerConfig.Kind, config: IntegrationManagerConfig?) { + val hasBeenRemoved = currentConfigs.removeAll { currentConfig -> + currentConfig.kind == kind + } + if (config != null) { + currentConfigs.add(config) + } + if (hasBeenRemoved || config != null) { + notifyConfigurationChanged() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManagerConfigExtractor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManagerConfigExtractor.kt new file mode 100644 index 0000000000..78edc59fca --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManagerConfigExtractor.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.integrationmanager + +import org.matrix.android.sdk.api.auth.data.WellKnown +import org.matrix.android.sdk.internal.database.model.WellknownIntegrationManagerConfigEntity +import javax.inject.Inject + +internal class IntegrationManagerConfigExtractor @Inject constructor() { + + fun extract(wellKnown: WellKnown): WellknownIntegrationManagerConfigEntity? { + wellKnown.integrations?.get("managers")?.let { + (it as? List<*>)?.let { configs -> + configs.forEach { config -> + (config as? Map<*, *>)?.let { map -> + val apiUrl = map["api_url"] as? String + val uiUrl = map["ui_url"] as? String ?: apiUrl + if (apiUrl != null + && apiUrl.startsWith("https://") + && uiUrl!!.startsWith("https://")) { + return WellknownIntegrationManagerConfigEntity( + apiUrl = apiUrl, + uiUrl = uiUrl + ) + } + } + } + } + } + return null + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManagerModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManagerModule.kt new file mode 100644 index 0000000000..fb7f835d9b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManagerModule.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.integrationmanager + +import dagger.Binds +import dagger.Module +import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService + +@Module +internal abstract class IntegrationManagerModule { + + @Binds + abstract fun bindIntegrationManagerService(service: DefaultIntegrationManagerService): IntegrationManagerService +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManagerWidgetData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManagerWidgetData.kt new file mode 100644 index 0000000000..c592237a1f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManagerWidgetData.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.integrationmanager + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class IntegrationManagerWidgetData( + @Json(name = "api_url") val apiUrl: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationProvisioningContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationProvisioningContent.kt new file mode 100644 index 0000000000..e48a6fd84f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationProvisioningContent.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.integrationmanager + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class IntegrationProvisioningContent( + @Json(name = "enabled") val enabled: Boolean +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt new file mode 100644 index 0000000000..ffe11ee04b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt @@ -0,0 +1,224 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.notification + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.pushrules.PushRuleService +import org.matrix.android.sdk.api.pushrules.RuleKind +import org.matrix.android.sdk.api.pushrules.RuleSetKey +import org.matrix.android.sdk.api.pushrules.getActions +import org.matrix.android.sdk.api.pushrules.rest.PushRule +import org.matrix.android.sdk.api.pushrules.rest.RuleSet +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.database.mapper.PushRulesMapper +import org.matrix.android.sdk.internal.database.model.PushRulesEntity +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.pushers.AddPushRuleTask +import org.matrix.android.sdk.internal.session.pushers.GetPushRulesTask +import org.matrix.android.sdk.internal.session.pushers.RemovePushRuleTask +import org.matrix.android.sdk.internal.session.pushers.UpdatePushRuleActionsTask +import org.matrix.android.sdk.internal.session.pushers.UpdatePushRuleEnableStatusTask +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import timber.log.Timber +import javax.inject.Inject + +@SessionScope +internal class DefaultPushRuleService @Inject constructor( + private val getPushRulesTask: GetPushRulesTask, + private val updatePushRuleEnableStatusTask: UpdatePushRuleEnableStatusTask, + private val addPushRuleTask: AddPushRuleTask, + private val updatePushRuleActionsTask: UpdatePushRuleActionsTask, + private val removePushRuleTask: RemovePushRuleTask, + private val taskExecutor: TaskExecutor, + @SessionDatabase private val monarchy: Monarchy +) : PushRuleService { + + private var listeners = mutableSetOf() + + override fun fetchPushRules(scope: String) { + getPushRulesTask + .configureWith(GetPushRulesTask.Params(scope)) + .executeBy(taskExecutor) + } + + override fun getPushRules(scope: String): RuleSet { + var contentRules: List = emptyList() + var overrideRules: List = emptyList() + var roomRules: List = emptyList() + var senderRules: List = emptyList() + var underrideRules: List = emptyList() + + monarchy.doWithRealm { realm -> + PushRulesEntity.where(realm, scope, RuleSetKey.CONTENT) + .findFirst() + ?.let { pushRulesEntity -> + contentRules = pushRulesEntity.pushRules.map { PushRulesMapper.mapContentRule(it) } + } + PushRulesEntity.where(realm, scope, RuleSetKey.OVERRIDE) + .findFirst() + ?.let { pushRulesEntity -> + overrideRules = pushRulesEntity.pushRules.map { PushRulesMapper.map(it) } + } + PushRulesEntity.where(realm, scope, RuleSetKey.ROOM) + .findFirst() + ?.let { pushRulesEntity -> + roomRules = pushRulesEntity.pushRules.map { PushRulesMapper.mapRoomRule(it) } + } + PushRulesEntity.where(realm, scope, RuleSetKey.SENDER) + .findFirst() + ?.let { pushRulesEntity -> + senderRules = pushRulesEntity.pushRules.map { PushRulesMapper.mapSenderRule(it) } + } + PushRulesEntity.where(realm, scope, RuleSetKey.UNDERRIDE) + .findFirst() + ?.let { pushRulesEntity -> + underrideRules = pushRulesEntity.pushRules.map { PushRulesMapper.map(it) } + } + } + + return RuleSet( + content = contentRules, + override = overrideRules, + room = roomRules, + sender = senderRules, + underride = underrideRules + ) + } + + override fun updatePushRuleEnableStatus(kind: RuleKind, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback): Cancelable { + // The rules will be updated, and will come back from the next sync response + return updatePushRuleEnableStatusTask + .configureWith(UpdatePushRuleEnableStatusTask.Params(kind, pushRule, enabled)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun addPushRule(kind: RuleKind, pushRule: PushRule, callback: MatrixCallback): Cancelable { + return addPushRuleTask + .configureWith(AddPushRuleTask.Params(kind, pushRule)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun updatePushRuleActions(kind: RuleKind, oldPushRule: PushRule, newPushRule: PushRule, callback: MatrixCallback): Cancelable { + return updatePushRuleActionsTask + .configureWith(UpdatePushRuleActionsTask.Params(kind, oldPushRule, newPushRule)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun removePushRule(kind: RuleKind, pushRule: PushRule, callback: MatrixCallback): Cancelable { + return removePushRuleTask + .configureWith(RemovePushRuleTask.Params(kind, pushRule)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun removePushRuleListener(listener: PushRuleService.PushRuleListener) { + synchronized(listeners) { + listeners.remove(listener) + } + } + + override fun addPushRuleListener(listener: PushRuleService.PushRuleListener) { + synchronized(listeners) { + listeners.add(listener) + } + } + +// fun processEvents(events: List) { +// var hasDoneSomething = false +// events.forEach { event -> +// fulfilledBingRule(event)?.let { +// hasDoneSomething = true +// dispatchBing(event, it) +// } +// } +// if (hasDoneSomething) +// dispatchFinish() +// } + + fun dispatchBing(event: Event, rule: PushRule) { + synchronized(listeners) { + val actionsList = rule.getActions() + listeners.forEach { + try { + it.onMatchRule(event, actionsList) + } catch (e: Throwable) { + Timber.e(e, "Error while dispatching bing") + } + } + } + } + + fun dispatchRoomJoined(roomId: String) { + synchronized(listeners) { + listeners.forEach { + try { + it.onRoomJoined(roomId) + } catch (e: Throwable) { + Timber.e(e, "Error while dispatching room joined") + } + } + } + } + + fun dispatchRoomLeft(roomId: String) { + synchronized(listeners) { + listeners.forEach { + try { + it.onRoomLeft(roomId) + } catch (e: Throwable) { + Timber.e(e, "Error while dispatching room left") + } + } + } + } + + fun dispatchRedactedEventId(redactedEventId: String) { + synchronized(listeners) { + listeners.forEach { + try { + it.onEventRedacted(redactedEventId) + } catch (e: Throwable) { + Timber.e(e, "Error while dispatching redacted event") + } + } + } + } + + fun dispatchFinish() { + synchronized(listeners) { + listeners.forEach { + try { + it.batchFinish() + } catch (e: Throwable) { + Timber.e(e, "Error while dispatching finish") + } + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt new file mode 100644 index 0000000000..49a92acc54 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.notification + +import org.matrix.android.sdk.api.pushrules.ConditionResolver +import org.matrix.android.sdk.api.pushrules.rest.PushRule +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.sync.model.RoomsSyncResponse +import org.matrix.android.sdk.internal.task.Task +import timber.log.Timber +import javax.inject.Inject + +internal interface ProcessEventForPushTask : Task { + data class Params( + val syncResponse: RoomsSyncResponse, + val rules: List + ) +} + +internal class DefaultProcessEventForPushTask @Inject constructor( + private val defaultPushRuleService: DefaultPushRuleService, + private val conditionResolver: ConditionResolver, + @UserId private val userId: String +) : ProcessEventForPushTask { + + override suspend fun execute(params: ProcessEventForPushTask.Params) { + // Handle left rooms + params.syncResponse.leave.keys.forEach { + defaultPushRuleService.dispatchRoomLeft(it) + } + // Handle joined rooms + params.syncResponse.join.keys.forEach { + defaultPushRuleService.dispatchRoomJoined(it) + } + val newJoinEvents = params.syncResponse.join + .mapNotNull { (key, value) -> + value.timeline?.events?.map { it.copy(roomId = key) } + } + .flatten() + val inviteEvents = params.syncResponse.invite + .mapNotNull { (key, value) -> + value.inviteState?.events?.map { it.copy(roomId = key) } + } + .flatten() + val allEvents = (newJoinEvents + inviteEvents).filter { event -> + when (event.type) { + EventType.MESSAGE, + EventType.REDACTION, + EventType.ENCRYPTED, + EventType.STATE_ROOM_MEMBER -> true + else -> false + } + }.filter { + it.senderId != userId + } + Timber.v("[PushRules] Found ${allEvents.size} out of ${(newJoinEvents + inviteEvents).size}" + + " to check for push rules with ${params.rules.size} rules") + allEvents.forEach { event -> + fulfilledBingRule(event, params.rules)?.let { + Timber.v("[PushRules] Rule $it match for event ${event.eventId}") + defaultPushRuleService.dispatchBing(event, it) + } + } + + val allRedactedEvents = params.syncResponse.join + .asSequence() + .mapNotNull { (_, value) -> value.timeline?.events } + .flatten() + .filter { it.type == EventType.REDACTION } + .mapNotNull { it.redacts } + .toList() + + Timber.v("[PushRules] Found ${allRedactedEvents.size} redacted events") + + allRedactedEvents.forEach { redactedEventId -> + defaultPushRuleService.dispatchRedactedEventId(redactedEventId) + } + + defaultPushRuleService.dispatchFinish() + } + + private fun fulfilledBingRule(event: Event, rules: List): PushRule? { + return rules.firstOrNull { rule -> + // All conditions must hold true for an event in order to apply the action for the event. + rule.enabled && rule.conditions?.all { + it.asExecutableCondition()?.isSatisfied(event, conditionResolver) ?: false + } ?: false + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/GetOpenIdTokenTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/GetOpenIdTokenTask.kt new file mode 100644 index 0000000000..3da6fdca93 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/GetOpenIdTokenTask.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.openid + +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface GetOpenIdTokenTask : Task + +internal class DefaultGetOpenIdTokenTask @Inject constructor( + @UserId private val userId: String, + private val openIdAPI: OpenIdAPI, + private val eventBus: EventBus) : GetOpenIdTokenTask { + + override suspend fun execute(params: Unit): RequestOpenIdTokenResponse { + return executeRequest(eventBus) { + apiCall = openIdAPI.openIdToken(userId) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/OpenIdAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/OpenIdAPI.kt new file mode 100644 index 0000000000..e56e2e630e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/OpenIdAPI.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.openid + +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.POST +import retrofit2.http.Path + +internal interface OpenIdAPI { + + /** + * Gets a bearer token from the homeserver that the user can + * present to a third party in order to prove their ownership + * of the Matrix account they are logged into. + * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-user-userid-openid-request-token + * + * @param userId the user id + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/openid/request_token") + fun openIdToken(@Path("userId") userId: String, + @Body body: JsonDict = emptyMap()): Call +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/OpenIdModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/OpenIdModule.kt new file mode 100644 index 0000000000..60ee7fb747 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/OpenIdModule.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.openid + +import dagger.Binds +import dagger.Module +import dagger.Provides +import retrofit2.Retrofit + +@Module +internal abstract class OpenIdModule { + + @Module + companion object { + @JvmStatic + @Provides + fun providesOpenIdAPI(retrofit: Retrofit): OpenIdAPI { + return retrofit.create(OpenIdAPI::class.java) + } + } + + @Binds + abstract fun bindGetOpenIdTokenTask(task: DefaultGetOpenIdTokenTask): GetOpenIdTokenTask +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/RequestOpenIdTokenResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/RequestOpenIdTokenResponse.kt new file mode 100644 index 0000000000..8103efb895 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/RequestOpenIdTokenResponse.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.openid + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class RequestOpenIdTokenResponse( + /** + * Required. An access token the consumer may use to verify the identity of the person who generated the token. + * This is given to the federation API GET /openid/userinfo to verify the user's identity. + */ + @Json(name = "access_token") + val openIdToken: String, + + /** + * Required. The string "Bearer". + */ + @Json(name = "token_type") + val tokenType: String, + + /** + * Required. The homeserver domain the consumer should use when attempting to verify the user's identity. + */ + @Json(name = "matrix_server_name") + val matrixServerName: String, + + /** + * Required. The number of seconds before this token expires and a new one must be generated. + */ + @Json(name = "expires_in") + val expiresIn: Int +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AccountThreePidsResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AccountThreePidsResponse.kt new file mode 100644 index 0000000000..185294fd3b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AccountThreePidsResponse.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.profile + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing the ThreePids response + */ +@JsonClass(generateAdapter = true) +internal data class AccountThreePidsResponse( + @Json(name = "threepids") + val threePids: List? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/BindThreePidBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/BindThreePidBody.kt new file mode 100644 index 0000000000..dff246e6f1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/BindThreePidBody.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.profile + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class BindThreePidBody( + /** + * Required. The client secret used in the session with the identity server. + */ + @Json(name = "client_secret") + val clientSecret: String, + + /** + * Required. The identity server to use. (without "https://") + */ + @Json(name = "id_server") + var identityServerUrlWithoutProtocol: String, + + /** + * Required. An access token previously registered with the identity server. + */ + @Json(name = "id_access_token") + var identityServerAccessToken: String, + + /** + * Required. The session identifier given by the identity server. + */ + @Json(name = "sid") + var sid: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/BindThreePidsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/BindThreePidsTask.kt new file mode 100644 index 0000000000..52fbcb5185 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/BindThreePidsTask.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.profile + +import org.matrix.android.sdk.api.session.identity.IdentityServiceError +import org.matrix.android.sdk.api.session.identity.ThreePid +import org.matrix.android.sdk.internal.di.AuthenticatedIdentity +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.network.token.AccessTokenProvider +import org.matrix.android.sdk.internal.session.identity.data.IdentityStore +import org.matrix.android.sdk.internal.session.identity.data.getIdentityServerUrlWithoutProtocol +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal abstract class BindThreePidsTask : Task { + data class Params( + val threePid: ThreePid + ) +} + +internal class DefaultBindThreePidsTask @Inject constructor(private val profileAPI: ProfileAPI, + private val identityStore: IdentityStore, + @AuthenticatedIdentity + private val accessTokenProvider: AccessTokenProvider, + private val eventBus: EventBus) : BindThreePidsTask() { + override suspend fun execute(params: Params) { + val identityServerUrlWithoutProtocol = identityStore.getIdentityServerUrlWithoutProtocol() ?: throw IdentityServiceError.NoIdentityServerConfigured + val identityServerAccessToken = accessTokenProvider.getToken() ?: throw IdentityServiceError.NoIdentityServerConfigured + val identityPendingBinding = identityStore.getPendingBinding(params.threePid) ?: throw IdentityServiceError.NoCurrentBindingError + + executeRequest(eventBus) { + apiCall = profileAPI.bindThreePid( + BindThreePidBody( + clientSecret = identityPendingBinding.clientSecret, + identityServerUrlWithoutProtocol = identityServerUrlWithoutProtocol, + identityServerAccessToken = identityServerAccessToken, + sid = identityPendingBinding.sid + )) + } + + // Binding is over, cleanup the store + identityStore.deletePendingBinding(params.threePid) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt new file mode 100644 index 0000000000..06dcaeccef --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt @@ -0,0 +1,141 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + * + */ + +package org.matrix.android.sdk.internal.session.profile + +import android.net.Uri +import androidx.lifecycle.LiveData +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.identity.ThreePid +import org.matrix.android.sdk.api.session.profile.ProfileService +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.internal.database.model.UserThreePidEntity +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.content.FileUploader +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import org.matrix.android.sdk.internal.task.launchToCallback +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import io.realm.kotlin.where +import javax.inject.Inject + +internal class DefaultProfileService @Inject constructor(private val taskExecutor: TaskExecutor, + @SessionDatabase private val monarchy: Monarchy, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val refreshUserThreePidsTask: RefreshUserThreePidsTask, + private val getProfileInfoTask: GetProfileInfoTask, + private val setDisplayNameTask: SetDisplayNameTask, + private val setAvatarUrlTask: SetAvatarUrlTask, + private val fileUploader: FileUploader) : ProfileService { + + override fun getDisplayName(userId: String, matrixCallback: MatrixCallback>): Cancelable { + val params = GetProfileInfoTask.Params(userId) + return getProfileInfoTask + .configureWith(params) { + this.callback = object : MatrixCallback { + override fun onSuccess(data: JsonDict) { + val displayName = data[ProfileService.DISPLAY_NAME_KEY] as? String + matrixCallback.onSuccess(Optional.from(displayName)) + } + + override fun onFailure(failure: Throwable) { + matrixCallback.onFailure(failure) + } + } + } + .executeBy(taskExecutor) + } + + override fun setDisplayName(userId: String, newDisplayName: String, matrixCallback: MatrixCallback): Cancelable { + return setDisplayNameTask + .configureWith(SetDisplayNameTask.Params(userId = userId, newDisplayName = newDisplayName)) { + callback = matrixCallback + } + .executeBy(taskExecutor) + } + + override fun updateAvatar(userId: String, newAvatarUri: Uri, fileName: String, matrixCallback: MatrixCallback): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, matrixCallback) { + val response = fileUploader.uploadFromUri(newAvatarUri, fileName, "image/jpeg") + setAvatarUrlTask + .configureWith(SetAvatarUrlTask.Params(userId = userId, newAvatarUrl = response.contentUri)) { + callback = matrixCallback + } + .executeBy(taskExecutor) + } + } + + override fun getAvatarUrl(userId: String, matrixCallback: MatrixCallback>): Cancelable { + val params = GetProfileInfoTask.Params(userId) + return getProfileInfoTask + .configureWith(params) { + this.callback = object : MatrixCallback { + override fun onSuccess(data: JsonDict) { + val avatarUrl = data[ProfileService.AVATAR_URL_KEY] as? String + matrixCallback.onSuccess(Optional.from(avatarUrl)) + } + + override fun onFailure(failure: Throwable) { + matrixCallback.onFailure(failure) + } + } + } + .executeBy(taskExecutor) + } + + override fun getProfile(userId: String, matrixCallback: MatrixCallback): Cancelable { + val params = GetProfileInfoTask.Params(userId) + return getProfileInfoTask + .configureWith(params) { + this.callback = matrixCallback + } + .executeBy(taskExecutor) + } + + override fun getThreePids(): List { + return monarchy.fetchAllMappedSync( + { it.where() }, + { it.asDomain() } + ) + } + + override fun getThreePidsLive(refreshData: Boolean): LiveData> { + if (refreshData) { + // Force a refresh of the values + refreshUserThreePidsTask + .configureWith() + .executeBy(taskExecutor) + } + + return monarchy.findAllMappedWithChanges( + { it.where() }, + { it.asDomain() } + ) + } +} + +private fun UserThreePidEntity.asDomain(): ThreePid { + return when (medium) { + ThirdPartyIdentifier.MEDIUM_EMAIL -> ThreePid.Email(address) + ThirdPartyIdentifier.MEDIUM_MSISDN -> ThreePid.Msisdn(address) + else -> error("Invalid medium type") + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/GetProfileInfoTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/GetProfileInfoTask.kt new file mode 100644 index 0000000000..7889dbf240 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/GetProfileInfoTask.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + * + */ + +package org.matrix.android.sdk.internal.session.profile + +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal abstract class GetProfileInfoTask : Task { + data class Params( + val userId: String + ) +} + +internal class DefaultGetProfileInfoTask @Inject constructor(private val profileAPI: ProfileAPI, + private val eventBus: EventBus) : GetProfileInfoTask() { + + override suspend fun execute(params: Params): JsonDict { + return executeRequest(eventBus) { + apiCall = profileAPI.getProfile(params.userId) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileAPI.kt new file mode 100644 index 0000000000..31e1f09bbd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileAPI.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + * + */ + +package org.matrix.android.sdk.internal.session.profile + +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.PUT +import retrofit2.http.Path + +internal interface ProfileAPI { + + /** + * Get the combined profile information for this user. + * This API may be used to fetch the user's own profile information or other users; either locally or on remote homeservers. + * This API may return keys which are not limited to displayname or avatar_url. + * @param userId the user id to fetch profile info + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "profile/{userId}") + fun getProfile(@Path("userId") userId: String): Call + + /** + * List all 3PIDs linked to the Matrix user account. + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid") + fun getThreePIDs(): Call + + /** + * Change user display name + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "profile/{userId}/displayname") + fun setDisplayName(@Path("userId") userId: String, + @Body body: SetDisplayNameBody): Call + + /** + * Change user avatar url. + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "profile/{userId}/avatar_url") + fun setAvatarUrl(@Path("userId") userId: String, + @Body body: SetAvatarUrlBody): Call + + /** + * Bind a threePid + * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-account-3pid-bind + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "account/3pid/bind") + fun bindThreePid(@Body body: BindThreePidBody): Call + + /** + * Unbind a threePid + * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-account-3pid-unbind + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "account/3pid/unbind") + fun unbindThreePid(@Body body: UnbindThreePidBody): Call +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileModule.kt new file mode 100644 index 0000000000..57a86d03e0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileModule.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + * + */ + +package org.matrix.android.sdk.internal.session.profile + +import dagger.Binds +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.api.session.profile.ProfileService +import org.matrix.android.sdk.internal.session.SessionScope +import retrofit2.Retrofit + +@Module +internal abstract class ProfileModule { + + @Module + companion object { + @Provides + @JvmStatic + @SessionScope + fun providesProfileAPI(retrofit: Retrofit): ProfileAPI { + return retrofit.create(ProfileAPI::class.java) + } + } + + @Binds + abstract fun bindProfileService(service: DefaultProfileService): ProfileService + + @Binds + abstract fun bindGetProfileTask(task: DefaultGetProfileInfoTask): GetProfileInfoTask + + @Binds + abstract fun bindRefreshUserThreePidsTask(task: DefaultRefreshUserThreePidsTask): RefreshUserThreePidsTask + + @Binds + abstract fun bindBindThreePidsTask(task: DefaultBindThreePidsTask): BindThreePidsTask + + @Binds + abstract fun bindUnbindThreePidsTask(task: DefaultUnbindThreePidsTask): UnbindThreePidsTask + + @Binds + abstract fun bindSetDisplayNameTask(task: DefaultSetDisplayNameTask): SetDisplayNameTask + + @Binds + abstract fun bindSetAvatarUrlTask(task: DefaultSetAvatarUrlTask): SetAvatarUrlTask +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/RefreshUserThreePidsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/RefreshUserThreePidsTask.kt new file mode 100644 index 0000000000..dcc0db8ad1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/RefreshUserThreePidsTask.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.profile + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.internal.database.model.UserThreePidEntity +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import javax.inject.Inject + +internal abstract class RefreshUserThreePidsTask : Task + +internal class DefaultRefreshUserThreePidsTask @Inject constructor(private val profileAPI: ProfileAPI, + @SessionDatabase private val monarchy: Monarchy, + private val eventBus: EventBus) : RefreshUserThreePidsTask() { + + override suspend fun execute(params: Unit) { + val accountThreePidsResponse = executeRequest(eventBus) { + apiCall = profileAPI.getThreePIDs() + } + + Timber.d("Get ${accountThreePidsResponse.threePids?.size} threePids") + // Store the list in DB + monarchy.writeAsync { realm -> + realm.where(UserThreePidEntity::class.java).findAll().deleteAllFromRealm() + accountThreePidsResponse.threePids?.forEach { + val entity = UserThreePidEntity( + it.medium?.takeIf { med -> med in ThirdPartyIdentifier.SUPPORTED_MEDIUM } ?: return@forEach, + it.address ?: return@forEach, + it.validatedAt.toLong(), + it.addedAt.toLong()) + realm.insertOrUpdate(entity) + } + } + } +} + +private fun Any?.toLong(): Long { + return when (this) { + null -> 0L + is Long -> this + is Double -> this.toLong() + else -> 0L + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/SetAvatarUrlBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/SetAvatarUrlBody.kt new file mode 100644 index 0000000000..25d995fbdf --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/SetAvatarUrlBody.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.profile + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class SetAvatarUrlBody( + /** + * The new avatar url for this user. + */ + @Json(name = "avatar_url") + val avatarUrl: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/SetAvatarUrlTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/SetAvatarUrlTask.kt new file mode 100644 index 0000000000..1eaedb0220 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/SetAvatarUrlTask.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.profile + +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal abstract class SetAvatarUrlTask : Task { + data class Params( + val userId: String, + val newAvatarUrl: String + ) +} + +internal class DefaultSetAvatarUrlTask @Inject constructor( + private val profileAPI: ProfileAPI, + private val eventBus: EventBus) : SetAvatarUrlTask() { + + override suspend fun execute(params: Params) { + return executeRequest(eventBus) { + val body = SetAvatarUrlBody( + avatarUrl = params.newAvatarUrl + ) + apiCall = profileAPI.setAvatarUrl(params.userId, body) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/SetDisplayNameBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/SetDisplayNameBody.kt new file mode 100644 index 0000000000..306aca6f44 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/SetDisplayNameBody.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.profile + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class SetDisplayNameBody( + /** + * The new display name for this user. + */ + @Json(name = "displayname") + val displayName: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/SetDisplayNameTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/SetDisplayNameTask.kt new file mode 100644 index 0000000000..66406a480c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/SetDisplayNameTask.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.profile + +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal abstract class SetDisplayNameTask : Task { + data class Params( + val userId: String, + val newDisplayName: String + ) +} + +internal class DefaultSetDisplayNameTask @Inject constructor( + private val profileAPI: ProfileAPI, + private val eventBus: EventBus) : SetDisplayNameTask() { + + override suspend fun execute(params: Params) { + return executeRequest(eventBus) { + val body = SetDisplayNameBody( + displayName = params.newDisplayName + ) + apiCall = profileAPI.setDisplayName(params.userId, body) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ThirdPartyIdentifier.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ThirdPartyIdentifier.kt new file mode 100755 index 0000000000..b7c756cbb7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ThirdPartyIdentifier.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.profile + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class ThirdPartyIdentifier( + /** + * Required. The medium of the third party identifier. One of: ["email", "msisdn"] + */ + @Json(name = "medium") + val medium: String? = null, + + /** + * Required. The third party identifier address. + */ + @Json(name = "address") + val address: String? = null, + + /** + * Required. The timestamp in milliseconds when this 3PID has been validated. + * Define as Object because it should be Long and it is a Double. + * So, it might change. + */ + @Json(name = "validated_at") + val validatedAt: Any? = null, + + /** + * Required. The timestamp in milliseconds when this 3PID has been added to the user account. + * Define as Object because it should be Long and it is a Double. + * So, it might change. + */ + @Json(name = "added_at") + val addedAt: Any? = null +) { + companion object { + const val MEDIUM_EMAIL = "email" + const val MEDIUM_MSISDN = "msisdn" + + val SUPPORTED_MEDIUM = listOf(MEDIUM_EMAIL, MEDIUM_MSISDN) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/UnbindThreePidBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/UnbindThreePidBody.kt new file mode 100644 index 0000000000..1a91245894 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/UnbindThreePidBody.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.profile + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class UnbindThreePidBody( + /** + * The identity server to unbind from. If not provided, the homeserver MUST use the id_server the identifier was added through. + * If the homeserver does not know the original id_server, it MUST return a id_server_unbind_result of no-support. + */ + @Json(name = "id_server") + val identityServerUrlWithoutProtocol: String?, + + /** + * Required. The medium of the third party identifier being removed. One of: ["email", "msisdn"] + */ + @Json(name = "medium") + val medium: String, + + /** + * Required. The third party address being removed. + */ + @Json(name = "address") + val address: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/UnbindThreePidResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/UnbindThreePidResponse.kt new file mode 100644 index 0000000000..df31efdb6c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/UnbindThreePidResponse.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.profile + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class UnbindThreePidResponse( + @Json(name = "id_server_unbind_result") + val idServerUnbindResult: String? +) { + fun isSuccess() = idServerUnbindResult == "success" +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/UnbindThreePidsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/UnbindThreePidsTask.kt new file mode 100644 index 0000000000..b08c283765 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/UnbindThreePidsTask.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.profile + +import org.matrix.android.sdk.api.session.identity.IdentityServiceError +import org.matrix.android.sdk.api.session.identity.ThreePid +import org.matrix.android.sdk.api.session.identity.toMedium +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.identity.data.IdentityStore +import org.matrix.android.sdk.internal.session.identity.data.getIdentityServerUrlWithoutProtocol +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal abstract class UnbindThreePidsTask : Task { + data class Params( + val threePid: ThreePid + ) +} + +internal class DefaultUnbindThreePidsTask @Inject constructor(private val profileAPI: ProfileAPI, + private val identityStore: IdentityStore, + private val eventBus: EventBus) : UnbindThreePidsTask() { + override suspend fun execute(params: Params): Boolean { + val identityServerUrlWithoutProtocol = identityStore.getIdentityServerUrlWithoutProtocol() + ?: throw IdentityServiceError.NoIdentityServerConfigured + + return executeRequest(eventBus) { + apiCall = profileAPI.unbindThreePid( + UnbindThreePidBody( + identityServerUrlWithoutProtocol, + params.threePid.toMedium(), + params.threePid.value + )) + }.isSuccess() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddHttpPusherWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddHttpPusherWorker.kt new file mode 100644 index 0000000000..b7f1fb2b93 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddHttpPusherWorker.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.pushers + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.squareup.moshi.JsonClass +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.session.pushers.PusherState +import org.matrix.android.sdk.internal.database.mapper.toEntity +import org.matrix.android.sdk.internal.database.model.PusherEntity +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.util.awaitTransaction +import org.matrix.android.sdk.internal.worker.SessionWorkerParams +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import org.matrix.android.sdk.internal.worker.getSessionComponent +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import javax.inject.Inject + +internal class AddHttpPusherWorker(context: Context, params: WorkerParameters) + : CoroutineWorker(context, params) { + + @JsonClass(generateAdapter = true) + internal data class Params( + override val sessionId: String, + val pusher: JsonPusher, + override val lastFailureMessage: String? = null + ) : SessionWorkerParams + + @Inject lateinit var pushersAPI: PushersAPI + @Inject @SessionDatabase lateinit var monarchy: Monarchy + @Inject lateinit var eventBus: EventBus + + override suspend fun doWork(): Result { + val params = WorkerParamsFactory.fromData(inputData) + ?: return Result.failure() + .also { Timber.e("Unable to parse work parameters") } + + val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() + sessionComponent.inject(this) + + val pusher = params.pusher + + if (pusher.pushKey.isBlank()) { + return Result.failure() + } + return try { + setPusher(pusher) + Result.success() + } catch (exception: Throwable) { + when (exception) { + is Failure.NetworkConnection -> Result.retry() + else -> { + monarchy.awaitTransaction { realm -> + PusherEntity.where(realm, pusher.pushKey).findFirst()?.let { + // update it + it.state = PusherState.FAILED_TO_REGISTER + } + } + Result.failure() + } + } + } + } + + private suspend fun setPusher(pusher: JsonPusher) { + executeRequest(eventBus) { + apiCall = pushersAPI.setPusher(pusher) + } + monarchy.awaitTransaction { realm -> + val echo = PusherEntity.where(realm, pusher.pushKey).findFirst() + if (echo != null) { + // update it + echo.appDisplayName = pusher.appDisplayName + echo.appId = pusher.appId + echo.kind = pusher.kind + echo.lang = pusher.lang + echo.profileTag = pusher.profileTag + echo.data?.format = pusher.data?.format + echo.data?.url = pusher.data?.url + echo.state = PusherState.REGISTERED + } else { + pusher.toEntity().also { + it.state = PusherState.REGISTERED + realm.insertOrUpdate(it) + } + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddPushRuleTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddPushRuleTask.kt new file mode 100644 index 0000000000..1c8f11a12d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddPushRuleTask.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.pushers + +import org.matrix.android.sdk.api.pushrules.RuleKind +import org.matrix.android.sdk.api.pushrules.rest.PushRule +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface AddPushRuleTask : Task { + data class Params( + val kind: RuleKind, + val pushRule: PushRule + ) +} + +internal class DefaultAddPushRuleTask @Inject constructor( + private val pushRulesApi: PushRulesApi, + private val eventBus: EventBus +) : AddPushRuleTask { + + override suspend fun execute(params: AddPushRuleTask.Params) { + return executeRequest(eventBus) { + apiCall = pushRulesApi.addRule(params.kind.value, params.pushRule.ruleId, params.pushRule) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultConditionResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultConditionResolver.kt new file mode 100644 index 0000000000..0d3ad340f5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultConditionResolver.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.pushers + +import org.matrix.android.sdk.api.pushrules.ConditionResolver +import org.matrix.android.sdk.api.pushrules.ContainsDisplayNameCondition +import org.matrix.android.sdk.api.pushrules.EventMatchCondition +import org.matrix.android.sdk.api.pushrules.RoomMemberCountCondition +import org.matrix.android.sdk.api.pushrules.SenderNotificationPermissionCondition +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.room.RoomGetter +import javax.inject.Inject + +internal class DefaultConditionResolver @Inject constructor( + private val roomGetter: RoomGetter, + @UserId private val userId: String +) : ConditionResolver { + + override fun resolveEventMatchCondition(event: Event, + condition: EventMatchCondition): Boolean { + return condition.isSatisfied(event) + } + + override fun resolveRoomMemberCountCondition(event: Event, + condition: RoomMemberCountCondition): Boolean { + return condition.isSatisfied(event, roomGetter) + } + + override fun resolveSenderNotificationPermissionCondition(event: Event, + condition: SenderNotificationPermissionCondition): Boolean { + val roomId = event.roomId ?: return false + val room = roomGetter.getRoom(roomId) ?: return false + + val powerLevelsContent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS) + ?.content + ?.toModel() + ?: PowerLevelsContent() + + return condition.isSatisfied(event, powerLevelsContent) + } + + override fun resolveContainsDisplayNameCondition(event: Event, + condition: ContainsDisplayNameCondition): Boolean { + val roomId = event.roomId ?: return false + val room = roomGetter.getRoom(roomId) ?: return false + val myDisplayName = room.getRoomMember(userId)?.displayName ?: return false + return condition.isSatisfied(event, myDisplayName) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt new file mode 100644 index 0000000000..3ef46785b2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt @@ -0,0 +1,116 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.pushers + +import androidx.lifecycle.LiveData +import androidx.work.BackoffPolicy +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.pushers.Pusher +import org.matrix.android.sdk.api.session.pushers.PushersService +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.database.mapper.asDomain +import org.matrix.android.sdk.internal.database.model.PusherEntity +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.SessionId +import org.matrix.android.sdk.internal.di.WorkManagerProvider +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import java.security.InvalidParameterException +import java.util.UUID +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +internal class DefaultPushersService @Inject constructor( + private val workManagerProvider: WorkManagerProvider, + @SessionDatabase private val monarchy: Monarchy, + @SessionId private val sessionId: String, + private val getPusherTask: GetPushersTask, + private val removePusherTask: RemovePusherTask, + private val taskExecutor: TaskExecutor +) : PushersService { + + override fun refreshPushers() { + getPusherTask + .configureWith() + .executeBy(taskExecutor) + } + + override fun addHttpPusher(pushkey: String, + appId: String, + profileTag: String, + lang: String, + appDisplayName: String, + deviceDisplayName: String, + url: String, + append: Boolean, + withEventIdOnly: Boolean) + : UUID { + // Do some parameter checks. It's ok to throw Exception, to inform developer of the problem + if (pushkey.length > 512) throw InvalidParameterException("pushkey should not exceed 512 chars") + if (appId.length > 64) throw InvalidParameterException("appId should not exceed 64 chars") + if ("/_matrix/push/v1/notify" !in url) throw InvalidParameterException("url should contain '/_matrix/push/v1/notify'") + + val pusher = JsonPusher( + pushKey = pushkey, + kind = "http", + appId = appId, + appDisplayName = appDisplayName, + deviceDisplayName = deviceDisplayName, + profileTag = profileTag, + lang = lang, + data = JsonPusherData(url, EVENT_ID_ONLY.takeIf { withEventIdOnly }), + append = append) + + val params = AddHttpPusherWorker.Params(sessionId, pusher) + + val request = workManagerProvider.matrixOneTimeWorkRequestBuilder() + .setConstraints(WorkManagerProvider.workConstraints) + .setInputData(WorkerParamsFactory.toData(params)) + .setBackoffCriteria(BackoffPolicy.LINEAR, 10_000L, TimeUnit.MILLISECONDS) + .build() + workManagerProvider.workManager.enqueue(request) + return request.id + } + + override fun removeHttpPusher(pushkey: String, appId: String, callback: MatrixCallback): Cancelable { + val params = RemovePusherTask.Params(pushkey, appId) + return removePusherTask + .configureWith(params) { + this.callback = callback + } + // .enableRetry() ?? + .executeBy(taskExecutor) + } + + override fun getPushersLive(): LiveData> { + return monarchy.findAllMappedWithChanges( + { realm -> PusherEntity.where(realm) }, + { it.asDomain() } + ) + } + + override fun getPushers(): List { + return monarchy.fetchAllCopiedSync { PusherEntity.where(it) }.map { it.asDomain() } + } + + companion object { + const val EVENT_ID_ONLY = "event_id_only" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/GetPushRulesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/GetPushRulesTask.kt new file mode 100644 index 0000000000..de96db01dd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/GetPushRulesTask.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.pushers + +import org.matrix.android.sdk.api.pushrules.rest.GetPushRulesResponse +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface GetPushRulesTask : Task { + data class Params(val scope: String) +} + +/** + * We keep this task, but it should not be used anymore, the push rules comes from the sync response + */ +internal class DefaultGetPushRulesTask @Inject constructor( + private val pushRulesApi: PushRulesApi, + private val savePushRulesTask: SavePushRulesTask, + private val eventBus: EventBus +) : GetPushRulesTask { + + override suspend fun execute(params: GetPushRulesTask.Params) { + val response = executeRequest(eventBus) { + apiCall = pushRulesApi.getAllRules() + } + + savePushRulesTask.execute(SavePushRulesTask.Params(response)) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/GetPushersResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/GetPushersResponse.kt new file mode 100644 index 0000000000..a36705cc57 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/GetPushersResponse.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.pushers + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal class GetPushersResponse( + @Json(name = "pushers") + val pushers: List? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/GetPushersTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/GetPushersTask.kt new file mode 100644 index 0000000000..bad507555d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/GetPushersTask.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.pushers + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.pushers.PusherState +import org.matrix.android.sdk.internal.database.mapper.toEntity +import org.matrix.android.sdk.internal.database.model.PusherEntity +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface GetPushersTask : Task + +internal class DefaultGetPushersTask @Inject constructor( + private val pushersAPI: PushersAPI, + @SessionDatabase private val monarchy: Monarchy, + private val eventBus: EventBus +) : GetPushersTask { + + override suspend fun execute(params: Unit) { + val response = executeRequest(eventBus) { + apiCall = pushersAPI.getPushers() + } + monarchy.awaitTransaction { realm -> + // clear existings? + realm.where(PusherEntity::class.java) + .findAll().deleteAllFromRealm() + response.pushers?.forEach { jsonPusher -> + jsonPusher.toEntity().also { + it.state = PusherState.REGISTERED + realm.insertOrUpdate(it) + } + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/JsonPusher.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/JsonPusher.kt new file mode 100644 index 0000000000..89dae0c7e9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/JsonPusher.kt @@ -0,0 +1,116 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.pushers + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.di.SerializeNulls + +/** + * Example: + * + * + * { + * "pushers": [ + * { + * "pushkey": "Xp/MzCt8/9DcSNE9cuiaoT5Ac55job3TdLSSmtmYl4A=", + * "kind": "http", + * "app_id": "face.mcapp.appy.prod", + * "app_display_name": "Appy McAppface", + * "device_display_name": "Alice's Phone", + * "profile_tag": "xyz", + * "lang": "en-US", + * "data": { + * "url": "https://example.com/_matrix/push/v1/notify" + * } + * }] + * } + * + */ +@JsonClass(generateAdapter = true) +internal data class JsonPusher( + /** + * Required. This is a unique identifier for this pusher. The value you should use for this is the routing or + * destination address information for the notification, for example, the APNS token for APNS or the + * Registration ID for GCM. If your notification client has no such concept, use any unique identifier. + * Max length, 512 bytes. + * + * If the kind is "email", this is the email address to send notifications to. + */ + @Json(name = "pushkey") + val pushKey: String, + + /** + * Required. The kind of pusher to configure. + * "http" makes a pusher that sends HTTP pokes. + * "email" makes a pusher that emails the user with unread notifications. + * null deletes the pusher. + */ + @SerializeNulls + @Json(name = "kind") + val kind: String?, + + /** + * Required. This is a reverse-DNS style identifier for the application. It is recommended that this end + * with the platform, such that different platform versions get different app identifiers. + * Max length, 64 chars. + * + * If the kind is "email", this is "m.email". + */ + @Json(name = "app_id") + val appId: String, + + /** + * Required. A string that will allow the user to identify what application owns this pusher. + */ + @Json(name = "app_display_name") + val appDisplayName: String? = null, + + /** + * Required. A string that will allow the user to identify what device owns this pusher. + */ + @Json(name = "device_display_name") + val deviceDisplayName: String? = null, + + /** + * This string determines which set of device specific rules this pusher executes. + */ + @Json(name = "profile_tag") + val profileTag: String? = null, + + /** + * Required. The preferred language for receiving notifications (e.g. 'en' or 'en-US') + */ + @Json(name = "lang") + val lang: String? = null, + + /** + * Required. A dictionary of information for the pusher implementation itself. + * If kind is http, this should contain url which is the URL to use to send notifications to. + */ + @Json(name = "data") + val data: JsonPusherData? = null, + + /** + * If true, the homeserver should add another pusher with the given pushkey and App ID in addition to any others + * with different user IDs. Otherwise, the homeserver must remove any other pushers with the same App ID and pushkey + * for different users. + * The default is false. + */ + @Json(name = "append") + val append: Boolean? = false +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/JsonPusherData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/JsonPusherData.kt new file mode 100644 index 0000000000..d2520a915b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/JsonPusherData.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.pushers + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class JsonPusherData( + /** + * Required if kind is http. The URL to use to send notifications to. + * MUST be an HTTPS URL with a path of /_matrix/push/v1/notify. + */ + @Json(name = "url") + val url: String? = null, + + /** + * The format to send notifications in to Push Gateways if the kind is http. + * Currently the only format available is 'event_id_only'. + */ + @Json(name = "format") + val format: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushRulesApi.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushRulesApi.kt new file mode 100644 index 0000000000..166e8ac3be --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushRulesApi.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.pushers + +import org.matrix.android.sdk.api.pushrules.rest.GetPushRulesResponse +import org.matrix.android.sdk.api.pushrules.rest.PushRule +import org.matrix.android.sdk.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.DELETE +import retrofit2.http.GET +import retrofit2.http.PUT +import retrofit2.http.Path + +internal interface PushRulesApi { + /** + * Get all push rules + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushrules/") + fun getAllRules(): Call + + /** + * Update the ruleID enable status + * + * @param kind the notification kind (sender, room...) + * @param ruleId the ruleId + * @param enable the new enable status + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushrules/global/{kind}/{ruleId}/enabled") + fun updateEnableRuleStatus(@Path("kind") kind: String, + @Path("ruleId") ruleId: String, + @Body enable: Boolean?) + : Call + + /** + * Update the ruleID action + * Ref: https://matrix.org/docs/spec/client_server/latest#put-matrix-client-r0-pushrules-scope-kind-ruleid-actions + * + * @param kind the notification kind (sender, room...) + * @param ruleId the ruleId + * @param actions the actions + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushrules/global/{kind}/{ruleId}/actions") + fun updateRuleActions(@Path("kind") kind: String, + @Path("ruleId") ruleId: String, + @Body actions: Any) + : Call + + /** + * Delete a rule + * + * @param kind the notification kind (sender, room...) + * @param ruleId the ruleId + */ + @DELETE(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushrules/global/{kind}/{ruleId}") + fun deleteRule(@Path("kind") kind: String, + @Path("ruleId") ruleId: String) + : Call + + /** + * Add the ruleID enable status + * + * @param kind the notification kind (sender, room...) + * @param ruleId the ruleId. + * @param rule the rule to add. + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushrules/global/{kind}/{ruleId}") + fun addRule(@Path("kind") kind: String, + @Path("ruleId") ruleId: String, + @Body rule: PushRule) + : Call +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushersAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushersAPI.kt new file mode 100644 index 0000000000..6ad70db5e4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushersAPI.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.pushers + +import org.matrix.android.sdk.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST + +internal interface PushersAPI { + + /** + * Get the pushers for this user. + * + * Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-pushers + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushers") + fun getPushers(): Call + + /** + * This endpoint allows the creation, modification and deletion of pushers for this user ID. + * The behaviour of this endpoint varies depending on the values in the JSON body. + * + * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-pushers-set + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushers/set") + fun setPusher(@Body jsonPusher: JsonPusher): Call +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushersModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushersModule.kt new file mode 100644 index 0000000000..9569574fce --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushersModule.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.pushers + +import dagger.Binds +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.api.pushrules.ConditionResolver +import org.matrix.android.sdk.api.pushrules.PushRuleService +import org.matrix.android.sdk.api.session.pushers.PushersService +import org.matrix.android.sdk.internal.session.notification.DefaultProcessEventForPushTask +import org.matrix.android.sdk.internal.session.notification.DefaultPushRuleService +import org.matrix.android.sdk.internal.session.notification.ProcessEventForPushTask +import org.matrix.android.sdk.internal.session.room.notification.DefaultSetRoomNotificationStateTask +import org.matrix.android.sdk.internal.session.room.notification.SetRoomNotificationStateTask +import retrofit2.Retrofit + +@Module +internal abstract class PushersModule { + + @Module + companion object { + + @JvmStatic + @Provides + fun providesPushersAPI(retrofit: Retrofit): PushersAPI { + return retrofit.create(PushersAPI::class.java) + } + + @JvmStatic + @Provides + fun providesPushRulesApi(retrofit: Retrofit): PushRulesApi { + return retrofit.create(PushRulesApi::class.java) + } + } + + @Binds + abstract fun bindPusherService(service: DefaultPushersService): PushersService + + @Binds + abstract fun bindConditionResolver(resolver: DefaultConditionResolver): ConditionResolver + + @Binds + abstract fun bindGetPushersTask(task: DefaultGetPushersTask): GetPushersTask + + @Binds + abstract fun bindGetPushRulesTask(task: DefaultGetPushRulesTask): GetPushRulesTask + + @Binds + abstract fun bindSavePushRulesTask(task: DefaultSavePushRulesTask): SavePushRulesTask + + @Binds + abstract fun bindRemovePusherTask(task: DefaultRemovePusherTask): RemovePusherTask + + @Binds + abstract fun bindUpdatePushRuleEnableStatusTask(task: DefaultUpdatePushRuleEnableStatusTask): UpdatePushRuleEnableStatusTask + + @Binds + abstract fun bindAddPushRuleTask(task: DefaultAddPushRuleTask): AddPushRuleTask + + @Binds + abstract fun bindUpdatePushRuleActionTask(task: DefaultUpdatePushRuleActionsTask): UpdatePushRuleActionsTask + + @Binds + abstract fun bindRemovePushRuleTask(task: DefaultRemovePushRuleTask): RemovePushRuleTask + + @Binds + abstract fun bindSetRoomNotificationStateTask(task: DefaultSetRoomNotificationStateTask): SetRoomNotificationStateTask + + @Binds + abstract fun bindPushRuleService(service: DefaultPushRuleService): PushRuleService + + @Binds + abstract fun bindProcessEventForPushTask(task: DefaultProcessEventForPushTask): ProcessEventForPushTask +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/RemovePushRuleTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/RemovePushRuleTask.kt new file mode 100644 index 0000000000..cb46c1342d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/RemovePushRuleTask.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.pushers + +import org.matrix.android.sdk.api.pushrules.RuleKind +import org.matrix.android.sdk.api.pushrules.rest.PushRule +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface RemovePushRuleTask : Task { + data class Params( + val kind: RuleKind, + val pushRule: PushRule + ) +} + +internal class DefaultRemovePushRuleTask @Inject constructor( + private val pushRulesApi: PushRulesApi, + private val eventBus: EventBus +) : RemovePushRuleTask { + + override suspend fun execute(params: RemovePushRuleTask.Params) { + return executeRequest(eventBus) { + apiCall = pushRulesApi.deleteRule(params.kind.value, params.pushRule.ruleId) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/RemovePusherTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/RemovePusherTask.kt new file mode 100644 index 0000000000..cf8cd1e10b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/RemovePusherTask.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.pushers + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.pushers.PusherState +import org.matrix.android.sdk.internal.database.mapper.asDomain +import org.matrix.android.sdk.internal.database.model.PusherEntity +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import io.realm.Realm +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface RemovePusherTask : Task { + data class Params(val pushKey: String, + val pushAppId: String) +} + +internal class DefaultRemovePusherTask @Inject constructor( + private val pushersAPI: PushersAPI, + @SessionDatabase private val monarchy: Monarchy, + private val eventBus: EventBus +) : RemovePusherTask { + + override suspend fun execute(params: RemovePusherTask.Params) { + monarchy.awaitTransaction { realm -> + val existingEntity = PusherEntity.where(realm, params.pushKey).findFirst() + existingEntity?.state = PusherState.UNREGISTERING + } + + val existing = Realm.getInstance(monarchy.realmConfiguration).use { realm -> + PusherEntity.where(realm, params.pushKey).findFirst()?.asDomain() + } ?: throw Exception("No existing pusher") + + val deleteBody = JsonPusher( + pushKey = params.pushKey, + appId = params.pushAppId, + // kind null deletes the pusher + kind = null, + appDisplayName = existing.appDisplayName ?: "", + deviceDisplayName = existing.deviceDisplayName ?: "", + profileTag = existing.profileTag ?: "", + lang = existing.lang, + data = JsonPusherData(existing.data.url, existing.data.format), + append = false + ) + executeRequest(eventBus) { + apiCall = pushersAPI.setPusher(deleteBody) + } + monarchy.awaitTransaction { + PusherEntity.where(it, params.pushKey).findFirst()?.deleteFromRealm() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/SavePushRulesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/SavePushRulesTask.kt new file mode 100644 index 0000000000..7761544d48 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/SavePushRulesTask.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.pushers + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.pushrules.RuleScope +import org.matrix.android.sdk.api.pushrules.RuleSetKey +import org.matrix.android.sdk.api.pushrules.rest.GetPushRulesResponse +import org.matrix.android.sdk.internal.database.mapper.PushRulesMapper +import org.matrix.android.sdk.internal.database.model.PushRulesEntity +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import javax.inject.Inject + +/** + * Save the push rules in DB + */ +internal interface SavePushRulesTask : Task { + data class Params(val pushRules: GetPushRulesResponse) +} + +internal class DefaultSavePushRulesTask @Inject constructor(@SessionDatabase private val monarchy: Monarchy) : SavePushRulesTask { + + override suspend fun execute(params: SavePushRulesTask.Params) { + monarchy.awaitTransaction { realm -> + // clear current push rules + realm.where(PushRulesEntity::class.java) + .findAll() + .deleteAllFromRealm() + + // Save only global rules for the moment + val globalRules = params.pushRules.global + + val content = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.CONTENT } + globalRules.content?.forEach { rule -> + content.pushRules.add(PushRulesMapper.map(rule)) + } + realm.insertOrUpdate(content) + + val override = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.OVERRIDE } + globalRules.override?.forEach { rule -> + PushRulesMapper.map(rule).also { + override.pushRules.add(it) + } + } + realm.insertOrUpdate(override) + + val rooms = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.ROOM } + globalRules.room?.forEach { rule -> + rooms.pushRules.add(PushRulesMapper.map(rule)) + } + realm.insertOrUpdate(rooms) + + val senders = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.SENDER } + globalRules.sender?.forEach { rule -> + senders.pushRules.add(PushRulesMapper.map(rule)) + } + realm.insertOrUpdate(senders) + + val underrides = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.UNDERRIDE } + globalRules.underride?.forEach { rule -> + underrides.pushRules.add(PushRulesMapper.map(rule)) + } + realm.insertOrUpdate(underrides) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/UpdatePushRuleActionsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/UpdatePushRuleActionsTask.kt new file mode 100644 index 0000000000..d68888a3f5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/UpdatePushRuleActionsTask.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.pushers + +import org.matrix.android.sdk.api.pushrules.RuleKind +import org.matrix.android.sdk.api.pushrules.rest.PushRule +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface UpdatePushRuleActionsTask : Task { + data class Params( + val kind: RuleKind, + val oldPushRule: PushRule, + val newPushRule: PushRule + ) +} + +internal class DefaultUpdatePushRuleActionsTask @Inject constructor( + private val pushRulesApi: PushRulesApi, + private val eventBus: EventBus +) : UpdatePushRuleActionsTask { + + override suspend fun execute(params: UpdatePushRuleActionsTask.Params) { + if (params.oldPushRule.enabled != params.newPushRule.enabled) { + // First change enabled state + executeRequest(eventBus) { + apiCall = pushRulesApi.updateEnableRuleStatus(params.kind.value, params.newPushRule.ruleId, params.newPushRule.enabled) + } + } + + if (params.newPushRule.enabled) { + // Also ensure the actions are up to date + val body = mapOf("actions" to params.newPushRule.actions) + + executeRequest(eventBus) { + apiCall = pushRulesApi.updateRuleActions(params.kind.value, params.newPushRule.ruleId, body) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/UpdatePushRuleEnableStatusTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/UpdatePushRuleEnableStatusTask.kt new file mode 100644 index 0000000000..2f9ac3edc0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/UpdatePushRuleEnableStatusTask.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.pushers + +import org.matrix.android.sdk.api.pushrules.RuleKind +import org.matrix.android.sdk.api.pushrules.rest.PushRule +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface UpdatePushRuleEnableStatusTask : Task { + data class Params(val kind: RuleKind, + val pushRule: PushRule, + val enabled: Boolean) +} + +internal class DefaultUpdatePushRuleEnableStatusTask @Inject constructor( + private val pushRulesApi: PushRulesApi, + private val eventBus: EventBus +) : UpdatePushRuleEnableStatusTask { + + override suspend fun execute(params: UpdatePushRuleEnableStatusTask.Params) { + return executeRequest(eventBus) { + apiCall = pushRulesApi.updateEnableRuleStatus(params.kind.value, params.pushRule.ruleId, params.enabled) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt new file mode 100644 index 0000000000..27a51594c3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt @@ -0,0 +1,126 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.room.Room +import org.matrix.android.sdk.api.session.room.call.RoomCallService +import org.matrix.android.sdk.api.session.room.members.MembershipService +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.relation.RelationService +import org.matrix.android.sdk.api.session.room.notification.RoomPushRuleService +import org.matrix.android.sdk.api.session.room.read.ReadService +import org.matrix.android.sdk.api.session.room.reporting.ReportingService +import org.matrix.android.sdk.api.session.room.send.DraftService +import org.matrix.android.sdk.api.session.room.send.SendService +import org.matrix.android.sdk.api.session.room.state.StateService +import org.matrix.android.sdk.api.session.room.tags.TagsService +import org.matrix.android.sdk.api.session.room.timeline.TimelineService +import org.matrix.android.sdk.api.session.room.typing.TypingService +import org.matrix.android.sdk.api.session.room.uploads.UploadsService +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.internal.session.room.state.SendStateTask +import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import java.security.InvalidParameterException +import javax.inject.Inject + +internal class DefaultRoom @Inject constructor(override val roomId: String, + private val roomSummaryDataSource: RoomSummaryDataSource, + private val timelineService: TimelineService, + private val sendService: SendService, + private val draftService: DraftService, + private val stateService: StateService, + private val uploadsService: UploadsService, + private val reportingService: ReportingService, + private val roomCallService: RoomCallService, + private val readService: ReadService, + private val typingService: TypingService, + private val tagsService: TagsService, + private val cryptoService: CryptoService, + private val relationService: RelationService, + private val roomMembersService: MembershipService, + private val roomPushRuleService: RoomPushRuleService, + private val taskExecutor: TaskExecutor, + private val sendStateTask: SendStateTask) : + Room, + TimelineService by timelineService, + SendService by sendService, + DraftService by draftService, + StateService by stateService, + UploadsService by uploadsService, + ReportingService by reportingService, + RoomCallService by roomCallService, + ReadService by readService, + TypingService by typingService, + TagsService by tagsService, + RelationService by relationService, + MembershipService by roomMembersService, + RoomPushRuleService by roomPushRuleService { + + override fun getRoomSummaryLive(): LiveData> { + return roomSummaryDataSource.getRoomSummaryLive(roomId) + } + + override fun roomSummary(): RoomSummary? { + return roomSummaryDataSource.getRoomSummary(roomId) + } + + override fun isEncrypted(): Boolean { + return cryptoService.isRoomEncrypted(roomId) + } + + override fun encryptionAlgorithm(): String? { + return cryptoService.getEncryptionAlgorithm(roomId) + } + + override fun shouldEncryptForInvitedMembers(): Boolean { + return cryptoService.shouldEncryptForInvitedMembers(roomId) + } + + override fun enableEncryption(algorithm: String, callback: MatrixCallback) { + when { + isEncrypted() -> { + callback.onFailure(IllegalStateException("Encryption is already enabled for this room")) + } + algorithm != MXCRYPTO_ALGORITHM_MEGOLM -> { + callback.onFailure(InvalidParameterException("Only MXCRYPTO_ALGORITHM_MEGOLM algorithm is supported")) + } + else -> { + val params = SendStateTask.Params( + roomId = roomId, + stateKey = null, + eventType = EventType.STATE_ROOM_ENCRYPTION, + body = mapOf( + "algorithm" to algorithm + )) + + sendStateTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomDirectoryService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomDirectoryService.kt new file mode 100644 index 0000000000..12d9d5bcdc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomDirectoryService.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.room.RoomDirectoryService +import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams +import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsResponse +import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.session.room.directory.GetPublicRoomTask +import org.matrix.android.sdk.internal.session.room.directory.GetThirdPartyProtocolsTask +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import javax.inject.Inject + +internal class DefaultRoomDirectoryService @Inject constructor(private val getPublicRoomTask: GetPublicRoomTask, + private val getThirdPartyProtocolsTask: GetThirdPartyProtocolsTask, + private val taskExecutor: TaskExecutor) : RoomDirectoryService { + + override fun getPublicRooms(server: String?, + publicRoomsParams: PublicRoomsParams, + callback: MatrixCallback): Cancelable { + return getPublicRoomTask + .configureWith(GetPublicRoomTask.Params(server, publicRoomsParams)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun getThirdPartyProtocol(callback: MatrixCallback>): Cancelable { + return getThirdPartyProtocolsTask + .configureWith { + this.callback = callback + } + .executeBy(taskExecutor) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt new file mode 100644 index 0000000000..17c724368d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt @@ -0,0 +1,122 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.room.Room +import org.matrix.android.sdk.api.session.room.RoomService +import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams +import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.internal.session.room.alias.GetRoomIdByAliasTask +import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask +import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource +import org.matrix.android.sdk.internal.session.room.membership.joining.JoinRoomTask +import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask +import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource +import org.matrix.android.sdk.internal.session.user.accountdata.UpdateBreadcrumbsTask +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import javax.inject.Inject + +internal class DefaultRoomService @Inject constructor( + private val createRoomTask: CreateRoomTask, + private val joinRoomTask: JoinRoomTask, + private val markAllRoomsReadTask: MarkAllRoomsReadTask, + private val updateBreadcrumbsTask: UpdateBreadcrumbsTask, + private val roomIdByAliasTask: GetRoomIdByAliasTask, + private val roomGetter: RoomGetter, + private val roomSummaryDataSource: RoomSummaryDataSource, + private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource, + private val taskExecutor: TaskExecutor +) : RoomService { + + override fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback): Cancelable { + return createRoomTask + .configureWith(createRoomParams) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun getRoom(roomId: String): Room? { + return roomGetter.getRoom(roomId) + } + + override fun getExistingDirectRoomWithUser(otherUserId: String): Room? { + return roomGetter.getDirectRoomWith(otherUserId) + } + + override fun getRoomSummary(roomIdOrAlias: String): RoomSummary? { + return roomSummaryDataSource.getRoomSummary(roomIdOrAlias) + } + + override fun getRoomSummaries(queryParams: RoomSummaryQueryParams): List { + return roomSummaryDataSource.getRoomSummaries(queryParams) + } + + override fun getRoomSummariesLive(queryParams: RoomSummaryQueryParams): LiveData> { + return roomSummaryDataSource.getRoomSummariesLive(queryParams) + } + + override fun getBreadcrumbs(queryParams: RoomSummaryQueryParams): List { + return roomSummaryDataSource.getBreadcrumbs(queryParams) + } + + override fun getBreadcrumbsLive(queryParams: RoomSummaryQueryParams): LiveData> { + return roomSummaryDataSource.getBreadcrumbsLive(queryParams) + } + + override fun onRoomDisplayed(roomId: String): Cancelable { + return updateBreadcrumbsTask + .configureWith(UpdateBreadcrumbsTask.Params(roomId)) + .executeBy(taskExecutor) + } + + override fun joinRoom(roomIdOrAlias: String, reason: String?, viaServers: List, callback: MatrixCallback): Cancelable { + return joinRoomTask + .configureWith(JoinRoomTask.Params(roomIdOrAlias, reason, viaServers)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun markAllAsRead(roomIds: List, callback: MatrixCallback): Cancelable { + return markAllRoomsReadTask + .configureWith(MarkAllRoomsReadTask.Params(roomIds)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun getRoomIdByAlias(roomAlias: String, searchOnServer: Boolean, callback: MatrixCallback>): Cancelable { + return roomIdByAliasTask + .configureWith(GetRoomIdByAliasTask.Params(roomAlias, searchOnServer)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun getChangeMembershipsLive(): LiveData> { + return roomChangeMembershipStateDataSource.getLiveStates() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt new file mode 100644 index 0000000000..4893947fc3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt @@ -0,0 +1,573 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.room + +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.events.model.AggregatedAnnotation +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.LocalEcho +import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.PollSummaryContent +import org.matrix.android.sdk.api.session.room.model.ReferencesAggregatedContent +import org.matrix.android.sdk.api.session.room.model.VoteInfo +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent +import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent +import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent +import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent +import org.matrix.android.sdk.internal.database.mapper.ContentMapper +import org.matrix.android.sdk.internal.database.mapper.EventMapper +import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.EventInsertType +import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.model.ReactionAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.model.ReactionAggregatedSummaryEntityFields +import org.matrix.android.sdk.internal.database.model.ReferencesAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.query.create +import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor +import io.realm.Realm +import timber.log.Timber +import javax.inject.Inject + +enum class VerificationState { + REQUEST, + WAITING, + CANCELED_BY_ME, + CANCELED_BY_OTHER, + DONE +} + +fun VerificationState.isCanceled(): Boolean { + return this == VerificationState.CANCELED_BY_ME || this == VerificationState.CANCELED_BY_OTHER +} + +// State transition with control +private fun VerificationState?.toState(newState: VerificationState): VerificationState { + // Cancel is always prioritary ? + // Eg id i found that mac or keys mismatch and send a cancel and the other send a done, i have to + // consider as canceled + if (newState.isCanceled()) { + return newState + } + // never move out of cancel + if (this?.isCanceled() == true) { + return this + } + return newState +} + +internal class EventRelationsAggregationProcessor @Inject constructor(@UserId private val userId: String, + private val cryptoService: CryptoService +) : EventInsertLiveProcessor { + + private val allowedTypes = listOf( + EventType.MESSAGE, + EventType.REDACTION, + EventType.REACTION, + EventType.KEY_VERIFICATION_DONE, + EventType.KEY_VERIFICATION_CANCEL, + EventType.KEY_VERIFICATION_ACCEPT, + EventType.KEY_VERIFICATION_START, + EventType.KEY_VERIFICATION_MAC, + // TODO Add ? + // EventType.KEY_VERIFICATION_READY, + EventType.KEY_VERIFICATION_KEY, + EventType.ENCRYPTED + ) + + override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean { + return allowedTypes.contains(eventType) + } + + override suspend fun process(realm: Realm, event: Event) { + try { // Temporary catch, should be removed + val roomId = event.roomId + if (roomId == null) { + Timber.w("Event has no room id ${event.eventId}") + return + } + val isLocalEcho = LocalEcho.isLocalEchoId(event.eventId ?: "") + when (event.type) { + EventType.REACTION -> { + // we got a reaction!! + Timber.v("###REACTION in room $roomId , reaction eventID ${event.eventId}") + handleReaction(event, roomId, realm, userId, isLocalEcho) + } + EventType.MESSAGE -> { + if (event.unsignedData?.relations?.annotations != null) { + Timber.v("###REACTION Agreggation in room $roomId for event ${event.eventId}") + handleInitialAggregatedRelations(event, roomId, event.unsignedData.relations.annotations, realm) + + EventAnnotationsSummaryEntity.where(realm, event.eventId + ?: "").findFirst()?.let { + TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId + ?: "").findFirst()?.let { tet -> + tet.annotations = it + } + } + } + + val content: MessageContent? = event.content.toModel() + if (content?.relatesTo?.type == RelationType.REPLACE) { + Timber.v("###REPLACE in room $roomId for event ${event.eventId}") + // A replace! + handleReplace(realm, event, content, roomId, isLocalEcho) + } else if (content?.relatesTo?.type == RelationType.RESPONSE) { + Timber.v("###RESPONSE in room $roomId for event ${event.eventId}") + handleResponse(realm, userId, event, content, roomId, isLocalEcho) + } + } + + EventType.KEY_VERIFICATION_DONE, + EventType.KEY_VERIFICATION_CANCEL, + EventType.KEY_VERIFICATION_ACCEPT, + EventType.KEY_VERIFICATION_START, + EventType.KEY_VERIFICATION_MAC, + EventType.KEY_VERIFICATION_READY, + EventType.KEY_VERIFICATION_KEY -> { + Timber.v("## SAS REF in room $roomId for event ${event.eventId}") + event.content.toModel()?.relatesTo?.let { + if (it.type == RelationType.REFERENCE && it.eventId != null) { + handleVerification(realm, event, roomId, isLocalEcho, it.eventId, userId) + } + } + } + + EventType.ENCRYPTED -> { + // Relation type is in clear + val encryptedEventContent = event.content.toModel() + if (encryptedEventContent?.relatesTo?.type == RelationType.REPLACE + || encryptedEventContent?.relatesTo?.type == RelationType.RESPONSE + ) { + event.getClearContent().toModel()?.let { + if (encryptedEventContent.relatesTo.type == RelationType.REPLACE) { + Timber.v("###REPLACE in room $roomId for event ${event.eventId}") + // A replace! + handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) + } else if (encryptedEventContent.relatesTo.type == RelationType.RESPONSE) { + Timber.v("###RESPONSE in room $roomId for event ${event.eventId}") + handleResponse(realm, userId, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) + } + } + } else if (encryptedEventContent?.relatesTo?.type == RelationType.REFERENCE) { + when (event.getClearType()) { + EventType.KEY_VERIFICATION_DONE, + EventType.KEY_VERIFICATION_CANCEL, + EventType.KEY_VERIFICATION_ACCEPT, + EventType.KEY_VERIFICATION_START, + EventType.KEY_VERIFICATION_MAC, + EventType.KEY_VERIFICATION_READY, + EventType.KEY_VERIFICATION_KEY -> { + Timber.v("## SAS REF in room $roomId for event ${event.eventId}") + encryptedEventContent.relatesTo.eventId?.let { + handleVerification(realm, event, roomId, isLocalEcho, it, userId) + } + } + } + } + } + EventType.REDACTION -> { + val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() } + ?: return + when (eventToPrune.type) { + EventType.MESSAGE -> { + Timber.d("REDACTION for message ${eventToPrune.eventId}") +// val unsignedData = EventMapper.map(eventToPrune).unsignedData +// ?: UnsignedData(null, null) + + // was this event a m.replace + val contentModel = ContentMapper.map(eventToPrune.content)?.toModel() + if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) { + handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm) + } + } + EventType.REACTION -> { + handleReactionRedact(eventToPrune, realm, userId) + } + } + } + else -> Timber.v("UnHandled event ${event.eventId}") + } + } catch (t: Throwable) { + Timber.e(t, "## Should not happen ") + } + } + + // OPT OUT serer aggregation until API mature enough + private val SHOULD_HANDLE_SERVER_AGREGGATION = false + + private fun handleReplace(realm: Realm, event: Event, content: MessageContent, roomId: String, isLocalEcho: Boolean, relatedEventId: String? = null) { + val eventId = event.eventId ?: return + val targetEventId = relatedEventId ?: content.relatesTo?.eventId ?: return + val newContent = content.newContent ?: return + // ok, this is a replace + val existing = EventAnnotationsSummaryEntity.getOrCreate(realm, roomId, targetEventId) + + // we have it + val existingSummary = existing.editSummary + if (existingSummary == null) { + Timber.v("###REPLACE new edit summary for $targetEventId, creating one (localEcho:$isLocalEcho)") + // create the edit summary + val editSummary = realm.createObject(EditAggregatedSummaryEntity::class.java) + editSummary.aggregatedContent = ContentMapper.map(newContent) + if (isLocalEcho) { + editSummary.lastEditTs = 0 + editSummary.sourceLocalEchoEvents.add(eventId) + } else { + editSummary.lastEditTs = event.originServerTs ?: 0 + editSummary.sourceEvents.add(eventId) + } + + existing.editSummary = editSummary + } else { + if (existingSummary.sourceEvents.contains(eventId)) { + // ignore this event, we already know it (??) + Timber.v("###REPLACE ignoring event for summary, it's known $eventId") + return + } + val txId = event.unsignedData?.transactionId + // is it a remote echo? + if (!isLocalEcho && existingSummary.sourceLocalEchoEvents.contains(txId)) { + // ok it has already been managed + Timber.v("###REPLACE Receiving remote echo of edit (edit already done)") + existingSummary.sourceLocalEchoEvents.remove(txId) + existingSummary.sourceEvents.add(event.eventId) + } else if ( + isLocalEcho // do not rely on ts for local echo, take it + || event.originServerTs ?: 0 >= existingSummary.lastEditTs + ) { + Timber.v("###REPLACE Computing aggregated edit summary (isLocalEcho:$isLocalEcho)") + if (!isLocalEcho) { + // Do not take local echo originServerTs here, could mess up ordering (keep old ts) + existingSummary.lastEditTs = event.originServerTs ?: System.currentTimeMillis() + } + existingSummary.aggregatedContent = ContentMapper.map(newContent) + if (isLocalEcho) { + existingSummary.sourceLocalEchoEvents.add(eventId) + } else { + existingSummary.sourceEvents.add(eventId) + } + } else { + // ignore this event for the summary (back paginate) + if (!isLocalEcho) { + existingSummary.sourceEvents.add(eventId) + } + Timber.v("###REPLACE ignoring event for summary, it's to old $eventId") + } + } + } + + private fun handleResponse(realm: Realm, + userId: String, + event: Event, + content: MessageContent, + roomId: String, + isLocalEcho: Boolean, + relatedEventId: String? = null) { + val eventId = event.eventId ?: return + val senderId = event.senderId ?: return + val targetEventId = relatedEventId ?: content.relatesTo?.eventId ?: return + val eventTimestamp = event.originServerTs ?: return + + // ok, this is a poll response + var existing = EventAnnotationsSummaryEntity.where(realm, targetEventId).findFirst() + if (existing == null) { + Timber.v("## POLL creating new relation summary for $targetEventId") + existing = EventAnnotationsSummaryEntity.create(realm, roomId, targetEventId) + } + + // we have it + val existingPollSummary = existing.pollResponseSummary + ?: realm.createObject(PollResponseAggregatedSummaryEntity::class.java).also { + existing.pollResponseSummary = it + } + + val closedTime = existingPollSummary?.closedTime + if (closedTime != null && eventTimestamp > closedTime) { + Timber.v("## POLL is closed ignore event poll:$targetEventId, event :${event.eventId}") + return + } + + val sumModel = ContentMapper.map(existingPollSummary?.aggregatedContent).toModel() ?: PollSummaryContent() + + if (existingPollSummary!!.sourceEvents.contains(eventId)) { + // ignore this event, we already know it (??) + Timber.v("## POLL ignoring event for summary, it's known eventId:$eventId") + return + } + val txId = event.unsignedData?.transactionId + // is it a remote echo? + if (!isLocalEcho && existingPollSummary.sourceLocalEchoEvents.contains(txId)) { + // ok it has already been managed + Timber.v("## POLL Receiving remote echo of response eventId:$eventId") + existingPollSummary.sourceLocalEchoEvents.remove(txId) + existingPollSummary.sourceEvents.add(event.eventId) + return + } + + val responseContent = event.content.toModel() ?: return Unit.also { + Timber.d("## POLL Receiving malformed response eventId:$eventId content: ${event.content}") + } + + val optionIndex = responseContent.relatesTo?.option ?: return Unit.also { + Timber.d("## POLL Ignoring malformed response no option eventId:$eventId content: ${event.content}") + } + + val votes = sumModel.votes?.toMutableList() ?: ArrayList() + val existingVoteIndex = votes.indexOfFirst { it.userId == senderId } + if (existingVoteIndex != -1) { + // Is the vote newer? + val existingVote = votes[existingVoteIndex] + if (existingVote.voteTimestamp < eventTimestamp) { + // Take the new one + votes[existingVoteIndex] = VoteInfo(senderId, optionIndex, eventTimestamp) + if (userId == senderId) { + sumModel.myVote = optionIndex + } + Timber.v("## POLL adding vote $optionIndex for user $senderId in poll :$relatedEventId ") + } else { + Timber.v("## POLL Ignoring vote (older than known one) eventId:$eventId ") + } + } else { + votes.add(VoteInfo(senderId, optionIndex, eventTimestamp)) + if (userId == senderId) { + sumModel.myVote = optionIndex + } + Timber.v("## POLL adding vote $optionIndex for user $senderId in poll :$relatedEventId ") + } + sumModel.votes = votes + if (isLocalEcho) { + existingPollSummary.sourceLocalEchoEvents.add(eventId) + } else { + existingPollSummary.sourceEvents.add(eventId) + } + + existingPollSummary.aggregatedContent = ContentMapper.map(sumModel.toContent()) + } + + private fun handleInitialAggregatedRelations(event: Event, roomId: String, aggregation: AggregatedAnnotation, realm: Realm) { + if (SHOULD_HANDLE_SERVER_AGREGGATION) { + aggregation.chunk?.forEach { + if (it.type == EventType.REACTION) { + val eventId = event.eventId ?: "" + val existing = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() + if (existing == null) { + val eventSummary = EventAnnotationsSummaryEntity.create(realm, roomId, eventId) + val sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java) + sum.key = it.key + sum.firstTimestamp = event.originServerTs + ?: 0 // TODO how to maintain order? + sum.count = it.count + eventSummary.reactionsSummary.add(sum) + } else { + // TODO how to handle that + } + } + } + } + } + + private fun handleReaction(event: Event, roomId: String, realm: Realm, userId: String, isLocalEcho: Boolean) { + val content = event.content.toModel() + if (content == null) { + Timber.e("Malformed reaction content ${event.content}") + return + } + // rel_type must be m.annotation + if (RelationType.ANNOTATION == content.relatesTo?.type) { + val reaction = content.relatesTo.key + val relatedEventID = content.relatesTo.eventId + val reactionEventId = event.eventId + Timber.v("Reaction $reactionEventId relates to $relatedEventID") + val eventSummary = EventAnnotationsSummaryEntity.getOrCreate(realm, roomId, relatedEventID) + + var sum = eventSummary.reactionsSummary.find { it.key == reaction } + val txId = event.unsignedData?.transactionId + if (isLocalEcho && txId.isNullOrBlank()) { + Timber.w("Received a local echo with no transaction ID") + } + if (sum == null) { + sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java) + sum.key = reaction + sum.firstTimestamp = event.originServerTs ?: 0 + if (isLocalEcho) { + Timber.v("Adding local echo reaction $reaction") + sum.sourceLocalEcho.add(txId) + sum.count = 1 + } else { + Timber.v("Adding synced reaction $reaction") + sum.count = 1 + sum.sourceEvents.add(reactionEventId) + } + sum.addedByMe = sum.addedByMe || (userId == event.senderId) + eventSummary.reactionsSummary.add(sum) + } else { + // is this a known event (is possible? pagination?) + if (!sum.sourceEvents.contains(reactionEventId)) { + // check if it's not the sync of a local echo + if (!isLocalEcho && sum.sourceLocalEcho.contains(txId)) { + // ok it has already been counted, just sync the list, do not touch count + Timber.v("Ignoring synced of local echo for reaction $reaction") + sum.sourceLocalEcho.remove(txId) + sum.sourceEvents.add(reactionEventId) + } else { + sum.count += 1 + if (isLocalEcho) { + Timber.v("Adding local echo reaction $reaction") + sum.sourceLocalEcho.add(txId) + } else { + Timber.v("Adding synced reaction $reaction") + sum.sourceEvents.add(reactionEventId) + } + + sum.addedByMe = sum.addedByMe || (userId == event.senderId) + } + } + } + } else { + Timber.e("Unknown relation type ${content.relatesTo?.type} for event ${event.eventId}") + } + } + + /** + * Called when an event is deleted + */ + private fun handleRedactionOfReplace(redacted: EventEntity, relatedEventId: String, realm: Realm) { + Timber.d("Handle redaction of m.replace") + val eventSummary = EventAnnotationsSummaryEntity.where(realm, relatedEventId).findFirst() + if (eventSummary == null) { + Timber.w("Redaction of a replace targeting an unknown event $relatedEventId") + return + } + val sourceEvents = eventSummary.editSummary?.sourceEvents + val sourceToDiscard = sourceEvents?.indexOf(redacted.eventId) + if (sourceToDiscard == null) { + Timber.w("Redaction of a replace that was not known in aggregation $sourceToDiscard") + return + } + // Need to remove this event from the redaction list and compute new aggregation state + sourceEvents.removeAt(sourceToDiscard) + val previousEdit = sourceEvents.mapNotNull { EventEntity.where(realm, it).findFirst() }.sortedBy { it.originServerTs }.lastOrNull() + if (previousEdit == null) { + // revert to original + eventSummary.editSummary?.deleteFromRealm() + } else { + // I have the last event + ContentMapper.map(previousEdit.content)?.toModel()?.newContent?.let { newContent -> + eventSummary.editSummary?.lastEditTs = previousEdit.originServerTs + ?: System.currentTimeMillis() + eventSummary.editSummary?.aggregatedContent = ContentMapper.map(newContent) + } ?: run { + Timber.e("Failed to udate edited summary") + // TODO how to reccover that + } + } + } + + fun handleReactionRedact(eventToPrune: EventEntity, realm: Realm, userId: String) { + Timber.v("REDACTION of reaction ${eventToPrune.eventId}") + // delete a reaction, need to update the annotation summary if any + val reactionContent: ReactionContent = EventMapper.map(eventToPrune).content.toModel() + ?: return + val eventThatWasReacted = reactionContent.relatesTo?.eventId ?: return + + val reactionKey = reactionContent.relatesTo.key + Timber.v("REMOVE reaction for key $reactionKey") + val summary = EventAnnotationsSummaryEntity.where(realm, eventThatWasReacted).findFirst() + if (summary != null) { + summary.reactionsSummary.where() + .equalTo(ReactionAggregatedSummaryEntityFields.KEY, reactionKey) + .findFirst()?.let { aggregation -> + Timber.v("Find summary for key with ${aggregation.sourceEvents.size} known reactions (count:${aggregation.count})") + Timber.v("Known reactions ${aggregation.sourceEvents.joinToString(",")}") + if (aggregation.sourceEvents.contains(eventToPrune.eventId)) { + Timber.v("REMOVE reaction for key $reactionKey") + aggregation.sourceEvents.remove(eventToPrune.eventId) + Timber.v("Known reactions after ${aggregation.sourceEvents.joinToString(",")}") + aggregation.count = aggregation.count - 1 + if (eventToPrune.sender == userId) { + // Was it a redact on my reaction? + aggregation.addedByMe = false + } + if (aggregation.count == 0) { + // delete! + aggregation.deleteFromRealm() + } + } else { + Timber.e("## Cannot remove summary from count, corresponding reaction ${eventToPrune.eventId} is not known") + } + } + } else { + Timber.e("## Cannot find summary for key $reactionKey") + } + } + + private fun handleVerification(realm: Realm, event: Event, roomId: String, isLocalEcho: Boolean, relatedEventId: String, userId: String) { + val eventSummary = EventAnnotationsSummaryEntity.getOrCreate(realm, roomId, relatedEventId) + + val verifSummary = eventSummary.referencesSummaryEntity + ?: ReferencesAggregatedSummaryEntity.create(realm, relatedEventId).also { + eventSummary.referencesSummaryEntity = it + } + + val txId = event.unsignedData?.transactionId + + if (!isLocalEcho && verifSummary.sourceLocalEcho.contains(txId)) { + // ok it has already been handled + } else { + ContentMapper.map(verifSummary.content)?.toModel() + var data = ContentMapper.map(verifSummary.content)?.toModel() + ?: ReferencesAggregatedContent(VerificationState.REQUEST) + // TODO ignore invalid messages? e.g a START after a CANCEL? + // i.e. never change state if already canceled/done + val currentState = data.verificationState + val newState = when (event.getClearType()) { + EventType.KEY_VERIFICATION_START, + EventType.KEY_VERIFICATION_ACCEPT, + EventType.KEY_VERIFICATION_READY, + EventType.KEY_VERIFICATION_KEY, + EventType.KEY_VERIFICATION_MAC -> currentState.toState(VerificationState.WAITING) + EventType.KEY_VERIFICATION_CANCEL -> currentState.toState(if (event.senderId == userId) { + VerificationState.CANCELED_BY_ME + } else { + VerificationState.CANCELED_BY_OTHER + }) + EventType.KEY_VERIFICATION_DONE -> currentState.toState(VerificationState.DONE) + else -> VerificationState.REQUEST + } + + data = data.copy(verificationState = newState) + verifSummary.content = ContentMapper.map(data.toContent()) + } + + if (isLocalEcho) { + verifSummary.sourceLocalEcho.add(event.eventId) + } else { + verifSummary.sourceLocalEcho.remove(txId) + verifSummary.sourceEvents.add(event.eventId) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt new file mode 100644 index 0000000000..25dcc69fa8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt @@ -0,0 +1,383 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room + +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams +import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsResponse +import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.network.NetworkConstants +import org.matrix.android.sdk.internal.session.room.alias.AddRoomAliasBody +import org.matrix.android.sdk.internal.session.room.alias.RoomAliasDescription +import org.matrix.android.sdk.internal.session.room.create.CreateRoomBody +import org.matrix.android.sdk.internal.session.room.create.CreateRoomResponse +import org.matrix.android.sdk.internal.session.room.create.JoinRoomResponse +import org.matrix.android.sdk.internal.session.room.membership.RoomMembersResponse +import org.matrix.android.sdk.internal.session.room.membership.admin.UserIdAndReason +import org.matrix.android.sdk.internal.session.room.membership.joining.InviteBody +import org.matrix.android.sdk.internal.session.room.membership.threepid.ThreePidInviteBody +import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse +import org.matrix.android.sdk.internal.session.room.reporting.ReportContentBody +import org.matrix.android.sdk.internal.session.room.send.SendResponse +import org.matrix.android.sdk.internal.session.room.tags.TagBody +import org.matrix.android.sdk.internal.session.room.timeline.EventContextResponse +import org.matrix.android.sdk.internal.session.room.timeline.PaginationResponse +import org.matrix.android.sdk.internal.session.room.typing.TypingBody +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.DELETE +import retrofit2.http.GET +import retrofit2.http.Headers +import retrofit2.http.POST +import retrofit2.http.PUT +import retrofit2.http.Path +import retrofit2.http.Query + +internal interface RoomAPI { + + /** + * Get the third party server protocols. + * + * Ref: https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-thirdparty-protocols + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "thirdparty/protocols") + fun thirdPartyProtocols(): Call> + + /** + * Lists the public rooms on the server, with optional filter. + * This API returns paginated responses. The rooms are ordered by the number of joined members, with the largest rooms first. + * + * Ref: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-publicrooms + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "publicRooms") + fun publicRooms(@Query("server") server: String?, + @Body publicRoomsParams: PublicRoomsParams + ): Call + + /** + * Create a room. + * Ref: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-createroom + * Set all the timeouts to 1 minute, because if the server takes time to answer, we will not execute the + * create direct chat request if any + * + * @param param the creation room parameter + */ + @Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000") + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "createRoom") + fun createRoom(@Body param: CreateRoomBody): Call + + /** + * Get a list of messages starting from a reference. + * + * @param roomId the room id + * @param from the token identifying where to start. Required. + * @param dir The direction to return messages from. Required. + * @param limit the maximum number of messages to retrieve. Optional. + * @param filter A JSON RoomEventFilter to filter returned events with. Optional. + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/messages") + fun getRoomMessagesFrom(@Path("roomId") roomId: String, + @Query("from") from: String, + @Query("dir") dir: String, + @Query("limit") limit: Int, + @Query("filter") filter: String? + ): Call + + /** + * Get all members of a room + * + * @param roomId the room id where to get the members + * @param syncToken the sync token (optional) + * @param membership to include only one type of membership (optional) + * @param notMembership to exclude one type of membership (optional) + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/members") + fun getMembers(@Path("roomId") roomId: String, + @Query("at") syncToken: String?, + @Query("membership") membership: String?, + @Query("not_membership") notMembership: String? + ): Call + + /** + * Send an event to a room. + * + * @param txId the transaction Id + * @param roomId the room id + * @param eventType the event type + * @param content the event content + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/send/{eventType}/{txId}") + fun send(@Path("txId") txId: String, + @Path("roomId") roomId: String, + @Path("eventType") eventType: String, + @Body content: Content? + ): Call + + /** + * Send an event to a room. + * + * @param txId the transaction Id + * @param roomId the room id + * @param eventType the event type + * @param content the event content as string + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/send/{eventType}/{txId}") + fun send(@Path("txId") txId: String, + @Path("roomId") roomId: String, + @Path("eventType") eventType: String, + @Body content: String? + ): Call + + /** + * Get the context surrounding an event. + * + * @param roomId the room id + * @param eventId the event Id + * @param limit the maximum number of messages to retrieve + * @param filter A JSON RoomEventFilter to filter returned events with. Optional. + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/context/{eventId}") + fun getContextOfEvent(@Path("roomId") roomId: String, + @Path("eventId") eventId: String, + @Query("limit") limit: Int, + @Query("filter") filter: String? = null): Call + + /** + * Retrieve an event from its room id / events id + * + * @param roomId the room id + * @param eventId the event Id + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/event/{eventId}") + fun getEvent(@Path("roomId") roomId: String, + @Path("eventId") eventId: String): Call + + /** + * Send read markers. + * + * @param roomId the room id + * @param markers the read markers + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/read_markers") + fun sendReadMarker(@Path("roomId") roomId: String, + @Body markers: Map): Call + + /** + * Invite a user to the given room. + * Ref: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-rooms-roomid-invite + * + * @param roomId the room id + * @param body a object that just contains a user id + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/invite") + fun invite(@Path("roomId") roomId: String, + @Body body: InviteBody): Call + + /** + * Invite a user to a room, using a ThreePid + * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#id101 + * @param roomId Required. The room identifier (not alias) to which to invite the user. + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/invite") + fun invite3pid(@Path("roomId") roomId: String, + @Body body: ThreePidInviteBody): Call + + /** + * Send a generic state events + * + * @param roomId the room id. + * @param stateEventType the state event type + * @param params the request parameters + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/state/{state_event_type}") + fun sendStateEvent(@Path("roomId") roomId: String, + @Path("state_event_type") stateEventType: String, + @Body params: JsonDict): Call + + /** + * Send a generic state events + * + * @param roomId the room id. + * @param stateEventType the state event type + * @param stateKey the state keys + * @param params the request parameters + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/state/{state_event_type}/{state_key}") + fun sendStateEvent(@Path("roomId") roomId: String, + @Path("state_event_type") stateEventType: String, + @Path("state_key") stateKey: String, + @Body params: JsonDict): Call + + /** + * Send a relation event to a room. + * + * @param txId the transaction Id + * @param roomId the room id + * @param eventType the event type + * @param content the event content + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/send_relation/{parent_id}/{relation_type}/{event_type}") + fun sendRelation(@Path("roomId") roomId: String, + @Path("parentId") parent_id: String, + @Path("relation_type") relationType: String, + @Path("eventType") eventType: String, + @Body content: Content? + ): Call + + /** + * Paginate relations for event based in normal topological order + * + * @param relationType filter for this relation type + * @param eventType filter for this event type + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/relations/{eventId}/{relationType}/{eventType}") + fun getRelations(@Path("roomId") roomId: String, + @Path("eventId") eventId: String, + @Path("relationType") relationType: String, + @Path("eventType") eventType: String + ): Call + + /** + * Join the given room. + * + * @param roomIdOrAlias the room id or alias + * @param viaServers the servers to attempt to join the room through + * @param params the request body + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "join/{roomIdOrAlias}") + fun join(@Path("roomIdOrAlias") roomIdOrAlias: String, + @Query("server_name") viaServers: List, + @Body params: Map): Call + + /** + * Leave the given room. + * + * @param roomId the room id + * @param params the request body + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/leave") + fun leave(@Path("roomId") roomId: String, + @Body params: Map): Call + + /** + * Ban a user from the given room. + * + * @param roomId the room id + * @param userIdAndReason the banned user object (userId and reason for ban) + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/ban") + fun ban(@Path("roomId") roomId: String, + @Body userIdAndReason: UserIdAndReason): Call + + /** + * unban a user from the given room. + * + * @param roomId the room id + * @param userIdAndReason the unbanned user object (userId and reason for unban) + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/unban") + fun unban(@Path("roomId") roomId: String, + @Body userIdAndReason: UserIdAndReason): Call + + /** + * Kick a user from the given room. + * + * @param roomId the room id + * @param userIdAndReason the kicked user object (userId and reason for kicking) + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/kick") + fun kick(@Path("roomId") roomId: String, + @Body userIdAndReason: UserIdAndReason): Call + + /** + * Strips all information out of an event which isn't critical to the integrity of the server-side representation of the room. + * This cannot be undone. + * Users may redact their own events, and any user with a power level greater than or equal to the redact power level of the room may redact events there. + * + * @param txId the transaction Id + * @param roomId the room id + * @param eventId the event to delete + * @param reason json containing reason key {"reason": "Indecent material"} + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/redact/{eventId}/{txnId}") + fun redactEvent( + @Path("txnId") txId: String, + @Path("roomId") roomId: String, + @Path("eventId") parent_id: String, + @Body reason: Map + ): Call + + /** + * Reports an event as inappropriate to the server, which may then notify the appropriate people. + * + * @param roomId the room id + * @param eventId the event to report content + * @param body body containing score and reason + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/report/{eventId}") + fun reportContent(@Path("roomId") roomId: String, + @Path("eventId") eventId: String, + @Body body: ReportContentBody): Call + + /** + * Get the room ID associated to the room alias. + * + * @param roomAlias the room alias. + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/room/{roomAlias}") + fun getRoomIdByAlias(@Path("roomAlias") roomAlias: String): Call + + /** + * Add alias to the room. + * @param roomAlias the room alias. + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/room/{roomAlias}") + fun addRoomAlias(@Path("roomAlias") roomAlias: String, + @Body body: AddRoomAliasBody): Call + + /** + * Inform that the user is starting to type or has stopped typing + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/typing/{userId}") + fun sendTypingState(@Path("roomId") roomId: String, + @Path("userId") userId: String, + @Body body: TypingBody): Call + + /** + * Room tagging + */ + + /** + * Add a tag to a room. + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/rooms/{roomId}/tags/{tag}") + fun putTag(@Path("userId") userId: String, + @Path("roomId") roomId: String, + @Path("tag") tag: String, + @Body body: TagBody): Call + + /** + * Delete a tag from a room. + */ + @DELETE(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/rooms/{roomId}/tags/{tag}") + fun deleteTag(@Path("userId") userId: String, + @Path("roomId") roomId: String, + @Path("tag") tag: String): Call +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAvatarResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAvatarResolver.kt new file mode 100644 index 0000000000..6851780a62 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAvatarResolver.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room + +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.RoomAvatarContent +import org.matrix.android.sdk.internal.database.mapper.ContentMapper +import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields +import org.matrix.android.sdk.internal.database.query.getOrNull +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper +import io.realm.Realm +import javax.inject.Inject + +internal class RoomAvatarResolver @Inject constructor(@UserId private val userId: String) { + + /** + * Compute the room avatar url + * @param realm: the current instance of realm + * @param roomId the roomId of the room to resolve avatar + * @return the room avatar url, can be a fallback to a room member avatar or null + */ + fun resolve(realm: Realm, roomId: String): String? { + var res: String? + val roomName = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_AVATAR, stateKey = "")?.root + res = ContentMapper.map(roomName?.content).toModel()?.avatarUrl + if (!res.isNullOrEmpty()) { + return res + } + val roomMembers = RoomMemberHelper(realm, roomId) + val members = roomMembers.queryActiveRoomMembersEvent().findAll() + // detect if it is a room with no more than 2 members (i.e. an alone or a 1:1 chat) + if (members.size == 1) { + res = members.firstOrNull()?.avatarUrl + } else if (members.size == 2) { + val firstOtherMember = members.where().notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId).findFirst() + res = firstOtherMember?.avatarUrl + } + return res + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt new file mode 100644 index 0000000000..ac8bdb3992 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room + +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.room.Room +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.room.call.DefaultRoomCallService +import org.matrix.android.sdk.internal.session.room.draft.DefaultDraftService +import org.matrix.android.sdk.internal.session.room.membership.DefaultMembershipService +import org.matrix.android.sdk.internal.session.room.notification.DefaultRoomPushRuleService +import org.matrix.android.sdk.internal.session.room.read.DefaultReadService +import org.matrix.android.sdk.internal.session.room.relation.DefaultRelationService +import org.matrix.android.sdk.internal.session.room.reporting.DefaultReportingService +import org.matrix.android.sdk.internal.session.room.send.DefaultSendService +import org.matrix.android.sdk.internal.session.room.state.DefaultStateService +import org.matrix.android.sdk.internal.session.room.state.SendStateTask +import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource +import org.matrix.android.sdk.internal.session.room.tags.DefaultTagsService +import org.matrix.android.sdk.internal.session.room.timeline.DefaultTimelineService +import org.matrix.android.sdk.internal.session.room.typing.DefaultTypingService +import org.matrix.android.sdk.internal.session.room.uploads.DefaultUploadsService +import org.matrix.android.sdk.internal.task.TaskExecutor +import javax.inject.Inject + +internal interface RoomFactory { + fun create(roomId: String): Room +} + +@SessionScope +internal class DefaultRoomFactory @Inject constructor(private val cryptoService: CryptoService, + private val roomSummaryDataSource: RoomSummaryDataSource, + private val timelineServiceFactory: DefaultTimelineService.Factory, + private val sendServiceFactory: DefaultSendService.Factory, + private val draftServiceFactory: DefaultDraftService.Factory, + private val stateServiceFactory: DefaultStateService.Factory, + private val uploadsServiceFactory: DefaultUploadsService.Factory, + private val reportingServiceFactory: DefaultReportingService.Factory, + private val roomCallServiceFactory: DefaultRoomCallService.Factory, + private val readServiceFactory: DefaultReadService.Factory, + private val typingServiceFactory: DefaultTypingService.Factory, + private val tagsServiceFactory: DefaultTagsService.Factory, + private val relationServiceFactory: DefaultRelationService.Factory, + private val membershipServiceFactory: DefaultMembershipService.Factory, + private val roomPushRuleServiceFactory: DefaultRoomPushRuleService.Factory, + private val taskExecutor: TaskExecutor, + private val sendStateTask: SendStateTask) : + RoomFactory { + + override fun create(roomId: String): Room { + return DefaultRoom( + roomId = roomId, + roomSummaryDataSource = roomSummaryDataSource, + timelineService = timelineServiceFactory.create(roomId), + sendService = sendServiceFactory.create(roomId), + draftService = draftServiceFactory.create(roomId), + stateService = stateServiceFactory.create(roomId), + uploadsService = uploadsServiceFactory.create(roomId), + reportingService = reportingServiceFactory.create(roomId), + roomCallService = roomCallServiceFactory.create(roomId), + readService = readServiceFactory.create(roomId), + typingService = typingServiceFactory.create(roomId), + tagsService = tagsServiceFactory.create(roomId), + cryptoService = cryptoService, + relationService = relationServiceFactory.create(roomId), + roomMembersService = membershipServiceFactory.create(roomId), + roomPushRuleService = roomPushRuleServiceFactory.create(roomId), + taskExecutor = taskExecutor, + sendStateTask = sendStateTask + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomGetter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomGetter.kt new file mode 100644 index 0000000000..38dcad2311 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomGetter.kt @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.room.Room +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.internal.database.model.RoomEntity +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper +import io.realm.Realm +import javax.inject.Inject + +internal interface RoomGetter { + fun getRoom(roomId: String): Room? + + fun getDirectRoomWith(otherUserId: String): Room? +} + +@SessionScope +internal class DefaultRoomGetter @Inject constructor( + @SessionDatabase private val monarchy: Monarchy, + private val roomFactory: RoomFactory +) : RoomGetter { + + override fun getRoom(roomId: String): Room? { + return Realm.getInstance(monarchy.realmConfiguration).use { realm -> + createRoom(realm, roomId) + } + } + + override fun getDirectRoomWith(otherUserId: String): Room? { + return Realm.getInstance(monarchy.realmConfiguration).use { realm -> + RoomSummaryEntity.where(realm) + .equalTo(RoomSummaryEntityFields.IS_DIRECT, true) + .equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name) + .findAll() + .filter { dm -> dm.otherMemberIds.contains(otherUserId) } + .map { it.roomId } + .firstOrNull { roomId -> otherUserId in RoomMemberHelper(realm, roomId).getActiveRoomMemberIds() } + ?.let { roomId -> createRoom(realm, roomId) } + } + } + + private fun createRoom(realm: Realm, roomId: String): Room? { + return RoomEntity.where(realm, roomId).findFirst() + ?.let { roomFactory.create(roomId) } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt new file mode 100644 index 0000000000..7f21ee84f6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt @@ -0,0 +1,207 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room + +import dagger.Binds +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.api.session.file.FileService +import org.matrix.android.sdk.api.session.room.RoomDirectoryService +import org.matrix.android.sdk.api.session.room.RoomService +import org.matrix.android.sdk.internal.session.DefaultFileService +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.room.alias.AddRoomAliasTask +import org.matrix.android.sdk.internal.session.room.alias.DefaultAddRoomAliasTask +import org.matrix.android.sdk.internal.session.room.alias.DefaultGetRoomIdByAliasTask +import org.matrix.android.sdk.internal.session.room.alias.GetRoomIdByAliasTask +import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask +import org.matrix.android.sdk.internal.session.room.create.DefaultCreateRoomTask +import org.matrix.android.sdk.internal.session.room.directory.DefaultGetPublicRoomTask +import org.matrix.android.sdk.internal.session.room.directory.DefaultGetThirdPartyProtocolsTask +import org.matrix.android.sdk.internal.session.room.directory.GetPublicRoomTask +import org.matrix.android.sdk.internal.session.room.directory.GetThirdPartyProtocolsTask +import org.matrix.android.sdk.internal.session.room.membership.DefaultLoadRoomMembersTask +import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask +import org.matrix.android.sdk.internal.session.room.membership.admin.DefaultMembershipAdminTask +import org.matrix.android.sdk.internal.session.room.membership.admin.MembershipAdminTask +import org.matrix.android.sdk.internal.session.room.membership.joining.DefaultInviteTask +import org.matrix.android.sdk.internal.session.room.membership.joining.DefaultJoinRoomTask +import org.matrix.android.sdk.internal.session.room.membership.joining.InviteTask +import org.matrix.android.sdk.internal.session.room.membership.joining.JoinRoomTask +import org.matrix.android.sdk.internal.session.room.membership.leaving.DefaultLeaveRoomTask +import org.matrix.android.sdk.internal.session.room.membership.leaving.LeaveRoomTask +import org.matrix.android.sdk.internal.session.room.membership.threepid.DefaultInviteThreePidTask +import org.matrix.android.sdk.internal.session.room.membership.threepid.InviteThreePidTask +import org.matrix.android.sdk.internal.session.room.read.DefaultMarkAllRoomsReadTask +import org.matrix.android.sdk.internal.session.room.read.DefaultSetReadMarkersTask +import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask +import org.matrix.android.sdk.internal.session.room.read.SetReadMarkersTask +import org.matrix.android.sdk.internal.session.room.relation.DefaultFetchEditHistoryTask +import org.matrix.android.sdk.internal.session.room.relation.DefaultFindReactionEventForUndoTask +import org.matrix.android.sdk.internal.session.room.relation.DefaultUpdateQuickReactionTask +import org.matrix.android.sdk.internal.session.room.relation.FetchEditHistoryTask +import org.matrix.android.sdk.internal.session.room.relation.FindReactionEventForUndoTask +import org.matrix.android.sdk.internal.session.room.relation.UpdateQuickReactionTask +import org.matrix.android.sdk.internal.session.room.reporting.DefaultReportContentTask +import org.matrix.android.sdk.internal.session.room.reporting.ReportContentTask +import org.matrix.android.sdk.internal.session.room.state.DefaultSendStateTask +import org.matrix.android.sdk.internal.session.room.state.SendStateTask +import org.matrix.android.sdk.internal.session.room.tags.AddTagToRoomTask +import org.matrix.android.sdk.internal.session.room.tags.DefaultAddTagToRoomTask +import org.matrix.android.sdk.internal.session.room.tags.DefaultDeleteTagFromRoomTask +import org.matrix.android.sdk.internal.session.room.tags.DeleteTagFromRoomTask +import org.matrix.android.sdk.internal.session.room.timeline.DefaultFetchTokenAndPaginateTask +import org.matrix.android.sdk.internal.session.room.timeline.DefaultGetContextOfEventTask +import org.matrix.android.sdk.internal.session.room.timeline.DefaultPaginationTask +import org.matrix.android.sdk.internal.session.room.timeline.FetchTokenAndPaginateTask +import org.matrix.android.sdk.internal.session.room.timeline.GetContextOfEventTask +import org.matrix.android.sdk.internal.session.room.timeline.PaginationTask +import org.matrix.android.sdk.internal.session.room.typing.DefaultSendTypingTask +import org.matrix.android.sdk.internal.session.room.typing.SendTypingTask +import org.matrix.android.sdk.internal.session.room.uploads.DefaultGetUploadsTask +import org.matrix.android.sdk.internal.session.room.uploads.GetUploadsTask +import org.commonmark.parser.Parser +import org.commonmark.renderer.html.HtmlRenderer +import org.commonmark.renderer.text.TextContentRenderer +import retrofit2.Retrofit + +@Module +internal abstract class RoomModule { + + @Module + companion object { + @Provides + @JvmStatic + @SessionScope + fun providesRoomAPI(retrofit: Retrofit): RoomAPI { + return retrofit.create(RoomAPI::class.java) + } + + @Provides + @JvmStatic + fun providesParser(): Parser { + return Parser.builder().build() + } + + @Provides + @JvmStatic + fun providesHtmlRenderer(): HtmlRenderer { + return HtmlRenderer + .builder() + .build() + } + + @Provides + @JvmStatic + fun providesTextContentRenderer(): TextContentRenderer { + return TextContentRenderer + .builder() + .build() + } + } + + @Binds + abstract fun bindRoomFactory(factory: DefaultRoomFactory): RoomFactory + + @Binds + abstract fun bindRoomGetter(getter: DefaultRoomGetter): RoomGetter + + @Binds + abstract fun bindRoomService(service: DefaultRoomService): RoomService + + @Binds + abstract fun bindRoomDirectoryService(service: DefaultRoomDirectoryService): RoomDirectoryService + + @Binds + abstract fun bindFileService(service: DefaultFileService): FileService + + @Binds + abstract fun bindCreateRoomTask(task: DefaultCreateRoomTask): CreateRoomTask + + @Binds + abstract fun bindGetPublicRoomTask(task: DefaultGetPublicRoomTask): GetPublicRoomTask + + @Binds + abstract fun bindGetThirdPartyProtocolsTask(task: DefaultGetThirdPartyProtocolsTask): GetThirdPartyProtocolsTask + + @Binds + abstract fun bindInviteTask(task: DefaultInviteTask): InviteTask + + @Binds + abstract fun bindInviteThreePidTask(task: DefaultInviteThreePidTask): InviteThreePidTask + + @Binds + abstract fun bindJoinRoomTask(task: DefaultJoinRoomTask): JoinRoomTask + + @Binds + abstract fun bindLeaveRoomTask(task: DefaultLeaveRoomTask): LeaveRoomTask + + @Binds + abstract fun bindMembershipAdminTask(task: DefaultMembershipAdminTask): MembershipAdminTask + + @Binds + abstract fun bindLoadRoomMembersTask(task: DefaultLoadRoomMembersTask): LoadRoomMembersTask + + @Binds + abstract fun bindSetReadMarkersTask(task: DefaultSetReadMarkersTask): SetReadMarkersTask + + @Binds + abstract fun bindMarkAllRoomsReadTask(task: DefaultMarkAllRoomsReadTask): MarkAllRoomsReadTask + + @Binds + abstract fun bindFindReactionEventForUndoTask(task: DefaultFindReactionEventForUndoTask): FindReactionEventForUndoTask + + @Binds + abstract fun bindUpdateQuickReactionTask(task: DefaultUpdateQuickReactionTask): UpdateQuickReactionTask + + @Binds + abstract fun bindSendStateTask(task: DefaultSendStateTask): SendStateTask + + @Binds + abstract fun bindReportContentTask(task: DefaultReportContentTask): ReportContentTask + + @Binds + abstract fun bindGetContextOfEventTask(task: DefaultGetContextOfEventTask): GetContextOfEventTask + + @Binds + abstract fun bindPaginationTask(task: DefaultPaginationTask): PaginationTask + + @Binds + abstract fun bindFetchNextTokenAndPaginateTask(task: DefaultFetchTokenAndPaginateTask): FetchTokenAndPaginateTask + + @Binds + abstract fun bindFetchEditHistoryTask(task: DefaultFetchEditHistoryTask): FetchEditHistoryTask + + @Binds + abstract fun bindGetRoomIdByAliasTask(task: DefaultGetRoomIdByAliasTask): GetRoomIdByAliasTask + + @Binds + abstract fun bindAddRoomAliasTask(task: DefaultAddRoomAliasTask): AddRoomAliasTask + + @Binds + abstract fun bindSendTypingTask(task: DefaultSendTypingTask): SendTypingTask + + @Binds + abstract fun bindGetUploadsTask(task: DefaultGetUploadsTask): GetUploadsTask + + @Binds + abstract fun bindAddTagToRoomTask(task: DefaultAddTagToRoomTask): AddTagToRoomTask + + @Binds + abstract fun bindDeleteTagFromRoomTask(task: DefaultDeleteTagFromRoomTask): DeleteTagFromRoomTask +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/AddRoomAliasBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/AddRoomAliasBody.kt new file mode 100644 index 0000000000..b5938d592a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/AddRoomAliasBody.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.alias + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class AddRoomAliasBody( + /** + * Required. The room id which the alias will be added to. + */ + @Json(name = "room_id") val roomId: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/AddRoomAliasTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/AddRoomAliasTask.kt new file mode 100644 index 0000000000..510bd25d9c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/AddRoomAliasTask.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.alias + +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface AddRoomAliasTask : Task { + data class Params( + val roomId: String, + val roomAlias: String + ) +} + +internal class DefaultAddRoomAliasTask @Inject constructor( + private val roomAPI: RoomAPI, + private val eventBus: EventBus +) : AddRoomAliasTask { + + override suspend fun execute(params: AddRoomAliasTask.Params) { + executeRequest(eventBus) { + apiCall = roomAPI.addRoomAlias( + roomAlias = params.roomAlias, + body = AddRoomAliasBody( + roomId = params.roomId + ) + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomIdByAliasTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomIdByAliasTask.kt new file mode 100644 index 0000000000..e1d119c432 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomIdByAliasTask.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.alias + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.query.findByAlias +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.task.Task +import io.realm.Realm +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface GetRoomIdByAliasTask : Task> { + data class Params( + val roomAlias: String, + val searchOnServer: Boolean + ) +} + +internal class DefaultGetRoomIdByAliasTask @Inject constructor( + @SessionDatabase private val monarchy: Monarchy, + private val roomAPI: RoomAPI, + private val eventBus: EventBus +) : GetRoomIdByAliasTask { + + override suspend fun execute(params: GetRoomIdByAliasTask.Params): Optional { + var roomId = Realm.getInstance(monarchy.realmConfiguration).use { + RoomSummaryEntity.findByAlias(it, params.roomAlias)?.roomId + } + return if (roomId != null) { + Optional.from(roomId) + } else if (!params.searchOnServer) { + Optional.from(null) + } else { + roomId = executeRequest(eventBus) { + apiCall = roomAPI.getRoomIdByAlias(params.roomAlias) + }.roomId + Optional.from(roomId) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasDescription.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasDescription.kt new file mode 100644 index 0000000000..ac0b59c916 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasDescription.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.alias + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class RoomAliasDescription( + /** + * The room ID for this alias. + */ + @Json(name = "room_id") val roomId: String, + + /** + * A list of servers that are aware of this room ID. + */ + @Json(name = "servers") val servers: List = emptyList() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/call/DefaultRoomCallService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/call/DefaultRoomCallService.kt new file mode 100644 index 0000000000..3d764e001c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/call/DefaultRoomCallService.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.call + +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.room.call.RoomCallService +import org.matrix.android.sdk.internal.session.room.RoomGetter + +internal class DefaultRoomCallService @AssistedInject constructor( + @Assisted private val roomId: String, + private val roomGetter: RoomGetter +) : RoomCallService { + + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): RoomCallService + } + + override fun canStartCall(): Boolean { + return roomGetter.getRoom(roomId)?.roomSummary()?.canStartCall.orFalse() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBody.kt new file mode 100644 index 0000000000..e9ae5e7a6b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBody.kt @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.create + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent +import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility +import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset +import org.matrix.android.sdk.internal.session.room.membership.threepid.ThreePidInviteBody + +/** + * Parameter to create a room + */ +@JsonClass(generateAdapter = true) +internal data class CreateRoomBody( + /** + * A public visibility indicates that the room will be shown in the published room list. + * A private visibility will hide the room from the published room list. + * Rooms default to private visibility if this key is not included. + * NB: This should not be confused with join_rules which also uses the word public. One of: ["public", "private"] + */ + @Json(name = "visibility") + val visibility: RoomDirectoryVisibility?, + + /** + * The desired room alias local part. If this is included, a room alias will be created and mapped to the newly created room. + * The alias will belong on the same homeserver which created the room. + * For example, if this was set to "foo" and sent to the homeserver "example.com" the complete room alias would be #foo:example.com. + */ + @Json(name = "room_alias_name") + val roomAliasName: String?, + + /** + * If this is included, an m.room.name event will be sent into the room to indicate the name of the room. + * See Room Events for more information on m.room.name. + */ + @Json(name = "name") + val name: String?, + + /** + * If this is included, an m.room.topic event will be sent into the room to indicate the topic for the room. + * See Room Events for more information on m.room.topic. + */ + @Json(name = "topic") + val topic: String?, + + /** + * A list of user IDs to invite to the room. + * This will tell the server to invite everyone in the list to the newly created room. + */ + @Json(name = "invite") + val invitedUserIds: List?, + + /** + * A list of objects representing third party IDs to invite into the room. + */ + @Json(name = "invite_3pid") + val invite3pids: List?, + + /** + * Extra keys to be added to the content of the m.room.create. + * The server will clobber the following keys: creator. + * Future versions of the specification may allow the server to clobber other keys. + */ + @Json(name = "creation_content") + val creationContent: Any?, + + /** + * A list of state events to set in the new room. + * This allows the user to override the default state events set in the new room. + * The expected format of the state events are an object with type, state_key and content keys set. + * Takes precedence over events set by presets, but gets overridden by name and topic keys. + */ + @Json(name = "initial_state") + val initialStates: List?, + + /** + * Convenience parameter for setting various default state events based on a preset. Must be either: + * private_chat => join_rules is set to invite. history_visibility is set to shared. + * trusted_private_chat => join_rules is set to invite. history_visibility is set to shared. All invitees are given the same power level as the + * room creator. + * public_chat: => join_rules is set to public. history_visibility is set to shared. + */ + @Json(name = "preset") + val preset: CreateRoomPreset?, + + /** + * This flag makes the server set the is_direct flag on the m.room.member events sent to the users in invite and invite_3pid. + * See Direct Messaging for more information. + */ + @Json(name = "is_direct") + val isDirect: Boolean?, + + /** + * The power level content to override in the default power level event + */ + @Json(name = "power_level_content_override") + val powerLevelContentOverride: PowerLevelsContent? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt new file mode 100644 index 0000000000..6e450e5428 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.create + +import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.identity.IdentityServiceError +import org.matrix.android.sdk.api.session.identity.toMedium +import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams +import org.matrix.android.sdk.internal.crypto.DeviceListManager +import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.internal.di.AuthenticatedIdentity +import org.matrix.android.sdk.internal.network.token.AccessTokenProvider +import org.matrix.android.sdk.internal.session.identity.EnsureIdentityTokenTask +import org.matrix.android.sdk.internal.session.identity.data.IdentityStore +import org.matrix.android.sdk.internal.session.identity.data.getIdentityServerUrlWithoutProtocol +import org.matrix.android.sdk.internal.session.room.membership.threepid.ThreePidInviteBody +import java.security.InvalidParameterException +import javax.inject.Inject + +internal class CreateRoomBodyBuilder @Inject constructor( + private val ensureIdentityTokenTask: EnsureIdentityTokenTask, + private val crossSigningService: CrossSigningService, + private val deviceListManager: DeviceListManager, + private val identityStore: IdentityStore, + @AuthenticatedIdentity + private val accessTokenProvider: AccessTokenProvider +) { + + suspend fun build(params: CreateRoomParams): CreateRoomBody { + val invite3pids = params.invite3pids + .takeIf { it.isNotEmpty() } + ?.let { invites -> + // This can throw Exception if Identity server is not configured + ensureIdentityTokenTask.execute(Unit) + + val identityServerUrlWithoutProtocol = identityStore.getIdentityServerUrlWithoutProtocol() + ?: throw IdentityServiceError.NoIdentityServerConfigured + val identityServerAccessToken = accessTokenProvider.getToken() ?: throw IdentityServiceError.NoIdentityServerConfigured + + invites.map { + ThreePidInviteBody( + id_server = identityServerUrlWithoutProtocol, + id_access_token = identityServerAccessToken, + medium = it.toMedium(), + address = it.value + ) + } + } + + val initialStates = listOfNotNull( + buildEncryptionWithAlgorithmEvent(params), + buildHistoryVisibilityEvent(params) + ) + .takeIf { it.isNotEmpty() } + + return CreateRoomBody( + visibility = params.visibility, + roomAliasName = params.roomAliasName, + name = params.name, + topic = params.topic, + invitedUserIds = params.invitedUserIds, + invite3pids = invite3pids, + creationContent = params.creationContent, + initialStates = initialStates, + preset = params.preset, + isDirect = params.isDirect, + powerLevelContentOverride = params.powerLevelContentOverride + ) + } + + private fun buildHistoryVisibilityEvent(params: CreateRoomParams): Event? { + return params.historyVisibility + ?.let { + val contentMap = mapOf("history_visibility" to it) + + Event( + type = EventType.STATE_ROOM_HISTORY_VISIBILITY, + stateKey = "", + content = contentMap.toContent()) + } + } + + /** + * Add the crypto algorithm to the room creation parameters. + */ + private suspend fun buildEncryptionWithAlgorithmEvent(params: CreateRoomParams): Event? { + if (params.algorithm == null + && canEnableEncryption(params)) { + // Enable the encryption + params.enableEncryption() + } + return params.algorithm + ?.let { + if (it != MXCRYPTO_ALGORITHM_MEGOLM) { + throw InvalidParameterException("Unsupported algorithm: $it") + } + val contentMap = mapOf("algorithm" to it) + + Event( + type = EventType.STATE_ROOM_ENCRYPTION, + stateKey = "", + content = contentMap.toContent() + ) + } + } + + private suspend fun canEnableEncryption(params: CreateRoomParams): Boolean { + return (params.enableEncryptionIfInvitedUsersSupportIt + && crossSigningService.isCrossSigningVerified() + && params.invite3pids.isEmpty()) + && params.invitedUserIds.isNotEmpty() + && params.invitedUserIds.let { userIds -> + val keys = deviceListManager.downloadKeys(userIds, forceDownload = false) + + userIds.all { userId -> + keys.map[userId].let { deviceMap -> + if (deviceMap.isNullOrEmpty()) { + // A user has no device, so do not enable encryption + false + } else { + // Check that every user's device have at least one key + deviceMap.values.all { !it.keys.isNullOrEmpty() } + } + } + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomResponse.kt new file mode 100644 index 0000000000..cfc63bcb7e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomResponse.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.create + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class CreateRoomResponse( + /** + * Required. The created room's ID. + */ + @Json(name = "room_id") val roomId: String +) + +internal typealias JoinRoomResponse = CreateRoomResponse diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt new file mode 100644 index 0000000000..e13d5305b5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt @@ -0,0 +1,115 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.create + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure +import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams +import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset +import org.matrix.android.sdk.internal.database.awaitNotEmptyResult +import org.matrix.android.sdk.internal.database.model.RoomEntity +import org.matrix.android.sdk.internal.database.model.RoomEntityFields +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.session.room.read.SetReadMarkersTask +import org.matrix.android.sdk.internal.session.user.accountdata.DirectChatsHelper +import org.matrix.android.sdk.internal.session.user.accountdata.UpdateUserAccountDataTask +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import io.realm.RealmConfiguration +import kotlinx.coroutines.TimeoutCancellationException +import org.greenrobot.eventbus.EventBus +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +internal interface CreateRoomTask : Task + +internal class DefaultCreateRoomTask @Inject constructor( + private val roomAPI: RoomAPI, + @SessionDatabase private val monarchy: Monarchy, + private val directChatsHelper: DirectChatsHelper, + private val updateUserAccountDataTask: UpdateUserAccountDataTask, + private val readMarkersTask: SetReadMarkersTask, + @SessionDatabase + private val realmConfiguration: RealmConfiguration, + private val createRoomBodyBuilder: CreateRoomBodyBuilder, + private val eventBus: EventBus +) : CreateRoomTask { + + override suspend fun execute(params: CreateRoomParams): String { + val createRoomBody = createRoomBodyBuilder.build(params) + + val createRoomResponse = executeRequest(eventBus) { + apiCall = roomAPI.createRoom(createRoomBody) + } + val roomId = createRoomResponse.roomId + // Wait for room to come back from the sync (but it can maybe be in the DB if the sync response is received before) + try { + awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm -> + realm.where(RoomEntity::class.java) + .equalTo(RoomEntityFields.ROOM_ID, roomId) + } + } catch (exception: TimeoutCancellationException) { + throw CreateRoomFailure.CreatedWithTimeout + } + if (params.isDirect()) { + handleDirectChatCreation(params, roomId) + } + setReadMarkers(roomId) + return roomId + } + + private suspend fun handleDirectChatCreation(params: CreateRoomParams, roomId: String) { + val otherUserId = params.getFirstInvitedUserId() + ?: throw IllegalStateException("You can't create a direct room without an invitedUser") + + monarchy.awaitTransaction { realm -> + RoomSummaryEntity.where(realm, roomId).findFirst()?.apply { + this.directUserId = otherUserId + this.isDirect = true + } + } + val directChats = directChatsHelper.getLocalUserAccount() + updateUserAccountDataTask.execute(UpdateUserAccountDataTask.DirectChatParams(directMessages = directChats)) + } + + private suspend fun setReadMarkers(roomId: String) { + val setReadMarkerParams = SetReadMarkersTask.Params(roomId, forceReadReceipt = true, forceReadMarker = true) + return readMarkersTask.execute(setReadMarkerParams) + } + + /** + * Tells if the created room can be a direct chat one. + * + * @return true if it is a direct chat + */ + private fun CreateRoomParams.isDirect(): Boolean { + return preset == CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT + && isDirect == true + } + + /** + * @return the first invited user id + */ + private fun CreateRoomParams.getFirstInvitedUserId(): String? { + return invitedUserIds.firstOrNull() ?: invite3pids.firstOrNull()?.value + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/RoomCreateEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/RoomCreateEventProcessor.kt new file mode 100644 index 0000000000..e6de3fbd71 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/RoomCreateEventProcessor.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.create + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.VersioningState +import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent +import org.matrix.android.sdk.internal.database.model.EventInsertType +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor +import io.realm.Realm +import javax.inject.Inject + +internal class RoomCreateEventProcessor @Inject constructor() : EventInsertLiveProcessor { + + override suspend fun process(realm: Realm, event: Event) { + val createRoomContent = event.getClearContent().toModel() + val predecessorRoomId = createRoomContent?.predecessor?.roomId ?: return + + val predecessorRoomSummary = RoomSummaryEntity.where(realm, predecessorRoomId).findFirst() + ?: RoomSummaryEntity(predecessorRoomId) + predecessorRoomSummary.versioningState = VersioningState.UPGRADED_ROOM_JOINED + realm.insertOrUpdate(predecessorRoomSummary) + } + + override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean { + return eventType == EventType.STATE_ROOM_CREATE + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/directory/GetPublicRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/directory/GetPublicRoomTask.kt new file mode 100644 index 0000000000..4d760d1b09 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/directory/GetPublicRoomTask.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.directory + +import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams +import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsResponse +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface GetPublicRoomTask : Task { + data class Params( + val server: String?, + val publicRoomsParams: PublicRoomsParams + ) +} + +internal class DefaultGetPublicRoomTask @Inject constructor( + private val roomAPI: RoomAPI, + private val eventBus: EventBus +) : GetPublicRoomTask { + + override suspend fun execute(params: GetPublicRoomTask.Params): PublicRoomsResponse { + return executeRequest(eventBus) { + apiCall = roomAPI.publicRooms(params.server, params.publicRoomsParams) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/directory/GetThirdPartyProtocolsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/directory/GetThirdPartyProtocolsTask.kt new file mode 100644 index 0000000000..39f1c60829 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/directory/GetThirdPartyProtocolsTask.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.directory + +import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface GetThirdPartyProtocolsTask : Task> + +internal class DefaultGetThirdPartyProtocolsTask @Inject constructor( + private val roomAPI: RoomAPI, + private val eventBus: EventBus +) : GetThirdPartyProtocolsTask { + + override suspend fun execute(params: Unit): Map { + return executeRequest(eventBus) { + apiCall = roomAPI.thirdPartyProtocols() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DefaultDraftService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DefaultDraftService.kt new file mode 100644 index 0000000000..dafa7df0eb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DefaultDraftService.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.draft + +import androidx.lifecycle.LiveData +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.room.send.DraftService +import org.matrix.android.sdk.api.session.room.send.UserDraft +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.launchToCallback +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers + +internal class DefaultDraftService @AssistedInject constructor(@Assisted private val roomId: String, + private val draftRepository: DraftRepository, + private val taskExecutor: TaskExecutor, + private val coroutineDispatchers: MatrixCoroutineDispatchers +) : DraftService { + + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): DraftService + } + + /** + * The draft stack can contain several drafts. Depending of the draft to save, it will update the top draft, or create a new draft, + * or even move an existing draft to the top of the list + */ + override fun saveDraft(draft: UserDraft, callback: MatrixCallback): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + draftRepository.saveDraft(roomId, draft) + } + } + + override fun deleteDraft(callback: MatrixCallback): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + draftRepository.deleteDraft(roomId) + } + } + + override fun getDraftsLive(): LiveData> { + return draftRepository.getDraftsLive(roomId) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DraftRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DraftRepository.kt new file mode 100644 index 0000000000..bc50b2d990 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DraftRepository.kt @@ -0,0 +1,158 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.draft + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.BuildConfig +import org.matrix.android.sdk.api.session.room.send.UserDraft +import org.matrix.android.sdk.internal.database.mapper.DraftMapper +import org.matrix.android.sdk.internal.database.model.DraftEntity +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.model.UserDraftsEntity +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.util.awaitTransaction +import io.realm.Realm +import io.realm.kotlin.createObject +import timber.log.Timber +import javax.inject.Inject + +class DraftRepository @Inject constructor(@SessionDatabase private val monarchy: Monarchy) { + + suspend fun saveDraft(roomId: String, userDraft: UserDraft) { + monarchy.awaitTransaction { + saveDraft(it, userDraft, roomId) + } + } + + suspend fun deleteDraft(roomId: String) { + monarchy.awaitTransaction { + deleteDraft(it, roomId) + } + } + + private fun deleteDraft(realm: Realm, roomId: String) { + UserDraftsEntity.where(realm, roomId).findFirst()?.let { userDraftsEntity -> + if (userDraftsEntity.userDrafts.isNotEmpty()) { + userDraftsEntity.userDrafts.removeAt(userDraftsEntity.userDrafts.size - 1) + } + } + } + + private fun saveDraft(realm: Realm, draft: UserDraft, roomId: String) { + val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() + ?: realm.createObject(roomId) + + val userDraftsEntity = roomSummaryEntity.userDrafts + ?: realm.createObject().also { + roomSummaryEntity.userDrafts = it + } + + userDraftsEntity.let { userDraftEntity -> + // Save only valid draft + if (draft.isValid()) { + // Add a new draft or update the current one? + val newDraft = DraftMapper.map(draft) + + // Is it an update of the top draft? + val topDraft = userDraftEntity.userDrafts.lastOrNull() + + if (topDraft == null) { + Timber.d("Draft: create a new draft ${privacySafe(draft)}") + userDraftEntity.userDrafts.add(newDraft) + } else if (topDraft.draftMode == DraftEntity.MODE_EDIT) { + // top draft is an edit + if (newDraft.draftMode == DraftEntity.MODE_EDIT) { + if (topDraft.linkedEventId == newDraft.linkedEventId) { + // Update the top draft + Timber.d("Draft: update the top edit draft ${privacySafe(draft)}") + topDraft.content = newDraft.content + } else { + // Check a previously EDIT draft with the same id + val existingEditDraftOfSameEvent = userDraftEntity.userDrafts.find { + it.draftMode == DraftEntity.MODE_EDIT && it.linkedEventId == newDraft.linkedEventId + } + + if (existingEditDraftOfSameEvent != null) { + // Ignore the new text, restore what was typed before, by putting the draft to the top + Timber.d("Draft: restore a previously edit draft ${privacySafe(draft)}") + userDraftEntity.userDrafts.remove(existingEditDraftOfSameEvent) + userDraftEntity.userDrafts.add(existingEditDraftOfSameEvent) + } else { + Timber.d("Draft: add a new edit draft ${privacySafe(draft)}") + userDraftEntity.userDrafts.add(newDraft) + } + } + } else { + // Add a new regular draft to the top + Timber.d("Draft: add a new draft ${privacySafe(draft)}") + userDraftEntity.userDrafts.add(newDraft) + } + } else { + // Top draft is not an edit + if (newDraft.draftMode == DraftEntity.MODE_EDIT) { + Timber.d("Draft: create a new edit draft ${privacySafe(draft)}") + userDraftEntity.userDrafts.add(newDraft) + } else { + // Update the top draft + Timber.d("Draft: update the top draft ${privacySafe(draft)}") + topDraft.draftMode = newDraft.draftMode + topDraft.content = newDraft.content + topDraft.linkedEventId = newDraft.linkedEventId + } + } + } else { + // There is no draft to save, so the composer was clear + Timber.d("Draft: delete a draft") + + val topDraft = userDraftEntity.userDrafts.lastOrNull() + + if (topDraft == null) { + Timber.d("Draft: nothing to do") + } else { + // Remove the top draft + Timber.d("Draft: remove the top draft") + userDraftEntity.userDrafts.remove(topDraft) + } + } + } + } + + fun getDraftsLive(roomId: String): LiveData> { + val liveData = monarchy.findAllMappedWithChanges( + { UserDraftsEntity.where(it, roomId) }, + { + it.userDrafts.map { draft -> + DraftMapper.map(draft) + } + } + ) + return Transformations.map(liveData) { + it.firstOrNull().orEmpty() + } + } + + private fun privacySafe(o: Any): Any { + if (BuildConfig.LOG_PRIVATE_DATA) { + return o + } + return "" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt new file mode 100644 index 0000000000..91039f4c0f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt @@ -0,0 +1,185 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.membership + +import androidx.lifecycle.LiveData +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.identity.ThreePid +import org.matrix.android.sdk.api.session.room.members.MembershipService +import org.matrix.android.sdk.api.session.room.members.RoomMemberQueryParams +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.database.mapper.asDomain +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.query.process +import org.matrix.android.sdk.internal.session.room.membership.admin.MembershipAdminTask +import org.matrix.android.sdk.internal.session.room.membership.joining.InviteTask +import org.matrix.android.sdk.internal.session.room.membership.joining.JoinRoomTask +import org.matrix.android.sdk.internal.session.room.membership.leaving.LeaveRoomTask +import org.matrix.android.sdk.internal.session.room.membership.threepid.InviteThreePidTask +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import org.matrix.android.sdk.internal.util.fetchCopied +import io.realm.Realm +import io.realm.RealmQuery + +internal class DefaultMembershipService @AssistedInject constructor( + @Assisted private val roomId: String, + @SessionDatabase private val monarchy: Monarchy, + private val taskExecutor: TaskExecutor, + private val loadRoomMembersTask: LoadRoomMembersTask, + private val inviteTask: InviteTask, + private val inviteThreePidTask: InviteThreePidTask, + private val joinTask: JoinRoomTask, + private val leaveRoomTask: LeaveRoomTask, + private val membershipAdminTask: MembershipAdminTask, + @UserId + private val userId: String +) : MembershipService { + + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): MembershipService + } + + override fun loadRoomMembersIfNeeded(matrixCallback: MatrixCallback): Cancelable { + val params = LoadRoomMembersTask.Params(roomId, Membership.LEAVE) + return loadRoomMembersTask + .configureWith(params) { + this.callback = matrixCallback + } + .executeBy(taskExecutor) + } + + override fun getRoomMember(userId: String): RoomMemberSummary? { + val roomMemberEntity = monarchy.fetchCopied { + RoomMemberHelper(it, roomId).getLastRoomMember(userId) + } + return roomMemberEntity?.asDomain() + } + + override fun getRoomMembers(queryParams: RoomMemberQueryParams): List { + return monarchy.fetchAllMappedSync( + { + roomMembersQuery(it, queryParams) + }, + { + it.asDomain() + } + ) + } + + override fun getRoomMembersLive(queryParams: RoomMemberQueryParams): LiveData> { + return monarchy.findAllMappedWithChanges( + { + roomMembersQuery(it, queryParams) + }, + { + it.asDomain() + } + ) + } + + private fun roomMembersQuery(realm: Realm, queryParams: RoomMemberQueryParams): RealmQuery { + return RoomMemberHelper(realm, roomId).queryRoomMembersEvent() + .process(RoomMemberSummaryEntityFields.USER_ID, queryParams.userId) + .process(RoomMemberSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships) + .process(RoomMemberSummaryEntityFields.DISPLAY_NAME, queryParams.displayName) + .apply { + if (queryParams.excludeSelf) { + notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId) + } + } + } + + override fun getNumberOfJoinedMembers(): Int { + return Realm.getInstance(monarchy.realmConfiguration).use { + RoomMemberHelper(it, roomId).getNumberOfJoinedMembers() + } + } + + override fun ban(userId: String, reason: String?, callback: MatrixCallback): Cancelable { + val params = MembershipAdminTask.Params(MembershipAdminTask.Type.BAN, roomId, userId, reason) + return membershipAdminTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun unban(userId: String, reason: String?, callback: MatrixCallback): Cancelable { + val params = MembershipAdminTask.Params(MembershipAdminTask.Type.UNBAN, roomId, userId, reason) + return membershipAdminTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun kick(userId: String, reason: String?, callback: MatrixCallback): Cancelable { + val params = MembershipAdminTask.Params(MembershipAdminTask.Type.KICK, roomId, userId, reason) + return membershipAdminTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun invite(userId: String, reason: String?, callback: MatrixCallback): Cancelable { + val params = InviteTask.Params(roomId, userId, reason) + return inviteTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun invite3pid(threePid: ThreePid, callback: MatrixCallback): Cancelable { + val params = InviteThreePidTask.Params(roomId, threePid) + return inviteThreePidTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun join(reason: String?, viaServers: List, callback: MatrixCallback): Cancelable { + val params = JoinRoomTask.Params(roomId, reason, viaServers) + return joinTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun leave(reason: String?, callback: MatrixCallback): Cancelable { + val params = LeaveRoomTask.Params(roomId, reason) + return leaveRoomTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt new file mode 100644 index 0000000000..e51a4605c8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.membership + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.database.mapper.toEntity +import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity +import org.matrix.android.sdk.internal.database.model.EventInsertType +import org.matrix.android.sdk.internal.database.model.RoomEntity +import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore +import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater +import org.matrix.android.sdk.internal.session.sync.SyncTokenStore +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import io.realm.Realm +import io.realm.kotlin.createObject +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface LoadRoomMembersTask : Task { + + data class Params( + val roomId: String, + val excludeMembership: Membership? = null + ) +} + +internal class DefaultLoadRoomMembersTask @Inject constructor( + private val roomAPI: RoomAPI, + @SessionDatabase private val monarchy: Monarchy, + private val syncTokenStore: SyncTokenStore, + private val roomSummaryUpdater: RoomSummaryUpdater, + private val roomMemberEventHandler: RoomMemberEventHandler, + private val eventBus: EventBus +) : LoadRoomMembersTask { + + override suspend fun execute(params: LoadRoomMembersTask.Params) { + if (areAllMembersAlreadyLoaded(params.roomId)) { + return + } + val lastToken = syncTokenStore.getLastToken() + val response = executeRequest(eventBus) { + apiCall = roomAPI.getMembers(params.roomId, lastToken, null, params.excludeMembership?.value) + } + insertInDb(response, params.roomId) + } + + private suspend fun insertInDb(response: RoomMembersResponse, roomId: String) { + monarchy.awaitTransaction { realm -> + // We ignore all the already known members + val roomEntity = RoomEntity.where(realm, roomId).findFirst() + ?: realm.createObject(roomId) + val now = System.currentTimeMillis() + for (roomMemberEvent in response.roomMemberEvents) { + if (roomMemberEvent.eventId == null || roomMemberEvent.stateKey == null) { + continue + } + val ageLocalTs = roomMemberEvent.unsignedData?.age?.let { now - it } + val eventEntity = roomMemberEvent.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) + CurrentStateEventEntity.getOrCreate(realm, roomId, roomMemberEvent.stateKey, roomMemberEvent.type).apply { + eventId = roomMemberEvent.eventId + root = eventEntity + } + roomMemberEventHandler.handle(realm, roomId, roomMemberEvent) + } + roomEntity.areAllMembersLoaded = true + roomSummaryUpdater.update(realm, roomId, updateMembers = true) + } + } + + private fun areAllMembersAlreadyLoaded(roomId: String): Boolean { + return Realm.getInstance(monarchy.realmConfiguration).use { + RoomEntity.where(it, roomId).findFirst()?.areAllMembersLoaded ?: false + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomChangeMembershipStateDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomChangeMembershipStateDataSource.kt new file mode 100644 index 0000000000..00d54a62e7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomChangeMembershipStateDataSource.kt @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.membership + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.internal.session.SessionScope +import javax.inject.Inject + +/** + * This class holds information about rooms that current user is joining or leaving. + */ +@SessionScope +internal class RoomChangeMembershipStateDataSource @Inject constructor() { + + private val mutableLiveStates = MutableLiveData>(emptyMap()) + private val states = HashMap() + + /** + * This will update local states to be synced with the server. + */ + fun setMembershipFromSync(roomId: String, membership: Membership) { + if (states.containsKey(roomId)) { + val newState = membership.toMembershipChangeState() + updateState(roomId, newState) + } + } + + fun updateState(roomId: String, state: ChangeMembershipState) { + states[roomId] = state + mutableLiveStates.postValue(states.toMap()) + } + + fun getLiveStates(): LiveData> { + return mutableLiveStates + } + + fun getState(roomId: String): ChangeMembershipState { + return states.getOrElse(roomId) { + ChangeMembershipState.Unknown + } + } + + private fun Membership.toMembershipChangeState(): ChangeMembershipState { + return when { + this == Membership.JOIN -> ChangeMembershipState.Joined + this.isLeft() -> ChangeMembershipState.Left + else -> ChangeMembershipState.Unknown + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt new file mode 100644 index 0000000000..d11226bdb1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt @@ -0,0 +1,137 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.membership + +import android.content.Context +import org.matrix.android.sdk.R +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.RoomAliasesContent +import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent +import org.matrix.android.sdk.api.session.room.model.RoomNameContent +import org.matrix.android.sdk.internal.database.mapper.ContentMapper +import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity +import org.matrix.android.sdk.internal.database.model.RoomEntity +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.query.getOrNull +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.UserId +import io.realm.Realm +import javax.inject.Inject + +/** + * This class computes room display name + */ +internal class RoomDisplayNameResolver @Inject constructor(private val context: Context, + @UserId private val userId: String +) { + + /** + * Compute the room display name + * + * @param realm: the current instance of realm + * @param roomId: the roomId to resolve the name of. + * @return the room display name + */ + fun resolve(realm: Realm, roomId: String): CharSequence { + // this algorithm is the one defined in + // https://github.com/matrix-org/matrix-js-sdk/blob/develop/lib/models/room.js#L617 + // calculateRoomName(room, userId) + + // For Lazy Loaded room, see algorithm here: + // https://docs.google.com/document/d/11i14UI1cUz-OJ0knD5BFu7fmT6Fo327zvMYqfSAR7xs/edit#heading=h.qif6pkqyjgzn + var name: CharSequence? + val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() + val roomName = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_NAME, stateKey = "")?.root + name = ContentMapper.map(roomName?.content).toModel()?.name + if (!name.isNullOrEmpty()) { + return name + } + val canonicalAlias = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_CANONICAL_ALIAS, stateKey = "")?.root + name = ContentMapper.map(canonicalAlias?.content).toModel()?.canonicalAlias + if (!name.isNullOrEmpty()) { + return name + } + + val aliases = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_ALIASES, stateKey = "")?.root + name = ContentMapper.map(aliases?.content).toModel()?.aliases?.firstOrNull() + if (!name.isNullOrEmpty()) { + return name + } + + val roomMembers = RoomMemberHelper(realm, roomId) + val activeMembers = roomMembers.queryActiveRoomMembersEvent().findAll() + + if (roomEntity?.membership == Membership.INVITE) { + val inviteMeEvent = roomMembers.getLastStateEvent(userId) + val inviterId = inviteMeEvent?.sender + name = if (inviterId != null) { + activeMembers.where() + .equalTo(RoomMemberSummaryEntityFields.USER_ID, inviterId) + .findFirst() + ?.displayName + } else { + context.getString(R.string.room_displayname_room_invite) + } + } else if (roomEntity?.membership == Membership.JOIN) { + val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() + val otherMembersSubset: List = if (roomSummary?.heroes?.isNotEmpty() == true) { + roomSummary.heroes.mapNotNull { userId -> + roomMembers.getLastRoomMember(userId)?.takeIf { + it.membership == Membership.INVITE || it.membership == Membership.JOIN + } + } + } else { + activeMembers.where() + .notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId) + .limit(3) + .findAll() + .createSnapshot() + } + val otherMembersCount = otherMembersSubset.count() + name = when (otherMembersCount) { + 0 -> context.getString(R.string.room_displayname_empty_room) + 1 -> resolveRoomMemberName(otherMembersSubset[0], roomMembers) + 2 -> context.getString(R.string.room_displayname_two_members, + resolveRoomMemberName(otherMembersSubset[0], roomMembers), + resolveRoomMemberName(otherMembersSubset[1], roomMembers) + ) + else -> context.resources.getQuantityString(R.plurals.room_displayname_three_and_more_members, + roomMembers.getNumberOfJoinedMembers() - 1, + resolveRoomMemberName(otherMembersSubset[0], roomMembers), + roomMembers.getNumberOfJoinedMembers() - 1) + } + } + return name ?: roomId + } + + /** See [org.matrix.android.sdk.api.session.room.sender.SenderInfo.disambiguatedDisplayName] */ + private fun resolveRoomMemberName(roomMemberSummary: RoomMemberSummaryEntity?, + roomMemberHelper: RoomMemberHelper): String? { + if (roomMemberSummary == null) return null + val isUnique = roomMemberHelper.isUniqueDisplayName(roomMemberSummary.displayName) + return if (isUnique) { + roomMemberSummary.displayName + } else { + "${roomMemberSummary.displayName} (${roomMemberSummary.userId})" + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEntityFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEntityFactory.kt new file mode 100644 index 0000000000..f526550918 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEntityFactory.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.membership + +import org.matrix.android.sdk.api.session.room.model.RoomMemberContent +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity + +internal object RoomMemberEntityFactory { + + fun create(roomId: String, userId: String, roomMember: RoomMemberContent): RoomMemberSummaryEntity { + val primaryKey = "${roomId}_$userId" + return RoomMemberSummaryEntity( + primaryKey = primaryKey, + userId = userId, + roomId = roomId, + displayName = roomMember.displayName, + avatarUrl = roomMember.avatarUrl + ).apply { + membership = roomMember.membership + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEventHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEventHandler.kt new file mode 100644 index 0000000000..2821eb2fb9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEventHandler.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.membership + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.RoomMemberContent +import org.matrix.android.sdk.internal.session.user.UserEntityFactory +import io.realm.Realm +import javax.inject.Inject + +internal class RoomMemberEventHandler @Inject constructor() { + + fun handle(realm: Realm, roomId: String, event: Event): Boolean { + if (event.type != EventType.STATE_ROOM_MEMBER) { + return false + } + val userId = event.stateKey ?: return false + val roomMember = event.content.toModel() + return handle(realm, roomId, userId, roomMember) + } + + fun handle(realm: Realm, roomId: String, userId: String, roomMember: RoomMemberContent?): Boolean { + if (roomMember == null) { + return false + } + val roomMemberEntity = RoomMemberEntityFactory.create(roomId, userId, roomMember) + realm.insertOrUpdate(roomMemberEntity) + if (roomMember.membership.isActive()) { + val userEntity = UserEntityFactory.create(userId, roomMember) + realm.insertOrUpdate(userEntity) + } + return true + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberHelper.kt new file mode 100644 index 0000000000..4ef2d973c0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberHelper.kt @@ -0,0 +1,119 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.membership + +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.query.getOrNull +import org.matrix.android.sdk.internal.database.query.where +import io.realm.Realm +import io.realm.RealmQuery + +/** + * This class is an helper around STATE_ROOM_MEMBER events. + * It allows to get the live membership of a user. + */ + +internal class RoomMemberHelper(private val realm: Realm, + private val roomId: String +) { + + private val roomSummary: RoomSummaryEntity? by lazy { + RoomSummaryEntity.where(realm, roomId).findFirst() + } + + fun getLastStateEvent(userId: String): EventEntity? { + return CurrentStateEventEntity.getOrNull(realm, roomId, userId, EventType.STATE_ROOM_MEMBER)?.root + } + + fun getLastRoomMember(userId: String): RoomMemberSummaryEntity? { + return RoomMemberSummaryEntity + .where(realm, roomId, userId) + .findFirst() + } + + fun isUniqueDisplayName(displayName: String?): Boolean { + if (displayName.isNullOrEmpty()) { + return true + } + return RoomMemberSummaryEntity.where(realm, roomId) + .equalTo(RoomMemberSummaryEntityFields.DISPLAY_NAME, displayName) + .findAll() + .size == 1 + } + + fun queryRoomMembersEvent(): RealmQuery { + return RoomMemberSummaryEntity.where(realm, roomId) + } + + fun queryJoinedRoomMembersEvent(): RealmQuery { + return queryRoomMembersEvent() + .equalTo(RoomMemberSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name) + } + + fun queryInvitedRoomMembersEvent(): RealmQuery { + return queryRoomMembersEvent() + .equalTo(RoomMemberSummaryEntityFields.MEMBERSHIP_STR, Membership.INVITE.name) + } + + fun queryActiveRoomMembersEvent(): RealmQuery { + return queryRoomMembersEvent() + .beginGroup() + .equalTo(RoomMemberSummaryEntityFields.MEMBERSHIP_STR, Membership.INVITE.name) + .or() + .equalTo(RoomMemberSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name) + .endGroup() + } + + fun getNumberOfJoinedMembers(): Int { + return roomSummary?.joinedMembersCount + ?: queryJoinedRoomMembersEvent().findAll().size + } + + fun getNumberOfInvitedMembers(): Int { + return roomSummary?.invitedMembersCount + ?: queryInvitedRoomMembersEvent().findAll().size + } + + fun getNumberOfMembers(): Int { + return getNumberOfJoinedMembers() + getNumberOfInvitedMembers() + } + + /** + * Return all the roomMembers ids which are joined or invited to the room + * + * @return a roomMember id list of joined or invited members. + */ + fun getActiveRoomMemberIds(): List { + return queryActiveRoomMembersEvent().findAll().map { it.userId } + } + + /** + * Return all the roomMembers ids which are joined to the room + * + * @return a roomMember id list of joined members. + */ + fun getJoinedRoomMemberIds(): List { + return queryJoinedRoomMembersEvent().findAll().map { it.userId } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMembersResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMembersResponse.kt new file mode 100644 index 0000000000..d8ac0983eb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMembersResponse.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.membership + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Event + +@JsonClass(generateAdapter = true) +internal data class RoomMembersResponse( + @Json(name = "chunk") val roomMemberEvents: List +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/admin/MembershipAdminTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/admin/MembershipAdminTask.kt new file mode 100644 index 0000000000..948bab39fb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/admin/MembershipAdminTask.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.membership.admin + +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal interface MembershipAdminTask : Task { + + enum class Type { + BAN, + UNBAN, + KICK + } + + data class Params( + val type: Type, + val roomId: String, + val userId: String, + val reason: String? + ) +} + +internal class DefaultMembershipAdminTask @Inject constructor(private val roomAPI: RoomAPI) : MembershipAdminTask { + + override suspend fun execute(params: MembershipAdminTask.Params) { + val userIdAndReason = UserIdAndReason(params.userId, params.reason) + executeRequest(null) { + apiCall = when (params.type) { + MembershipAdminTask.Type.BAN -> roomAPI.ban(params.roomId, userIdAndReason) + MembershipAdminTask.Type.UNBAN -> roomAPI.unban(params.roomId, userIdAndReason) + MembershipAdminTask.Type.KICK -> roomAPI.kick(params.roomId, userIdAndReason) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/admin/UserIdAndReason.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/admin/UserIdAndReason.kt new file mode 100644 index 0000000000..4589b5e4bc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/admin/UserIdAndReason.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.room.membership.admin + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class UserIdAndReason( + @Json(name = "user_id") val userId: String, + @Json(name = "reason") val reason: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/InviteBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/InviteBody.kt new file mode 100644 index 0000000000..6fad0b10b7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/InviteBody.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.membership.joining + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class InviteBody( + @Json(name = "user_id") val userId: String, + @Json(name = "reason") val reason: String? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/InviteTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/InviteTask.kt new file mode 100644 index 0000000000..4b9935d2fa --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/InviteTask.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.membership.joining + +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface InviteTask : Task { + data class Params( + val roomId: String, + val userId: String, + val reason: String? + ) +} + +internal class DefaultInviteTask @Inject constructor( + private val roomAPI: RoomAPI, + private val eventBus: EventBus +) : InviteTask { + + override suspend fun execute(params: InviteTask.Params) { + return executeRequest(eventBus) { + val body = InviteBody(params.userId, params.reason) + apiCall = roomAPI.invite(params.roomId, body) + isRetryable = true + maxRetryCount = 3 + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt new file mode 100644 index 0000000000..f3e2efcde3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.membership.joining + +import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure +import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState +import org.matrix.android.sdk.internal.database.awaitNotEmptyResult +import org.matrix.android.sdk.internal.database.model.RoomEntity +import org.matrix.android.sdk.internal.database.model.RoomEntityFields +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.session.room.create.JoinRoomResponse +import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource +import org.matrix.android.sdk.internal.session.room.read.SetReadMarkersTask +import org.matrix.android.sdk.internal.task.Task +import io.realm.RealmConfiguration +import kotlinx.coroutines.TimeoutCancellationException +import org.greenrobot.eventbus.EventBus +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +internal interface JoinRoomTask : Task { + data class Params( + val roomIdOrAlias: String, + val reason: String?, + val viaServers: List = emptyList() + ) +} + +internal class DefaultJoinRoomTask @Inject constructor( + private val roomAPI: RoomAPI, + private val readMarkersTask: SetReadMarkersTask, + @SessionDatabase + private val realmConfiguration: RealmConfiguration, + private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource, + private val eventBus: EventBus +) : JoinRoomTask { + + override suspend fun execute(params: JoinRoomTask.Params) { + roomChangeMembershipStateDataSource.updateState(params.roomIdOrAlias, ChangeMembershipState.Joining) + val joinRoomResponse = try { + executeRequest(eventBus) { + apiCall = roomAPI.join(params.roomIdOrAlias, params.viaServers, mapOf("reason" to params.reason)) + } + } catch (failure: Throwable) { + roomChangeMembershipStateDataSource.updateState(params.roomIdOrAlias, ChangeMembershipState.FailedJoining(failure)) + throw failure + } + // Wait for room to come back from the sync (but it can maybe be in the DB is the sync response is received before) + val roomId = joinRoomResponse.roomId + try { + awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm -> + realm.where(RoomEntity::class.java) + .equalTo(RoomEntityFields.ROOM_ID, roomId) + } + } catch (exception: TimeoutCancellationException) { + throw JoinRoomFailure.JoinedWithTimeout + } + setReadMarkers(roomId) + } + + private suspend fun setReadMarkers(roomId: String) { + val setReadMarkerParams = SetReadMarkersTask.Params(roomId, forceReadMarker = true, forceReadReceipt = true) + readMarkersTask.execute(setReadMarkerParams) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/leaving/LeaveRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/leaving/LeaveRoomTask.kt new file mode 100644 index 0000000000..2a3e6c0aaa --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/leaving/LeaveRoomTask.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.membership.leaving + +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState +import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource +import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource +import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import javax.inject.Inject + +internal interface LeaveRoomTask : Task { + data class Params( + val roomId: String, + val reason: String? + ) +} + +internal class DefaultLeaveRoomTask @Inject constructor( + private val roomAPI: RoomAPI, + private val eventBus: EventBus, + private val stateEventDataSource: StateEventDataSource, + private val roomSummaryDataSource: RoomSummaryDataSource, + private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource +) : LeaveRoomTask { + + override suspend fun execute(params: LeaveRoomTask.Params) { + leaveRoom(params.roomId, params.reason) + } + + private suspend fun leaveRoom(roomId: String, reason: String?) { + val roomSummary = roomSummaryDataSource.getRoomSummary(roomId) + if (roomSummary?.membership?.isActive() == false) { + Timber.v("Room $roomId is not joined so can't be left") + return + } + roomChangeMembershipStateDataSource.updateState(roomId, ChangeMembershipState.Leaving) + val roomCreateStateEvent = stateEventDataSource.getStateEvent( + roomId = roomId, + eventType = EventType.STATE_ROOM_CREATE, + stateKey = QueryStringValue.NoCondition + ) + // Server is not cleaning predecessor rooms, so we also try to left them + val predecessorRoomId = roomCreateStateEvent?.getClearContent()?.toModel()?.predecessor?.roomId + if (predecessorRoomId != null) { + leaveRoom(predecessorRoomId, reason) + } + try { + executeRequest(eventBus) { + apiCall = roomAPI.leave(roomId, mapOf("reason" to reason)) + } + } catch (failure: Throwable) { + roomChangeMembershipStateDataSource.updateState(roomId, ChangeMembershipState.FailedLeaving(failure)) + throw failure + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/threepid/InviteThreePidTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/threepid/InviteThreePidTask.kt new file mode 100644 index 0000000000..b18e44360d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/threepid/InviteThreePidTask.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.membership.threepid + +import org.matrix.android.sdk.api.session.identity.IdentityServiceError +import org.matrix.android.sdk.api.session.identity.ThreePid +import org.matrix.android.sdk.api.session.identity.toMedium +import org.matrix.android.sdk.internal.di.AuthenticatedIdentity +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.network.token.AccessTokenProvider +import org.matrix.android.sdk.internal.session.identity.EnsureIdentityTokenTask +import org.matrix.android.sdk.internal.session.identity.data.IdentityStore +import org.matrix.android.sdk.internal.session.identity.data.getIdentityServerUrlWithoutProtocol +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface InviteThreePidTask : Task { + data class Params( + val roomId: String, + val threePid: ThreePid + ) +} + +internal class DefaultInviteThreePidTask @Inject constructor( + private val roomAPI: RoomAPI, + private val eventBus: EventBus, + private val identityStore: IdentityStore, + private val ensureIdentityTokenTask: EnsureIdentityTokenTask, + @AuthenticatedIdentity + private val accessTokenProvider: AccessTokenProvider +) : InviteThreePidTask { + + override suspend fun execute(params: InviteThreePidTask.Params) { + ensureIdentityTokenTask.execute(Unit) + + val identityServerUrlWithoutProtocol = identityStore.getIdentityServerUrlWithoutProtocol() ?: throw IdentityServiceError.NoIdentityServerConfigured + val identityServerAccessToken = accessTokenProvider.getToken() ?: throw IdentityServiceError.NoIdentityServerConfigured + + return executeRequest(eventBus) { + val body = ThreePidInviteBody( + id_server = identityServerUrlWithoutProtocol, + id_access_token = identityServerAccessToken, + medium = params.threePid.toMedium(), + address = params.threePid.value + ) + apiCall = roomAPI.invite3pid(params.roomId, body) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/threepid/ThreePidInviteBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/threepid/ThreePidInviteBody.kt new file mode 100644 index 0000000000..93b5c577fc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/threepid/ThreePidInviteBody.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.membership.threepid + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class ThreePidInviteBody( + /** + * Required. The hostname+port of the identity server which should be used for third party identifier lookups. + */ + @Json(name = "id_server") val id_server: String, + /** + * Required. An access token previously registered with the identity server. Servers can treat this as optional + * to distinguish between r0.5-compatible clients and this specification version. + */ + @Json(name = "id_access_token") val id_access_token: String, + /** + * Required. The kind of address being passed in the address field, for example email. + */ + @Json(name = "medium") val medium: String, + /** + * Required. The invitee's third party identifier. + */ + @Json(name = "address") val address: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/DefaultRoomPushRuleService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/DefaultRoomPushRuleService.kt new file mode 100644 index 0000000000..93e2881c13 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/DefaultRoomPushRuleService.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.notification + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.pushrules.RuleScope +import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState +import org.matrix.android.sdk.api.session.room.notification.RoomPushRuleService +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.database.model.PushRuleEntity +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith + +internal class DefaultRoomPushRuleService @AssistedInject constructor(@Assisted private val roomId: String, + private val setRoomNotificationStateTask: SetRoomNotificationStateTask, + @SessionDatabase private val monarchy: Monarchy, + private val taskExecutor: TaskExecutor) + : RoomPushRuleService { + + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): RoomPushRuleService + } + + override fun getLiveRoomNotificationState(): LiveData { + return Transformations.map(getPushRuleForRoom()) { + it?.toRoomNotificationState() ?: RoomNotificationState.ALL_MESSAGES + } + } + + override fun setRoomNotificationState(roomNotificationState: RoomNotificationState, matrixCallback: MatrixCallback): Cancelable { + return setRoomNotificationStateTask + .configureWith(SetRoomNotificationStateTask.Params(roomId, roomNotificationState)) { + this.callback = matrixCallback + } + .executeBy(taskExecutor) + } + + private fun getPushRuleForRoom(): LiveData { + val liveData = monarchy.findAllMappedWithChanges( + { realm -> + PushRuleEntity.where(realm, scope = RuleScope.GLOBAL, ruleId = roomId) + }, + { result -> + result.toRoomPushRule() + } + ) + return Transformations.map(liveData) { results -> + results.firstOrNull() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/RoomPushRule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/RoomPushRule.kt new file mode 100644 index 0000000000..a7c7719342 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/RoomPushRule.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.notification + +import org.matrix.android.sdk.api.pushrules.RuleKind +import org.matrix.android.sdk.api.pushrules.rest.PushRule + +internal data class RoomPushRule( + val kind: RuleKind, + val rule: PushRule +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/RoomPushRuleMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/RoomPushRuleMapper.kt new file mode 100644 index 0000000000..1a19a40602 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/RoomPushRuleMapper.kt @@ -0,0 +1,106 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.notification + +import org.matrix.android.sdk.api.pushrules.Action +import org.matrix.android.sdk.api.pushrules.Condition +import org.matrix.android.sdk.api.pushrules.RuleSetKey +import org.matrix.android.sdk.api.pushrules.getActions +import org.matrix.android.sdk.api.pushrules.rest.PushCondition +import org.matrix.android.sdk.api.pushrules.rest.PushRule +import org.matrix.android.sdk.api.pushrules.toJson +import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState +import org.matrix.android.sdk.internal.database.mapper.PushRulesMapper +import org.matrix.android.sdk.internal.database.model.PushRuleEntity + +internal fun PushRuleEntity.toRoomPushRule(): RoomPushRule? { + val kind = parent?.firstOrNull()?.kind + val pushRule = when (kind) { + RuleSetKey.OVERRIDE -> { + PushRulesMapper.map(this) + } + RuleSetKey.ROOM -> { + PushRulesMapper.mapRoomRule(this) + } + else -> null + } + return if (pushRule == null || kind == null) { + null + } else { + RoomPushRule(kind, pushRule) + } +} + +internal fun RoomNotificationState.toRoomPushRule(roomId: String): RoomPushRule? { + return when { + this == RoomNotificationState.ALL_MESSAGES -> null + this == RoomNotificationState.ALL_MESSAGES_NOISY -> { + val rule = PushRule( + actions = listOf(Action.Notify, Action.Sound()).toJson(), + enabled = true, + ruleId = roomId + ) + return RoomPushRule(RuleSetKey.ROOM, rule) + } + else -> { + val condition = PushCondition( + kind = Condition.Kind.EventMatch.value, + key = "room_id", + pattern = roomId + ) + val rule = PushRule( + actions = listOf(Action.DoNotNotify).toJson(), + enabled = true, + ruleId = roomId, + conditions = listOf(condition) + ) + val kind = if (this == RoomNotificationState.MUTE) { + RuleSetKey.OVERRIDE + } else { + RuleSetKey.ROOM + } + return RoomPushRule(kind, rule) + } + } +} + +internal fun RoomPushRule.toRoomNotificationState(): RoomNotificationState { + return if (rule.enabled) { + val actions = rule.getActions() + if (actions.contains(Action.DoNotNotify)) { + if (kind == RuleSetKey.OVERRIDE) { + RoomNotificationState.MUTE + } else { + RoomNotificationState.MENTIONS_ONLY + } + } else if (actions.contains(Action.Notify)) { + val hasSoundAction = actions.find { + it is Action.Sound + } != null + if (hasSoundAction) { + RoomNotificationState.ALL_MESSAGES_NOISY + } else { + RoomNotificationState.ALL_MESSAGES + } + } else { + RoomNotificationState.ALL_MESSAGES + } + } else { + RoomNotificationState.ALL_MESSAGES + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/SetRoomNotificationStateTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/SetRoomNotificationStateTask.kt new file mode 100644 index 0000000000..5a1b8d2a65 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/SetRoomNotificationStateTask.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.notification + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.pushrules.RuleScope +import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState +import org.matrix.android.sdk.internal.database.model.PushRuleEntity +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.pushers.AddPushRuleTask +import org.matrix.android.sdk.internal.session.pushers.RemovePushRuleTask +import org.matrix.android.sdk.internal.task.Task +import io.realm.Realm +import javax.inject.Inject + +internal interface SetRoomNotificationStateTask : Task { + data class Params( + val roomId: String, + val roomNotificationState: RoomNotificationState + ) +} + +internal class DefaultSetRoomNotificationStateTask @Inject constructor(@SessionDatabase private val monarchy: Monarchy, + private val removePushRuleTask: RemovePushRuleTask, + private val addPushRuleTask: AddPushRuleTask) + : SetRoomNotificationStateTask { + + override suspend fun execute(params: SetRoomNotificationStateTask.Params) { + val currentRoomPushRule = Realm.getInstance(monarchy.realmConfiguration).use { + PushRuleEntity.where(it, scope = RuleScope.GLOBAL, ruleId = params.roomId).findFirst()?.toRoomPushRule() + } + if (currentRoomPushRule != null) { + removePushRuleTask.execute(RemovePushRuleTask.Params(currentRoomPushRule.kind, currentRoomPushRule.rule)) + } + val newRoomPushRule = params.roomNotificationState.toRoomPushRule(params.roomId) + if (newRoomPushRule != null) { + addPushRuleTask.execute(AddPushRuleTask.Params(newRoomPushRule.kind, newRoomPushRule.rule)) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt new file mode 100644 index 0000000000..5ff7ae69bb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt @@ -0,0 +1,126 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.prune + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.LocalEcho +import org.matrix.android.sdk.api.session.events.model.UnsignedData +import org.matrix.android.sdk.internal.database.mapper.ContentMapper +import org.matrix.android.sdk.internal.database.mapper.EventMapper +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.EventInsertType +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.query.findWithSenderMembershipEvent +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor +import io.realm.Realm +import timber.log.Timber +import javax.inject.Inject + +/** + * Listens to the database for the insertion of any redaction event. + * As it will actually delete the content, it should be called last in the list of listener. + */ +internal class RedactionEventProcessor @Inject constructor() : EventInsertLiveProcessor { + + override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean { + return eventType == EventType.REDACTION + } + + override suspend fun process(realm: Realm, event: Event) { + pruneEvent(realm, event) + } + + private fun pruneEvent(realm: Realm, redactionEvent: Event) { + if (redactionEvent.redacts.isNullOrBlank()) { + return + } + + // Check that we know this event + EventEntity.where(realm, eventId = redactionEvent.eventId ?: "").findFirst() ?: return + + val isLocalEcho = LocalEcho.isLocalEchoId(redactionEvent.eventId ?: "") + Timber.v("Redact event for ${redactionEvent.redacts} localEcho=$isLocalEcho") + + val eventToPrune = EventEntity.where(realm, eventId = redactionEvent.redacts).findFirst() + ?: return + + val typeToPrune = eventToPrune.type + val stateKey = eventToPrune.stateKey + val allowedKeys = computeAllowedKeys(typeToPrune) + if (allowedKeys.isNotEmpty()) { + val prunedContent = ContentMapper.map(eventToPrune.content)?.filterKeys { key -> allowedKeys.contains(key) } + eventToPrune.content = ContentMapper.map(prunedContent) + } else { + when (typeToPrune) { + EventType.ENCRYPTED, + EventType.MESSAGE -> { + Timber.d("REDACTION for message ${eventToPrune.eventId}") + val unsignedData = EventMapper.map(eventToPrune).unsignedData + ?: UnsignedData(null, null) + + // was this event a m.replace +// val contentModel = ContentMapper.map(eventToPrune.content)?.toModel() +// if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) { +// eventRelationsAggregationUpdater.handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm) +// } + + val modified = unsignedData.copy(redactedEvent = redactionEvent) + eventToPrune.content = ContentMapper.map(emptyMap()) + eventToPrune.unsignedData = MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(modified) + eventToPrune.decryptionResultJson = null + eventToPrune.decryptionErrorCode = null + } +// EventType.REACTION -> { +// eventRelationsAggregationUpdater.handleReactionRedact(eventToPrune, realm, userId) +// } + } + } + if (typeToPrune == EventType.STATE_ROOM_MEMBER && stateKey != null) { + TimelineEventEntity.findWithSenderMembershipEvent(realm, eventToPrune.eventId).forEach { + it.senderName = null + it.isUniqueDisplayName = false + it.senderAvatar = null + } + } + } + + private fun computeAllowedKeys(type: String): List { + // Add filtered content, allowed keys in content depends on the event type + return when (type) { + EventType.STATE_ROOM_MEMBER -> listOf("membership") + EventType.STATE_ROOM_CREATE -> listOf("creator") + EventType.STATE_ROOM_JOIN_RULES -> listOf("join_rule") + EventType.STATE_ROOM_POWER_LEVELS -> listOf("users", + "users_default", + "events", + "events_default", + "state_default", + "ban", + "kick", + "redact", + "invite") + EventType.STATE_ROOM_ALIASES -> listOf("aliases") + EventType.STATE_ROOM_CANONICAL_ALIAS -> listOf("alias") + EventType.FEEDBACK -> listOf("type", "target_event_id") + else -> emptyList() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt new file mode 100644 index 0000000000..a5520972b0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt @@ -0,0 +1,127 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.read + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.room.model.ReadReceipt +import org.matrix.android.sdk.api.session.room.read.ReadService +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.toOptional +import org.matrix.android.sdk.internal.database.mapper.ReadReceiptsSummaryMapper +import org.matrix.android.sdk.internal.database.model.ReadMarkerEntity +import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity +import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntity +import org.matrix.android.sdk.internal.database.query.isEventRead +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith + +internal class DefaultReadService @AssistedInject constructor( + @Assisted private val roomId: String, + @SessionDatabase private val monarchy: Monarchy, + private val taskExecutor: TaskExecutor, + private val setReadMarkersTask: SetReadMarkersTask, + private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper, + @UserId private val userId: String +) : ReadService { + + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): ReadService + } + + override fun markAsRead(params: ReadService.MarkAsReadParams, callback: MatrixCallback) { + val taskParams = SetReadMarkersTask.Params( + roomId = roomId, + forceReadMarker = params.forceReadMarker(), + forceReadReceipt = params.forceReadReceipt() + ) + setReadMarkersTask + .configureWith(taskParams) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun setReadReceipt(eventId: String, callback: MatrixCallback) { + val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = null, readReceiptEventId = eventId) + setReadMarkersTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun setReadMarker(fullyReadEventId: String, callback: MatrixCallback) { + val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = fullyReadEventId, readReceiptEventId = null) + setReadMarkersTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun isEventRead(eventId: String): Boolean { + return isEventRead(monarchy.realmConfiguration, userId, roomId, eventId) + } + + override fun getReadMarkerLive(): LiveData> { + val liveRealmData = monarchy.findAllMappedWithChanges( + { ReadMarkerEntity.where(it, roomId) }, + { it.eventId } + ) + return Transformations.map(liveRealmData) { + it.firstOrNull().toOptional() + } + } + + override fun getMyReadReceiptLive(): LiveData> { + val liveRealmData = monarchy.findAllMappedWithChanges( + { ReadReceiptEntity.where(it, roomId = roomId, userId = userId) }, + { it.eventId } + ) + return Transformations.map(liveRealmData) { + it.firstOrNull().toOptional() + } + } + + override fun getEventReadReceiptsLive(eventId: String): LiveData> { + val liveRealmData = monarchy.findAllMappedWithChanges( + { ReadReceiptsSummaryEntity.where(it, eventId) }, + { readReceiptsSummaryMapper.map(it) } + ) + return Transformations.map(liveRealmData) { + it.firstOrNull().orEmpty() + } + } + + private fun ReadService.MarkAsReadParams.forceReadMarker(): Boolean { + return this == ReadService.MarkAsReadParams.READ_MARKER || this == ReadService.MarkAsReadParams.BOTH + } + + private fun ReadService.MarkAsReadParams.forceReadReceipt(): Boolean { + return this == ReadService.MarkAsReadParams.READ_RECEIPT || this == ReadService.MarkAsReadParams.BOTH + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/FullyReadContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/FullyReadContent.kt new file mode 100644 index 0000000000..d2b216d625 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/FullyReadContent.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.read + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class FullyReadContent( + @Json(name = "event_id") val eventId: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/MarkAllRoomsReadTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/MarkAllRoomsReadTask.kt new file mode 100644 index 0000000000..b06b83aac3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/MarkAllRoomsReadTask.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.read + +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal interface MarkAllRoomsReadTask : Task { + data class Params( + val roomIds: List + ) +} + +internal class DefaultMarkAllRoomsReadTask @Inject constructor(private val readMarkersTask: SetReadMarkersTask) : MarkAllRoomsReadTask { + + override suspend fun execute(params: MarkAllRoomsReadTask.Params) { + params.roomIds.forEach { roomId -> + readMarkersTask.execute(SetReadMarkersTask.Params(roomId, forceReadMarker = true, forceReadReceipt = true)) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt new file mode 100644 index 0000000000..f750735bbb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt @@ -0,0 +1,132 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.read + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.events.model.LocalEcho +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.query.isEventRead +import org.matrix.android.sdk.internal.database.query.isReadMarkerMoreRecent +import org.matrix.android.sdk.internal.database.query.latestEvent +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.session.sync.ReadReceiptHandler +import org.matrix.android.sdk.internal.session.sync.RoomFullyReadHandler +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import io.realm.Realm +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import javax.inject.Inject +import kotlin.collections.set + +internal interface SetReadMarkersTask : Task { + + data class Params( + val roomId: String, + val fullyReadEventId: String? = null, + val readReceiptEventId: String? = null, + val forceReadReceipt: Boolean = false, + val forceReadMarker: Boolean = false + ) +} + +private const val READ_MARKER = "m.fully_read" +private const val READ_RECEIPT = "m.read" + +internal class DefaultSetReadMarkersTask @Inject constructor( + private val roomAPI: RoomAPI, + @SessionDatabase private val monarchy: Monarchy, + private val roomFullyReadHandler: RoomFullyReadHandler, + private val readReceiptHandler: ReadReceiptHandler, + @UserId private val userId: String, + private val eventBus: EventBus +) : SetReadMarkersTask { + + override suspend fun execute(params: SetReadMarkersTask.Params) { + val markers = HashMap() + Timber.v("Execute set read marker with params: $params") + val latestSyncedEventId = latestSyncedEventId(params.roomId) + val fullyReadEventId = if (params.forceReadMarker) { + latestSyncedEventId + } else { + params.fullyReadEventId + } + val readReceiptEventId = if (params.forceReadReceipt) { + latestSyncedEventId + } else { + params.readReceiptEventId + } + if (fullyReadEventId != null && !isReadMarkerMoreRecent(monarchy.realmConfiguration, params.roomId, fullyReadEventId)) { + if (LocalEcho.isLocalEchoId(fullyReadEventId)) { + Timber.w("Can't set read marker for local event $fullyReadEventId") + } else { + markers[READ_MARKER] = fullyReadEventId + } + } + if (readReceiptEventId != null + && !isEventRead(monarchy.realmConfiguration, userId, params.roomId, readReceiptEventId)) { + if (LocalEcho.isLocalEchoId(readReceiptEventId)) { + Timber.w("Can't set read receipt for local event $readReceiptEventId") + } else { + markers[READ_RECEIPT] = readReceiptEventId + } + } + + val shouldUpdateRoomSummary = readReceiptEventId != null && readReceiptEventId == latestSyncedEventId + if (markers.isNotEmpty() || shouldUpdateRoomSummary) { + updateDatabase(params.roomId, markers, shouldUpdateRoomSummary) + } + if (markers.isNotEmpty()) { + executeRequest(eventBus) { + isRetryable = true + apiCall = roomAPI.sendReadMarker(params.roomId, markers) + } + } + } + + private fun latestSyncedEventId(roomId: String): String? = + Realm.getInstance(monarchy.realmConfiguration).use { realm -> + TimelineEventEntity.latestEvent(realm, roomId = roomId, includesSending = false)?.eventId + } + + private suspend fun updateDatabase(roomId: String, markers: HashMap, shouldUpdateRoomSummary: Boolean) { + monarchy.awaitTransaction { realm -> + val readMarkerId = markers[READ_MARKER] + val readReceiptId = markers[READ_RECEIPT] + if (readMarkerId != null) { + roomFullyReadHandler.handle(realm, roomId, FullyReadContent(readMarkerId)) + } + if (readReceiptId != null) { + val readReceiptContent = ReadReceiptHandler.createContent(userId, readReceiptId) + readReceiptHandler.handle(realm, roomId, readReceiptContent, false) + } + if (shouldUpdateRoomSummary) { + val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() + ?: return@awaitTransaction + roomSummary.notificationCount = 0 + roomSummary.highlightCount = 0 + roomSummary.hasUnreadMessages = false + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt new file mode 100644 index 0000000000..458d98bd52 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt @@ -0,0 +1,240 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.room.relation + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import androidx.work.OneTimeWorkRequest +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary +import org.matrix.android.sdk.api.session.room.model.message.MessageType +import org.matrix.android.sdk.api.session.room.model.relation.RelationService +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.NoOpCancellable +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.toOptional +import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper +import org.matrix.android.sdk.internal.database.mapper.asDomain +import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.SessionId +import org.matrix.android.sdk.internal.session.room.send.EncryptEventWorker +import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory +import org.matrix.android.sdk.internal.session.room.send.RedactEventWorker +import org.matrix.android.sdk.internal.session.room.send.SendEventWorker +import org.matrix.android.sdk.internal.session.room.timeline.TimelineSendEventWorkCommon +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import org.matrix.android.sdk.internal.util.fetchCopyMap +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import timber.log.Timber + +internal class DefaultRelationService @AssistedInject constructor( + @Assisted private val roomId: String, + @SessionId private val sessionId: String, + private val timeLineSendEventWorkCommon: TimelineSendEventWorkCommon, + private val eventFactory: LocalEchoEventFactory, + private val cryptoService: CryptoService, + private val findReactionEventForUndoTask: FindReactionEventForUndoTask, + private val fetchEditHistoryTask: FetchEditHistoryTask, + private val timelineEventMapper: TimelineEventMapper, + @SessionDatabase private val monarchy: Monarchy, + private val taskExecutor: TaskExecutor) + : RelationService { + + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): RelationService + } + + override fun sendReaction(targetEventId: String, reaction: String): Cancelable { + return if (monarchy + .fetchCopyMap( + { realm -> + TimelineEventEntity.where(realm, roomId, targetEventId).findFirst() + }, + { entity, _ -> + timelineEventMapper.map(entity) + }) + ?.annotations + ?.reactionsSummary + .orEmpty() + .none { it.addedByMe && it.key == reaction }) { + val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction) + .also { saveLocalEcho(it) } + val sendRelationWork = createSendEventWork(event, true) + timeLineSendEventWorkCommon.postWork(roomId, sendRelationWork) + } else { + Timber.w("Reaction already added") + NoOpCancellable + } + } + + override fun undoReaction(targetEventId: String, reaction: String): Cancelable { + val params = FindReactionEventForUndoTask.Params( + roomId, + targetEventId, + reaction + ) + // TODO We should avoid using MatrixCallback internally + val callback = object : MatrixCallback { + override fun onSuccess(data: FindReactionEventForUndoTask.Result) { + if (data.redactEventId == null) { + Timber.w("Cannot find reaction to undo (not yet synced?)") + // TODO? + } + data.redactEventId?.let { toRedact -> + val redactEvent = eventFactory.createRedactEvent(roomId, toRedact, null) + .also { saveLocalEcho(it) } + val redactWork = createRedactEventWork(redactEvent, toRedact, null) + + timeLineSendEventWorkCommon.postWork(roomId, redactWork) + } + } + } + return findReactionEventForUndoTask + .configureWith(params) { + this.retryCount = Int.MAX_VALUE + this.callback = callback + } + .executeBy(taskExecutor) + } + + // TODO duplicate with send service? + private fun createRedactEventWork(localEvent: Event, eventId: String, reason: String?): OneTimeWorkRequest { + val sendContentWorkerParams = RedactEventWorker.Params( + sessionId, + localEvent.eventId!!, + roomId, + eventId, + reason) + val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) + return timeLineSendEventWorkCommon.createWork(redactWorkData, true) + } + + override fun editTextMessage(targetEventId: String, + msgType: String, + newBodyText: CharSequence, + newBodyAutoMarkdown: Boolean, + compatibilityBodyText: String): Cancelable { + val event = eventFactory + .createReplaceTextEvent(roomId, targetEventId, newBodyText, newBodyAutoMarkdown, msgType, compatibilityBodyText) + .also { saveLocalEcho(it) } + return if (cryptoService.isRoomEncrypted(roomId)) { + val encryptWork = createEncryptEventWork(event, listOf("m.relates_to")) + val workRequest = createSendEventWork(event, false) + timeLineSendEventWorkCommon.postSequentialWorks(roomId, encryptWork, workRequest) + } else { + val workRequest = createSendEventWork(event, true) + timeLineSendEventWorkCommon.postWork(roomId, workRequest) + } + } + + override fun editReply(replyToEdit: TimelineEvent, + originalTimelineEvent: TimelineEvent, + newBodyText: String, + compatibilityBodyText: String): Cancelable { + val event = eventFactory + .createReplaceTextOfReply(roomId, + replyToEdit, + originalTimelineEvent, + newBodyText, true, MessageType.MSGTYPE_TEXT, compatibilityBodyText) + .also { saveLocalEcho(it) } + return if (cryptoService.isRoomEncrypted(roomId)) { + val encryptWork = createEncryptEventWork(event, listOf("m.relates_to")) + val workRequest = createSendEventWork(event, false) + timeLineSendEventWorkCommon.postSequentialWorks(roomId, encryptWork, workRequest) + } else { + val workRequest = createSendEventWork(event, true) + timeLineSendEventWorkCommon.postWork(roomId, workRequest) + } + } + + override fun fetchEditHistory(eventId: String, callback: MatrixCallback>) { + val params = FetchEditHistoryTask.Params(roomId, cryptoService.isRoomEncrypted(roomId), eventId) + fetchEditHistoryTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun replyToMessage(eventReplied: TimelineEvent, replyText: CharSequence, autoMarkdown: Boolean): Cancelable? { + val event = eventFactory.createReplyTextEvent(roomId, eventReplied, replyText, autoMarkdown) + ?.also { saveLocalEcho(it) } + ?: return null + + return if (cryptoService.isRoomEncrypted(roomId)) { + val encryptWork = createEncryptEventWork(event, listOf("m.relates_to")) + val workRequest = createSendEventWork(event, false) + timeLineSendEventWorkCommon.postSequentialWorks(roomId, encryptWork, workRequest) + } else { + val workRequest = createSendEventWork(event, true) + timeLineSendEventWorkCommon.postWork(roomId, workRequest) + } + } + + private fun createEncryptEventWork(event: Event, keepKeys: List?): OneTimeWorkRequest { + // Same parameter + val params = EncryptEventWorker.Params(sessionId, event, keepKeys) + val sendWorkData = WorkerParamsFactory.toData(params) + return timeLineSendEventWorkCommon.createWork(sendWorkData, true) + } + + private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { + val sendContentWorkerParams = SendEventWorker.Params(sessionId, event) + val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) + return timeLineSendEventWorkCommon.createWork(sendWorkData, startChain) + } + + override fun getEventAnnotationsSummary(eventId: String): EventAnnotationsSummary? { + return monarchy.fetchCopyMap( + { EventAnnotationsSummaryEntity.where(it, eventId).findFirst() }, + { entity, _ -> + entity.asDomain() + } + ) + } + + override fun getEventAnnotationsSummaryLive(eventId: String): LiveData> { + val liveData = monarchy.findAllMappedWithChanges( + { EventAnnotationsSummaryEntity.where(it, eventId) }, + { it.asDomain() } + ) + return Transformations.map(liveData) { results -> + results.firstOrNull().toOptional() + } + } + + /** + * Saves the event in database as a local echo. + * SendState is set to UNSENT and it's added to a the sendingTimelineEvents list of the room. + * The sendingTimelineEvents is checked on new sync and will remove the local echo if an event with + * the same transaction id is received (in unsigned data) + */ + private fun saveLocalEcho(event: Event) { + eventFactory.createLocalEcho(event) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/FetchEditHistoryTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/FetchEditHistoryTask.kt new file mode 100644 index 0000000000..28cdd9a72b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/FetchEditHistoryTask.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.room.relation + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface FetchEditHistoryTask : Task> { + + data class Params( + val roomId: String, + val isRoomEncrypted: Boolean, + val eventId: String + ) +} + +internal class DefaultFetchEditHistoryTask @Inject constructor( + private val roomAPI: RoomAPI, + private val eventBus: EventBus +) : FetchEditHistoryTask { + + override suspend fun execute(params: FetchEditHistoryTask.Params): List { + val response = executeRequest(eventBus) { + apiCall = roomAPI.getRelations(params.roomId, + params.eventId, + RelationType.REPLACE, + if (params.isRoomEncrypted) EventType.ENCRYPTED else EventType.MESSAGE) + } + + val events = response.chunks.toMutableList() + response.originalEvent?.let { events.add(it) } + return events + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/FindReactionEventForUndoTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/FindReactionEventForUndoTask.kt new file mode 100644 index 0000000000..86fe75d9ed --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/FindReactionEventForUndoTask.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.room.relation + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.ReactionAggregatedSummaryEntityFields +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.task.Task +import io.realm.Realm +import javax.inject.Inject + +internal interface FindReactionEventForUndoTask : Task { + + data class Params( + val roomId: String, + val eventId: String, + val reaction: String + ) + + data class Result( + val redactEventId: String? + ) +} + +internal class DefaultFindReactionEventForUndoTask @Inject constructor( + @SessionDatabase private val monarchy: Monarchy, + @UserId private val userId: String) : FindReactionEventForUndoTask { + + override suspend fun execute(params: FindReactionEventForUndoTask.Params): FindReactionEventForUndoTask.Result { + val eventId = Realm.getInstance(monarchy.realmConfiguration).use { realm -> + getReactionToRedact(realm, params.reaction, params.eventId)?.eventId + } + return FindReactionEventForUndoTask.Result(eventId) + } + + private fun getReactionToRedact(realm: Realm, reaction: String, eventId: String): EventEntity? { + val summary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() ?: return null + + val rase = summary.reactionsSummary.where() + .equalTo(ReactionAggregatedSummaryEntityFields.KEY, reaction) + .findFirst() ?: return null + + // want to find the event originated by me! + return rase.sourceEvents + .asSequence() + .mapNotNull { + // find source event + EventEntity.where(realm, it).findFirst() + } + .firstOrNull { eventEntity -> + // is it mine? + eventEntity.sender == userId + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/RelationsResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/RelationsResponse.kt new file mode 100644 index 0000000000..24fcf89bde --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/RelationsResponse.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.room.relation + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Event + +@JsonClass(generateAdapter = true) +internal data class RelationsResponse( + @Json(name = "chunk") val chunks: List, + @Json(name = "original_event") val originalEvent: Event?, + @Json(name = "next_batch") val nextBatch: String?, + @Json(name = "prev_batch") val prevBatch: String? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/SendRelationWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/SendRelationWorker.kt new file mode 100644 index 0000000000..dc72c3b96b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/SendRelationWorker.kt @@ -0,0 +1,102 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.room.relation + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent +import org.matrix.android.sdk.api.session.room.model.relation.ReactionInfo +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.session.room.send.SendResponse +import org.matrix.android.sdk.internal.worker.SessionWorkerParams +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import org.matrix.android.sdk.internal.worker.getSessionComponent +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import javax.inject.Inject + +// TODO This is not used. Delete? +internal class SendRelationWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { + + @JsonClass(generateAdapter = true) + internal data class Params( + override val sessionId: String, + val roomId: String, + val event: Event, + val relationType: String? = null, + override val lastFailureMessage: String? = null + ) : SessionWorkerParams + + @Inject lateinit var roomAPI: RoomAPI + @Inject lateinit var eventBus: EventBus + + override suspend fun doWork(): Result { + val params = WorkerParamsFactory.fromData(inputData) + ?: return Result.failure() + .also { Timber.e("Unable to parse work parameters") } + + if (params.lastFailureMessage != null) { + // Transmit the error + return Result.success(inputData) + .also { Timber.e("Work cancelled due to input error from parent") } + } + + val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() + sessionComponent.inject(this) + + val localEvent = params.event + if (localEvent.eventId == null) { + return Result.failure() + } + val relationContent = localEvent.content.toModel() + ?: return Result.failure() + val relatedEventId = relationContent.relatesTo?.eventId ?: return Result.failure() + val relationType = (relationContent.relatesTo as? ReactionInfo)?.type ?: params.relationType + ?: return Result.failure() + return try { + sendRelation(params.roomId, relationType, relatedEventId, localEvent) + Result.success() + } catch (exception: Throwable) { + when (exception) { + is Failure.NetworkConnection -> Result.retry() + else -> { + // TODO mark as failed to send? + // always return success, or the chain will be stuck for ever! + Result.success() + } + } + } + } + + private suspend fun sendRelation(roomId: String, relationType: String, relatedEventId: String, localEvent: Event) { + executeRequest(eventBus) { + apiCall = roomAPI.sendRelation( + roomId = roomId, + parent_id = relatedEventId, + relationType = relationType, + eventType = localEvent.type, + content = localEvent.content + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/UpdateQuickReactionTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/UpdateQuickReactionTask.kt new file mode 100644 index 0000000000..d235cdba3b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/UpdateQuickReactionTask.kt @@ -0,0 +1,88 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.room.relation + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.ReactionAggregatedSummaryEntityFields +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.task.Task +import io.realm.Realm +import javax.inject.Inject + +internal interface UpdateQuickReactionTask : Task { + + data class Params( + val roomId: String, + val eventId: String, + val reaction: String, + val oppositeReaction: String + ) + + data class Result( + val reactionToAdd: String?, + val reactionToRedact: List + ) +} + +internal class DefaultUpdateQuickReactionTask @Inject constructor(@SessionDatabase private val monarchy: Monarchy, + @UserId private val userId: String) : UpdateQuickReactionTask { + + override suspend fun execute(params: UpdateQuickReactionTask.Params): UpdateQuickReactionTask.Result { + var res: Pair?>? = null + monarchy.doWithRealm { realm -> + res = updateQuickReaction(realm, params.reaction, params.oppositeReaction, params.eventId) + } + return UpdateQuickReactionTask.Result(res?.first, res?.second.orEmpty()) + } + + private fun updateQuickReaction(realm: Realm, reaction: String, oppositeReaction: String, eventId: String): Pair?> { + // the emoji reaction has been selected, we need to check if we have reacted it or not + val existingSummary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() + ?: return Pair(reaction, null) + + // Ok there is already reactions on this event, have we reacted to it + val aggregationForReaction = existingSummary.reactionsSummary.where() + .equalTo(ReactionAggregatedSummaryEntityFields.KEY, reaction) + .findFirst() + val aggregationForOppositeReaction = existingSummary.reactionsSummary.where() + .equalTo(ReactionAggregatedSummaryEntityFields.KEY, oppositeReaction) + .findFirst() + + if (aggregationForReaction == null || !aggregationForReaction.addedByMe) { + // i haven't yet reacted to it, so need to add it, but do I need to redact the opposite? + val toRedact = aggregationForOppositeReaction?.sourceEvents?.mapNotNull { + // find source event + val entity = EventEntity.where(realm, it).findFirst() + if (entity?.sender == userId) entity.eventId else null + } + return Pair(reaction, toRedact) + } else { + // I already added it, so i need to undo it (like a toggle) + // find all m.redaction coming from me to readact them + val toRedact = aggregationForReaction.sourceEvents.mapNotNull { + // find source event + val entity = EventEntity.where(realm, it).findFirst() + if (entity?.sender == userId) entity.eventId else null + } + return Pair(null, toRedact) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/DefaultReportingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/DefaultReportingService.kt new file mode 100644 index 0000000000..1117ed1c29 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/DefaultReportingService.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.reporting + +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.room.reporting.ReportingService +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith + +internal class DefaultReportingService @AssistedInject constructor(@Assisted private val roomId: String, + private val taskExecutor: TaskExecutor, + private val reportContentTask: ReportContentTask +) : ReportingService { + + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): ReportingService + } + + override fun reportContent(eventId: String, score: Int, reason: String, callback: MatrixCallback): Cancelable { + val params = ReportContentTask.Params(roomId, eventId, score, reason) + + return reportContentTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/ReportContentBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/ReportContentBody.kt new file mode 100644 index 0000000000..bd9f09f4fd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/ReportContentBody.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.reporting + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class ReportContentBody( + /** + * Required. The score to rate this content as where -100 is most offensive and 0 is inoffensive. + */ + @Json(name = "score") val score: Int, + + /** + * Required. The reason the content is being reported. May be blank. + */ + @Json(name = "reason") val reason: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/ReportContentTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/ReportContentTask.kt new file mode 100644 index 0000000000..b13c21fa2a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/ReportContentTask.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.reporting + +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface ReportContentTask : Task { + data class Params( + val roomId: String, + val eventId: String, + val score: Int, + val reason: String + ) +} + +internal class DefaultReportContentTask @Inject constructor( + private val roomAPI: RoomAPI, + private val eventBus: EventBus +) : ReportContentTask { + + override suspend fun execute(params: ReportContentTask.Params) { + return executeRequest(eventBus) { + apiCall = roomAPI.reportContent(params.roomId, params.eventId, ReportContentBody(params.score, params.reason)) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt new file mode 100644 index 0000000000..d6fa6775ee --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt @@ -0,0 +1,323 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.send + +import androidx.work.BackoffPolicy +import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequest +import androidx.work.Operation +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import org.matrix.android.sdk.api.session.content.ContentAttachmentData +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.isImageMessage +import org.matrix.android.sdk.api.session.events.model.isTextMessage +import org.matrix.android.sdk.api.session.room.model.message.OptionItem +import org.matrix.android.sdk.api.session.room.send.SendService +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.CancelableBag +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.di.SessionId +import org.matrix.android.sdk.internal.di.WorkManagerProvider +import org.matrix.android.sdk.internal.session.content.UploadContentWorker +import org.matrix.android.sdk.internal.session.room.timeline.TimelineSendEventWorkCommon +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.util.CancelableWork +import org.matrix.android.sdk.internal.worker.AlwaysSuccessfulWorker +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import org.matrix.android.sdk.internal.worker.startChain +import kotlinx.coroutines.launch +import timber.log.Timber +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit + +private const val UPLOAD_WORK = "UPLOAD_WORK" + +internal class DefaultSendService @AssistedInject constructor( + @Assisted private val roomId: String, + private val workManagerProvider: WorkManagerProvider, + private val timelineSendEventWorkCommon: TimelineSendEventWorkCommon, + @SessionId private val sessionId: String, + private val localEchoEventFactory: LocalEchoEventFactory, + private val cryptoService: CryptoService, + private val taskExecutor: TaskExecutor, + private val localEchoRepository: LocalEchoRepository, + private val roomEventSender: RoomEventSender +) : SendService { + + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): SendService + } + + private val workerFutureListenerExecutor = Executors.newSingleThreadExecutor() + + override fun sendEvent(eventType: String, content: JsonDict?): Cancelable { + return localEchoEventFactory.createEvent(roomId, eventType, content) + .also { createLocalEcho(it) } + .let { sendEvent(it) } + } + + override fun sendTextMessage(text: CharSequence, msgType: String, autoMarkdown: Boolean): Cancelable { + return localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown) + .also { createLocalEcho(it) } + .let { sendEvent(it) } + } + + // For test only + private fun sendTextMessages(text: CharSequence, msgType: String, autoMarkdown: Boolean, times: Int): Cancelable { + return CancelableBag().apply { + // Send the event several times + repeat(times) { i -> + localEchoEventFactory.createTextEvent(roomId, msgType, "$text - $i", autoMarkdown) + .also { createLocalEcho(it) } + .let { sendEvent(it) } + .also { add(it) } + } + } + } + + override fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String): Cancelable { + return localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText), msgType) + .also { createLocalEcho(it) } + .let { sendEvent(it) } + } + + override fun sendPoll(question: String, options: List): Cancelable { + return localEchoEventFactory.createPollEvent(roomId, question, options) + .also { createLocalEcho(it) } + .let { sendEvent(it) } + } + + override fun sendOptionsReply(pollEventId: String, optionIndex: Int, optionValue: String): Cancelable { + return localEchoEventFactory.createOptionsReplyEvent(roomId, pollEventId, optionIndex, optionValue) + .also { createLocalEcho(it) } + .let { sendEvent(it) } + } + + override fun sendMedias(attachments: List, + compressBeforeSending: Boolean, + roomIds: Set): Cancelable { + return attachments.mapTo(CancelableBag()) { + sendMedia(it, compressBeforeSending, roomIds) + } + } + + override fun redactEvent(event: Event, reason: String?): Cancelable { + // TODO manage media/attachements? + return createRedactEventWork(event, reason) + .let { timelineSendEventWorkCommon.postWork(roomId, it) } + } + + override fun resendTextMessage(localEcho: TimelineEvent): Cancelable? { + if (localEcho.root.isTextMessage() && localEcho.root.sendState.hasFailed()) { + localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT) + return sendEvent(localEcho.root) + } + return null + } + + override fun resendMediaMessage(localEcho: TimelineEvent): Cancelable? { + if (localEcho.root.isImageMessage() && localEcho.root.sendState.hasFailed()) { + // TODO this need a refactoring of attachement sending +// val clearContent = localEcho.root.getClearContent() +// val messageContent = clearContent?.toModel() ?: return null +// when (messageContent.type) { +// MessageType.MSGTYPE_IMAGE -> { +// val imageContent = clearContent.toModel() ?: return null +// val url = imageContent.url ?: return null +// if (url.startsWith("mxc://")) { +// //TODO +// } else { +// //The image has not yet been sent +// val attachmentData = ContentAttachmentData( +// size = imageContent.info!!.size.toLong(), +// mimeType = imageContent.info.mimeType!!, +// width = imageContent.info.width.toLong(), +// height = imageContent.info.height.toLong(), +// name = imageContent.body, +// path = imageContent.url, +// type = ContentAttachmentData.Type.IMAGE +// ) +// monarchy.runTransactionSync { +// EventEntity.where(it,eventId = localEcho.root.eventId ?: "").findFirst()?.let { +// it.sendState = SendState.UNSENT +// } +// } +// return internalSendMedia(localEcho.root,attachmentData) +// } +// } +// } + return null + } + return null + } + + override fun deleteFailedEcho(localEcho: TimelineEvent) { + taskExecutor.executorScope.launch { + localEchoRepository.deleteFailedEcho(roomId, localEcho) + } + } + + override fun clearSendingQueue() { + timelineSendEventWorkCommon.cancelAllWorks(roomId) + workManagerProvider.workManager.cancelUniqueWork(buildWorkName(UPLOAD_WORK)) + + // Replace the worker chains with a AlwaysSuccessfulWorker, to ensure the queues are well emptied + workManagerProvider.matrixOneTimeWorkRequestBuilder() + .build().let { + timelineSendEventWorkCommon.postWork(roomId, it, ExistingWorkPolicy.REPLACE) + + // need to clear also image sending queue + workManagerProvider.workManager + .beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.REPLACE, it) + .enqueue() + } + taskExecutor.executorScope.launch { + localEchoRepository.clearSendingQueue(roomId) + } + } + + override fun resendAllFailedMessages() { + taskExecutor.executorScope.launch { + val eventsToResend = localEchoRepository.getAllFailedEventsToResend(roomId) + eventsToResend.forEach { + sendEvent(it) + } + localEchoRepository.updateSendState(roomId, eventsToResend.mapNotNull { it.eventId }, SendState.UNSENT) + } + } + + override fun sendMedia(attachment: ContentAttachmentData, + compressBeforeSending: Boolean, + roomIds: Set): Cancelable { + // Create an event with the media file path + // Ensure current roomId is included in the set + val allRoomIds = (roomIds + roomId).toList() + + // Create local echo for each room + val allLocalEchoes = allRoomIds.map { + localEchoEventFactory.createMediaEvent(it, attachment).also { event -> + createLocalEcho(event) + } + } + return internalSendMedia(allLocalEchoes, attachment, compressBeforeSending) + } + + /** + * We use the roomId of the local echo event + */ + private fun internalSendMedia(allLocalEchoes: List, attachment: ContentAttachmentData, compressBeforeSending: Boolean): Cancelable { + val cancelableBag = CancelableBag() + + allLocalEchoes.groupBy { cryptoService.isRoomEncrypted(it.roomId!!) } + .apply { + keys.forEach { isRoomEncrypted -> + // Should never be empty + val localEchoes = get(isRoomEncrypted).orEmpty() + val uploadWork = createUploadMediaWork(localEchoes, attachment, isRoomEncrypted, compressBeforeSending) + + val dispatcherWork = createMultipleEventDispatcherWork(isRoomEncrypted) + + workManagerProvider.workManager + .beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork) + .then(dispatcherWork) + .enqueue() + .also { operation -> + operation.result.addListener(Runnable { + if (operation.result.isCancelled) { + Timber.e("CHAIN WAS CANCELLED") + } else if (operation.state.value is Operation.State.FAILURE) { + Timber.e("CHAIN DID FAIL") + } + }, workerFutureListenerExecutor) + } + + cancelableBag.add(CancelableWork(workManagerProvider.workManager, dispatcherWork.id)) + } + } + + return cancelableBag + } + + private fun sendEvent(event: Event): Cancelable { + return roomEventSender.sendEvent(event) + } + + private fun createLocalEcho(event: Event) { + localEchoEventFactory.createLocalEcho(event) + } + + private fun buildWorkName(identifier: String): String { + return "${roomId}_$identifier" + } + + private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { + // Same parameter + return EncryptEventWorker.Params(sessionId, event) + .let { WorkerParamsFactory.toData(it) } + .let { + workManagerProvider.matrixOneTimeWorkRequestBuilder() + .setConstraints(WorkManagerProvider.workConstraints) + .setInputData(it) + .startChain(startChain) + .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS) + .build() + } + } + + private fun createRedactEventWork(event: Event, reason: String?): OneTimeWorkRequest { + return localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason) + .also { createLocalEcho(it) } + .let { RedactEventWorker.Params(sessionId, it.eventId!!, roomId, event.eventId, reason) } + .let { WorkerParamsFactory.toData(it) } + .let { timelineSendEventWorkCommon.createWork(it, true) } + } + + private fun createUploadMediaWork(allLocalEchos: List, + attachment: ContentAttachmentData, + isRoomEncrypted: Boolean, + compressBeforeSending: Boolean): OneTimeWorkRequest { + val uploadMediaWorkerParams = UploadContentWorker.Params(sessionId, allLocalEchos, attachment, isRoomEncrypted, compressBeforeSending) + val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams) + + return workManagerProvider.matrixOneTimeWorkRequestBuilder() + .setConstraints(WorkManagerProvider.workConstraints) + .startChain(true) + .setInputData(uploadWorkData) + .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS) + .build() + } + + private fun createMultipleEventDispatcherWork(isRoomEncrypted: Boolean): OneTimeWorkRequest { + // the list of events will be replaced by the result of the media upload work + val params = MultipleEventSendingDispatcherWorker.Params(sessionId, emptyList(), isRoomEncrypted) + val workData = WorkerParamsFactory.toData(params) + + return workManagerProvider.matrixOneTimeWorkRequestBuilder() + // No constraint + // .setConstraints(WorkManagerProvider.workConstraints) + .startChain(false) + .setInputData(workData) + .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS) + .build() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/EncryptEventWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/EncryptEventWorker.kt new file mode 100644 index 0000000000..d23835e838 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/EncryptEventWorker.kt @@ -0,0 +1,137 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.send + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult +import org.matrix.android.sdk.internal.crypto.model.MXEncryptEventContentResult +import org.matrix.android.sdk.internal.util.awaitCallback +import org.matrix.android.sdk.internal.worker.SessionWorkerParams +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import org.matrix.android.sdk.internal.worker.getSessionComponent +import timber.log.Timber +import javax.inject.Inject + +/** + * Possible previous worker: None + * Possible next worker : Always [SendEventWorker] + */ +internal class EncryptEventWorker(context: Context, params: WorkerParameters) + : CoroutineWorker(context, params) { + + @JsonClass(generateAdapter = true) + internal data class Params( + override val sessionId: String, + val event: Event, + /** Do not encrypt these keys, keep them as is in encrypted content (e.g. m.relates_to) */ + val keepKeys: List? = null, + override val lastFailureMessage: String? = null + ) : SessionWorkerParams + + @Inject lateinit var crypto: CryptoService + @Inject lateinit var localEchoRepository: LocalEchoRepository + + override suspend fun doWork(): Result { + Timber.v("Start Encrypt work") + val params = WorkerParamsFactory.fromData(inputData) + ?: return Result.success() + .also { Timber.e("Unable to parse work parameters") } + + Timber.v("Start Encrypt work for event ${params.event.eventId}") + if (params.lastFailureMessage != null) { + // Transmit the error + return Result.success(inputData) + .also { Timber.e("Work cancelled due to input error from parent") } + } + + val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() + sessionComponent.inject(this) + + val localEvent = params.event + if (localEvent.eventId == null) { + return Result.success() + } + localEchoRepository.updateSendState(localEvent.eventId, SendState.ENCRYPTING) + + val localMutableContent = localEvent.content?.toMutableMap() ?: mutableMapOf() + params.keepKeys?.forEach { + localMutableContent.remove(it) + } + + var error: Throwable? = null + var result: MXEncryptEventContentResult? = null + try { + result = awaitCallback { + crypto.encryptEventContent(localMutableContent, localEvent.type, localEvent.roomId!!, it) + } + } catch (throwable: Throwable) { + error = throwable + } + if (result != null) { + val modifiedContent = HashMap(result.eventContent) + params.keepKeys?.forEach { toKeep -> + localEvent.content?.get(toKeep)?.let { + // put it back in the encrypted thing + modifiedContent[toKeep] = it + } + } + val safeResult = result.copy(eventContent = modifiedContent) + val encryptedEvent = localEvent.copy( + type = safeResult.eventType, + content = safeResult.eventContent + ) + // Better handling of local echo, to avoid decrypting transition on remote echo + // Should I only do it for text messages? + if (result.eventContent["algorithm"] == MXCRYPTO_ALGORITHM_MEGOLM) { + val decryptionLocalEcho = MXEventDecryptionResult( + clearEvent = Event( + type = localEvent.type, + content = localEvent.content, + roomId = localEvent.roomId + ).toContent(), + forwardingCurve25519KeyChain = emptyList(), + senderCurve25519Key = result.eventContent["sender_key"] as? String, + claimedEd25519Key = crypto.getMyDevice().fingerprint() + ) + localEchoRepository.updateEncryptedEcho(localEvent.eventId, safeResult.eventContent, decryptionLocalEcho) + } + + val nextWorkerParams = SendEventWorker.Params(params.sessionId, encryptedEvent) + return Result.success(WorkerParamsFactory.toData(nextWorkerParams)) + } else { + val sendState = when (error) { + is Failure.CryptoError -> SendState.FAILED_UNKNOWN_DEVICES + else -> SendState.UNDELIVERED + } + localEchoRepository.updateSendState(localEvent.eventId, sendState) + // always return success, or the chain will be stuck for ever! + val nextWorkerParams = SendEventWorker.Params(params.sessionId, localEvent, error?.localizedMessage + ?: "Error") + return Result.success(WorkerParamsFactory.toData(nextWorkerParams)) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt new file mode 100644 index 0000000000..fe6daad3b0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt @@ -0,0 +1,493 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.send + +import android.content.Context +import android.graphics.Bitmap +import android.media.MediaMetadataRetriever +import androidx.exifinterface.media.ExifInterface +import org.matrix.android.sdk.R +import org.matrix.android.sdk.api.permalinks.PermalinkFactory +import org.matrix.android.sdk.api.session.content.ContentAttachmentData +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.LocalEcho +import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.events.model.UnsignedData +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.model.message.AudioInfo +import org.matrix.android.sdk.api.session.room.model.message.FileInfo +import org.matrix.android.sdk.api.session.room.model.message.ImageInfo +import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageContentWithFormattedBody +import org.matrix.android.sdk.api.session.room.model.message.MessageFileContent +import org.matrix.android.sdk.api.session.room.model.message.MessageFormat +import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageOptionsContent +import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent +import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent +import org.matrix.android.sdk.api.session.room.model.message.MessageType +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent +import org.matrix.android.sdk.api.session.room.model.message.OPTION_TYPE_POLL +import org.matrix.android.sdk.api.session.room.model.message.OptionItem +import org.matrix.android.sdk.api.session.room.model.message.ThumbnailInfo +import org.matrix.android.sdk.api.session.room.model.message.VideoInfo +import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent +import org.matrix.android.sdk.api.session.room.model.relation.ReactionInfo +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.api.session.room.model.relation.ReplyToContent +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent +import org.matrix.android.sdk.api.session.room.timeline.isReply +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.content.ThumbnailExtractor +import org.matrix.android.sdk.internal.session.room.send.pills.TextPillsUtils +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.util.StringProvider +import javax.inject.Inject + +/** + * Creates local echo of events for room events. + * A local echo is an event that is persisted even if not yet sent to the server, + * in an optimistic way (as if the server as responded immediately). Local echo are using a local id, + * (the transaction ID), this id is used when receiving an event from a sync to check if this event + * is matching an existing local echo. + * + * The transactionId is used as loc + */ +internal class LocalEchoEventFactory @Inject constructor( + private val context: Context, + @UserId private val userId: String, + private val stringProvider: StringProvider, + private val markdownParser: MarkdownParser, + private val textPillsUtils: TextPillsUtils, + private val taskExecutor: TaskExecutor, + private val localEchoRepository: LocalEchoRepository +) { + fun createTextEvent(roomId: String, msgType: String, text: CharSequence, autoMarkdown: Boolean): Event { + if (msgType == MessageType.MSGTYPE_TEXT || msgType == MessageType.MSGTYPE_EMOTE) { + return createFormattedTextEvent(roomId, createTextContent(text, autoMarkdown), msgType) + } + val content = MessageTextContent(msgType = msgType, body = text.toString()) + return createMessageEvent(roomId, content) + } + + private fun createTextContent(text: CharSequence, autoMarkdown: Boolean): TextContent { + if (autoMarkdown) { + val source = textPillsUtils.processSpecialSpansToMarkdown(text) ?: text.toString() + return markdownParser.parse(source) + } else { + // Try to detect pills + textPillsUtils.processSpecialSpansToHtml(text)?.let { + return TextContent(text.toString(), it) + } + } + + return TextContent(text.toString()) + } + + fun createFormattedTextEvent(roomId: String, textContent: TextContent, msgType: String): Event { + return createMessageEvent(roomId, textContent.toMessageTextContent(msgType)) + } + + fun createReplaceTextEvent(roomId: String, + targetEventId: String, + newBodyText: CharSequence, + newBodyAutoMarkdown: Boolean, + msgType: String, + compatibilityText: String): Event { + return createMessageEvent(roomId, + MessageTextContent( + msgType = msgType, + body = compatibilityText, + relatesTo = RelationDefaultContent(RelationType.REPLACE, targetEventId), + newContent = createTextContent(newBodyText, newBodyAutoMarkdown) + .toMessageTextContent(msgType) + .toContent() + )) + } + + fun createOptionsReplyEvent(roomId: String, + pollEventId: String, + optionIndex: Int, + optionLabel: String): Event { + return createMessageEvent(roomId, + MessagePollResponseContent( + body = optionLabel, + relatesTo = RelationDefaultContent( + type = RelationType.RESPONSE, + option = optionIndex, + eventId = pollEventId) + + )) + } + + fun createPollEvent(roomId: String, + question: String, + options: List): Event { + val compatLabel = buildString { + append("[Poll] ") + append(question) + options.forEach { + append("\n") + append(it.value) + } + } + return createMessageEvent( + roomId, + MessageOptionsContent( + body = compatLabel, + label = question, + optionType = OPTION_TYPE_POLL, + options = options.toList() + ) + ) + } + + fun createReplaceTextOfReply(roomId: String, + eventReplaced: TimelineEvent, + originalEvent: TimelineEvent, + newBodyText: String, + newBodyAutoMarkdown: Boolean, + msgType: String, + compatibilityText: String): Event { + val permalink = PermalinkFactory.createPermalink(roomId, originalEvent.root.eventId ?: "") + val userLink = originalEvent.root.senderId?.let { PermalinkFactory.createPermalink(it) } + ?: "" + + val body = bodyForReply(originalEvent.getLastMessageContent(), originalEvent.isReply()) + val replyFormatted = REPLY_PATTERN.format( + permalink, + userLink, + originalEvent.senderInfo.disambiguatedDisplayName, + // Remove inner mx_reply tags if any + body.takeFormatted().replace(MX_REPLY_REGEX, ""), + createTextContent(newBodyText, newBodyAutoMarkdown).takeFormatted() + ) + // + // > <@alice:example.org> This is the original body + // + val replyFallback = buildReplyFallback(body, originalEvent.root.senderId ?: "", newBodyText) + + return createMessageEvent(roomId, + MessageTextContent( + msgType = msgType, + body = compatibilityText, + relatesTo = RelationDefaultContent(RelationType.REPLACE, eventReplaced.root.eventId), + newContent = MessageTextContent( + msgType = msgType, + format = MessageFormat.FORMAT_MATRIX_HTML, + body = replyFallback, + formattedBody = replyFormatted + ) + .toContent() + )) + } + + fun createMediaEvent(roomId: String, attachment: ContentAttachmentData): Event { + return when (attachment.type) { + ContentAttachmentData.Type.IMAGE -> createImageEvent(roomId, attachment) + ContentAttachmentData.Type.VIDEO -> createVideoEvent(roomId, attachment) + ContentAttachmentData.Type.AUDIO -> createAudioEvent(roomId, attachment) + ContentAttachmentData.Type.FILE -> createFileEvent(roomId, attachment) + } + } + + fun createReactionEvent(roomId: String, targetEventId: String, reaction: String): Event { + val content = ReactionContent( + ReactionInfo( + RelationType.ANNOTATION, + targetEventId, + reaction + ) + ) + val localId = LocalEcho.createLocalEchoId() + return Event( + roomId = roomId, + originServerTs = dummyOriginServerTs(), + senderId = userId, + eventId = localId, + type = EventType.REACTION, + content = content.toContent(), + unsignedData = UnsignedData(age = null, transactionId = localId)) + } + + private fun createImageEvent(roomId: String, attachment: ContentAttachmentData): Event { + var width = attachment.width + var height = attachment.height + + when (attachment.exifOrientation) { + ExifInterface.ORIENTATION_ROTATE_90, + ExifInterface.ORIENTATION_TRANSVERSE, + ExifInterface.ORIENTATION_ROTATE_270, + ExifInterface.ORIENTATION_TRANSPOSE -> { + val tmp = width + width = height + height = tmp + } + } + + val content = MessageImageContent( + msgType = MessageType.MSGTYPE_IMAGE, + body = attachment.name ?: "image", + info = ImageInfo( + mimeType = attachment.getSafeMimeType(), + width = width?.toInt() ?: 0, + height = height?.toInt() ?: 0, + size = attachment.size.toInt() + ), + url = attachment.queryUri.toString() + ) + return createMessageEvent(roomId, content) + } + + private fun createVideoEvent(roomId: String, attachment: ContentAttachmentData): Event { + val mediaDataRetriever = MediaMetadataRetriever() + mediaDataRetriever.setDataSource(context, attachment.queryUri) + + // Use frame to calculate height and width as we are sure to get the right ones + val firstFrame: Bitmap? = mediaDataRetriever.frameAtTime + val height = firstFrame?.height ?: 0 + val width = firstFrame?.width ?: 0 + mediaDataRetriever.release() + + val thumbnailInfo = ThumbnailExtractor.extractThumbnail(context, attachment)?.let { + ThumbnailInfo( + width = it.width, + height = it.height, + size = it.size, + mimeType = it.mimeType + ) + } + val content = MessageVideoContent( + msgType = MessageType.MSGTYPE_VIDEO, + body = attachment.name ?: "video", + videoInfo = VideoInfo( + mimeType = attachment.getSafeMimeType(), + width = width, + height = height, + size = attachment.size, + duration = attachment.duration?.toInt() ?: 0, + // Glide will be able to use the local path and extract a thumbnail. + thumbnailUrl = attachment.queryUri.toString(), + thumbnailInfo = thumbnailInfo + ), + url = attachment.queryUri.toString() + ) + return createMessageEvent(roomId, content) + } + + private fun createAudioEvent(roomId: String, attachment: ContentAttachmentData): Event { + val content = MessageAudioContent( + msgType = MessageType.MSGTYPE_AUDIO, + body = attachment.name ?: "audio", + audioInfo = AudioInfo( + mimeType = attachment.getSafeMimeType()?.takeIf { it.isNotBlank() }, + size = attachment.size + ), + url = attachment.queryUri.toString() + ) + return createMessageEvent(roomId, content) + } + + private fun createFileEvent(roomId: String, attachment: ContentAttachmentData): Event { + val content = MessageFileContent( + msgType = MessageType.MSGTYPE_FILE, + body = attachment.name ?: "file", + info = FileInfo( + mimeType = attachment.getSafeMimeType()?.takeIf { it.isNotBlank() }, + size = attachment.size + ), + url = attachment.queryUri.toString() + ) + return createMessageEvent(roomId, content) + } + + private fun createMessageEvent(roomId: String, content: MessageContent? = null): Event { + return createEvent(roomId, EventType.MESSAGE, content.toContent()) + } + + fun createEvent(roomId: String, type: String, content: Content?): Event { + val localId = LocalEcho.createLocalEchoId() + return Event( + roomId = roomId, + originServerTs = dummyOriginServerTs(), + senderId = userId, + eventId = localId, + type = type, + content = content, + unsignedData = UnsignedData(age = null, transactionId = localId) + ) + } + + fun createVerificationRequest(roomId: String, fromDevice: String, toUserId: String, methods: List): Event { + val localId = LocalEcho.createLocalEchoId() + return Event( + roomId = roomId, + originServerTs = dummyOriginServerTs(), + senderId = userId, + eventId = localId, + type = EventType.MESSAGE, + content = MessageVerificationRequestContent( + body = stringProvider.getString(R.string.key_verification_request_fallback_message, userId), + fromDevice = fromDevice, + toUserId = toUserId, + timestamp = System.currentTimeMillis(), + methods = methods + ).toContent(), + unsignedData = UnsignedData(age = null, transactionId = localId) + ) + } + + private fun dummyOriginServerTs(): Long { + return System.currentTimeMillis() + } + + fun createReplyTextEvent(roomId: String, eventReplied: TimelineEvent, replyText: CharSequence, autoMarkdown: Boolean): Event? { + // Fallbacks and event representation + // TODO Add error/warning logs when any of this is null + val permalink = PermalinkFactory.createPermalink(eventReplied.root) ?: return null + val userId = eventReplied.root.senderId ?: return null + val userLink = PermalinkFactory.createPermalink(userId) ?: return null + + val body = bodyForReply(eventReplied.getLastMessageContent(), eventReplied.isReply()) + val replyFormatted = REPLY_PATTERN.format( + permalink, + userLink, + userId, + // Remove inner mx_reply tags if any + body.takeFormatted().replace(MX_REPLY_REGEX, ""), + createTextContent(replyText, autoMarkdown).takeFormatted() + ) + // + // > <@alice:example.org> This is the original body + // + val replyFallback = buildReplyFallback(body, userId, replyText.toString()) + + val eventId = eventReplied.root.eventId ?: return null + val content = MessageTextContent( + msgType = MessageType.MSGTYPE_TEXT, + format = MessageFormat.FORMAT_MATRIX_HTML, + body = replyFallback, + formattedBody = replyFormatted, + relatesTo = RelationDefaultContent(null, null, ReplyToContent(eventId)) + ) + return createMessageEvent(roomId, content) + } + + private fun buildReplyFallback(body: TextContent, originalSenderId: String?, newBodyText: String): String { + return buildString { + append("> <") + append(originalSenderId) + append(">") + + val lines = body.text.split("\n") + lines.forEachIndexed { index, s -> + if (index == 0) { + append(" $s") + } else { + append("\n> $s") + } + } + append("\n\n") + append(newBodyText) + } + } + + /** + * Returns a TextContent used for the fallback event representation in a reply message. + * In case of an edit of a reply the last content is not + * himself a reply, but it will contain the fallbacks, so we have to trim them. + */ + private fun bodyForReply(content: MessageContent?, isReply: Boolean): TextContent { + when (content?.msgType) { + MessageType.MSGTYPE_EMOTE, + MessageType.MSGTYPE_TEXT, + MessageType.MSGTYPE_NOTICE -> { + var formattedText: String? = null + if (content is MessageContentWithFormattedBody) { + formattedText = content.matrixFormattedBody + } + return if (isReply) { + TextContent(content.body, formattedText).removeInReplyFallbacks() + } else { + TextContent(content.body, formattedText) + } + } + MessageType.MSGTYPE_FILE -> return TextContent("sent a file.") + MessageType.MSGTYPE_AUDIO -> return TextContent("sent an audio file.") + MessageType.MSGTYPE_IMAGE -> return TextContent("sent an image.") + MessageType.MSGTYPE_VIDEO -> return TextContent("sent a video.") + else -> return TextContent(content?.body ?: "") + } + } + + /* + * { + "content": { + "reason": "Spamming" + }, + "event_id": "$143273582443PhrSn:domain.com", + "origin_server_ts": 1432735824653, + "redacts": "$fukweghifu23:localhost", + "room_id": "!jEsUZKDJdhlrceRyVU:domain.com", + "sender": "@example:domain.com", + "type": "m.room.redaction", + "unsigned": { + "age": 1234 + } + } + */ + fun createRedactEvent(roomId: String, eventId: String, reason: String?): Event { + val localId = LocalEcho.createLocalEchoId() + return Event( + roomId = roomId, + originServerTs = dummyOriginServerTs(), + senderId = userId, + eventId = localId, + type = EventType.REDACTION, + redacts = eventId, + content = reason?.let { mapOf("reason" to it).toContent() }, + unsignedData = UnsignedData(age = null, transactionId = localId) + ) + } + + fun createLocalEcho(event: Event) { + checkNotNull(event.roomId) { "Your event should have a roomId" } + localEchoRepository.createLocalEcho(event) + } + + companion object { + // + //
+ // In reply to + // @alice:example.org + //
+ // + //
+ //
+ // No whitespace because currently breaks temporary formatted text to Span + const val REPLY_PATTERN = """
In reply to %s
%s
%s""" + + // This is used to replace inner mx-reply tags + val MX_REPLY_REGEX = ".*".toRegex() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt new file mode 100644 index 0000000000..a9859136ad --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt @@ -0,0 +1,190 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.send + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageType +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult +import org.matrix.android.sdk.internal.database.helper.nextId +import org.matrix.android.sdk.internal.database.mapper.ContentMapper +import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper +import org.matrix.android.sdk.internal.database.mapper.asDomain +import org.matrix.android.sdk.internal.database.mapper.toEntity +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.EventInsertEntity +import org.matrix.android.sdk.internal.database.model.EventInsertType +import org.matrix.android.sdk.internal.database.model.RoomEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.query.findAllInRoomWithSendStates +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper +import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater +import org.matrix.android.sdk.internal.session.room.timeline.DefaultTimeline +import org.matrix.android.sdk.internal.util.awaitTransaction +import io.realm.Realm +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import javax.inject.Inject + +internal class LocalEchoRepository @Inject constructor(@SessionDatabase private val monarchy: Monarchy, + private val roomSummaryUpdater: RoomSummaryUpdater, + private val eventBus: EventBus, + private val timelineEventMapper: TimelineEventMapper) { + + fun createLocalEcho(event: Event) { + val roomId = event.roomId ?: throw IllegalStateException("You should have set a roomId for your event") + val senderId = event.senderId ?: throw IllegalStateException("You should have set a senderIf for your event") + if (event.eventId == null) { + throw IllegalStateException("You should have set an eventId for your event") + } + val timelineEventEntity = Realm.getInstance(monarchy.realmConfiguration).use { realm -> + val eventEntity = event.toEntity(roomId, SendState.UNSENT, System.currentTimeMillis()) + val roomMemberHelper = RoomMemberHelper(realm, roomId) + val myUser = roomMemberHelper.getLastRoomMember(senderId) + val localId = TimelineEventEntity.nextId(realm) + TimelineEventEntity(localId).also { + it.root = eventEntity + it.eventId = event.eventId + it.roomId = roomId + it.senderName = myUser?.displayName + it.senderAvatar = myUser?.avatarUrl + it.isUniqueDisplayName = roomMemberHelper.isUniqueDisplayName(myUser?.displayName) + } + } + val timelineEvent = timelineEventMapper.map(timelineEventEntity) + eventBus.post(DefaultTimeline.OnLocalEchoCreated(roomId = roomId, timelineEvent = timelineEvent)) + monarchy.writeAsync { realm -> + val eventInsertEntity = EventInsertEntity(event.eventId, event.type).apply { + this.insertType = EventInsertType.LOCAL_ECHO + } + realm.insert(eventInsertEntity) + val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: return@writeAsync + roomEntity.sendingTimelineEvents.add(0, timelineEventEntity) + roomSummaryUpdater.updateSendingInformation(realm, roomId) + } + } + + fun updateSendState(eventId: String, sendState: SendState) { + Timber.v("Update local state of $eventId to ${sendState.name}") + monarchy.writeAsync { realm -> + val sendingEventEntity = EventEntity.where(realm, eventId).findFirst() + if (sendingEventEntity != null) { + if (sendState == SendState.SENT && sendingEventEntity.sendState == SendState.SYNCED) { + // If already synced, do not put as sent + } else { + sendingEventEntity.sendState = sendState + } + roomSummaryUpdater.updateSendingInformation(realm, sendingEventEntity.roomId) + } + } + } + + fun updateEncryptedEcho(eventId: String, encryptedContent: Content, mxEventDecryptionResult: MXEventDecryptionResult) { + monarchy.writeAsync { realm -> + val sendingEventEntity = EventEntity.where(realm, eventId).findFirst() + if (sendingEventEntity != null) { + sendingEventEntity.type = EventType.ENCRYPTED + sendingEventEntity.content = ContentMapper.map(encryptedContent) + sendingEventEntity.setDecryptionResult(mxEventDecryptionResult) + } + } + } + + suspend fun deleteFailedEcho(roomId: String, localEcho: TimelineEvent) { + monarchy.awaitTransaction { realm -> + TimelineEventEntity.where(realm, roomId = roomId, eventId = localEcho.root.eventId ?: "").findFirst()?.deleteFromRealm() + EventEntity.where(realm, eventId = localEcho.root.eventId ?: "").findFirst()?.deleteFromRealm() + roomSummaryUpdater.updateSendingInformation(realm, roomId) + } + } + + suspend fun clearSendingQueue(roomId: String) { + monarchy.awaitTransaction { realm -> + TimelineEventEntity + .findAllInRoomWithSendStates(realm, roomId, SendState.IS_SENDING_STATES) + .forEach { + it.root?.sendState = SendState.UNSENT + } + roomSummaryUpdater.updateSendingInformation(realm, roomId) + } + } + + suspend fun updateSendState(roomId: String, eventIds: List, sendState: SendState) { + monarchy.awaitTransaction { realm -> + val timelineEvents = TimelineEventEntity.where(realm, roomId, eventIds).findAll() + timelineEvents.forEach { + it.root?.sendState = sendState + } + roomSummaryUpdater.updateSendingInformation(realm, roomId) + } + } + + fun getAllFailedEventsToResend(roomId: String): List { + return Realm.getInstance(monarchy.realmConfiguration).use { realm -> + TimelineEventEntity + .findAllInRoomWithSendStates(realm, roomId, SendState.HAS_FAILED_STATES) + .sortedByDescending { it.displayIndex } + .mapNotNull { it.root?.asDomain() } + .filter { event -> + when (event.getClearType()) { + EventType.MESSAGE, + EventType.REDACTION, + EventType.REACTION -> { + val content = event.getClearContent().toModel() + if (content != null) { + when (content.msgType) { + MessageType.MSGTYPE_EMOTE, + MessageType.MSGTYPE_NOTICE, + MessageType.MSGTYPE_LOCATION, + MessageType.MSGTYPE_TEXT -> { + true + } + MessageType.MSGTYPE_FILE, + MessageType.MSGTYPE_VIDEO, + MessageType.MSGTYPE_IMAGE, + MessageType.MSGTYPE_AUDIO -> { + // need to resend the attachment + false + } + else -> { + Timber.e("Cannot resend message ${event.type} / ${content.msgType}") + false + } + } + } else { + Timber.e("Unsupported message to resend ${event.type}") + false + } + } + else -> { + Timber.e("Unsupported message to resend ${event.type}") + false + } + } + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParser.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParser.kt new file mode 100644 index 0000000000..3390d9dc79 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParser.kt @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.send + +import org.commonmark.parser.Parser +import org.commonmark.renderer.html.HtmlRenderer +import org.commonmark.renderer.text.TextContentRenderer +import javax.inject.Inject + +/** + * This class convert a text to an html text + * This class is tested by [MarkdownParserTest]. + * If any change is required, please add a test covering the problem and make sure all the tests are still passing. + */ +internal class MarkdownParser @Inject constructor( + private val parser: Parser, + private val htmlRenderer: HtmlRenderer, + private val textContentRenderer: TextContentRenderer +) { + + private val mdSpecialChars = "[`_\\-\\*>\\.\\[\\]#~]".toRegex() + + fun parse(text: String): TextContent { + // If no special char are detected, just return plain text + if (text.contains(mdSpecialChars).not()) { + return TextContent(text) + } + + val document = parser.parse(text) + val htmlText = htmlRenderer.render(document) + + // Cleanup extra paragraph + val cleanHtmlText = if (htmlText.lastIndexOf("

") == 0) { + htmlText.removeSurrounding("

", "

\n") + } else { + htmlText + } + + return if (isFormattedTextPertinent(text, cleanHtmlText)) { + // According to https://matrix.org/docs/spec/client_server/latest#m-room-message-msgtypes: + // The plain text version of the HTML should be provided in the body. + val plainText = textContentRenderer.render(document) + TextContent(plainText, cleanHtmlText.postTreatment()) + } else { + TextContent(text) + } + } + + private fun isFormattedTextPertinent(text: String, htmlText: String?) = + text != htmlText && htmlText != "

${text.trim()}

\n" + + /** + * The parser makes some mistakes, so deal with it here + */ + private fun String.postTreatment(): String { + return this + // Remove extra space before and after the content + .trim() + // There is no need to include new line in an html-like source + .replace("\n", "") + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt new file mode 100644 index 0000000000..73791e8412 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.send + +import android.content.Context +import androidx.work.BackoffPolicy +import androidx.work.CoroutineWorker +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkerParameters +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.di.WorkManagerProvider +import org.matrix.android.sdk.internal.session.content.UploadContentWorker +import org.matrix.android.sdk.internal.session.room.timeline.TimelineSendEventWorkCommon +import org.matrix.android.sdk.internal.worker.SessionWorkerParams +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import org.matrix.android.sdk.internal.worker.getSessionComponent +import org.matrix.android.sdk.internal.worker.startChain +import timber.log.Timber +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +/** + * This worker creates a new work for each events passed in parameter + * + * Possible previous worker: Always [UploadContentWorker] + * Possible next worker : None, but it will post new work to send events, encrypted or not + */ +internal class MultipleEventSendingDispatcherWorker(context: Context, params: WorkerParameters) + : CoroutineWorker(context, params) { + + @JsonClass(generateAdapter = true) + internal data class Params( + override val sessionId: String, + val events: List, + val isEncrypted: Boolean, + override val lastFailureMessage: String? = null + ) : SessionWorkerParams + + @Inject lateinit var workManagerProvider: WorkManagerProvider + @Inject lateinit var timelineSendEventWorkCommon: TimelineSendEventWorkCommon + @Inject lateinit var localEchoRepository: LocalEchoRepository + + override suspend fun doWork(): Result { + Timber.v("Start dispatch sending multiple event work") + val params = WorkerParamsFactory.fromData(inputData) + ?: return Result.success() + .also { Timber.e("Unable to parse work parameters") } + + val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() + sessionComponent.inject(this) + + if (params.lastFailureMessage != null) { + params.events.forEach { event -> + event.eventId?.let { localEchoRepository.updateSendState(it, SendState.UNDELIVERED) } + } + // Transmit the error if needed? + return Result.success(inputData) + .also { Timber.e("Work cancelled due to input error from parent") } + } + + // Create a work for every event + params.events.forEach { event -> + if (params.isEncrypted) { + Timber.v("Send event in encrypted room") + val encryptWork = createEncryptEventWork(params.sessionId, event, true) + // Note that event will be replaced by the result of the previous work + val sendWork = createSendEventWork(params.sessionId, event, false) + timelineSendEventWorkCommon.postSequentialWorks(event.roomId!!, encryptWork, sendWork) + } else { + val sendWork = createSendEventWork(params.sessionId, event, true) + timelineSendEventWorkCommon.postWork(event.roomId!!, sendWork) + } + } + + return Result.success() + } + + private fun createEncryptEventWork(sessionId: String, event: Event, startChain: Boolean): OneTimeWorkRequest { + val params = EncryptEventWorker.Params(sessionId, event) + val sendWorkData = WorkerParamsFactory.toData(params) + + return workManagerProvider.matrixOneTimeWorkRequestBuilder() + .setConstraints(WorkManagerProvider.workConstraints) + .setInputData(sendWorkData) + .startChain(startChain) + .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS) + .build() + } + + private fun createSendEventWork(sessionId: String, event: Event, startChain: Boolean): OneTimeWorkRequest { + val sendContentWorkerParams = SendEventWorker.Params(sessionId, event) + val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) + + return timelineSendEventWorkCommon.createWork(sendWorkData, startChain) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/NoMerger.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/NoMerger.kt new file mode 100644 index 0000000000..7b9e1ec9d8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/NoMerger.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.room.send + +import androidx.work.Data +import androidx.work.InputMerger + +/** + * InputMerger which takes only the first input, to ensure an appended work will only have the specified parameters + */ +internal class NoMerger : InputMerger() { + override fun merge(inputs: MutableList): Data { + return inputs.first() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RedactEventWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RedactEventWorker.kt new file mode 100644 index 0000000000..e1e780c35a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RedactEventWorker.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.room.send + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.worker.SessionWorkerParams +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import org.matrix.android.sdk.internal.worker.getSessionComponent +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import javax.inject.Inject + +/** + * Possible previous worker: None + * Possible next worker : None + */ +internal class RedactEventWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { + + @JsonClass(generateAdapter = true) + internal data class Params( + override val sessionId: String, + val txID: String, + val roomId: String, + val eventId: String, + val reason: String?, + override val lastFailureMessage: String? = null + ) : SessionWorkerParams + + @Inject lateinit var roomAPI: RoomAPI + @Inject lateinit var eventBus: EventBus + + override suspend fun doWork(): Result { + val params = WorkerParamsFactory.fromData(inputData) + ?: return Result.failure() + .also { Timber.e("Unable to parse work parameters") } + + if (params.lastFailureMessage != null) { + // Transmit the error + return Result.success(inputData) + .also { Timber.e("Work cancelled due to input error from parent") } + } + + val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() + sessionComponent.inject(this) + + val eventId = params.eventId + return runCatching { + executeRequest(eventBus) { + apiCall = roomAPI.redactEvent( + params.txID, + params.roomId, + eventId, + if (params.reason == null) emptyMap() else mapOf("reason" to params.reason) + ) + } + }.fold( + { + Result.success() + }, + { + when (it) { + is Failure.NetworkConnection -> Result.retry() + else -> { + // TODO mark as failed to send? + // always return success, or the chain will be stuck for ever! + Result.success(WorkerParamsFactory.toData(params.copy( + lastFailureMessage = it.localizedMessage + ))) + } + } + } + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RoomEventSender.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RoomEventSender.kt new file mode 100644 index 0000000000..65c692f42e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RoomEventSender.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.send + +import androidx.work.BackoffPolicy +import androidx.work.OneTimeWorkRequest +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.di.SessionId +import org.matrix.android.sdk.internal.di.WorkManagerProvider +import org.matrix.android.sdk.internal.session.room.timeline.TimelineSendEventWorkCommon +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import org.matrix.android.sdk.internal.worker.startChain +import timber.log.Timber +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +internal class RoomEventSender @Inject constructor( + private val workManagerProvider: WorkManagerProvider, + private val timelineSendEventWorkCommon: TimelineSendEventWorkCommon, + @SessionId private val sessionId: String, + private val cryptoService: CryptoService +) { + fun sendEvent(event: Event): Cancelable { + // Encrypted room handling + return if (cryptoService.isRoomEncrypted(event.roomId ?: "")) { + Timber.v("Send event in encrypted room") + val encryptWork = createEncryptEventWork(event, true) + // Note that event will be replaced by the result of the previous work + val sendWork = createSendEventWork(event, false) + timelineSendEventWorkCommon.postSequentialWorks(event.roomId ?: "", encryptWork, sendWork) + } else { + val sendWork = createSendEventWork(event, true) + timelineSendEventWorkCommon.postWork(event.roomId ?: "", sendWork) + } + } + + private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { + // Same parameter + val params = EncryptEventWorker.Params(sessionId, event) + val sendWorkData = WorkerParamsFactory.toData(params) + + return workManagerProvider.matrixOneTimeWorkRequestBuilder() + .setConstraints(WorkManagerProvider.workConstraints) + .setInputData(sendWorkData) + .startChain(startChain) + .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS) + .build() + } + + private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { + val sendContentWorkerParams = SendEventWorker.Params(sessionId, event) + val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) + + return timelineSendEventWorkCommon.createWork(sendWorkData, startChain) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt new file mode 100644 index 0000000000..2868ce29c1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt @@ -0,0 +1,115 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.send + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.failure.shouldBeRetried +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.database.mapper.ContentMapper +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.worker.SessionWorkerParams +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import org.matrix.android.sdk.internal.worker.getSessionComponent +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import javax.inject.Inject + +private const val MAX_NUMBER_OF_RETRY_BEFORE_FAILING = 3 + +/** + * Possible previous worker: [EncryptEventWorker] or first worker + * Possible next worker : None + */ +internal class SendEventWorker(context: Context, + params: WorkerParameters) + : CoroutineWorker(context, params) { + + @JsonClass(generateAdapter = true) + internal data class Params( + override val sessionId: String, + // TODO remove after some time, it's used for compat + val event: Event? = null, + val eventId: String? = null, + val roomId: String? = null, + val type: String? = null, + val contentStr: String? = null, + override val lastFailureMessage: String? = null + ) : SessionWorkerParams { + + constructor(sessionId: String, event: Event, lastFailureMessage: String? = null) : this( + sessionId = sessionId, + eventId = event.eventId, + roomId = event.roomId, + type = event.type, + contentStr = ContentMapper.map(event.content), + lastFailureMessage = lastFailureMessage + ) + } + + @Inject lateinit var localEchoRepository: LocalEchoRepository + @Inject lateinit var roomAPI: RoomAPI + @Inject lateinit var eventBus: EventBus + + override suspend fun doWork(): Result { + val params = WorkerParamsFactory.fromData(inputData) + ?: return Result.success() + .also { Timber.e("Unable to parse work parameters") } + + val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() + sessionComponent.inject(this) + if (params.eventId == null || params.roomId == null || params.type == null) { + // compat with old params, make it fail if any + if (params.event?.eventId != null) { + localEchoRepository.updateSendState(params.event.eventId, SendState.UNDELIVERED) + } + return Result.success() + } + if (params.lastFailureMessage != null) { + localEchoRepository.updateSendState(params.eventId, SendState.UNDELIVERED) + // Transmit the error + return Result.success(inputData) + .also { Timber.e("Work cancelled due to input error from parent") } + } + return try { + sendEvent(params.eventId, params.roomId, params.type, params.contentStr) + Result.success() + } catch (exception: Throwable) { + // It does start from 0, we want it to stop if it fails the third time + val currentAttemptCount = runAttemptCount + 1 + if (currentAttemptCount >= MAX_NUMBER_OF_RETRY_BEFORE_FAILING || !exception.shouldBeRetried()) { + localEchoRepository.updateSendState(params.eventId, SendState.UNDELIVERED) + return Result.success() + } else { + Result.retry() + } + } + } + + private suspend fun sendEvent(eventId: String, roomId: String, type: String, contentStr: String?) { + localEchoRepository.updateSendState(eventId, SendState.SENDING) + executeRequest(eventBus) { + apiCall = roomAPI.send(eventId, roomId, type, contentStr) + } + localEchoRepository.updateSendState(eventId, SendState.SENT) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendResponse.kt new file mode 100644 index 0000000000..d9ba553ce5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendResponse.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.send + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class SendResponse( + /** + * A unique identifier for the event. + */ + @Json(name = "event_id") val eventId: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/TextContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/TextContent.kt new file mode 100644 index 0000000000..33490a4a03 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/TextContent.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.send + +import org.matrix.android.sdk.api.session.room.model.message.MessageFormat +import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent +import org.matrix.android.sdk.api.session.room.model.message.MessageType +import org.matrix.android.sdk.api.util.ContentUtils.extractUsefulTextFromHtmlReply +import org.matrix.android.sdk.api.util.ContentUtils.extractUsefulTextFromReply + +/** + * Contains a text and eventually a formatted text + */ +data class TextContent( + val text: String, + val formattedText: String? = null +) { + fun takeFormatted() = formattedText ?: text +} + +fun TextContent.toMessageTextContent(msgType: String = MessageType.MSGTYPE_TEXT): MessageTextContent { + return MessageTextContent( + msgType = msgType, + format = MessageFormat.FORMAT_MATRIX_HTML.takeIf { formattedText != null }, + body = text, + formattedBody = formattedText + ) +} + +fun TextContent.removeInReplyFallbacks(): TextContent { + return copy( + text = extractUsefulTextFromReply(this.text), + formattedText = this.formattedText?.let { extractUsefulTextFromHtmlReply(it) } + ) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/MentionLinkSpec.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/MentionLinkSpec.kt new file mode 100644 index 0000000000..18063bbc1e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/MentionLinkSpec.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.send.pills + +import org.matrix.android.sdk.api.session.room.send.MatrixItemSpan + +internal data class MentionLinkSpec( + val span: MatrixItemSpan, + val start: Int, + val end: Int +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/MentionLinkSpecComparator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/MentionLinkSpecComparator.kt new file mode 100644 index 0000000000..e6bc19f3ae --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/MentionLinkSpecComparator.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.send.pills + +import javax.inject.Inject + +internal class MentionLinkSpecComparator @Inject constructor() : Comparator { + + override fun compare(o1: MentionLinkSpec, o2: MentionLinkSpec): Int { + return when { + o1.start < o2.start -> -1 + o1.start > o2.start -> 1 + o1.end < o2.end -> 1 + o1.end > o2.end -> -1 + else -> 0 + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt new file mode 100644 index 0000000000..e0fe580cba --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.room.send.pills + +import android.text.SpannableString +import org.matrix.android.sdk.api.session.room.send.MatrixItemSpan +import java.util.Collections +import javax.inject.Inject + +/** + * Utility class to detect special span in CharSequence and turn them into + * formatted text to send them as a Matrix messages. + */ +internal class TextPillsUtils @Inject constructor( + private val mentionLinkSpecComparator: MentionLinkSpecComparator +) { + + /** + * Detects if transformable spans are present in the text. + * @return the transformed String or null if no Span found + */ + fun processSpecialSpansToHtml(text: CharSequence): String? { + return transformPills(text, MENTION_SPAN_TO_HTML_TEMPLATE) + } + + /** + * Detects if transformable spans are present in the text. + * @return the transformed String or null if no Span found + */ + fun processSpecialSpansToMarkdown(text: CharSequence): String? { + return transformPills(text, MENTION_SPAN_TO_MD_TEMPLATE) + } + + private fun transformPills(text: CharSequence, template: String): String? { + val spannableString = SpannableString.valueOf(text) + val pills = spannableString + ?.getSpans(0, text.length, MatrixItemSpan::class.java) + ?.map { MentionLinkSpec(it, spannableString.getSpanStart(it), spannableString.getSpanEnd(it)) } + ?.toMutableList() + ?.takeIf { it.isNotEmpty() } + ?: return null + + // we need to prune overlaps! + pruneOverlaps(pills) + + return buildString { + var currIndex = 0 + pills.forEachIndexed { _, (urlSpan, start, end) -> + // We want to replace with the pill with a html link + // append text before pill + append(text, currIndex, start) + // append the pill + append(String.format(template, urlSpan.matrixItem.id, urlSpan.matrixItem.getBestName())) + currIndex = end + } + // append text after the last pill + append(text, currIndex, text.length) + } + } + + private fun pruneOverlaps(links: MutableList) { + Collections.sort(links, mentionLinkSpecComparator) + var len = links.size + var i = 0 + while (i < len - 1) { + val a = links[i] + val b = links[i + 1] + var remove = -1 + + // test if there is an overlap + if (b.start in a.start until a.end) { + when { + b.end <= a.end -> + // b is inside a -> b should be removed + remove = i + 1 + a.end - a.start > b.end - b.start -> + // overlap and a is bigger -> b should be removed + remove = i + 1 + a.end - a.start < b.end - b.start -> + // overlap and a is smaller -> a should be removed + remove = i + } + + if (remove != -1) { + links.removeAt(remove) + len-- + continue + } + } + i++ + } + } + + companion object { + private const val MENTION_SPAN_TO_HTML_TEMPLATE = "%2\$s" + + private const val MENTION_SPAN_TO_MD_TEMPLATE = "[%2\$s](https://matrix.to/#/%1\$s)" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt new file mode 100644 index 0000000000..0150acd1e2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt @@ -0,0 +1,144 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.state + +import android.net.Uri +import androidx.lifecycle.LiveData +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility +import org.matrix.android.sdk.api.session.room.state.StateService +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.internal.session.content.FileUploader +import org.matrix.android.sdk.internal.session.room.alias.AddRoomAliasTask +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import org.matrix.android.sdk.internal.task.launchToCallback +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers + +internal class DefaultStateService @AssistedInject constructor(@Assisted private val roomId: String, + private val stateEventDataSource: StateEventDataSource, + private val taskExecutor: TaskExecutor, + private val sendStateTask: SendStateTask, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val fileUploader: FileUploader, + private val addRoomAliasTask: AddRoomAliasTask +) : StateService { + + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): StateService + } + + override fun getStateEvent(eventType: String, stateKey: QueryStringValue): Event? { + return stateEventDataSource.getStateEvent(roomId, eventType, stateKey) + } + + override fun getStateEventLive(eventType: String, stateKey: QueryStringValue): LiveData> { + return stateEventDataSource.getStateEventLive(roomId, eventType, stateKey) + } + + override fun getStateEvents(eventTypes: Set, stateKey: QueryStringValue): List { + return stateEventDataSource.getStateEvents(roomId, eventTypes, stateKey) + } + + override fun getStateEventsLive(eventTypes: Set, stateKey: QueryStringValue): LiveData> { + return stateEventDataSource.getStateEventsLive(roomId, eventTypes, stateKey) + } + + override fun sendStateEvent( + eventType: String, + stateKey: String?, + body: JsonDict, + callback: MatrixCallback + ): Cancelable { + val params = SendStateTask.Params( + roomId = roomId, + stateKey = stateKey, + eventType = eventType, + body = body + ) + return sendStateTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun updateTopic(topic: String, callback: MatrixCallback): Cancelable { + return sendStateEvent( + eventType = EventType.STATE_ROOM_TOPIC, + body = mapOf("topic" to topic), + callback = callback, + stateKey = null + ) + } + + override fun updateName(name: String, callback: MatrixCallback): Cancelable { + return sendStateEvent( + eventType = EventType.STATE_ROOM_NAME, + body = mapOf("name" to name), + callback = callback, + stateKey = null + ) + } + + override fun addRoomAlias(roomAlias: String, callback: MatrixCallback): Cancelable { + return addRoomAliasTask + .configureWith(AddRoomAliasTask.Params(roomId, roomAlias)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun updateCanonicalAlias(alias: String, callback: MatrixCallback): Cancelable { + return sendStateEvent( + eventType = EventType.STATE_ROOM_CANONICAL_ALIAS, + body = mapOf("alias" to alias), + callback = callback, + stateKey = null + ) + } + + override fun updateHistoryReadability(readability: RoomHistoryVisibility, callback: MatrixCallback): Cancelable { + return sendStateEvent( + eventType = EventType.STATE_ROOM_HISTORY_VISIBILITY, + body = mapOf("history_visibility" to readability), + callback = callback, + stateKey = null + ) + } + + override fun updateAvatar(avatarUri: Uri, fileName: String, callback: MatrixCallback): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + val response = fileUploader.uploadFromUri(avatarUri, fileName, "image/jpeg") + sendStateEvent( + eventType = EventType.STATE_ROOM_AVATAR, + body = mapOf("url" to response.contentUri), + callback = callback, + stateKey = null + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/SendStateTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/SendStateTask.kt new file mode 100644 index 0000000000..52e865c4e2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/SendStateTask.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.state + +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface SendStateTask : Task { + data class Params( + val roomId: String, + val stateKey: String?, + val eventType: String, + val body: JsonDict + ) +} + +internal class DefaultSendStateTask @Inject constructor( + private val roomAPI: RoomAPI, + private val eventBus: EventBus +) : SendStateTask { + + override suspend fun execute(params: SendStateTask.Params) { + return executeRequest(eventBus) { + apiCall = if (params.stateKey == null) { + roomAPI.sendStateEvent( + roomId = params.roomId, + stateEventType = params.eventType, + params = params.body + ) + } else { + roomAPI.sendStateEvent( + roomId = params.roomId, + stateEventType = params.eventType, + stateKey = params.stateKey, + params = params.body + ) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/StateEventDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/StateEventDataSource.kt new file mode 100644 index 0000000000..e8dc2ddf40 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/StateEventDataSource.kt @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.state + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.toOptional +import org.matrix.android.sdk.internal.database.mapper.asDomain +import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity +import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.query.process +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.where +import javax.inject.Inject + +internal class StateEventDataSource @Inject constructor(@SessionDatabase private val monarchy: Monarchy) { + + fun getStateEvent(roomId: String, eventType: String, stateKey: QueryStringValue): Event? { + return Realm.getInstance(monarchy.realmConfiguration).use { realm -> + buildStateEventQuery(realm, roomId, setOf(eventType), stateKey).findFirst()?.root?.asDomain() + } + } + + fun getStateEventLive(roomId: String, eventType: String, stateKey: QueryStringValue): LiveData> { + val liveData = monarchy.findAllMappedWithChanges( + { realm -> buildStateEventQuery(realm, roomId, setOf(eventType), stateKey) }, + { it.root?.asDomain() } + ) + return Transformations.map(liveData) { results -> + results.firstOrNull().toOptional() + } + } + + fun getStateEvents(roomId: String, eventTypes: Set, stateKey: QueryStringValue): List { + return Realm.getInstance(monarchy.realmConfiguration).use { realm -> + buildStateEventQuery(realm, roomId, eventTypes, stateKey) + .findAll() + .mapNotNull { + it.root?.asDomain() + } + } + } + + fun getStateEventsLive(roomId: String, eventTypes: Set, stateKey: QueryStringValue): LiveData> { + val liveData = monarchy.findAllMappedWithChanges( + { realm -> buildStateEventQuery(realm, roomId, eventTypes, stateKey) }, + { it.root?.asDomain() } + ) + return Transformations.map(liveData) { results -> + results.filterNotNull() + } + } + + private fun buildStateEventQuery(realm: Realm, + roomId: String, + eventTypes: Set, + stateKey: QueryStringValue + ): RealmQuery { + return realm.where() + .equalTo(CurrentStateEventEntityFields.ROOM_ID, roomId) + .`in`(CurrentStateEventEntityFields.TYPE, eventTypes.toTypedArray()) + .process(CurrentStateEventEntityFields.STATE_KEY, stateKey) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt new file mode 100644 index 0000000000..a43241a657 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.summary + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.VersioningState +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.toOptional +import org.matrix.android.sdk.internal.database.mapper.RoomSummaryMapper +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields +import org.matrix.android.sdk.internal.database.query.findByAlias +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.query.process +import org.matrix.android.sdk.internal.util.fetchCopyMap +import io.realm.Realm +import io.realm.RealmQuery +import javax.inject.Inject + +internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase private val monarchy: Monarchy, + private val roomSummaryMapper: RoomSummaryMapper) { + + fun getRoomSummary(roomIdOrAlias: String): RoomSummary? { + return monarchy + .fetchCopyMap({ + if (roomIdOrAlias.startsWith("!")) { + // It's a roomId + RoomSummaryEntity.where(it, roomId = roomIdOrAlias).findFirst() + } else { + // Assume it's a room alias + RoomSummaryEntity.findByAlias(it, roomIdOrAlias) + } + }, { entity, _ -> + roomSummaryMapper.map(entity) + }) + } + + fun getRoomSummaryLive(roomId: String): LiveData> { + val liveData = monarchy.findAllMappedWithChanges( + { realm -> RoomSummaryEntity.where(realm, roomId).isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) }, + { roomSummaryMapper.map(it) } + ) + return Transformations.map(liveData) { results -> + results.firstOrNull().toOptional() + } + } + + fun getRoomSummaries(queryParams: RoomSummaryQueryParams): List { + return monarchy.fetchAllMappedSync( + { roomSummariesQuery(it, queryParams) }, + { roomSummaryMapper.map(it) } + ) + } + + fun getRoomSummariesLive(queryParams: RoomSummaryQueryParams): LiveData> { + return monarchy.findAllMappedWithChanges( + { roomSummariesQuery(it, queryParams) }, + { roomSummaryMapper.map(it) } + ) + } + + fun getBreadcrumbs(queryParams: RoomSummaryQueryParams): List { + return monarchy.fetchAllMappedSync( + { breadcrumbsQuery(it, queryParams) }, + { roomSummaryMapper.map(it) } + ) + } + + fun getBreadcrumbsLive(queryParams: RoomSummaryQueryParams): LiveData> { + return monarchy.findAllMappedWithChanges( + { breadcrumbsQuery(it, queryParams) }, + { roomSummaryMapper.map(it) } + ) + } + + private fun breadcrumbsQuery(realm: Realm, queryParams: RoomSummaryQueryParams): RealmQuery { + return roomSummariesQuery(realm, queryParams) + .greaterThan(RoomSummaryEntityFields.BREADCRUMBS_INDEX, RoomSummary.NOT_IN_BREADCRUMBS) + .sort(RoomSummaryEntityFields.BREADCRUMBS_INDEX) + } + + private fun roomSummariesQuery(realm: Realm, queryParams: RoomSummaryQueryParams): RealmQuery { + val query = RoomSummaryEntity.where(realm) + query.process(RoomSummaryEntityFields.ROOM_ID, queryParams.roomId) + query.process(RoomSummaryEntityFields.DISPLAY_NAME, queryParams.displayName) + query.process(RoomSummaryEntityFields.CANONICAL_ALIAS, queryParams.canonicalAlias) + query.process(RoomSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships) + query.notEqualTo(RoomSummaryEntityFields.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name) + return query + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt new file mode 100644 index 0000000000..99671c232a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt @@ -0,0 +1,193 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.summary + +import dagger.Lazy +import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.RoomAliasesContent +import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent +import org.matrix.android.sdk.api.session.room.model.RoomNameContent +import org.matrix.android.sdk.api.session.room.model.RoomTopicContent +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.internal.crypto.crosssigning.SessionToCryptoRoomMembersUpdate +import org.matrix.android.sdk.internal.database.mapper.ContentMapper +import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.EventEntityFields +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.query.findAllInRoomWithSendStates +import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.database.query.getOrNull +import org.matrix.android.sdk.internal.database.query.isEventRead +import org.matrix.android.sdk.internal.database.query.latestEvent +import org.matrix.android.sdk.internal.database.query.whereType +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.room.RoomAvatarResolver +import org.matrix.android.sdk.internal.session.room.membership.RoomDisplayNameResolver +import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper +import org.matrix.android.sdk.internal.session.room.timeline.TimelineEventDecryptor +import org.matrix.android.sdk.internal.session.sync.model.RoomSyncSummary +import org.matrix.android.sdk.internal.session.sync.model.RoomSyncUnreadNotifications +import io.realm.Realm +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import javax.inject.Inject + +internal class RoomSummaryUpdater @Inject constructor( + @UserId private val userId: String, + private val roomDisplayNameResolver: RoomDisplayNameResolver, + private val roomAvatarResolver: RoomAvatarResolver, + private val timelineEventDecryptor: Lazy, + private val eventBus: EventBus) { + + companion object { + // TODO: maybe allow user of SDK to give that list + val PREVIEWABLE_TYPES = listOf( + // TODO filter message type (KEY_VERIFICATION_READY, etc.) + EventType.MESSAGE, + EventType.STATE_ROOM_NAME, + EventType.STATE_ROOM_TOPIC, + EventType.STATE_ROOM_AVATAR, + EventType.STATE_ROOM_MEMBER, + EventType.STATE_ROOM_HISTORY_VISIBILITY, + EventType.CALL_INVITE, + EventType.CALL_HANGUP, + EventType.CALL_ANSWER, + EventType.ENCRYPTED, + EventType.STATE_ROOM_ENCRYPTION, + EventType.STATE_ROOM_THIRD_PARTY_INVITE, + EventType.STICKER, + EventType.REACTION, + EventType.STATE_ROOM_CREATE + ) + } + + fun update(realm: Realm, + roomId: String, + membership: Membership? = null, + roomSummary: RoomSyncSummary? = null, + unreadNotifications: RoomSyncUnreadNotifications? = null, + updateMembers: Boolean = false, + inviterId: String? = null) { + val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId) + if (roomSummary != null) { + if (roomSummary.heroes.isNotEmpty()) { + roomSummaryEntity.heroes.clear() + roomSummaryEntity.heroes.addAll(roomSummary.heroes) + } + if (roomSummary.invitedMembersCount != null) { + roomSummaryEntity.invitedMembersCount = roomSummary.invitedMembersCount + } + if (roomSummary.joinedMembersCount != null) { + roomSummaryEntity.joinedMembersCount = roomSummary.joinedMembersCount + } + } + roomSummaryEntity.highlightCount = unreadNotifications?.highlightCount ?: 0 + roomSummaryEntity.notificationCount = unreadNotifications?.notificationCount ?: 0 + + if (membership != null) { + roomSummaryEntity.membership = membership + } + + val latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, + filterTypes = PREVIEWABLE_TYPES, filterContentRelation = true) + + val lastNameEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_NAME, stateKey = "")?.root + val lastTopicEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_TOPIC, stateKey = "")?.root + val lastCanonicalAliasEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_CANONICAL_ALIAS, stateKey = "")?.root + val lastAliasesEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_ALIASES, stateKey = "")?.root + + // Don't use current state for this one as we are only interested in having MXCRYPTO_ALGORITHM_MEGOLM event in the room + val encryptionEvent = EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION) + .contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"") + .findFirst() + + roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0 + // avoid this call if we are sure there are unread events + || !isEventRead(realm.configuration, userId, roomId, latestPreviewableEvent?.eventId) + + roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(realm, roomId).toString() + roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(realm, roomId) + roomSummaryEntity.name = ContentMapper.map(lastNameEvent?.content).toModel()?.name + roomSummaryEntity.topic = ContentMapper.map(lastTopicEvent?.content).toModel()?.topic + roomSummaryEntity.latestPreviewableEvent = latestPreviewableEvent + roomSummaryEntity.canonicalAlias = ContentMapper.map(lastCanonicalAliasEvent?.content).toModel() + ?.canonicalAlias + + val roomAliases = ContentMapper.map(lastAliasesEvent?.content).toModel()?.aliases + .orEmpty() + roomSummaryEntity.aliases.clear() + roomSummaryEntity.aliases.addAll(roomAliases) + roomSummaryEntity.flatAliases = roomAliases.joinToString(separator = "|", prefix = "|") + roomSummaryEntity.isEncrypted = encryptionEvent != null + roomSummaryEntity.encryptionEventTs = encryptionEvent?.originServerTs + + if (roomSummaryEntity.membership == Membership.INVITE && inviterId != null) { + roomSummaryEntity.inviterId = inviterId + } else if (roomSummaryEntity.membership != Membership.INVITE) { + roomSummaryEntity.inviterId = null + } + roomSummaryEntity.updateHasFailedSending() + + if (latestPreviewableEvent?.root?.type == EventType.ENCRYPTED && latestPreviewableEvent.root?.decryptionResultJson == null) { + Timber.v("Should decrypt ${latestPreviewableEvent.eventId}") + timelineEventDecryptor.get().requestDecryption(TimelineEventDecryptor.DecryptionRequest(latestPreviewableEvent.eventId, "")) + } + + if (updateMembers) { + val otherRoomMembers = RoomMemberHelper(realm, roomId) + .queryActiveRoomMembersEvent() + .notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId) + .findAll() + .asSequence() + .map { it.userId } + + roomSummaryEntity.otherMemberIds.clear() + roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers) + if (roomSummaryEntity.isEncrypted) { + eventBus.post(SessionToCryptoRoomMembersUpdate(roomId, roomSummaryEntity.isDirect, roomSummaryEntity.otherMemberIds.toList() + userId)) + } + } + } + + private fun RoomSummaryEntity.updateHasFailedSending() { + hasFailedSending = TimelineEventEntity.findAllInRoomWithSendStates(realm, roomId, SendState.HAS_FAILED_STATES).isNotEmpty() + } + + fun updateSendingInformation(realm: Realm, roomId: String) { + val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId) + roomSummaryEntity.updateHasFailedSending() + roomSummaryEntity.latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, + filterTypes = PREVIEWABLE_TYPES, filterContentRelation = true) + } + + fun updateShieldTrust(realm: Realm, + roomId: String, + trust: RoomEncryptionTrustLevel?) { + val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId) + if (roomSummaryEntity.isEncrypted) { + roomSummaryEntity.roomEncryptionTrustLevel = trust + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/AddTagToRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/AddTagToRoomTask.kt new file mode 100644 index 0000000000..d78a7f338a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/AddTagToRoomTask.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.tags + +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface AddTagToRoomTask : Task { + + data class Params( + val roomId: String, + val tag: String, + val order: Double? + ) +} + +internal class DefaultAddTagToRoomTask @Inject constructor( + private val roomAPI: RoomAPI, + @UserId private val userId: String, + private val eventBus: EventBus +) : AddTagToRoomTask { + + override suspend fun execute(params: AddTagToRoomTask.Params) { + executeRequest(eventBus) { + apiCall = roomAPI.putTag( + userId = userId, + roomId = params.roomId, + tag = params.tag, + body = TagBody( + order = params.order + ) + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/DefaultTagsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/DefaultTagsService.kt new file mode 100644 index 0000000000..141adad643 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/DefaultTagsService.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.tags + +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.room.tags.TagsService +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith + +internal class DefaultTagsService @AssistedInject constructor( + @Assisted private val roomId: String, + private val taskExecutor: TaskExecutor, + private val addTagToRoomTask: AddTagToRoomTask, + private val deleteTagFromRoomTask: DeleteTagFromRoomTask +) : TagsService { + + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): TagsService + } + + override fun addTag(tag: String, order: Double?, callback: MatrixCallback): Cancelable { + val params = AddTagToRoomTask.Params(roomId, tag, order) + return addTagToRoomTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun deleteTag(tag: String, callback: MatrixCallback): Cancelable { + val params = DeleteTagFromRoomTask.Params(roomId, tag) + return deleteTagFromRoomTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/DeleteTagFromRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/DeleteTagFromRoomTask.kt new file mode 100644 index 0000000000..9017338503 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/DeleteTagFromRoomTask.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.tags + +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface DeleteTagFromRoomTask : Task { + + data class Params( + val roomId: String, + val tag: String + ) +} + +internal class DefaultDeleteTagFromRoomTask @Inject constructor( + private val roomAPI: RoomAPI, + @UserId private val userId: String, + private val eventBus: EventBus +) : DeleteTagFromRoomTask { + + override suspend fun execute(params: DeleteTagFromRoomTask.Params) { + executeRequest(eventBus) { + apiCall = roomAPI.deleteTag( + userId = userId, + roomId = params.roomId, + tag = params.tag + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/TagBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/TagBody.kt new file mode 100644 index 0000000000..33d39ba4d1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/TagBody.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.tags + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class TagBody( + /** + * A number in a range [0,1] describing a relative position of the room under the given tag. + */ + @Json(name = "order") + val order: Double? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultGetContextOfEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultGetContextOfEventTask.kt new file mode 100644 index 0000000000..9d880c0428 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultGetContextOfEventTask.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.timeline + +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.filter.FilterRepository +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface GetContextOfEventTask : Task { + + data class Params( + val roomId: String, + val eventId: String + ) +} + +internal class DefaultGetContextOfEventTask @Inject constructor( + private val roomAPI: RoomAPI, + private val filterRepository: FilterRepository, + private val tokenChunkEventPersistor: TokenChunkEventPersistor, + private val eventBus: EventBus +) : GetContextOfEventTask { + + override suspend fun execute(params: GetContextOfEventTask.Params): TokenChunkEventPersistor.Result { + val filter = filterRepository.getRoomFilter() + val response = executeRequest(eventBus) { + // We are limiting the response to the event with eventId to be sure we don't have any issue with potential merging process. + apiCall = roomAPI.getContextOfEvent(params.roomId, params.eventId, 0, filter) + } + return tokenChunkEventPersistor.insertInDb(response, params.roomId, PaginationDirection.FORWARDS) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultPaginationTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultPaginationTask.kt new file mode 100644 index 0000000000..55b8fb6ff4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultPaginationTask.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.timeline + +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.filter.FilterRepository +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface PaginationTask : Task { + + data class Params( + val roomId: String, + val from: String, + val direction: PaginationDirection, + val limit: Int + ) +} + +internal class DefaultPaginationTask @Inject constructor( + private val roomAPI: RoomAPI, + private val filterRepository: FilterRepository, + private val tokenChunkEventPersistor: TokenChunkEventPersistor, + private val eventBus: EventBus +) : PaginationTask { + + override suspend fun execute(params: PaginationTask.Params): TokenChunkEventPersistor.Result { + val filter = filterRepository.getRoomFilter() + val chunk = executeRequest(eventBus) { + isRetryable = true + apiCall = roomAPI.getRoomMessagesFrom(params.roomId, params.from, params.direction.value, params.limit, filter) + } + return tokenChunkEventPersistor.insertInDb(chunk, params.roomId, params.direction) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt new file mode 100644 index 0000000000..b4c32c045e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -0,0 +1,798 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.timeline + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.ReadReceipt +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.api.session.room.timeline.Timeline +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings +import org.matrix.android.sdk.api.util.CancelableBag +import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper +import org.matrix.android.sdk.internal.database.model.ChunkEntity +import org.matrix.android.sdk.internal.database.model.ChunkEntityFields +import org.matrix.android.sdk.internal.database.model.RoomEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields +import org.matrix.android.sdk.internal.database.query.TimelineEventFilter +import org.matrix.android.sdk.internal.database.query.findAllInRoomWithSendStates +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.database.query.whereRoomId +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import org.matrix.android.sdk.internal.util.Debouncer +import org.matrix.android.sdk.internal.util.createBackgroundHandler +import org.matrix.android.sdk.internal.util.createUIHandler +import io.realm.OrderedCollectionChangeSet +import io.realm.OrderedRealmCollectionChangeListener +import io.realm.Realm +import io.realm.RealmConfiguration +import io.realm.RealmQuery +import io.realm.RealmResults +import io.realm.Sort +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import timber.log.Timber +import java.util.Collections +import java.util.UUID +import java.util.concurrent.CopyOnWriteArrayList +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference +import kotlin.math.max + +private const val MIN_FETCHING_COUNT = 30 + +internal class DefaultTimeline( + private val roomId: String, + private var initialEventId: String? = null, + private val realmConfiguration: RealmConfiguration, + private val taskExecutor: TaskExecutor, + private val contextOfEventTask: GetContextOfEventTask, + private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, + private val paginationTask: PaginationTask, + private val timelineEventMapper: TimelineEventMapper, + private val settings: TimelineSettings, + private val hiddenReadReceipts: TimelineHiddenReadReceipts, + private val eventBus: EventBus, + private val eventDecryptor: TimelineEventDecryptor +) : Timeline, TimelineHiddenReadReceipts.Delegate { + + data class OnNewTimelineEvents(val roomId: String, val eventIds: List) + data class OnLocalEchoCreated(val roomId: String, val timelineEvent: TimelineEvent) + + companion object { + val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD") + } + + private val listeners = CopyOnWriteArrayList() + private val isStarted = AtomicBoolean(false) + private val isReady = AtomicBoolean(false) + private val mainHandler = createUIHandler() + private val backgroundRealm = AtomicReference() + private val cancelableBag = CancelableBag() + private val debouncer = Debouncer(mainHandler) + + private lateinit var nonFilteredEvents: RealmResults + private lateinit var filteredEvents: RealmResults + private lateinit var sendingEvents: RealmResults + + private var prevDisplayIndex: Int? = null + private var nextDisplayIndex: Int? = null + private val inMemorySendingEvents = Collections.synchronizedList(ArrayList()) + private val builtEvents = Collections.synchronizedList(ArrayList()) + private val builtEventsIdMap = Collections.synchronizedMap(HashMap()) + private val backwardsState = AtomicReference(State()) + private val forwardsState = AtomicReference(State()) + + override val timelineID = UUID.randomUUID().toString() + + override val isLive + get() = !hasMoreToLoad(Timeline.Direction.FORWARDS) + + private val eventsChangeListener = OrderedRealmCollectionChangeListener> { results, changeSet -> + if (!results.isLoaded || !results.isValid) { + return@OrderedRealmCollectionChangeListener + } + handleUpdates(results, changeSet) + } + + // Public methods ****************************************************************************** + + override fun paginate(direction: Timeline.Direction, count: Int) { + BACKGROUND_HANDLER.post { + if (!canPaginate(direction)) { + return@post + } + Timber.v("Paginate $direction of $count items") + val startDisplayIndex = if (direction == Timeline.Direction.BACKWARDS) prevDisplayIndex else nextDisplayIndex + val shouldPostSnapshot = paginateInternal(startDisplayIndex, direction, count) + if (shouldPostSnapshot) { + postSnapshot() + } + } + } + + override fun pendingEventCount(): Int { + return Realm.getInstance(realmConfiguration).use { + RoomEntity.where(it, roomId).findFirst()?.sendingTimelineEvents?.count() ?: 0 + } + } + + override fun failedToDeliverEventCount(): Int { + return Realm.getInstance(realmConfiguration).use { + TimelineEventEntity.findAllInRoomWithSendStates(it, roomId, SendState.HAS_FAILED_STATES).count() + } + } + + override fun start() { + if (isStarted.compareAndSet(false, true)) { + Timber.v("Start timeline for roomId: $roomId and eventId: $initialEventId") + eventBus.register(this) + BACKGROUND_HANDLER.post { + eventDecryptor.start() + val realm = Realm.getInstance(realmConfiguration) + backgroundRealm.set(realm) + + val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() + ?: throw IllegalStateException("Can't open a timeline without a room") + + sendingEvents = roomEntity.sendingTimelineEvents.where().filterEventsWithSettings().findAll() + sendingEvents.addChangeListener { events -> + // Remove in memory as soon as they are known by database + events.forEach { te -> + inMemorySendingEvents.removeAll { te.eventId == it.eventId } + } + postSnapshot() + } + nonFilteredEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll() + filteredEvents = nonFilteredEvents.where() + .filterEventsWithSettings() + .findAll() + nonFilteredEvents.addChangeListener(eventsChangeListener) + handleInitialLoad() + if (settings.shouldHandleHiddenReadReceipts()) { + hiddenReadReceipts.start(realm, filteredEvents, nonFilteredEvents, this) + } + isReady.set(true) + } + } + } + + private fun TimelineSettings.shouldHandleHiddenReadReceipts(): Boolean { + return buildReadReceipts && (filterEdits || filterTypes) + } + + override fun dispose() { + if (isStarted.compareAndSet(true, false)) { + isReady.set(false) + eventBus.unregister(this) + Timber.v("Dispose timeline for roomId: $roomId and eventId: $initialEventId") + cancelableBag.cancel() + BACKGROUND_HANDLER.removeCallbacksAndMessages(null) + BACKGROUND_HANDLER.post { + if (this::sendingEvents.isInitialized) { + sendingEvents.removeAllChangeListeners() + } + if (this::nonFilteredEvents.isInitialized) { + nonFilteredEvents.removeAllChangeListeners() + } + if (settings.shouldHandleHiddenReadReceipts()) { + hiddenReadReceipts.dispose() + } + clearAllValues() + backgroundRealm.getAndSet(null).also { + it?.close() + } + eventDecryptor.destroy() + } + } + } + + override fun restartWithEventId(eventId: String?) { + dispose() + initialEventId = eventId + start() + postSnapshot() + } + + override fun getTimelineEventAtIndex(index: Int): TimelineEvent? { + return builtEvents.getOrNull(index) + } + + override fun getIndexOfEvent(eventId: String?): Int? { + return builtEventsIdMap[eventId] + } + + override fun getTimelineEventWithId(eventId: String?): TimelineEvent? { + return builtEventsIdMap[eventId]?.let { + getTimelineEventAtIndex(it) + } + } + + override fun getFirstDisplayableEventId(eventId: String): String? { + // If the item is built, the id is obviously displayable + val builtIndex = builtEventsIdMap[eventId] + if (builtIndex != null) { + return eventId + } + // Otherwise, we should check if the event is in the db, but is hidden because of filters + return Realm.getInstance(realmConfiguration).use { localRealm -> + val nonFilteredEvents = buildEventQuery(localRealm) + .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) + .findAll() + + val nonFilteredEvent = nonFilteredEvents.where() + .equalTo(TimelineEventEntityFields.EVENT_ID, eventId) + .findFirst() + + val filteredEvents = nonFilteredEvents.where().filterEventsWithSettings().findAll() + val isEventInDb = nonFilteredEvent != null + + val isHidden = isEventInDb && filteredEvents.where() + .equalTo(TimelineEventEntityFields.EVENT_ID, eventId) + .findFirst() == null + + if (isHidden) { + val displayIndex = nonFilteredEvent?.displayIndex + if (displayIndex != null) { + // Then we are looking for the first displayable event after the hidden one + val firstDisplayedEvent = filteredEvents.where() + .lessThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, displayIndex) + .findFirst() + firstDisplayedEvent?.eventId + } else { + null + } + } else { + null + } + } + } + + override fun hasMoreToLoad(direction: Timeline.Direction): Boolean { + return hasMoreInCache(direction) || !hasReachedEnd(direction) + } + + override fun addListener(listener: Timeline.Listener): Boolean { + if (listeners.contains(listener)) { + return false + } + return listeners.add(listener).also { + postSnapshot() + } + } + + override fun removeListener(listener: Timeline.Listener): Boolean { + return listeners.remove(listener) + } + + override fun removeAllListeners() { + listeners.clear() + } + +// TimelineHiddenReadReceipts.Delegate + + override fun rebuildEvent(eventId: String, readReceipts: List): Boolean { + return rebuildEvent(eventId) { te -> + te.copy(readReceipts = readReceipts) + } + } + + override fun onReadReceiptsUpdated() { + postSnapshot() + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onNewTimelineEvents(onNewTimelineEvents: OnNewTimelineEvents) { + if (isLive && onNewTimelineEvents.roomId == roomId) { + listeners.forEach { + it.onNewTimelineEvents(onNewTimelineEvents.eventIds) + } + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onLocalEchoCreated(onLocalEchoCreated: OnLocalEchoCreated) { + if (isLive && onLocalEchoCreated.roomId == roomId) { + listeners.forEach { + it.onNewTimelineEvents(listOf(onLocalEchoCreated.timelineEvent.eventId)) + } + Timber.v("On local echo created: $onLocalEchoCreated") + inMemorySendingEvents.add(0, onLocalEchoCreated.timelineEvent) + postSnapshot() + } + } + +// Private methods ***************************************************************************** + + private fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent): Boolean { + return builtEventsIdMap[eventId]?.let { builtIndex -> + // Update the relation of existing event + builtEvents[builtIndex]?.let { te -> + builtEvents[builtIndex] = builder(te) + true + } + } ?: false + } + + private fun hasMoreInCache(direction: Timeline.Direction) = getState(direction).hasMoreInCache + + private fun hasReachedEnd(direction: Timeline.Direction) = getState(direction).hasReachedEnd + + private fun updateLoadingStates(results: RealmResults) { + val lastCacheEvent = results.lastOrNull() + val firstCacheEvent = results.firstOrNull() + val chunkEntity = getLiveChunk() + + updateState(Timeline.Direction.FORWARDS) { + it.copy( + hasMoreInCache = !builtEventsIdMap.containsKey(firstCacheEvent?.eventId), + hasReachedEnd = chunkEntity?.isLastForward ?: false + ) + } + updateState(Timeline.Direction.BACKWARDS) { + it.copy( + hasMoreInCache = !builtEventsIdMap.containsKey(lastCacheEvent?.eventId), + hasReachedEnd = chunkEntity?.isLastBackward ?: false || lastCacheEvent?.root?.type == EventType.STATE_ROOM_CREATE + ) + } + } + + /** + * This has to be called on TimelineThread as it accesses realm live results + * @return true if createSnapshot should be posted + */ + private fun paginateInternal(startDisplayIndex: Int?, + direction: Timeline.Direction, + count: Int): Boolean { + if (count == 0) { + return false + } + updateState(direction) { it.copy(requestedPaginationCount = count, isPaginating = true) } + val builtCount = buildTimelineEvents(startDisplayIndex, direction, count.toLong()) + val shouldFetchMore = builtCount < count && !hasReachedEnd(direction) + if (shouldFetchMore) { + val newRequestedCount = count - builtCount + updateState(direction) { it.copy(requestedPaginationCount = newRequestedCount) } + val fetchingCount = max(MIN_FETCHING_COUNT, newRequestedCount) + executePaginationTask(direction, fetchingCount) + } else { + updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) } + } + return !shouldFetchMore + } + + private fun createSnapshot(): List { + return buildSendingEvents() + builtEvents.toList() + } + + private fun buildSendingEvents(): List { + val builtSendingEvents = ArrayList() + if (hasReachedEnd(Timeline.Direction.FORWARDS) && !hasMoreInCache(Timeline.Direction.FORWARDS)) { + builtSendingEvents.addAll(inMemorySendingEvents.filterEventsWithSettings()) + sendingEvents.forEach { timelineEventEntity -> + if (builtSendingEvents.find { it.eventId == timelineEventEntity.eventId } == null) { + builtSendingEvents.add(timelineEventMapper.map(timelineEventEntity)) + } + } + } + return builtSendingEvents + } + + private fun canPaginate(direction: Timeline.Direction): Boolean { + return isReady.get() && !getState(direction).isPaginating && hasMoreToLoad(direction) + } + + private fun getState(direction: Timeline.Direction): State { + return when (direction) { + Timeline.Direction.FORWARDS -> forwardsState.get() + Timeline.Direction.BACKWARDS -> backwardsState.get() + } + } + + private fun updateState(direction: Timeline.Direction, update: (State) -> State) { + val stateReference = when (direction) { + Timeline.Direction.FORWARDS -> forwardsState + Timeline.Direction.BACKWARDS -> backwardsState + } + val currentValue = stateReference.get() + val newValue = update(currentValue) + stateReference.set(newValue) + } + + /** + * This has to be called on TimelineThread as it accesses realm live results + */ + private fun handleInitialLoad() { + var shouldFetchInitialEvent = false + val currentInitialEventId = initialEventId + val initialDisplayIndex = if (currentInitialEventId == null) { + nonFilteredEvents.firstOrNull()?.displayIndex + } else { + val initialEvent = nonFilteredEvents.where() + .equalTo(TimelineEventEntityFields.EVENT_ID, initialEventId) + .findFirst() + + shouldFetchInitialEvent = initialEvent == null + initialEvent?.displayIndex + } + prevDisplayIndex = initialDisplayIndex + nextDisplayIndex = initialDisplayIndex + if (currentInitialEventId != null && shouldFetchInitialEvent) { + fetchEvent(currentInitialEventId) + } else { + val count = filteredEvents.size.coerceAtMost(settings.initialSize) + if (initialEventId == null) { + paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count) + } else { + paginateInternal(initialDisplayIndex, Timeline.Direction.FORWARDS, (count / 2).coerceAtLeast(1)) + paginateInternal(initialDisplayIndex?.minus(1), Timeline.Direction.BACKWARDS, (count / 2).coerceAtLeast(1)) + } + } + postSnapshot() + } + + /** + * This has to be called on TimelineThread as it accesses realm live results + */ + private fun handleUpdates(results: RealmResults, changeSet: OrderedCollectionChangeSet) { + // If changeSet has deletion we are having a gap, so we clear everything + if (changeSet.deletionRanges.isNotEmpty()) { + clearAllValues() + } + var postSnapshot = false + changeSet.insertionRanges.forEach { range -> + val (startDisplayIndex, direction) = if (range.startIndex == 0) { + Pair(results[range.length - 1]!!.displayIndex, Timeline.Direction.FORWARDS) + } else { + Pair(results[range.startIndex]!!.displayIndex, Timeline.Direction.BACKWARDS) + } + val state = getState(direction) + if (state.isPaginating) { + // We are getting new items from pagination + postSnapshot = paginateInternal(startDisplayIndex, direction, state.requestedPaginationCount) + } else { + // We are getting new items from sync + buildTimelineEvents(startDisplayIndex, direction, range.length.toLong()) + postSnapshot = true + } + } + changeSet.changes.forEach { index -> + val eventEntity = results[index] + eventEntity?.eventId?.let { eventId -> + postSnapshot = rebuildEvent(eventId) { + buildTimelineEvent(eventEntity) + } || postSnapshot + } + } + if (postSnapshot) { + postSnapshot() + } + } + + /** + * This has to be called on TimelineThread as it accesses realm live results + */ + private fun executePaginationTask(direction: Timeline.Direction, limit: Int) { + val currentChunk = getLiveChunk() + val token = if (direction == Timeline.Direction.BACKWARDS) currentChunk?.prevToken else currentChunk?.nextToken + if (token == null) { + if (direction == Timeline.Direction.BACKWARDS + || (direction == Timeline.Direction.FORWARDS && currentChunk?.hasBeenALastForwardChunk().orFalse())) { + // We are in the case where event exists, but we do not know the token. + // Fetch (again) the last event to get a token + val lastKnownEventId = if (direction == Timeline.Direction.FORWARDS) { + nonFilteredEvents.firstOrNull()?.eventId + } else { + nonFilteredEvents.lastOrNull()?.eventId + } + if (lastKnownEventId == null) { + updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) } + } else { + val params = FetchTokenAndPaginateTask.Params( + roomId = roomId, + limit = limit, + direction = direction.toPaginationDirection(), + lastKnownEventId = lastKnownEventId + ) + cancelableBag += fetchTokenAndPaginateTask + .configureWith(params) { + this.callback = createPaginationCallback(limit, direction) + } + .executeBy(taskExecutor) + } + } else { + updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) } + } + } else { + val params = PaginationTask.Params( + roomId = roomId, + from = token, + direction = direction.toPaginationDirection(), + limit = limit + ) + Timber.v("Should fetch $limit items $direction") + cancelableBag += paginationTask + .configureWith(params) { + this.callback = createPaginationCallback(limit, direction) + } + .executeBy(taskExecutor) + } + } + + // For debug purpose only + private fun dumpAndLogChunks() { + val liveChunk = getLiveChunk() + Timber.w("Live chunk: $liveChunk") + + Realm.getInstance(realmConfiguration).use { realm -> + ChunkEntity.where(realm, roomId).findAll() + .also { Timber.w("Found ${it.size} chunks") } + .forEach { + Timber.w("") + Timber.w("ChunkEntity: $it") + Timber.w("prevToken: ${it.prevToken}") + Timber.w("nextToken: ${it.nextToken}") + Timber.w("isLastBackward: ${it.isLastBackward}") + Timber.w("isLastForward: ${it.isLastForward}") + it.timelineEvents.forEach { tle -> + Timber.w(" TLE: ${tle.root?.content}") + } + } + } + } + + /** + * This has to be called on TimelineThread as it accesses realm live results + */ + private fun getTokenLive(direction: Timeline.Direction): String? { + val chunkEntity = getLiveChunk() ?: return null + return if (direction == Timeline.Direction.BACKWARDS) chunkEntity.prevToken else chunkEntity.nextToken + } + + /** + * This has to be called on TimelineThread as it accesses realm live results + * Return the current Chunk + */ + private fun getLiveChunk(): ChunkEntity? { + return nonFilteredEvents.firstOrNull()?.chunk?.firstOrNull() + } + + /** + * This has to be called on TimelineThread as it accesses realm live results + * @return the number of items who have been added + */ + private fun buildTimelineEvents(startDisplayIndex: Int?, + direction: Timeline.Direction, + count: Long): Int { + if (count < 1 || startDisplayIndex == null) { + return 0 + } + val start = System.currentTimeMillis() + val offsetResults = getOffsetResults(startDisplayIndex, direction, count) + if (offsetResults.isEmpty()) { + return 0 + } + val offsetIndex = offsetResults.last()!!.displayIndex + if (direction == Timeline.Direction.BACKWARDS) { + prevDisplayIndex = offsetIndex - 1 + } else { + nextDisplayIndex = offsetIndex + 1 + } + offsetResults.forEach { eventEntity -> + + val timelineEvent = buildTimelineEvent(eventEntity) + val transactionId = timelineEvent.root.unsignedData?.transactionId + val sendingEvent = inMemorySendingEvents.find { + it.eventId == transactionId + } + inMemorySendingEvents.remove(sendingEvent) + + if (timelineEvent.isEncrypted() + && timelineEvent.root.mxDecryptionResult == null) { + timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(it, timelineID)) } + } + + val position = if (direction == Timeline.Direction.FORWARDS) 0 else builtEvents.size + builtEvents.add(position, timelineEvent) + // Need to shift :/ + builtEventsIdMap.entries.filter { it.value >= position }.forEach { it.setValue(it.value + 1) } + builtEventsIdMap[eventEntity.eventId] = position + } + val time = System.currentTimeMillis() - start + Timber.v("Built ${offsetResults.size} items from db in $time ms") + // For the case where wo reach the lastForward chunk + updateLoadingStates(filteredEvents) + return offsetResults.size + } + + private fun buildTimelineEvent(eventEntity: TimelineEventEntity) = timelineEventMapper.map( + timelineEventEntity = eventEntity, + buildReadReceipts = settings.buildReadReceipts, + correctedReadReceipts = hiddenReadReceipts.correctedReadReceipts(eventEntity.eventId) + ) + + /** + * This has to be called on TimelineThread as it accesses realm live results + */ + private fun getOffsetResults(startDisplayIndex: Int, + direction: Timeline.Direction, + count: Long): RealmResults { + val offsetQuery = filteredEvents.where() + if (direction == Timeline.Direction.BACKWARDS) { + offsetQuery + .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) + .lessThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex) + } else { + offsetQuery + .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING) + .greaterThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex) + } + return offsetQuery + .limit(count) + .findAll() + } + + private fun buildEventQuery(realm: Realm): RealmQuery { + return if (initialEventId == null) { + TimelineEventEntity + .whereRoomId(realm, roomId = roomId) + .equalTo("${TimelineEventEntityFields.CHUNK}.${ChunkEntityFields.IS_LAST_FORWARD}", true) + } else { + TimelineEventEntity + .whereRoomId(realm, roomId = roomId) + .`in`("${TimelineEventEntityFields.CHUNK}.${ChunkEntityFields.TIMELINE_EVENTS.EVENT_ID}", arrayOf(initialEventId)) + } + } + + private fun fetchEvent(eventId: String) { + val params = GetContextOfEventTask.Params(roomId, eventId) + cancelableBag += contextOfEventTask.configureWith(params) { + callback = object : MatrixCallback { + override fun onSuccess(data: TokenChunkEventPersistor.Result) { + postSnapshot() + } + + override fun onFailure(failure: Throwable) { + postFailure(failure) + } + } + } + .executeBy(taskExecutor) + } + + private fun postSnapshot() { + BACKGROUND_HANDLER.post { + if (isReady.get().not()) { + return@post + } + updateLoadingStates(filteredEvents) + val snapshot = createSnapshot() + val runnable = Runnable { + listeners.forEach { + it.onTimelineUpdated(snapshot) + } + } + debouncer.debounce("post_snapshot", runnable, 1) + } + } + + private fun postFailure(throwable: Throwable) { + if (isReady.get().not()) { + return + } + val runnable = Runnable { + listeners.forEach { + it.onTimelineFailure(throwable) + } + } + mainHandler.post(runnable) + } + + private fun clearAllValues() { + prevDisplayIndex = null + nextDisplayIndex = null + builtEvents.clear() + builtEventsIdMap.clear() + backwardsState.set(State()) + forwardsState.set(State()) + } + + private fun createPaginationCallback(limit: Int, direction: Timeline.Direction): MatrixCallback { + return object : MatrixCallback { + override fun onSuccess(data: TokenChunkEventPersistor.Result) { + when (data) { + TokenChunkEventPersistor.Result.SUCCESS -> { + Timber.v("Success fetching $limit items $direction from pagination request") + } + TokenChunkEventPersistor.Result.REACHED_END -> { + postSnapshot() + } + TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE -> + // Database won't be updated, so we force pagination request + BACKGROUND_HANDLER.post { + executePaginationTask(direction, limit) + } + } + } + + override fun onFailure(failure: Throwable) { + updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) } + postSnapshot() + Timber.v("Failure fetching $limit items $direction from pagination request") + } + } + } + + // Extension methods *************************************************************************** + + private fun Timeline.Direction.toPaginationDirection(): PaginationDirection { + return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS + } + + private fun RealmQuery.filterEventsWithSettings(): RealmQuery { + if (settings.filterTypes) { + `in`(TimelineEventEntityFields.ROOT.TYPE, settings.allowedTypes.toTypedArray()) + } + if (settings.filterUseless) { + not() + .equalTo(TimelineEventEntityFields.ROOT.IS_USELESS, true) + } + if (settings.filterEdits) { + not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.EDIT) + not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.RESPONSE) + } + if (settings.filterRedacted) { + not().like(TimelineEventEntityFields.ROOT.UNSIGNED_DATA, TimelineEventFilter.Unsigned.REDACTED) + } + return this + } + + private fun List.filterEventsWithSettings(): List { + return filter { + val filterType = !settings.filterTypes || settings.allowedTypes.contains(it.root.type) + if (!filterType) return@filter false + + val filterEdits = if (settings.filterEdits && it.root.type == EventType.MESSAGE) { + val messageContent = it.root.content.toModel() + messageContent?.relatesTo?.type != RelationType.REPLACE + } else { + true + } + if (!filterEdits) return@filter false + + val filterRedacted = !settings.filterRedacted || it.root.isRedacted() + + filterRedacted + } + } + + private data class State( + val hasReachedEnd: Boolean = false, + val hasMoreInCache: Boolean = true, + val isPaginating: Boolean = false, + val requestedPaginationCount: Int = 0 + ) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt new file mode 100644 index 0000000000..db675f69f5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt @@ -0,0 +1,110 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.timeline + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.events.model.isImageMessage +import org.matrix.android.sdk.api.session.events.model.isVideoMessage +import org.matrix.android.sdk.api.session.room.timeline.Timeline +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.session.room.timeline.TimelineService +import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.toOptional +import org.matrix.android.sdk.internal.crypto.store.db.doWithRealm +import org.matrix.android.sdk.internal.database.mapper.ReadReceiptsSummaryMapper +import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.util.fetchCopyMap +import io.realm.Sort +import io.realm.kotlin.where +import org.greenrobot.eventbus.EventBus + +internal class DefaultTimelineService @AssistedInject constructor(@Assisted private val roomId: String, + @SessionDatabase private val monarchy: Monarchy, + private val eventBus: EventBus, + private val taskExecutor: TaskExecutor, + private val contextOfEventTask: GetContextOfEventTask, + private val eventDecryptor: TimelineEventDecryptor, + private val paginationTask: PaginationTask, + private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, + private val timelineEventMapper: TimelineEventMapper, + private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper +) : TimelineService { + + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): TimelineService + } + + override fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline { + return DefaultTimeline( + roomId = roomId, + initialEventId = eventId, + realmConfiguration = monarchy.realmConfiguration, + taskExecutor = taskExecutor, + contextOfEventTask = contextOfEventTask, + paginationTask = paginationTask, + timelineEventMapper = timelineEventMapper, + settings = settings, + hiddenReadReceipts = TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings), + eventBus = eventBus, + eventDecryptor = eventDecryptor, + fetchTokenAndPaginateTask = fetchTokenAndPaginateTask + ) + } + + override fun getTimeLineEvent(eventId: String): TimelineEvent? { + return monarchy + .fetchCopyMap({ + TimelineEventEntity.where(it, roomId = roomId, eventId = eventId).findFirst() + }, { entity, _ -> + timelineEventMapper.map(entity) + }) + } + + override fun getTimeLineEventLive(eventId: String): LiveData> { + val liveData = monarchy.findAllMappedWithChanges( + { TimelineEventEntity.where(it, roomId = roomId, eventId = eventId) }, + { timelineEventMapper.map(it) } + ) + return Transformations.map(liveData) { events -> + events.firstOrNull().toOptional() + } + } + + override fun getAttachmentMessages(): List { + // TODO pretty bad query.. maybe we should denormalize clear type in base? + return doWithRealm(monarchy.realmConfiguration) { realm -> + realm.where() + .equalTo(TimelineEventEntityFields.ROOM_ID, roomId) + .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING) + .findAll() + ?.mapNotNull { timelineEventMapper.map(it).takeIf { it.root.isImageMessage() || it.root.isVideoMessage() } } + ?: emptyList() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/EventContextResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/EventContextResponse.kt new file mode 100644 index 0000000000..27006c8183 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/EventContextResponse.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.timeline + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Event + +@JsonClass(generateAdapter = true) +data class EventContextResponse( + @Json(name = "event") val event: Event, + @Json(name = "start") override val start: String? = null, + @Json(name = "events_before") val eventsBefore: List = emptyList(), + @Json(name = "events_after") val eventsAfter: List = emptyList(), + @Json(name = "end") override val end: String? = null, + @Json(name = "state") override val stateEvents: List = emptyList() +) : TokenChunkEvent { + + override val events: List by lazy { + eventsAfter.reversed() + listOf(event) + eventsBefore + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/FetchTokenAndPaginateTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/FetchTokenAndPaginateTask.kt new file mode 100644 index 0000000000..23a32996fa --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/FetchTokenAndPaginateTask.kt @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.timeline + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.internal.database.model.ChunkEntity +import org.matrix.android.sdk.internal.database.query.findIncludingEvent +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.filter.FilterRepository +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface FetchTokenAndPaginateTask : Task { + + data class Params( + val roomId: String, + val lastKnownEventId: String, + val direction: PaginationDirection, + val limit: Int + ) +} + +internal class DefaultFetchTokenAndPaginateTask @Inject constructor( + private val roomAPI: RoomAPI, + @SessionDatabase private val monarchy: Monarchy, + private val filterRepository: FilterRepository, + private val paginationTask: PaginationTask, + private val eventBus: EventBus +) : FetchTokenAndPaginateTask { + + override suspend fun execute(params: FetchTokenAndPaginateTask.Params): TokenChunkEventPersistor.Result { + val filter = filterRepository.getRoomFilter() + val response = executeRequest(eventBus) { + apiCall = roomAPI.getContextOfEvent(params.roomId, params.lastKnownEventId, 0, filter) + } + val fromToken = if (params.direction == PaginationDirection.FORWARDS) { + response.end + } else { + response.start + } + ?: throw IllegalStateException("No token found") + + monarchy.awaitTransaction { realm -> + val chunkToUpdate = ChunkEntity.findIncludingEvent(realm, params.lastKnownEventId) + if (params.direction == PaginationDirection.FORWARDS) { + chunkToUpdate?.nextToken = fromToken + } else { + chunkToUpdate?.prevToken = fromToken + } + } + val paginationParams = PaginationTask.Params( + roomId = params.roomId, + from = fromToken, + direction = params.direction, + limit = params.limit + ) + return paginationTask.execute(paginationParams) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt new file mode 100644 index 0000000000..531fac4a57 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.timeline + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +// TODO Add parent task + +internal class GetEventTask @Inject constructor( + private val roomAPI: RoomAPI, + private val eventBus: EventBus +) : Task { + + internal data class Params( + val roomId: String, + val eventId: String + ) + + override suspend fun execute(params: Params): Event { + return executeRequest(eventBus) { + apiCall = roomAPI.getEvent(params.roomId, params.eventId) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationDirection.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationDirection.kt new file mode 100644 index 0000000000..581f23c403 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationDirection.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.timeline + +internal enum class PaginationDirection(val value: String) { + /** + * Forwards when the event is added to the end of the timeline. + * These events come from the /sync stream or from forwards pagination. + */ + FORWARDS("f"), + + /** + * Backwards when the event is added to the start of the timeline. + * These events come from a back pagination. + */ + BACKWARDS("b"); + + fun reversed(): PaginationDirection { + return when (this) { + FORWARDS -> BACKWARDS + BACKWARDS -> FORWARDS + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationResponse.kt new file mode 100644 index 0000000000..b0f2e693dc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationResponse.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.timeline + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Event + +@JsonClass(generateAdapter = true) +internal data class PaginationResponse( + @Json(name = "start") override val start: String? = null, + @Json(name = "end") override val end: String? = null, + @Json(name = "chunk") override val events: List = emptyList(), + @Json(name = "state") override val stateEvents: List = emptyList() +) : TokenChunkEvent diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt new file mode 100644 index 0000000000..0ca0d19b5e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt @@ -0,0 +1,145 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.room.timeline + +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.internal.crypto.NewSessionListener +import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent +import org.matrix.android.sdk.internal.database.mapper.asDomain +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.SessionScope +import io.realm.Realm +import io.realm.RealmConfiguration +import timber.log.Timber +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import javax.inject.Inject + +@SessionScope +internal class TimelineEventDecryptor @Inject constructor( + @SessionDatabase + private val realmConfiguration: RealmConfiguration, + private val cryptoService: CryptoService +) { + + private val newSessionListener = object : NewSessionListener { + override fun onNewSession(roomId: String?, senderKey: String, sessionId: String) { + synchronized(unknownSessionsFailure) { + unknownSessionsFailure[sessionId] + ?.toList() + .orEmpty() + .also { + unknownSessionsFailure[sessionId]?.clear() + } + }.forEach { + requestDecryption(it) + } + } + } + + private var executor: ExecutorService? = null + + // Set of eventIds which are currently decrypting + private val existingRequests = mutableSetOf() + // sessionId -> list of eventIds + private val unknownSessionsFailure = mutableMapOf>() + + fun start() { + executor = Executors.newSingleThreadExecutor() + cryptoService.addNewSessionListener(newSessionListener) + } + + fun destroy() { + cryptoService.removeSessionListener(newSessionListener) + executor?.shutdownNow() + executor = null + synchronized(unknownSessionsFailure) { + unknownSessionsFailure.clear() + } + synchronized(existingRequests) { + existingRequests.clear() + } + } + + fun requestDecryption(request: DecryptionRequest) { + synchronized(unknownSessionsFailure) { + for (requests in unknownSessionsFailure.values) { + if (request in requests) { + Timber.d("Skip Decryption request for event ${request.eventId}, unknown session") + return + } + } + } + synchronized(existingRequests) { + if (!existingRequests.add(request)) { + Timber.d("Skip Decryption request for event ${request.eventId}, already requested") + return + } + } + executor?.execute { + Realm.getInstance(realmConfiguration).use { realm -> + processDecryptRequest(request, realm) + } + } + } + + private fun processDecryptRequest(request: DecryptionRequest, realm: Realm) = realm.executeTransaction { + val eventId = request.eventId + val timelineId = request.timelineId + Timber.v("Decryption request for event $eventId") + val eventEntity = EventEntity.where(realm, eventId = eventId).findFirst() + ?: return@executeTransaction Unit.also { + Timber.d("Decryption request for unknown message") + } + val event = eventEntity.asDomain() + try { + val result = cryptoService.decryptEvent(event, timelineId) + Timber.v("Successfully decrypted event $eventId") + eventEntity.setDecryptionResult(result) + } catch (e: MXCryptoError) { + Timber.v(e, "Failed to decrypt event $eventId") + if (e is MXCryptoError.Base /*&& e.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID*/) { + // Keep track of unknown sessions to automatically try to decrypt on new session + eventEntity.decryptionErrorCode = e.errorType.name + eventEntity.decryptionErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription + event.content?.toModel()?.let { content -> + content.sessionId?.let { sessionId -> + synchronized(unknownSessionsFailure) { + val list = unknownSessionsFailure.getOrPut(sessionId) { mutableSetOf() } + list.add(request) + } + } + } + } + } catch (t: Throwable) { + Timber.e("Failed to decrypt event $eventId, ${t.localizedMessage}") + } finally { + synchronized(existingRequests) { + existingRequests.remove(request) + } + } + } + + data class DecryptionRequest( + val eventId: String, + val timelineId: String + ) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineHiddenReadReceipts.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineHiddenReadReceipts.kt new file mode 100644 index 0000000000..426daa4b57 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineHiddenReadReceipts.kt @@ -0,0 +1,177 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.timeline + +import android.util.SparseArray +import org.matrix.android.sdk.api.session.room.model.ReadReceipt +import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings +import org.matrix.android.sdk.internal.database.mapper.ReadReceiptsSummaryMapper +import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntity +import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntityFields +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields +import org.matrix.android.sdk.internal.database.query.TimelineEventFilter +import org.matrix.android.sdk.internal.database.query.whereInRoom +import io.realm.OrderedRealmCollectionChangeListener +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.RealmResults + +/** + * This class is responsible for handling the read receipts for hidden events (check [TimelineSettings] to see filtering). + * When an hidden event has read receipts, we want to transfer these read receipts on the first older displayed event. + * It has to be used in [DefaultTimeline] and we should call the [start] and [dispose] methods to properly handle realm subscription. + */ +internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper, + private val roomId: String, + private val settings: TimelineSettings) { + + interface Delegate { + fun rebuildEvent(eventId: String, readReceipts: List): Boolean + fun onReadReceiptsUpdated() + } + + private val correctedReadReceiptsEventByIndex = SparseArray() + private val correctedReadReceiptsByEvent = HashMap>() + + private lateinit var hiddenReadReceipts: RealmResults + private lateinit var nonFilteredEvents: RealmResults + private lateinit var filteredEvents: RealmResults + private lateinit var delegate: Delegate + + private val hiddenReadReceiptsListener = OrderedRealmCollectionChangeListener> { collection, changeSet -> + if (!collection.isLoaded || !collection.isValid) { + return@OrderedRealmCollectionChangeListener + } + var hasChange = false + // Deletion here means we don't have any readReceipts for the given hidden events + changeSet.deletions.forEach { + val eventId = correctedReadReceiptsEventByIndex.get(it, "") + val timelineEvent = filteredEvents.where() + .equalTo(TimelineEventEntityFields.EVENT_ID, eventId) + .findFirst() + + // We are rebuilding the corresponding event with only his own RR + val readReceipts = readReceiptsSummaryMapper.map(timelineEvent?.readReceipts) + hasChange = delegate.rebuildEvent(eventId, readReceipts) || hasChange + } + correctedReadReceiptsEventByIndex.clear() + correctedReadReceiptsByEvent.clear() + for (index in 0 until hiddenReadReceipts.size) { + val summary = hiddenReadReceipts[index] ?: continue + val timelineEvent = summary.timelineEvent?.firstOrNull() ?: continue + val isLoaded = nonFilteredEvents.where() + .equalTo(TimelineEventEntityFields.EVENT_ID, timelineEvent.eventId).findFirst() != null + val displayIndex = timelineEvent.displayIndex + + if (isLoaded) { + // Then we are looking for the first displayable event after the hidden one + val firstDisplayedEvent = filteredEvents.where() + .lessThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, displayIndex) + .findFirst() + + // If we find one, we should + if (firstDisplayedEvent != null) { + correctedReadReceiptsEventByIndex.put(index, firstDisplayedEvent.eventId) + correctedReadReceiptsByEvent + .getOrPut(firstDisplayedEvent.eventId, { + ArrayList(readReceiptsSummaryMapper.map(firstDisplayedEvent.readReceipts)) + }) + .addAll(readReceiptsSummaryMapper.map(summary)) + } + } + } + if (correctedReadReceiptsByEvent.isNotEmpty()) { + correctedReadReceiptsByEvent.forEach { (eventId, correctedReadReceipts) -> + val sortedReadReceipts = correctedReadReceipts.sortedByDescending { + it.originServerTs + } + hasChange = delegate.rebuildEvent(eventId, sortedReadReceipts) || hasChange + } + } + if (hasChange) { + delegate.onReadReceiptsUpdated() + } + } + + /** + * Start the realm query subscription. Has to be called on an HandlerThread + */ + fun start(realm: Realm, + filteredEvents: RealmResults, + nonFilteredEvents: RealmResults, + delegate: Delegate) { + this.filteredEvents = filteredEvents + this.nonFilteredEvents = nonFilteredEvents + this.delegate = delegate + // We are looking for read receipts set on hidden events. + // We only accept those with a timelineEvent (so coming from pagination/sync). + this.hiddenReadReceipts = ReadReceiptsSummaryEntity.whereInRoom(realm, roomId) + .isNotEmpty(ReadReceiptsSummaryEntityFields.TIMELINE_EVENT) + .isNotEmpty(ReadReceiptsSummaryEntityFields.READ_RECEIPTS.`$`) + .filterReceiptsWithSettings() + .findAllAsync() + .also { it.addChangeListener(hiddenReadReceiptsListener) } + } + + /** + * Dispose the realm query subscription. Has to be called on an HandlerThread + */ + fun dispose() { + if (this::hiddenReadReceipts.isInitialized) { + this.hiddenReadReceipts.removeAllChangeListeners() + } + } + + /** + * Return the current corrected [ReadReceipt] list for an event, or null + */ + fun correctedReadReceipts(eventId: String?): List? { + return correctedReadReceiptsByEvent[eventId] + } + + /** + * We are looking for receipts related to filtered events. So, it's the opposite of [DefaultTimeline.filterEventsWithSettings] method. + */ + private fun RealmQuery.filterReceiptsWithSettings(): RealmQuery { + beginGroup() + var needOr = false + if (settings.filterTypes) { + not().`in`("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.TYPE}", settings.allowedTypes.toTypedArray()) + needOr = true + } + if (settings.filterUseless) { + if (needOr) or() + equalTo("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.IS_USELESS}", true) + needOr = true + } + if (settings.filterEdits) { + if (needOr) or() + like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", TimelineEventFilter.Content.EDIT) + or() + like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", TimelineEventFilter.Content.RESPONSE) + needOr = true + } + if (settings.filterRedacted) { + if (needOr) or() + like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.UNSIGNED_DATA}", TimelineEventFilter.Unsigned.REDACTED) + } + endGroup() + return this + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineSendEventWorkCommon.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineSendEventWorkCommon.kt new file mode 100644 index 0000000000..d3124b68ca --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineSendEventWorkCommon.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.room.timeline + +import androidx.work.BackoffPolicy +import androidx.work.Data +import androidx.work.ExistingWorkPolicy +import androidx.work.ListenableWorker +import androidx.work.OneTimeWorkRequest +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.NoOpCancellable +import org.matrix.android.sdk.internal.di.WorkManagerProvider +import org.matrix.android.sdk.internal.util.CancelableWork +import org.matrix.android.sdk.internal.worker.startChain +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +/** + * Helper class for sending event related works. + * All send event from a room are using the same workchain, in order to ensure order. + * WorkRequest must always return success (even if server error, in this case marking the event as failed to send), + * if not the chain will be doomed in failed state. + */ +internal class TimelineSendEventWorkCommon @Inject constructor( + private val workManagerProvider: WorkManagerProvider +) { + + fun postSequentialWorks(roomId: String, vararg workRequests: OneTimeWorkRequest): Cancelable { + return when { + workRequests.isEmpty() -> NoOpCancellable + workRequests.size == 1 -> postWork(roomId, workRequests.first()) + else -> { + val firstWork = workRequests.first() + var continuation = workManagerProvider.workManager + .beginUniqueWork(buildWorkName(roomId), ExistingWorkPolicy.APPEND, firstWork) + for (i in 1 until workRequests.size) { + val workRequest = workRequests[i] + continuation = continuation.then(workRequest) + } + continuation.enqueue() + CancelableWork(workManagerProvider.workManager, firstWork.id) + } + } + } + + fun postWork(roomId: String, workRequest: OneTimeWorkRequest, policy: ExistingWorkPolicy = ExistingWorkPolicy.APPEND): Cancelable { + workManagerProvider.workManager + .beginUniqueWork(buildWorkName(roomId), policy, workRequest) + .enqueue() + + return CancelableWork(workManagerProvider.workManager, workRequest.id) + } + + inline fun createWork(data: Data, startChain: Boolean): OneTimeWorkRequest { + return workManagerProvider.matrixOneTimeWorkRequestBuilder() + .setConstraints(WorkManagerProvider.workConstraints) + .startChain(startChain) + .setInputData(data) + .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) + .build() + } + + private fun buildWorkName(roomId: String): String { + return "${roomId}_$SEND_WORK" + } + + fun cancelAllWorks(roomId: String) { + workManagerProvider.workManager + .cancelUniqueWork(buildWorkName(roomId)) + } + + companion object { + private const val SEND_WORK = "SEND_WORK" + private const val BACKOFF_DELAY = 10_000L + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEvent.kt new file mode 100644 index 0000000000..655af7c4e1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEvent.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.timeline + +import org.matrix.android.sdk.api.session.events.model.Event + +internal interface TokenChunkEvent { + val start: String? + val end: String? + val events: List + val stateEvents: List + + fun hasMore() = start != end +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt new file mode 100644 index 0000000000..da4eebe142 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt @@ -0,0 +1,264 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.timeline + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.RoomMemberContent +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.database.helper.addOrUpdate +import org.matrix.android.sdk.internal.database.helper.addStateEvent +import org.matrix.android.sdk.internal.database.helper.addTimelineEvent +import org.matrix.android.sdk.internal.database.helper.deleteOnCascade +import org.matrix.android.sdk.internal.database.helper.merge +import org.matrix.android.sdk.internal.database.mapper.toEntity +import org.matrix.android.sdk.internal.database.model.ChunkEntity +import org.matrix.android.sdk.internal.database.model.EventInsertType +import org.matrix.android.sdk.internal.database.model.RoomEntity +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore +import org.matrix.android.sdk.internal.database.query.create +import org.matrix.android.sdk.internal.database.query.find +import org.matrix.android.sdk.internal.database.query.findAllIncludingEvents +import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom +import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.database.query.latestEvent +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater +import org.matrix.android.sdk.internal.util.awaitTransaction +import io.realm.Realm +import timber.log.Timber +import javax.inject.Inject + +/** + * Insert Chunk in DB, and eventually merge with existing chunk event + */ +internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase private val monarchy: Monarchy) { + + /** + *
+     * ========================================================================================================
+     * | Backward case                                                                                        |
+     * ========================================================================================================
+     *
+     *                               *--------------------------*        *--------------------------*
+     *                               | startToken1              |        | startToken1              |
+     *                               *--------------------------*        *--------------------------*
+     *                               |                          |        |                          |
+     *                               |                          |        |                          |
+     *                               |  receivedChunk backward  |        |                          |
+     *                               |         Events           |        |                          |
+     *                               |                          |        |                          |
+     *                               |                          |        |                          |
+     *                               |                          |        |                          |
+     * *--------------------------*  *--------------------------*        |                          |
+     * | startToken0              |  | endToken1                |   =>   |       Merged chunk       |
+     * *--------------------------*  *--------------------------*        |          Events          |
+     * |                          |                                      |                          |
+     * |                          |                                      |                          |
+     * |      Current Chunk       |                                      |                          |
+     * |         Events           |                                      |                          |
+     * |                          |                                      |                          |
+     * |                          |                                      |                          |
+     * |                          |                                      |                          |
+     * *--------------------------*                                      *--------------------------*
+     * | endToken0                |                                      | endToken0                |
+     * *--------------------------*                                      *--------------------------*
+     *
+     *
+     * ========================================================================================================
+     * | Forward case                                                                                         |
+     * ========================================================================================================
+     *
+     * *--------------------------*                                      *--------------------------*
+     * | startToken0              |                                      | startToken0              |
+     * *--------------------------*                                      *--------------------------*
+     * |                          |                                      |                          |
+     * |                          |                                      |                          |
+     * |      Current Chunk       |                                      |                          |
+     * |         Events           |                                      |                          |
+     * |                          |                                      |                          |
+     * |                          |                                      |                          |
+     * |                          |                                      |                          |
+     * *--------------------------*  *--------------------------*        |                          |
+     * | endToken0                |  | startToken1              |   =>   |       Merged chunk       |
+     * *--------------------------*  *--------------------------*        |          Events          |
+     *                               |                          |        |                          |
+     *                               |                          |        |                          |
+     *                               |  receivedChunk forward   |        |                          |
+     *                               |         Events           |        |                          |
+     *                               |                          |        |                          |
+     *                               |                          |        |                          |
+     *                               |                          |        |                          |
+     *                               *--------------------------*        *--------------------------*
+     *                               | endToken1                |        | endToken1                |
+     *                               *--------------------------*        *--------------------------*
+     *
+     * ========================================================================================================
+     * 
+ */ + + enum class Result { + SHOULD_FETCH_MORE, + REACHED_END, + SUCCESS + } + + suspend fun insertInDb(receivedChunk: TokenChunkEvent, + roomId: String, + direction: PaginationDirection): Result { + monarchy + .awaitTransaction { realm -> + Timber.v("Start persisting ${receivedChunk.events.size} events in $roomId towards $direction") + + val nextToken: String? + val prevToken: String? + if (direction == PaginationDirection.FORWARDS) { + nextToken = receivedChunk.end + prevToken = receivedChunk.start + } else { + nextToken = receivedChunk.start + prevToken = receivedChunk.end + } + + val prevChunk = ChunkEntity.find(realm, roomId, nextToken = prevToken) + val nextChunk = ChunkEntity.find(realm, roomId, prevToken = nextToken) + + // The current chunk is the one we will keep all along the merge processChanges. + // We try to look for a chunk next to the token, + // otherwise we create a whole new one which is unlinked (not live) + val currentChunk = if (direction == PaginationDirection.FORWARDS) { + prevChunk?.apply { this.nextToken = nextToken } + } else { + nextChunk?.apply { this.prevToken = prevToken } + } + ?: ChunkEntity.create(realm, prevToken, nextToken) + + if (receivedChunk.events.isEmpty() && !receivedChunk.hasMore()) { + handleReachEnd(realm, roomId, direction, currentChunk) + } else { + handlePagination(realm, roomId, direction, receivedChunk, currentChunk) + } + } + return if (receivedChunk.events.isEmpty()) { + if (receivedChunk.start != receivedChunk.end) { + Result.SHOULD_FETCH_MORE + } else { + Result.REACHED_END + } + } else { + Result.SUCCESS + } + } + + private fun handleReachEnd(realm: Realm, roomId: String, direction: PaginationDirection, currentChunk: ChunkEntity) { + Timber.v("Reach end of $roomId") + if (direction == PaginationDirection.FORWARDS) { + val currentLastForwardChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId) + if (currentChunk != currentLastForwardChunk) { + currentChunk.isLastForward = true + currentLastForwardChunk?.deleteOnCascade() + RoomSummaryEntity.where(realm, roomId).findFirst()?.apply { + latestPreviewableEvent = TimelineEventEntity.latestEvent( + realm, + roomId, + includesSending = true, + filterTypes = RoomSummaryUpdater.PREVIEWABLE_TYPES + ) + } + } + } else { + currentChunk.isLastBackward = true + } + } + + private fun handlePagination( + realm: Realm, + roomId: String, + direction: PaginationDirection, + receivedChunk: TokenChunkEvent, + currentChunk: ChunkEntity + ) { + Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}") + val roomMemberContentsByUser = HashMap() + val eventList = receivedChunk.events + val stateEvents = receivedChunk.stateEvents + + val now = System.currentTimeMillis() + + for (stateEvent in stateEvents) { + val ageLocalTs = stateEvent.unsignedData?.age?.let { now - it } + val stateEventEntity = stateEvent.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) + currentChunk.addStateEvent(roomId, stateEventEntity, direction) + if (stateEvent.type == EventType.STATE_ROOM_MEMBER && stateEvent.stateKey != null) { + roomMemberContentsByUser[stateEvent.stateKey] = stateEvent.content.toModel() + } + } + val eventIds = ArrayList(eventList.size) + for (event in eventList) { + if (event.eventId == null || event.senderId == null) { + continue + } + val ageLocalTs = event.unsignedData?.age?.let { now - it } + eventIds.add(event.eventId) + val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) + if (event.type == EventType.STATE_ROOM_MEMBER && event.stateKey != null) { + val contentToUse = if (direction == PaginationDirection.BACKWARDS) { + event.prevContent + } else { + event.content + } + roomMemberContentsByUser[event.stateKey] = contentToUse.toModel() + } + + currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser) + } + // Find all the chunks which contain at least one event from the list of eventIds + val chunks = ChunkEntity.findAllIncludingEvents(realm, eventIds) + Timber.d("Found ${chunks.size} chunks containing at least one of the eventIds") + val chunksToDelete = ArrayList() + chunks.forEach { + if (it != currentChunk) { + Timber.d("Merge $it") + currentChunk.merge(roomId, it, direction) + chunksToDelete.add(it) + } + } + chunksToDelete.forEach { + it.deleteOnCascade() + } + val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId) + val shouldUpdateSummary = roomSummaryEntity.latestPreviewableEvent == null + || (chunksToDelete.isNotEmpty() && currentChunk.isLastForward && direction == PaginationDirection.FORWARDS) + if (shouldUpdateSummary) { + val latestPreviewableEvent = TimelineEventEntity.latestEvent( + realm, + roomId, + includesSending = true, + filterTypes = RoomSummaryUpdater.PREVIEWABLE_TYPES + ) + roomSummaryEntity.latestPreviewableEvent = latestPreviewableEvent + } + if (currentChunk.isValid) { + RoomEntity.where(realm, roomId).findFirst()?.addOrUpdate(currentChunk) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tombstone/RoomTombstoneEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tombstone/RoomTombstoneEventProcessor.kt new file mode 100644 index 0000000000..8707eb2429 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tombstone/RoomTombstoneEventProcessor.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.tombstone + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.VersioningState +import org.matrix.android.sdk.api.session.room.model.tombstone.RoomTombstoneContent +import org.matrix.android.sdk.internal.database.model.EventInsertType +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor +import io.realm.Realm +import javax.inject.Inject + +internal class RoomTombstoneEventProcessor @Inject constructor() : EventInsertLiveProcessor { + + override suspend fun process(realm: Realm, event: Event) { + if (event.roomId == null) return + val createRoomContent = event.getClearContent().toModel() + if (createRoomContent?.replacementRoomId == null) return + + val predecessorRoomSummary = RoomSummaryEntity.where(realm, event.roomId).findFirst() + ?: RoomSummaryEntity(event.roomId) + if (predecessorRoomSummary.versioningState == VersioningState.NONE) { + predecessorRoomSummary.versioningState = VersioningState.UPGRADED_ROOM_NOT_JOINED + } + realm.insertOrUpdate(predecessorRoomSummary) + } + + override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean { + return eventType == EventType.STATE_ROOM_TOMBSTONE + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/DefaultTypingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/DefaultTypingService.kt new file mode 100644 index 0000000000..b8db9ce69c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/DefaultTypingService.kt @@ -0,0 +1,119 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.typing + +import android.os.SystemClock +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.room.typing.TypingService +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import timber.log.Timber + +/** + * Rules: + * - user is typing: notify the homeserver (true), at least once every 10s + * - user stop typing: after 10s delay: notify the homeserver (false) + * - user empty the text composer or quit the timeline screen: notify the homeserver (false) + */ +internal class DefaultTypingService @AssistedInject constructor( + @Assisted private val roomId: String, + private val taskExecutor: TaskExecutor, + private val sendTypingTask: SendTypingTask +) : TypingService { + + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): TypingService + } + + private var currentTask: Cancelable? = null + private var currentAutoStopTask: Cancelable? = null + + // What the homeserver knows + private var userIsTyping = false + // Last time the user is typing event has been sent + private var lastRequestTimestamp: Long = 0 + + override fun userIsTyping() { + scheduleAutoStop() + + val now = SystemClock.elapsedRealtime() + + if (userIsTyping && now < lastRequestTimestamp + MIN_DELAY_BETWEEN_TWO_USER_IS_TYPING_REQUESTS_MILLIS) { + Timber.d("Typing: Skip start request") + return + } + + Timber.d("Typing: Send start request") + userIsTyping = true + lastRequestTimestamp = now + + currentTask?.cancel() + + val params = SendTypingTask.Params(roomId, true) + currentTask = sendTypingTask + .configureWith(params) + .executeBy(taskExecutor) + } + + override fun userStopsTyping() { + if (!userIsTyping) { + Timber.d("Typing: Skip stop request") + return + } + + Timber.d("Typing: Send stop request") + userIsTyping = false + lastRequestTimestamp = 0 + + currentAutoStopTask?.cancel() + currentTask?.cancel() + + val params = SendTypingTask.Params(roomId, false) + currentTask = sendTypingTask + .configureWith(params) + .executeBy(taskExecutor) + } + + private fun scheduleAutoStop() { + Timber.d("Typing: Schedule auto stop") + currentAutoStopTask?.cancel() + + val params = SendTypingTask.Params( + roomId, + false, + delay = MIN_DELAY_TO_SEND_STOP_TYPING_REQUEST_WHEN_NO_USER_ACTIVITY_MILLIS) + currentAutoStopTask = sendTypingTask + .configureWith(params) { + callback = object : MatrixCallback { + override fun onSuccess(data: Unit) { + userIsTyping = false + } + } + } + .executeBy(taskExecutor) + } + + companion object { + private const val MIN_DELAY_BETWEEN_TWO_USER_IS_TYPING_REQUESTS_MILLIS = 10_000L + private const val MIN_DELAY_TO_SEND_STOP_TYPING_REQUEST_WHEN_NO_USER_ACTIVITY_MILLIS = 10_000L + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/SendTypingTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/SendTypingTask.kt new file mode 100644 index 0000000000..719fffbb4e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/SendTypingTask.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.typing + +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.task.Task +import kotlinx.coroutines.delay +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface SendTypingTask : Task { + + data class Params( + val roomId: String, + val isTyping: Boolean, + val typingTimeoutMillis: Int? = 30_000, + // Optional delay before sending the request to the homeserver + val delay: Long? = null + ) +} + +internal class DefaultSendTypingTask @Inject constructor( + private val roomAPI: RoomAPI, + @UserId private val userId: String, + private val eventBus: EventBus +) : SendTypingTask { + + override suspend fun execute(params: SendTypingTask.Params) { + delay(params.delay ?: -1) + + executeRequest(eventBus) { + apiCall = roomAPI.sendTypingState( + params.roomId, + userId, + TypingBody(params.isTyping, params.typingTimeoutMillis?.takeIf { params.isTyping }) + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/TypingBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/TypingBody.kt new file mode 100644 index 0000000000..8ed77a4829 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/TypingBody.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.typing + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class TypingBody( + // Required. Whether the user is typing or not. If false, the timeout key can be omitted. + @Json(name = "typing") + val typing: Boolean, + // The length of time in milliseconds to mark this user as typing. + @Json(name = "timeout") + val timeout: Int? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/TypingEventContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/TypingEventContent.kt new file mode 100644 index 0000000000..f616bfefd0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/TypingEventContent.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.typing + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class TypingEventContent( + @Json(name = "user_ids") + val typingUserIds: List = emptyList() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/DefaultUploadsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/DefaultUploadsService.kt new file mode 100644 index 0000000000..76fb18b130 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/DefaultUploadsService.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.uploads + +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.room.uploads.GetUploadsResult +import org.matrix.android.sdk.api.session.room.uploads.UploadsService +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith + +internal class DefaultUploadsService @AssistedInject constructor( + @Assisted private val roomId: String, + private val taskExecutor: TaskExecutor, + private val getUploadsTask: GetUploadsTask, + private val cryptoService: CryptoService +) : UploadsService { + + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): UploadsService + } + + override fun getUploads(numberOfEvents: Int, since: String?, callback: MatrixCallback): Cancelable { + return getUploadsTask + .configureWith(GetUploadsTask.Params(roomId, cryptoService.isRoomEncrypted(roomId), numberOfEvents, since)) { + this.callback = callback + } + .executeBy(taskExecutor) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/GetUploadsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/GetUploadsTask.kt new file mode 100644 index 0000000000..be53b8afe1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/GetUploadsTask.kt @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.room.uploads + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent +import org.matrix.android.sdk.api.session.room.sender.SenderInfo +import org.matrix.android.sdk.api.session.room.uploads.GetUploadsResult +import org.matrix.android.sdk.api.session.room.uploads.UploadEvent +import org.matrix.android.sdk.internal.database.mapper.asDomain +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.EventEntityFields +import org.matrix.android.sdk.internal.database.query.TimelineEventFilter +import org.matrix.android.sdk.internal.database.query.whereType +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.filter.FilterFactory +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper +import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection +import org.matrix.android.sdk.internal.session.room.timeline.PaginationResponse +import org.matrix.android.sdk.internal.session.sync.SyncTokenStore +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface GetUploadsTask : Task { + + data class Params( + val roomId: String, + val isRoomEncrypted: Boolean, + val numberOfEvents: Int, + val since: String? + ) +} + +internal class DefaultGetUploadsTask @Inject constructor( + private val roomAPI: RoomAPI, + private val tokenStore: SyncTokenStore, + @SessionDatabase private val monarchy: Monarchy, + private val eventBus: EventBus) + : GetUploadsTask { + + override suspend fun execute(params: GetUploadsTask.Params): GetUploadsResult { + val result: GetUploadsResult + val events: List + + if (params.isRoomEncrypted) { + // Get a chunk of events from cache for e2e rooms + + result = GetUploadsResult( + uploadEvents = emptyList(), + nextToken = "", + hasMore = false + ) + + var eventsFromRealm = emptyList() + monarchy.doWithRealm { realm -> + eventsFromRealm = EventEntity.whereType(realm, EventType.ENCRYPTED, params.roomId) + .like(EventEntityFields.DECRYPTION_RESULT_JSON, TimelineEventFilter.DecryptedContent.URL) + .findAll() + .map { it.asDomain() } + // Exclude stickers + .filter { it.getClearType() != EventType.STICKER } + } + events = eventsFromRealm + } else { + val since = params.since ?: tokenStore.getLastToken() ?: throw IllegalStateException("No token available") + + val filter = FilterFactory.createUploadsFilter(params.numberOfEvents).toJSONString() + val chunk = executeRequest(eventBus) { + apiCall = roomAPI.getRoomMessagesFrom(params.roomId, since, PaginationDirection.BACKWARDS.value, params.numberOfEvents, filter) + } + + result = GetUploadsResult( + uploadEvents = emptyList(), + nextToken = chunk.end ?: "", + hasMore = chunk.hasMore() + ) + events = chunk.events + } + + var uploadEvents = listOf() + + val cacheOfSenderInfos = mutableMapOf() + + // Get a snapshot of all room members + monarchy.doWithRealm { realm -> + val roomMemberHelper = RoomMemberHelper(realm, params.roomId) + + uploadEvents = events.mapNotNull { event -> + val eventId = event.eventId ?: return@mapNotNull null + val messageContent = event.getClearContent()?.toModel() ?: return@mapNotNull null + val messageWithAttachmentContent = (messageContent as? MessageWithAttachmentContent) ?: return@mapNotNull null + val senderId = event.senderId ?: return@mapNotNull null + + val senderInfo = cacheOfSenderInfos.getOrPut(senderId) { + val roomMemberSummaryEntity = roomMemberHelper.getLastRoomMember(senderId) + SenderInfo( + userId = senderId, + displayName = roomMemberSummaryEntity?.displayName, + isUniqueDisplayName = roomMemberHelper.isUniqueDisplayName(roomMemberSummaryEntity?.displayName), + avatarUrl = roomMemberSummaryEntity?.avatarUrl + ) + } + + UploadEvent( + root = event, + eventId = eventId, + contentWithAttachmentContent = messageWithAttachmentContent, + senderInfo = senderInfo + ) + } + } + + return result.copy(uploadEvents = uploadEvents) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/DefaultSecureStorageService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/DefaultSecureStorageService.kt new file mode 100644 index 0000000000..4ac796791a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/DefaultSecureStorageService.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.securestorage + +import org.matrix.android.sdk.api.session.securestorage.SecureStorageService +import java.io.InputStream +import java.io.OutputStream +import javax.inject.Inject + +internal class DefaultSecureStorageService @Inject constructor(private val secretStoringUtils: SecretStoringUtils) : SecureStorageService { + + override fun securelyStoreObject(any: Any, keyAlias: String, outputStream: OutputStream) { + secretStoringUtils.securelyStoreObject(any, keyAlias, outputStream) + } + + override fun loadSecureSecret(inputStream: InputStream, keyAlias: String): T? { + return secretStoringUtils.loadSecureSecret(inputStream, keyAlias) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt new file mode 100644 index 0000000000..0d898e46ce --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt @@ -0,0 +1,572 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +@file:Suppress("DEPRECATION") + +package org.matrix.android.sdk.internal.session.securestorage + +import android.content.Context +import android.os.Build +import android.security.KeyPairGeneratorSpec +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import androidx.annotation.RequiresApi +import timber.log.Timber +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.InputStream +import java.io.ObjectInputStream +import java.io.ObjectOutputStream +import java.io.OutputStream +import java.math.BigInteger +import java.security.KeyPairGenerator +import java.security.KeyStore +import java.security.KeyStoreException +import java.security.SecureRandom +import java.util.Calendar +import javax.crypto.Cipher +import javax.crypto.CipherInputStream +import javax.crypto.CipherOutputStream +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey +import javax.crypto.SecretKeyFactory +import javax.crypto.spec.GCMParameterSpec +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.PBEKeySpec +import javax.crypto.spec.SecretKeySpec +import javax.inject.Inject +import javax.security.auth.x500.X500Principal + +/** + * Offers simple methods to securely store secrets in an Android Application. + * The encryption keys are randomly generated and securely managed by the key store, thus your secrets + * are safe. You only need to remember a key alias to perform encrypt/decrypt operations. + * + * Android M++ + * On android M+, the keystore can generates and store AES keys via API. But below API M this functionality + * is not available. + * + * Android [K-M[ + * For android >=KITKAT and Older androids + * For older androids as a fallback we generate an AES key from the alias using PBKDF2 with random salt. + * The salt and iv are stored with encrypted data. + * + * Sample usage: + * + * val secret = "The answer is 42" + * val KEncrypted = SecretStoringUtils.securelyStoreString(secret, "myAlias", context) + * //This can be stored anywhere e.g. encoded in b64 and stored in preference for example + * + * //to get back the secret, just call + * val kDecrypted = SecretStoringUtils.loadSecureSecret(KEncrypted!!, "myAlias", context) + * + * + * You can also just use this utility to store a secret key, and use any encryption algorithm that you want. + * + * Important: Keys stored in the keystore can be wiped out (depends of the OS version, like for example if you + * add a pin or change the schema); So you might and with a useless pile of bytes. + */ +internal class SecretStoringUtils @Inject constructor(private val context: Context) { + + companion object { + private const val ANDROID_KEY_STORE = "AndroidKeyStore" + private const val AES_MODE = "AES/GCM/NoPadding" + private const val RSA_MODE = "RSA/ECB/PKCS1Padding" + + private const val FORMAT_API_M: Byte = 0 + private const val FORMAT_1: Byte = 1 + private const val FORMAT_2: Byte = 2 + } + + private val keyStore: KeyStore by lazy { + KeyStore.getInstance(ANDROID_KEY_STORE).apply { + load(null) + } + } + + private val secureRandom = SecureRandom() + + fun safeDeleteKey(keyAlias: String) { + try { + keyStore.deleteEntry(keyAlias) + } catch (e: KeyStoreException) { + Timber.e(e) + } + } + + /** + * Encrypt the given secret using the android Keystore. + * On android >= M, will directly use the keystore to generate a symmetric key + * On android >= KitKat and = Build.VERSION_CODES.M -> encryptStringM(secret, keyAlias) + Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT -> encryptStringK(secret, keyAlias) + else -> encryptForOldDevicesNotGood(secret, keyAlias) + } + } + + /** + * Decrypt a secret that was encrypted by #securelyStoreString() + */ + @Throws(Exception::class) + fun loadSecureSecret(encrypted: ByteArray, keyAlias: String): String? { + return when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> decryptStringM(encrypted, keyAlias) + Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT -> decryptStringK(encrypted, keyAlias) + else -> decryptForOldDevicesNotGood(encrypted, keyAlias) + } + } + + fun securelyStoreObject(any: Any, keyAlias: String, output: OutputStream) { + when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> saveSecureObjectM(keyAlias, output, any) + Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT -> saveSecureObjectK(keyAlias, output, any) + else -> saveSecureObjectOldNotGood(keyAlias, output, any) + } + } + + fun loadSecureSecret(inputStream: InputStream, keyAlias: String): T? { + return when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> loadSecureObjectM(keyAlias, inputStream) + Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT -> loadSecureObjectK(keyAlias, inputStream) + else -> loadSecureObjectOldNotGood(keyAlias, inputStream) + } + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun getOrGenerateSymmetricKeyForAliasM(alias: String): SecretKey { + val secretKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.SecretKeyEntry) + ?.secretKey + if (secretKeyEntry == null) { + // we generate it + val generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore") + val keyGenSpec = KeyGenParameterSpec.Builder(alias, + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .setKeySize(128) + .build() + generator.init(keyGenSpec) + return generator.generateKey() + } + return secretKeyEntry + } + + /* + Symmetric Key Generation is only available in M, so before M the idea is to: + - Generate a pair of RSA keys; + - Generate a random AES key; + - Encrypt the AES key using the RSA public key; + - Store the encrypted AES + Generate a key pair for encryption + */ + @RequiresApi(Build.VERSION_CODES.KITKAT) + fun getOrGenerateKeyPairForAlias(alias: String): KeyStore.PrivateKeyEntry { + val privateKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.PrivateKeyEntry) + + if (privateKeyEntry != null) return privateKeyEntry + + val start = Calendar.getInstance() + val end = Calendar.getInstance() + end.add(Calendar.YEAR, 30) + + val spec = KeyPairGeneratorSpec.Builder(context) + .setAlias(alias) + .setSubject(X500Principal("CN=$alias")) + .setSerialNumber(BigInteger.TEN) + // .setEncryptionRequired() requires that the phone as a pin/schema + .setStartDate(start.time) + .setEndDate(end.time) + .build() + KeyPairGenerator.getInstance("RSA" /*KeyProperties.KEY_ALGORITHM_RSA*/, ANDROID_KEY_STORE).run { + initialize(spec) + generateKeyPair() + } + return (keyStore.getEntry(alias, null) as KeyStore.PrivateKeyEntry) + } + + @RequiresApi(Build.VERSION_CODES.M) + fun encryptStringM(text: String, keyAlias: String): ByteArray? { + val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias) + + val cipher = Cipher.getInstance(AES_MODE) + cipher.init(Cipher.ENCRYPT_MODE, secretKey) + val iv = cipher.iv + // we happen the iv to the final result + val encryptedBytes: ByteArray = cipher.doFinal(text.toByteArray(Charsets.UTF_8)) + return formatMMake(iv, encryptedBytes) + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun decryptStringM(encryptedChunk: ByteArray, keyAlias: String): String { + val (iv, encryptedText) = formatMExtract(ByteArrayInputStream(encryptedChunk)) + + val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias) + + val cipher = Cipher.getInstance(AES_MODE) + val spec = GCMParameterSpec(128, iv) + cipher.init(Cipher.DECRYPT_MODE, secretKey, spec) + + return String(cipher.doFinal(encryptedText), Charsets.UTF_8) + } + + @RequiresApi(Build.VERSION_CODES.KITKAT) + private fun encryptStringK(text: String, keyAlias: String): ByteArray? { + // we generate a random symmetric key + val key = ByteArray(16) + secureRandom.nextBytes(key) + val sKey = SecretKeySpec(key, "AES") + + // we encrypt this key thanks to the key store + val encryptedKey = rsaEncrypt(keyAlias, key) + + val cipher = Cipher.getInstance(AES_MODE) + cipher.init(Cipher.ENCRYPT_MODE, sKey) + val iv = cipher.iv + val encryptedBytes: ByteArray = cipher.doFinal(text.toByteArray(Charsets.UTF_8)) + + return format1Make(encryptedKey, iv, encryptedBytes) + } + + private fun encryptForOldDevicesNotGood(text: String, keyAlias: String): ByteArray { + val salt = ByteArray(8) + secureRandom.nextBytes(salt) + val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") + val spec = PBEKeySpec(keyAlias.toCharArray(), salt, 10000, 128) + val tmp = factory.generateSecret(spec) + val sKey = SecretKeySpec(tmp.encoded, "AES") + + val cipher = Cipher.getInstance(AES_MODE) + cipher.init(Cipher.ENCRYPT_MODE, sKey) + val iv = cipher.iv + val encryptedBytes: ByteArray = cipher.doFinal(text.toByteArray(Charsets.UTF_8)) + + return format2Make(salt, iv, encryptedBytes) + } + + private fun decryptForOldDevicesNotGood(data: ByteArray, keyAlias: String): String? { + val (salt, iv, encrypted) = format2Extract(ByteArrayInputStream(data)) + val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") + val spec = PBEKeySpec(keyAlias.toCharArray(), salt, 10_000, 128) + val tmp = factory.generateSecret(spec) + val sKey = SecretKeySpec(tmp.encoded, "AES") + + val cipher = Cipher.getInstance(AES_MODE) +// cipher.init(Cipher.ENCRYPT_MODE, sKey) +// val encryptedBytes: ByteArray = cipher.doFinal(text.toByteArray(Charsets.UTF_8)) + + val specIV = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) IvParameterSpec(iv) else GCMParameterSpec(128, iv) + cipher.init(Cipher.DECRYPT_MODE, sKey, specIV) + + return String(cipher.doFinal(encrypted), Charsets.UTF_8) + } + + @RequiresApi(Build.VERSION_CODES.KITKAT) + private fun decryptStringK(data: ByteArray, keyAlias: String): String? { + val (encryptedKey, iv, encrypted) = format1Extract(ByteArrayInputStream(data)) + + // we need to decrypt the key + val sKeyBytes = rsaDecrypt(keyAlias, ByteArrayInputStream(encryptedKey)) + val cipher = Cipher.getInstance(AES_MODE) + val spec = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) IvParameterSpec(iv) else GCMParameterSpec(128, iv) + cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(sKeyBytes, "AES"), spec) + + return String(cipher.doFinal(encrypted), Charsets.UTF_8) + } + + @RequiresApi(Build.VERSION_CODES.M) + @Throws(IOException::class) + private fun saveSecureObjectM(keyAlias: String, output: OutputStream, writeObject: Any) { + val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias) + + val cipher = Cipher.getInstance(AES_MODE) + cipher.init(Cipher.ENCRYPT_MODE, secretKey/*, spec*/) + val iv = cipher.iv + + val bos1 = ByteArrayOutputStream() + ObjectOutputStream(bos1).use { + it.writeObject(writeObject) + } + // Have to do it like that if i encapsulate the output stream, the cipher could fail saying reuse IV + val doFinal = cipher.doFinal(bos1.toByteArray()) + output.write(FORMAT_API_M.toInt()) + output.write(iv.size) + output.write(iv) + output.write(doFinal) + } + + @RequiresApi(Build.VERSION_CODES.KITKAT) + private fun saveSecureObjectK(keyAlias: String, output: OutputStream, writeObject: Any) { + // we generate a random symmetric key + val key = ByteArray(16) + secureRandom.nextBytes(key) + val sKey = SecretKeySpec(key, "AES") + + // we encrypt this key thanks to the key store + val encryptedKey = rsaEncrypt(keyAlias, key) + + val cipher = Cipher.getInstance(AES_MODE) + cipher.init(Cipher.ENCRYPT_MODE, sKey) + val iv = cipher.iv + + val bos1 = ByteArrayOutputStream() + val cos = CipherOutputStream(bos1, cipher) + ObjectOutputStream(cos).use { + it.writeObject(writeObject) + } + + output.write(FORMAT_1.toInt()) + output.write((encryptedKey.size and 0xFF00).shr(8)) + output.write(encryptedKey.size and 0x00FF) + output.write(encryptedKey) + output.write(iv.size) + output.write(iv) + output.write(bos1.toByteArray()) + } + + private fun saveSecureObjectOldNotGood(keyAlias: String, output: OutputStream, writeObject: Any) { + val salt = ByteArray(8) + secureRandom.nextBytes(salt) + val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") + val tmp = factory.generateSecret(PBEKeySpec(keyAlias.toCharArray(), salt, 10000, 128)) + val secretKey = SecretKeySpec(tmp.encoded, "AES") + + val cipher = Cipher.getInstance(AES_MODE) + cipher.init(Cipher.ENCRYPT_MODE, secretKey) + val iv = cipher.iv + + val bos1 = ByteArrayOutputStream() + ObjectOutputStream(bos1).use { + it.writeObject(writeObject) + } + // Have to do it like that if i encapsulate the output stream, the cipher could fail saying reuse IV + val doFinal = cipher.doFinal(bos1.toByteArray()) + + output.write(FORMAT_2.toInt()) + output.write(salt.size) + output.write(salt) + output.write(iv.size) + output.write(iv) + output.write(doFinal) + } + +// @RequiresApi(Build.VERSION_CODES.M) +// @Throws(IOException::class) +// fun saveSecureObjectM(keyAlias: String, file: File, writeObject: Any) { +// FileOutputStream(file).use { +// saveSecureObjectM(keyAlias, it, writeObject) +// } +// } +// +// @RequiresApi(Build.VERSION_CODES.M) +// @Throws(IOException::class) +// fun loadSecureObjectM(keyAlias: String, file: File): T? { +// FileInputStream(file).use { +// return loadSecureObjectM(keyAlias, it) +// } +// } + + @RequiresApi(Build.VERSION_CODES.M) + @Throws(IOException::class) + private fun loadSecureObjectM(keyAlias: String, inputStream: InputStream): T? { + val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias) + + val format = inputStream.read() + assert(format.toByte() == FORMAT_API_M) + + val ivSize = inputStream.read() + val iv = ByteArray(ivSize) + inputStream.read(iv, 0, ivSize) + val cipher = Cipher.getInstance(AES_MODE) + val spec = GCMParameterSpec(128, iv) + cipher.init(Cipher.DECRYPT_MODE, secretKey, spec) + + CipherInputStream(inputStream, cipher).use { cipherInputStream -> + ObjectInputStream(cipherInputStream).use { + val readObject = it.readObject() + @Suppress("UNCHECKED_CAST") + return readObject as? T + } + } + } + + @RequiresApi(Build.VERSION_CODES.KITKAT) + @Throws(IOException::class) + private fun loadSecureObjectK(keyAlias: String, inputStream: InputStream): T? { + val (encryptedKey, iv, encrypted) = format1Extract(inputStream) + + // we need to decrypt the key + val sKeyBytes = rsaDecrypt(keyAlias, ByteArrayInputStream(encryptedKey)) + val cipher = Cipher.getInstance(AES_MODE) + val spec = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) IvParameterSpec(iv) else GCMParameterSpec(128, iv) + cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(sKeyBytes, "AES"), spec) + + val encIS = ByteArrayInputStream(encrypted) + + CipherInputStream(encIS, cipher).use { cipherInputStream -> + ObjectInputStream(cipherInputStream).use { + val readObject = it.readObject() + @Suppress("UNCHECKED_CAST") + return readObject as? T + } + } + } + + @Throws(Exception::class) + private fun loadSecureObjectOldNotGood(keyAlias: String, inputStream: InputStream): T? { + val (salt, iv, encrypted) = format2Extract(inputStream) + + val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") + val tmp = factory.generateSecret(PBEKeySpec(keyAlias.toCharArray(), salt, 10000, 128)) + val sKey = SecretKeySpec(tmp.encoded, "AES") + // we need to decrypt the key + + val cipher = Cipher.getInstance(AES_MODE) + val spec = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) IvParameterSpec(iv) else GCMParameterSpec(128, iv) + cipher.init(Cipher.DECRYPT_MODE, sKey, spec) + + val encIS = ByteArrayInputStream(encrypted) + + CipherInputStream(encIS, cipher).use { + ObjectInputStream(it).use { ois -> + val readObject = ois.readObject() + @Suppress("UNCHECKED_CAST") + return readObject as? T + } + } + } + + @RequiresApi(Build.VERSION_CODES.KITKAT) + @Throws(Exception::class) + private fun rsaEncrypt(alias: String, secret: ByteArray): ByteArray { + val privateKeyEntry = getOrGenerateKeyPairForAlias(alias) + // Encrypt the text + val inputCipher = Cipher.getInstance(RSA_MODE) + inputCipher.init(Cipher.ENCRYPT_MODE, privateKeyEntry.certificate.publicKey) + + val outputStream = ByteArrayOutputStream() + CipherOutputStream(outputStream, inputCipher).use { + it.write(secret) + } + + return outputStream.toByteArray() + } + + @RequiresApi(Build.VERSION_CODES.KITKAT) + @Throws(Exception::class) + private fun rsaDecrypt(alias: String, encrypted: InputStream): ByteArray { + val privateKeyEntry = getOrGenerateKeyPairForAlias(alias) + val output = Cipher.getInstance(RSA_MODE) + output.init(Cipher.DECRYPT_MODE, privateKeyEntry.privateKey) + + return CipherInputStream(encrypted, output).use { it.readBytes() } + } + + private fun formatMExtract(bis: InputStream): Pair { + val format = bis.read().toByte() + assert(format == FORMAT_API_M) + + val ivSize = bis.read() + val iv = ByteArray(ivSize) + bis.read(iv, 0, ivSize) + + val encrypted = bis.readBytes() + return Pair(iv, encrypted) + } + + private fun formatMMake(iv: ByteArray, data: ByteArray): ByteArray { + val bos = ByteArrayOutputStream(2 + iv.size + data.size) + bos.write(FORMAT_API_M.toInt()) + bos.write(iv.size) + bos.write(iv) + bos.write(data) + return bos.toByteArray() + } + + private fun format1Extract(bis: InputStream): Triple { + val format = bis.read() + assert(format.toByte() == FORMAT_1) + + val keySizeBig = bis.read() + val keySizeLow = bis.read() + val encryptedKeySize = keySizeBig.shl(8) + keySizeLow + val encryptedKey = ByteArray(encryptedKeySize) + bis.read(encryptedKey) + + val ivSize = bis.read() + val iv = ByteArray(ivSize) + bis.read(iv) + + val encrypted = bis.readBytes() + return Triple(encryptedKey, iv, encrypted) + } + + private fun format1Make(encryptedKey: ByteArray, iv: ByteArray, encryptedBytes: ByteArray): ByteArray { + val bos = ByteArrayOutputStream(4 + encryptedKey.size + iv.size + encryptedBytes.size) + bos.write(FORMAT_1.toInt()) + bos.write((encryptedKey.size and 0xFF00).shr(8)) + bos.write(encryptedKey.size and 0x00FF) + bos.write(encryptedKey) + bos.write(iv.size) + bos.write(iv) + bos.write(encryptedBytes) + + return bos.toByteArray() + } + + private fun format2Make(salt: ByteArray, iv: ByteArray, encryptedBytes: ByteArray): ByteArray { + val bos = ByteArrayOutputStream(3 + salt.size + iv.size + encryptedBytes.size) + bos.write(FORMAT_2.toInt()) + bos.write(salt.size) + bos.write(salt) + bos.write(iv.size) + bos.write(iv) + bos.write(encryptedBytes) + + return bos.toByteArray() + } + + private fun format2Extract(bis: InputStream): Triple { + val format = bis.read() + assert(format.toByte() == FORMAT_2) + + val saltSize = bis.read() + val salt = ByteArray(saltSize) + bis.read(salt) + + val ivSize = bis.read() + val iv = ByteArray(ivSize) + bis.read(iv) + + val encrypted = bis.readBytes() + return Triple(salt, iv, encrypted) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/DefaultSignOutService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/DefaultSignOutService.kt new file mode 100644 index 0000000000..0fdecc8d21 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/DefaultSignOutService.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.signout + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.session.signout.SignOutService +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.auth.SessionParamsStore +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import org.matrix.android.sdk.internal.task.launchToCallback +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import javax.inject.Inject + +internal class DefaultSignOutService @Inject constructor(private val signOutTask: SignOutTask, + private val signInAgainTask: SignInAgainTask, + private val sessionParamsStore: SessionParamsStore, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val taskExecutor: TaskExecutor) : SignOutService { + + override fun signInAgain(password: String, + callback: MatrixCallback): Cancelable { + return signInAgainTask + .configureWith(SignInAgainTask.Params(password)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun updateCredentials(credentials: Credentials, + callback: MatrixCallback): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + sessionParamsStore.updateCredentials(credentials) + } + } + + override fun signOut(signOutFromHomeserver: Boolean, + callback: MatrixCallback): Cancelable { + return signOutTask + .configureWith(SignOutTask.Params(signOutFromHomeserver)) { + this.callback = callback + } + .executeBy(taskExecutor) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignInAgainTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignInAgainTask.kt new file mode 100644 index 0000000000..6f26fb25cd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignInAgainTask.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.signout + +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.auth.data.SessionParams +import org.matrix.android.sdk.internal.auth.SessionParamsStore +import org.matrix.android.sdk.internal.auth.data.PasswordLoginParams +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface SignInAgainTask : Task { + data class Params( + val password: String + ) +} + +internal class DefaultSignInAgainTask @Inject constructor( + private val signOutAPI: SignOutAPI, + private val sessionParams: SessionParams, + private val sessionParamsStore: SessionParamsStore, + private val eventBus: EventBus +) : SignInAgainTask { + + override suspend fun execute(params: SignInAgainTask.Params) { + val newCredentials = executeRequest(eventBus) { + apiCall = signOutAPI.loginAgain( + PasswordLoginParams.userIdentifier( + // Reuse the same userId + sessionParams.userId, + params.password, + // The spec says the initial device name will be ignored + // https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-login + // but https://github.com/matrix-org/synapse/issues/6525 + // Reuse the same deviceId + deviceId = sessionParams.deviceId + ) + ) + } + + sessionParamsStore.updateCredentials(newCredentials) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutAPI.kt new file mode 100644 index 0000000000..e4b05bfc67 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutAPI.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.signout + +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.internal.auth.data.PasswordLoginParams +import org.matrix.android.sdk.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.Headers +import retrofit2.http.POST + +internal interface SignOutAPI { + + /** + * Attempt to login again to the same account. + * Set all the timeouts to 1 minute + * It is similar to [AuthAPI.login] + * + * @param loginParams the login parameters + */ + @Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000") + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login") + fun loginAgain(@Body loginParams: PasswordLoginParams): Call + + /** + * Invalidate the access token, so that it can no longer be used for authorization. + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "logout") + fun signOut(): Call +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutModule.kt new file mode 100644 index 0000000000..c482200030 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutModule.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.signout + +import dagger.Binds +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.api.session.signout.SignOutService +import org.matrix.android.sdk.internal.session.SessionScope +import retrofit2.Retrofit + +@Module +internal abstract class SignOutModule { + + @Module + companion object { + @Provides + @JvmStatic + @SessionScope + fun providesSignOutAPI(retrofit: Retrofit): SignOutAPI { + return retrofit.create(SignOutAPI::class.java) + } + } + + @Binds + abstract fun bindSignOutTask(task: DefaultSignOutTask): SignOutTask + + @Binds + abstract fun bindSignInAgainTask(task: DefaultSignInAgainTask): SignInAgainTask + + @Binds + abstract fun bindSignOutService(service: DefaultSignOutService): SignOutService +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutTask.kt new file mode 100644 index 0000000000..ef507477cd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutTask.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.signout + +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.MatrixError +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.cleanup.CleanupSession +import org.matrix.android.sdk.internal.session.identity.IdentityDisconnectTask +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import java.net.HttpURLConnection +import javax.inject.Inject + +internal interface SignOutTask : Task { + data class Params( + val signOutFromHomeserver: Boolean + ) +} + +internal class DefaultSignOutTask @Inject constructor( + private val signOutAPI: SignOutAPI, + private val eventBus: EventBus, + private val identityDisconnectTask: IdentityDisconnectTask, + private val cleanupSession: CleanupSession +) : SignOutTask { + + override suspend fun execute(params: SignOutTask.Params) { + // It should be done even after a soft logout, to be sure the deviceId is deleted on the + if (params.signOutFromHomeserver) { + Timber.d("SignOut: send request...") + try { + executeRequest(eventBus) { + apiCall = signOutAPI.signOut() + } + } catch (throwable: Throwable) { + // Maybe due to https://github.com/matrix-org/synapse/issues/5756 + if (throwable is Failure.ServerError + && throwable.httpCode == HttpURLConnection.HTTP_UNAUTHORIZED /* 401 */ + && throwable.error.code == MatrixError.M_UNKNOWN_TOKEN) { + // Also throwable.error.isSoftLogout should be true + // Ignore + Timber.w("Ignore error due to https://github.com/matrix-org/synapse/issues/5755") + } else { + throw throwable + } + } + } + + // Logout from identity server if any + runCatching { identityDisconnectTask.execute(Unit) } + .onFailure { Timber.w(it, "Unable to disconnect identity server") } + + Timber.d("SignOut: cleanup session...") + cleanupSession.handle() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/CryptoSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/CryptoSyncHandler.kt new file mode 100644 index 0000000000..5c855e190c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/CryptoSyncHandler.kt @@ -0,0 +1,89 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.sync + +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.internal.crypto.DefaultCryptoService +import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult +import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult +import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService +import org.matrix.android.sdk.internal.session.DefaultInitialSyncProgressService +import org.matrix.android.sdk.internal.session.sync.model.SyncResponse +import org.matrix.android.sdk.internal.session.sync.model.ToDeviceSyncResponse +import timber.log.Timber +import javax.inject.Inject + +internal class CryptoSyncHandler @Inject constructor(private val cryptoService: DefaultCryptoService, + private val verificationService: DefaultVerificationService) { + + fun handleToDevice(toDevice: ToDeviceSyncResponse, initialSyncProgressService: DefaultInitialSyncProgressService? = null) { + val total = toDevice.events?.size ?: 0 + toDevice.events?.forEachIndexed { index, event -> + initialSyncProgressService?.reportProgress(((index / total.toFloat()) * 100).toInt()) + // Decrypt event if necessary + decryptToDeviceEvent(event, null) + if (event.getClearType() == EventType.MESSAGE + && event.getClearContent()?.toModel()?.msgType == "m.bad.encrypted") { + Timber.e("## CRYPTO | handleToDeviceEvent() : Warning: Unable to decrypt to-device event : ${event.content}") + } else { + verificationService.onToDeviceEvent(event) + cryptoService.onToDeviceEvent(event) + } + } + } + + fun onSyncCompleted(syncResponse: SyncResponse) { + cryptoService.onSyncCompleted(syncResponse) + } + + /** + * Decrypt an encrypted event + * + * @param event the event to decrypt + * @param timelineId the timeline identifier + * @return true if the event has been decrypted + */ + private fun decryptToDeviceEvent(event: Event, timelineId: String?): Boolean { + Timber.v("## CRYPTO | decryptToDeviceEvent") + if (event.getClearType() == EventType.ENCRYPTED) { + var result: MXEventDecryptionResult? = null + try { + result = cryptoService.decryptEvent(event, timelineId ?: "") + } catch (exception: MXCryptoError) { + event.mCryptoError = (exception as? MXCryptoError.Base)?.errorType // setCryptoError(exception.cryptoError) + Timber.e("## CRYPTO | Failed to decrypt to device event: ${event.mCryptoError ?: exception}") + } + + if (null != result) { + event.mxDecryptionResult = OlmDecryptionResult( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + ) + return true + } + } + + return false + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/GroupSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/GroupSyncHandler.kt new file mode 100644 index 0000000000..c3dd9fd577 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/GroupSyncHandler.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.sync + +import org.matrix.android.sdk.R +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.internal.database.model.GroupEntity +import org.matrix.android.sdk.internal.database.model.GroupSummaryEntity +import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.session.DefaultInitialSyncProgressService +import org.matrix.android.sdk.internal.session.mapWithProgress +import org.matrix.android.sdk.internal.session.sync.model.GroupsSyncResponse +import org.matrix.android.sdk.internal.session.sync.model.InvitedGroupSync +import io.realm.Realm +import javax.inject.Inject + +internal class GroupSyncHandler @Inject constructor() { + + sealed class HandlingStrategy { + data class JOINED(val data: Map) : HandlingStrategy() + data class INVITED(val data: Map) : HandlingStrategy() + data class LEFT(val data: Map) : HandlingStrategy() + } + + fun handle( + realm: Realm, + roomsSyncResponse: GroupsSyncResponse, + reporter: DefaultInitialSyncProgressService? = null + ) { + handleGroupSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), reporter) + handleGroupSync(realm, HandlingStrategy.INVITED(roomsSyncResponse.invite), reporter) + handleGroupSync(realm, HandlingStrategy.LEFT(roomsSyncResponse.leave), reporter) + } + + // PRIVATE METHODS ***************************************************************************** + + private fun handleGroupSync(realm: Realm, handlingStrategy: HandlingStrategy, reporter: DefaultInitialSyncProgressService?) { + val groups = when (handlingStrategy) { + is HandlingStrategy.JOINED -> + handlingStrategy.data.mapWithProgress(reporter, R.string.initial_sync_start_importing_account_groups, 0.6f) { + handleJoinedGroup(realm, it.key) + } + + is HandlingStrategy.INVITED -> + handlingStrategy.data.mapWithProgress(reporter, R.string.initial_sync_start_importing_account_groups, 0.3f) { + handleInvitedGroup(realm, it.key) + } + + is HandlingStrategy.LEFT -> + handlingStrategy.data.mapWithProgress(reporter, R.string.initial_sync_start_importing_account_groups, 0.1f) { + handleLeftGroup(realm, it.key) + } + } + realm.insertOrUpdate(groups) + } + + private fun handleJoinedGroup(realm: Realm, + groupId: String): GroupEntity { + val groupEntity = GroupEntity.where(realm, groupId).findFirst() ?: GroupEntity(groupId) + val groupSummaryEntity = GroupSummaryEntity.getOrCreate(realm, groupId) + groupEntity.membership = Membership.JOIN + groupSummaryEntity.membership = Membership.JOIN + return groupEntity + } + + private fun handleInvitedGroup(realm: Realm, + groupId: String): GroupEntity { + val groupEntity = GroupEntity.where(realm, groupId).findFirst() ?: GroupEntity(groupId) + val groupSummaryEntity = GroupSummaryEntity.getOrCreate(realm, groupId) + groupEntity.membership = Membership.INVITE + groupSummaryEntity.membership = Membership.INVITE + return groupEntity + } + + private fun handleLeftGroup(realm: Realm, + groupId: String): GroupEntity { + val groupEntity = GroupEntity.where(realm, groupId).findFirst() ?: GroupEntity(groupId) + val groupSummaryEntity = GroupSummaryEntity.getOrCreate(realm, groupId) + groupEntity.membership = Membership.LEAVE + groupSummaryEntity.membership = Membership.LEAVE + return groupEntity + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/ReadReceiptHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/ReadReceiptHandler.kt new file mode 100644 index 0000000000..5fe23e1b69 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/ReadReceiptHandler.kt @@ -0,0 +1,114 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.sync + +import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity +import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntity +import org.matrix.android.sdk.internal.database.query.createUnmanaged +import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.database.query.where +import io.realm.Realm +import timber.log.Timber +import javax.inject.Inject + +// the receipts dictionaries +// key : $EventId +// value : dict key $UserId +// value dict key ts +// dict value ts value +typealias ReadReceiptContent = Map>>> + +private const val READ_KEY = "m.read" +private const val TIMESTAMP_KEY = "ts" + +internal class ReadReceiptHandler @Inject constructor() { + + companion object { + + fun createContent(userId: String, eventId: String): ReadReceiptContent { + return mapOf( + eventId to mapOf( + READ_KEY to mapOf( + userId to mapOf( + TIMESTAMP_KEY to System.currentTimeMillis().toDouble() + ) + ) + ) + ) + } + } + + fun handle(realm: Realm, roomId: String, content: ReadReceiptContent?, isInitialSync: Boolean) { + if (content == null) { + return + } + try { + handleReadReceiptContent(realm, roomId, content, isInitialSync) + } catch (exception: Exception) { + Timber.e("Fail to handle read receipt for room $roomId") + } + } + + private fun handleReadReceiptContent(realm: Realm, roomId: String, content: ReadReceiptContent, isInitialSync: Boolean) { + if (isInitialSync) { + initialSyncStrategy(realm, roomId, content) + } else { + incrementalSyncStrategy(realm, roomId, content) + } + } + + private fun initialSyncStrategy(realm: Realm, roomId: String, content: ReadReceiptContent) { + val readReceiptSummaries = ArrayList() + for ((eventId, receiptDict) in content) { + val userIdsDict = receiptDict[READ_KEY] ?: continue + val readReceiptsSummary = ReadReceiptsSummaryEntity(eventId = eventId, roomId = roomId) + + for ((userId, paramsDict) in userIdsDict) { + val ts = paramsDict[TIMESTAMP_KEY] ?: 0.0 + val receiptEntity = ReadReceiptEntity.createUnmanaged(roomId, eventId, userId, ts) + readReceiptsSummary.readReceipts.add(receiptEntity) + } + readReceiptSummaries.add(readReceiptsSummary) + } + realm.insertOrUpdate(readReceiptSummaries) + } + + private fun incrementalSyncStrategy(realm: Realm, roomId: String, content: ReadReceiptContent) { + for ((eventId, receiptDict) in content) { + val userIdsDict = receiptDict[READ_KEY] ?: continue + val readReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst() + ?: realm.createObject(ReadReceiptsSummaryEntity::class.java, eventId).apply { + this.roomId = roomId + } + + for ((userId, paramsDict) in userIdsDict) { + val ts = paramsDict[TIMESTAMP_KEY] ?: 0.0 + val receiptEntity = ReadReceiptEntity.getOrCreate(realm, roomId, userId) + // ensure new ts is superior to the previous one + if (ts > receiptEntity.originServerTs) { + ReadReceiptsSummaryEntity.where(realm, receiptEntity.eventId).findFirst()?.also { + it.readReceipts.remove(receiptEntity) + } + receiptEntity.eventId = eventId + receiptEntity.originServerTs = ts + readReceiptsSummary.readReceipts.add(receiptEntity) + } + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomFullyReadHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomFullyReadHandler.kt new file mode 100644 index 0000000000..b0c44ef4f0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomFullyReadHandler.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.sync + +import org.matrix.android.sdk.internal.database.model.ReadMarkerEntity +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.session.room.read.FullyReadContent +import io.realm.Realm +import timber.log.Timber +import javax.inject.Inject + +internal class RoomFullyReadHandler @Inject constructor() { + + fun handle(realm: Realm, roomId: String, content: FullyReadContent?) { + if (content == null) { + return + } + Timber.v("Handle for roomId: $roomId eventId: ${content.eventId}") + + RoomSummaryEntity.getOrCreate(realm, roomId).apply { + readMarkerId = content.eventId + } + ReadMarkerEntity.getOrCreate(realm, roomId).apply { + this.eventId = content.eventId + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt new file mode 100644 index 0000000000..64c30825fc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt @@ -0,0 +1,430 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.sync + +import org.matrix.android.sdk.R +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.RoomMemberContent +import org.matrix.android.sdk.api.session.room.model.tag.RoomTagContent +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.crypto.DefaultCryptoService +import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult +import org.matrix.android.sdk.internal.database.helper.addOrUpdate +import org.matrix.android.sdk.internal.database.helper.addTimelineEvent +import org.matrix.android.sdk.internal.database.helper.deleteOnCascade +import org.matrix.android.sdk.internal.database.mapper.asDomain +import org.matrix.android.sdk.internal.database.mapper.toEntity +import org.matrix.android.sdk.internal.database.model.ChunkEntity +import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity +import org.matrix.android.sdk.internal.database.model.EventInsertType +import org.matrix.android.sdk.internal.database.model.RoomEntity +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity +import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore +import org.matrix.android.sdk.internal.database.query.find +import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom +import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.database.query.getOrNull +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.DefaultInitialSyncProgressService +import org.matrix.android.sdk.internal.session.mapWithProgress +import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource +import org.matrix.android.sdk.internal.session.room.membership.RoomMemberEventHandler +import org.matrix.android.sdk.internal.session.room.read.FullyReadContent +import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater +import org.matrix.android.sdk.internal.session.room.timeline.DefaultTimeline +import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection +import org.matrix.android.sdk.internal.session.room.timeline.TimelineEventDecryptor +import org.matrix.android.sdk.internal.session.room.typing.TypingEventContent +import org.matrix.android.sdk.internal.session.sync.model.InvitedRoomSync +import org.matrix.android.sdk.internal.session.sync.model.RoomSync +import org.matrix.android.sdk.internal.session.sync.model.RoomSyncAccountData +import org.matrix.android.sdk.internal.session.sync.model.RoomSyncEphemeral +import org.matrix.android.sdk.internal.session.sync.model.RoomsSyncResponse +import io.realm.Realm +import io.realm.kotlin.createObject +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import javax.inject.Inject + +internal class RoomSyncHandler @Inject constructor(private val readReceiptHandler: ReadReceiptHandler, + private val roomSummaryUpdater: RoomSummaryUpdater, + private val roomTagHandler: RoomTagHandler, + private val roomFullyReadHandler: RoomFullyReadHandler, + private val cryptoService: DefaultCryptoService, + private val roomMemberEventHandler: RoomMemberEventHandler, + private val roomTypingUsersHandler: RoomTypingUsersHandler, + private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource, + @UserId private val userId: String, + private val eventBus: EventBus, + private val timelineEventDecryptor: TimelineEventDecryptor) { + + sealed class HandlingStrategy { + data class JOINED(val data: Map) : HandlingStrategy() + data class INVITED(val data: Map) : HandlingStrategy() + data class LEFT(val data: Map) : HandlingStrategy() + } + + fun handle( + realm: Realm, + roomsSyncResponse: RoomsSyncResponse, + isInitialSync: Boolean, + reporter: DefaultInitialSyncProgressService? = null + ) { + Timber.v("Execute transaction from $this") + handleRoomSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), isInitialSync, reporter) + handleRoomSync(realm, HandlingStrategy.INVITED(roomsSyncResponse.invite), isInitialSync, reporter) + handleRoomSync(realm, HandlingStrategy.LEFT(roomsSyncResponse.leave), isInitialSync, reporter) + } + + // PRIVATE METHODS ***************************************************************************** + + private fun handleRoomSync(realm: Realm, handlingStrategy: HandlingStrategy, isInitialSync: Boolean, reporter: DefaultInitialSyncProgressService?) { + val insertType = if (isInitialSync) { + EventInsertType.INITIAL_SYNC + } else { + EventInsertType.INCREMENTAL_SYNC + } + val syncLocalTimeStampMillis = System.currentTimeMillis() + val rooms = when (handlingStrategy) { + is HandlingStrategy.JOINED -> + handlingStrategy.data.mapWithProgress(reporter, R.string.initial_sync_start_importing_account_joined_rooms, 0.6f) { + handleJoinedRoom(realm, it.key, it.value, isInitialSync, insertType, syncLocalTimeStampMillis) + } + is HandlingStrategy.INVITED -> + handlingStrategy.data.mapWithProgress(reporter, R.string.initial_sync_start_importing_account_invited_rooms, 0.1f) { + handleInvitedRoom(realm, it.key, it.value, insertType, syncLocalTimeStampMillis) + } + + is HandlingStrategy.LEFT -> { + handlingStrategy.data.mapWithProgress(reporter, R.string.initial_sync_start_importing_account_left_rooms, 0.3f) { + handleLeftRoom(realm, it.key, it.value, insertType, syncLocalTimeStampMillis) + } + } + } + realm.insertOrUpdate(rooms) + } + + private fun handleJoinedRoom(realm: Realm, + roomId: String, + roomSync: RoomSync, + isInitialSync: Boolean, + insertType: EventInsertType, + syncLocalTimestampMillis: Long): RoomEntity { + Timber.v("Handle join sync for room $roomId") + + var ephemeralResult: EphemeralResult? = null + if (roomSync.ephemeral?.events?.isNotEmpty() == true) { + ephemeralResult = handleEphemeral(realm, roomId, roomSync.ephemeral, isInitialSync) + } + + if (roomSync.accountData?.events?.isNotEmpty() == true) { + handleRoomAccountDataEvents(realm, roomId, roomSync.accountData) + } + + val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId) + + if (roomEntity.membership == Membership.INVITE) { + roomEntity.chunks.deleteAllFromRealm() + } + roomEntity.membership = Membership.JOIN + + // State event + if (roomSync.state?.events?.isNotEmpty() == true) { + for (event in roomSync.state.events) { + if (event.eventId == null || event.stateKey == null) { + continue + } + val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it } + val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType) + CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply { + eventId = event.eventId + root = eventEntity + } + // Give info to crypto module + cryptoService.onStateEvent(roomId, event) + roomMemberEventHandler.handle(realm, roomId, event) + } + } + if (roomSync.timeline?.events?.isNotEmpty() == true) { + val chunkEntity = handleTimelineEvents( + realm, + roomId, + roomEntity, + roomSync.timeline.events, + roomSync.timeline.prevToken, + roomSync.timeline.limited, + insertType, + syncLocalTimestampMillis, + isInitialSync + ) + roomEntity.addOrUpdate(chunkEntity) + } + val hasRoomMember = roomSync.state?.events?.firstOrNull { + it.type == EventType.STATE_ROOM_MEMBER + } != null || roomSync.timeline?.events?.firstOrNull { + it.type == EventType.STATE_ROOM_MEMBER + } != null + + roomTypingUsersHandler.handle(realm, roomId, ephemeralResult) + roomChangeMembershipStateDataSource.setMembershipFromSync(roomId, Membership.JOIN) + roomSummaryUpdater.update( + realm, + roomId, + Membership.JOIN, + roomSync.summary, + roomSync.unreadNotifications, + updateMembers = hasRoomMember + ) + return roomEntity + } + + private fun handleInvitedRoom(realm: Realm, + roomId: String, + roomSync: InvitedRoomSync, + insertType: EventInsertType, + syncLocalTimestampMillis: Long): RoomEntity { + Timber.v("Handle invited sync for room $roomId") + val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId) + roomEntity.membership = Membership.INVITE + if (roomSync.inviteState != null && roomSync.inviteState.events.isNotEmpty()) { + roomSync.inviteState.events.forEach { event -> + if (event.stateKey == null) { + return@forEach + } + val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it } + val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType) + CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply { + eventId = eventEntity.eventId + root = eventEntity + } + roomMemberEventHandler.handle(realm, roomId, event) + } + } + val inviterEvent = roomSync.inviteState?.events?.lastOrNull { + it.type == EventType.STATE_ROOM_MEMBER + } + roomChangeMembershipStateDataSource.setMembershipFromSync(roomId, Membership.INVITE) + roomSummaryUpdater.update(realm, roomId, Membership.INVITE, updateMembers = true, inviterId = inviterEvent?.senderId) + return roomEntity + } + + private fun handleLeftRoom(realm: Realm, + roomId: String, + roomSync: RoomSync, + insertType: EventInsertType, + syncLocalTimestampMillis: Long): RoomEntity { + val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId) + for (event in roomSync.state?.events.orEmpty()) { + if (event.eventId == null || event.stateKey == null) { + continue + } + val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it } + val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType) + CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply { + eventId = event.eventId + root = eventEntity + } + roomMemberEventHandler.handle(realm, roomId, event) + } + for (event in roomSync.timeline?.events.orEmpty()) { + if (event.eventId == null || event.senderId == null) { + continue + } + val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it } + val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType) + if (event.stateKey != null) { + CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply { + eventId = event.eventId + root = eventEntity + } + if (event.type == EventType.STATE_ROOM_MEMBER) { + roomMemberEventHandler.handle(realm, roomEntity.roomId, event) + } + } + } + val leftMember = RoomMemberSummaryEntity.where(realm, roomId, userId).findFirst() + val membership = leftMember?.membership ?: Membership.LEAVE + roomEntity.membership = membership + roomEntity.chunks.deleteAllFromRealm() + roomTypingUsersHandler.handle(realm, roomId, null) + roomChangeMembershipStateDataSource.setMembershipFromSync(roomId, Membership.LEAVE) + roomSummaryUpdater.update(realm, roomId, membership, roomSync.summary, roomSync.unreadNotifications) + return roomEntity + } + + private fun handleTimelineEvents(realm: Realm, + roomId: String, + roomEntity: RoomEntity, + eventList: List, + prevToken: String? = null, + isLimited: Boolean = true, + insertType: EventInsertType, + syncLocalTimestampMillis: Long, + isInitialSync: Boolean): ChunkEntity { + val lastChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomEntity.roomId) + val chunkEntity = if (!isLimited && lastChunk != null) { + lastChunk + } else { + realm.createObject().apply { this.prevToken = prevToken } + } + // Only one chunk has isLastForward set to true + lastChunk?.isLastForward = false + chunkEntity.isLastForward = true + + val eventIds = ArrayList(eventList.size) + val roomMemberContentsByUser = HashMap() + + for (event in eventList) { + if (event.eventId == null || event.senderId == null) { + continue + } + eventIds.add(event.eventId) + + if (event.isEncrypted() && !isInitialSync) { + decryptIfNeeded(event, roomId) + } + + val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it } + val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType) + if (event.stateKey != null) { + CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply { + eventId = event.eventId + root = eventEntity + } + if (event.type == EventType.STATE_ROOM_MEMBER) { + val fixedContent = event.getFixedRoomMemberContent() + roomMemberContentsByUser[event.stateKey] = fixedContent + roomMemberEventHandler.handle(realm, roomEntity.roomId, event.stateKey, fixedContent) + } + } + roomMemberContentsByUser.getOrPut(event.senderId) { + // If we don't have any new state on this user, get it from db + val rootStateEvent = CurrentStateEventEntity.getOrNull(realm, roomId, event.senderId, EventType.STATE_ROOM_MEMBER)?.root + rootStateEvent?.asDomain()?.getFixedRoomMemberContent() + } + + chunkEntity.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser) + // Give info to crypto module + cryptoService.onLiveEvent(roomEntity.roomId, event) + + // Try to remove local echo + event.unsignedData?.transactionId?.also { + val sendingEventEntity = roomEntity.sendingTimelineEvents.find(it) + if (sendingEventEntity != null) { + Timber.v("Remove local echo for tx:$it") + roomEntity.sendingTimelineEvents.remove(sendingEventEntity) + if (event.isEncrypted() && event.content?.get("algorithm") as? String == MXCRYPTO_ALGORITHM_MEGOLM) { + // updated with echo decryption, to avoid seeing it decrypt again + val adapter = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java) + sendingEventEntity.root?.decryptionResultJson?.let { json -> + eventEntity.decryptionResultJson = json + event.mxDecryptionResult = adapter.fromJson(json) + } + } + // Finally delete the local echo + sendingEventEntity.deleteOnCascade() + } else { + Timber.v("Can't find corresponding local echo for tx:$it") + } + } + } + // posting new events to timeline if any is registered + eventBus.post(DefaultTimeline.OnNewTimelineEvents(roomId = roomId, eventIds = eventIds)) + return chunkEntity + } + + private fun decryptIfNeeded(event: Event, roomId: String) { + try { + val result = cryptoService.decryptEvent(event.copy(roomId = roomId), "") + event.mxDecryptionResult = OlmDecryptionResult( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) }, + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + ) + } catch (e: MXCryptoError) { + if (e is MXCryptoError.Base) { + event.mCryptoError = e.errorType + event.mCryptoErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription + } + } + } + + data class EphemeralResult( + val typingUserIds: List = emptyList() + ) + + private fun handleEphemeral(realm: Realm, + roomId: String, + ephemeral: RoomSyncEphemeral, + isInitialSync: Boolean): EphemeralResult { + var result = EphemeralResult() + for (event in ephemeral.events) { + when (event.type) { + EventType.RECEIPT -> { + @Suppress("UNCHECKED_CAST") + (event.content as? ReadReceiptContent)?.let { readReceiptContent -> + readReceiptHandler.handle(realm, roomId, readReceiptContent, isInitialSync) + } + } + EventType.TYPING -> { + event.content.toModel()?.let { typingEventContent -> + result = result.copy(typingUserIds = typingEventContent.typingUserIds) + } + } + else -> Timber.w("Ephemeral event type '${event.type}' not yet supported") + } + } + + return result + } + + private fun handleRoomAccountDataEvents(realm: Realm, roomId: String, accountData: RoomSyncAccountData) { + for (event in accountData.events) { + val eventType = event.getClearType() + if (eventType == EventType.TAG) { + val content = event.getClearContent().toModel() + roomTagHandler.handle(realm, roomId, content) + } else if (eventType == EventType.FULLY_READ) { + val content = event.getClearContent().toModel() + roomFullyReadHandler.handle(realm, roomId, content) + } + } + } + + private fun Event.getFixedRoomMemberContent(): RoomMemberContent? { + val content = content.toModel() + // if user is leaving, we should grab his last name and avatar from prevContent + return if (content?.membership?.isLeft() == true) { + val prevContent = resolvedPrevContent().toModel() + content.copy( + displayName = prevContent?.displayName, + avatarUrl = prevContent?.avatarUrl + ) + } else { + content + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomTagHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomTagHandler.kt new file mode 100644 index 0000000000..8dbd77f3fd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomTagHandler.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.sync + +import org.matrix.android.sdk.api.session.room.model.tag.RoomTagContent +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.model.RoomTagEntity +import org.matrix.android.sdk.internal.database.query.where +import io.realm.Realm +import javax.inject.Inject + +internal class RoomTagHandler @Inject constructor() { + + fun handle(realm: Realm, roomId: String, content: RoomTagContent?) { + if (content == null) { + return + } + val tags = content.tags.entries.map { (tagName, params) -> + RoomTagEntity(tagName, params["order"] as? Double) + } + val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() + ?: RoomSummaryEntity(roomId) + + roomSummaryEntity.tags.clear() + roomSummaryEntity.tags.addAll(tags) + realm.insertOrUpdate(roomSummaryEntity) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomTypingUsersHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomTypingUsersHandler.kt new file mode 100644 index 0000000000..71a4d33d3f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomTypingUsersHandler.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.sync + +import org.matrix.android.sdk.api.session.room.sender.SenderInfo +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper +import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker +import io.realm.Realm +import javax.inject.Inject + +internal class RoomTypingUsersHandler @Inject constructor(@UserId private val userId: String, + private val typingUsersTracker: DefaultTypingUsersTracker) { + + fun handle(realm: Realm, roomId: String, ephemeralResult: RoomSyncHandler.EphemeralResult?) { + val roomMemberHelper = RoomMemberHelper(realm, roomId) + val typingIds = ephemeralResult?.typingUserIds?.filter { it != userId } ?: emptyList() + val senderInfo = typingIds.map { userId -> + val roomMemberSummaryEntity = roomMemberHelper.getLastRoomMember(userId) + SenderInfo( + userId = userId, + displayName = roomMemberSummaryEntity?.displayName, + isUniqueDisplayName = roomMemberHelper.isUniqueDisplayName(roomMemberSummaryEntity?.displayName), + avatarUrl = roomMemberSummaryEntity?.avatarUrl + ) + } + typingUsersTracker.setTypingUsersFromRoom(roomId, senderInfo) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncAPI.kt new file mode 100644 index 0000000000..db14c53456 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncAPI.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.sync + +import org.matrix.android.sdk.internal.network.NetworkConstants +import org.matrix.android.sdk.internal.session.sync.model.SyncResponse +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Headers +import retrofit2.http.QueryMap + +internal interface SyncAPI { + + /** + * Set all the timeouts to 1 minute + */ + @Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000") + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "sync") + fun sync(@QueryMap params: Map): Call +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncModule.kt new file mode 100644 index 0000000000..87afe78e70 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncModule.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.sync + +import dagger.Binds +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.internal.session.SessionScope +import retrofit2.Retrofit + +@Module +internal abstract class SyncModule { + + @Module + companion object { + @Provides + @JvmStatic + @SessionScope + fun providesSyncAPI(retrofit: Retrofit): SyncAPI { + return retrofit.create(SyncAPI::class.java) + } + } + + @Binds + abstract fun bindSyncTask(task: DefaultSyncTask): SyncTask +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt new file mode 100644 index 0000000000..b58727cbaa --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt @@ -0,0 +1,168 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.sync + +import androidx.work.ExistingPeriodicWorkPolicy +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.R +import org.matrix.android.sdk.api.pushrules.PushRuleService +import org.matrix.android.sdk.api.pushrules.RuleScope +import org.matrix.android.sdk.internal.crypto.DefaultCryptoService +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.SessionId +import org.matrix.android.sdk.internal.di.WorkManagerProvider +import org.matrix.android.sdk.internal.session.DefaultInitialSyncProgressService +import org.matrix.android.sdk.internal.session.group.GetGroupDataWorker +import org.matrix.android.sdk.internal.session.notification.ProcessEventForPushTask +import org.matrix.android.sdk.internal.session.reportSubtask +import org.matrix.android.sdk.internal.session.sync.model.GroupsSyncResponse +import org.matrix.android.sdk.internal.session.sync.model.RoomsSyncResponse +import org.matrix.android.sdk.internal.session.sync.model.SyncResponse +import org.matrix.android.sdk.internal.util.awaitTransaction +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import timber.log.Timber +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import kotlin.system.measureTimeMillis + +private const val GET_GROUP_DATA_WORKER = "GET_GROUP_DATA_WORKER" + +internal class SyncResponseHandler @Inject constructor(@SessionDatabase private val monarchy: Monarchy, + @SessionId private val sessionId: String, + private val workManagerProvider: WorkManagerProvider, + private val roomSyncHandler: RoomSyncHandler, + private val userAccountDataSyncHandler: UserAccountDataSyncHandler, + private val groupSyncHandler: GroupSyncHandler, + private val cryptoSyncHandler: CryptoSyncHandler, + private val cryptoService: DefaultCryptoService, + private val tokenStore: SyncTokenStore, + private val processEventForPushTask: ProcessEventForPushTask, + private val pushRuleService: PushRuleService, + private val initialSyncProgressService: DefaultInitialSyncProgressService) { + + suspend fun handleResponse(syncResponse: SyncResponse, fromToken: String?) { + val isInitialSync = fromToken == null + Timber.v("Start handling sync, is InitialSync: $isInitialSync") + val reporter = initialSyncProgressService.takeIf { isInitialSync } + + measureTimeMillis { + if (!cryptoService.isStarted()) { + Timber.v("Should start cryptoService") + cryptoService.start() + } + cryptoService.onSyncWillProcess(isInitialSync) + }.also { + Timber.v("Finish handling start cryptoService in $it ms") + } + + // Handle the to device events before the room ones + // to ensure to decrypt them properly + measureTimeMillis { + Timber.v("Handle toDevice") + reportSubtask(reporter, R.string.initial_sync_start_importing_account_crypto, 100, 0.1f) { + if (syncResponse.toDevice != null) { + cryptoSyncHandler.handleToDevice(syncResponse.toDevice, reporter) + } + } + }.also { + Timber.v("Finish handling toDevice in $it ms") + } + // Start one big transaction + monarchy.awaitTransaction { realm -> + measureTimeMillis { + Timber.v("Handle rooms") + reportSubtask(reporter, R.string.initial_sync_start_importing_account_rooms, 100, 0.7f) { + if (syncResponse.rooms != null) { + roomSyncHandler.handle(realm, syncResponse.rooms, isInitialSync, reporter) + } + } + }.also { + Timber.v("Finish handling rooms in $it ms") + } + + measureTimeMillis { + reportSubtask(reporter, R.string.initial_sync_start_importing_account_groups, 100, 0.1f) { + Timber.v("Handle groups") + if (syncResponse.groups != null) { + groupSyncHandler.handle(realm, syncResponse.groups, reporter) + } + } + }.also { + Timber.v("Finish handling groups in $it ms") + } + + measureTimeMillis { + reportSubtask(reporter, R.string.initial_sync_start_importing_account_data, 100, 0.1f) { + Timber.v("Handle accountData") + userAccountDataSyncHandler.handle(realm, syncResponse.accountData) + } + }.also { + Timber.v("Finish handling accountData in $it ms") + } + tokenStore.saveToken(realm, syncResponse.nextBatch) + } + // Everything else we need to do outside the transaction + syncResponse.rooms?.let { + checkPushRules(it, isInitialSync) + userAccountDataSyncHandler.synchronizeWithServerIfNeeded(it.invite) + } + syncResponse.groups?.let { + scheduleGroupDataFetchingIfNeeded(it) + } + + Timber.v("On sync completed") + cryptoSyncHandler.onSyncCompleted(syncResponse) + } + + /** + * At the moment we don't get any group data through the sync, so we poll where every hour. + You can also force to refetch group data using [Group] API. + */ + private fun scheduleGroupDataFetchingIfNeeded(groupsSyncResponse: GroupsSyncResponse) { + val groupIds = ArrayList() + groupIds.addAll(groupsSyncResponse.join.keys) + groupIds.addAll(groupsSyncResponse.invite.keys) + if (groupIds.isEmpty()) { + Timber.v("No new groups to fetch data for.") + return + } + Timber.v("There are ${groupIds.size} new groups to fetch data for.") + val getGroupDataWorkerParams = GetGroupDataWorker.Params(sessionId) + val workData = WorkerParamsFactory.toData(getGroupDataWorkerParams) + + val getGroupWork = workManagerProvider.matrixPeriodicWorkRequestBuilder(1, TimeUnit.HOURS) + .setInputData(workData) + .setConstraints(WorkManagerProvider.workConstraints) + .build() + + workManagerProvider.workManager + .enqueueUniquePeriodicWork(GET_GROUP_DATA_WORKER, ExistingPeriodicWorkPolicy.REPLACE, getGroupWork) + } + + private suspend fun checkPushRules(roomsSyncResponse: RoomsSyncResponse, isInitialSync: Boolean) { + Timber.v("[PushRules] --> checkPushRules") + if (isInitialSync) { + Timber.v("[PushRules] <-- No push rule check on initial sync") + return + } // nothing on initial sync + + val rules = pushRuleService.getPushRules(RuleScope.GLOBAL).getAllRules() + processEventForPushTask.execute(ProcessEventForPushTask.Params(roomsSyncResponse, rules)) + Timber.v("[PushRules] <-- Push task scheduled") + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt new file mode 100644 index 0000000000..02afd53908 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.sync + +import org.matrix.android.sdk.R +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.DefaultInitialSyncProgressService +import org.matrix.android.sdk.internal.session.filter.FilterRepository +import org.matrix.android.sdk.internal.session.homeserver.GetHomeServerCapabilitiesTask +import org.matrix.android.sdk.internal.session.sync.model.SyncResponse +import org.matrix.android.sdk.internal.session.user.UserStore +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import javax.inject.Inject + +internal interface SyncTask : Task { + + data class Params(var timeout: Long = 30_000L) +} + +internal class DefaultSyncTask @Inject constructor( + private val syncAPI: SyncAPI, + @UserId private val userId: String, + private val filterRepository: FilterRepository, + private val syncResponseHandler: SyncResponseHandler, + private val initialSyncProgressService: DefaultInitialSyncProgressService, + private val syncTokenStore: SyncTokenStore, + private val getHomeServerCapabilitiesTask: GetHomeServerCapabilitiesTask, + private val userStore: UserStore, + private val syncTaskSequencer: SyncTaskSequencer, + private val eventBus: EventBus +) : SyncTask { + + override suspend fun execute(params: SyncTask.Params) = syncTaskSequencer.post { + doSync(params) + } + + private suspend fun doSync(params: SyncTask.Params) { + Timber.v("Sync task started on Thread: ${Thread.currentThread().name}") + + val requestParams = HashMap() + var timeout = 0L + val token = syncTokenStore.getLastToken() + if (token != null) { + requestParams["since"] = token + timeout = params.timeout + } + requestParams["timeout"] = timeout.toString() + requestParams["filter"] = filterRepository.getFilter() + + val isInitialSync = token == null + if (isInitialSync) { + // We might want to get the user information in parallel too + userStore.createOrUpdate(userId) + initialSyncProgressService.endAll() + initialSyncProgressService.startTask(R.string.initial_sync_start_importing_account, 100) + } + // Maybe refresh the home server capabilities data we know + getHomeServerCapabilitiesTask.execute(Unit) + + val syncResponse = executeRequest(eventBus) { + apiCall = syncAPI.sync(requestParams) + } + syncResponseHandler.handleResponse(syncResponse, token) + if (isInitialSync) { + initialSyncProgressService.endAll() + } + Timber.v("Sync task finished on Thread: ${Thread.currentThread().name}") + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTaskSequencer.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTaskSequencer.kt new file mode 100644 index 0000000000..4b29a82ad4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTaskSequencer.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.sync + +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer +import javax.inject.Inject + +@SessionScope +internal class SyncTaskSequencer @Inject constructor() : SemaphoreCoroutineSequencer() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTokenStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTokenStore.kt new file mode 100644 index 0000000000..e001e61149 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTokenStore.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.sync + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.internal.database.model.SyncEntity +import org.matrix.android.sdk.internal.di.SessionDatabase +import io.realm.Realm +import javax.inject.Inject + +internal class SyncTokenStore @Inject constructor(@SessionDatabase private val monarchy: Monarchy) { + + fun getLastToken(): String? { + return Realm.getInstance(monarchy.realmConfiguration).use { + it.where(SyncEntity::class.java).findFirst()?.nextBatch + } + } + + fun saveToken(realm: Realm, token: String?) { + val sync = SyncEntity(token) + realm.insertOrUpdate(sync) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/UserAccountDataSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/UserAccountDataSyncHandler.kt new file mode 100644 index 0000000000..4ef6a5a3e1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/UserAccountDataSyncHandler.kt @@ -0,0 +1,223 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.sync + +import com.squareup.moshi.Moshi +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.pushrules.RuleScope +import org.matrix.android.sdk.api.pushrules.RuleSetKey +import org.matrix.android.sdk.api.pushrules.rest.GetPushRulesResponse +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.RoomMemberContent +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.internal.database.mapper.ContentMapper +import org.matrix.android.sdk.internal.database.mapper.PushRulesMapper +import org.matrix.android.sdk.internal.database.mapper.asDomain +import org.matrix.android.sdk.internal.database.model.BreadcrumbsEntity +import org.matrix.android.sdk.internal.database.model.IgnoredUserEntity +import org.matrix.android.sdk.internal.database.model.PushRulesEntity +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields +import org.matrix.android.sdk.internal.database.model.UserAccountDataEntity +import org.matrix.android.sdk.internal.database.model.UserAccountDataEntityFields +import org.matrix.android.sdk.internal.database.query.getDirectRooms +import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper +import org.matrix.android.sdk.internal.session.sync.model.InvitedRoomSync +import org.matrix.android.sdk.internal.session.sync.model.accountdata.BreadcrumbsContent +import org.matrix.android.sdk.internal.session.sync.model.accountdata.DirectMessagesContent +import org.matrix.android.sdk.internal.session.sync.model.accountdata.IgnoredUsersContent +import org.matrix.android.sdk.internal.session.sync.model.accountdata.UserAccountDataSync +import org.matrix.android.sdk.internal.session.user.accountdata.DirectChatsHelper +import org.matrix.android.sdk.internal.session.user.accountdata.UpdateUserAccountDataTask +import io.realm.Realm +import io.realm.RealmList +import io.realm.kotlin.where +import timber.log.Timber +import javax.inject.Inject + +internal class UserAccountDataSyncHandler @Inject constructor( + @SessionDatabase private val monarchy: Monarchy, + @UserId private val userId: String, + private val directChatsHelper: DirectChatsHelper, + private val moshi: Moshi, + private val updateUserAccountDataTask: UpdateUserAccountDataTask) { + + fun handle(realm: Realm, accountData: UserAccountDataSync?) { + accountData?.list?.forEach { event -> + // Generic handling, just save in base + handleGenericAccountData(realm, event.type, event.content) + when (event.type) { + UserAccountDataTypes.TYPE_DIRECT_MESSAGES -> handleDirectChatRooms(realm, event) + UserAccountDataTypes.TYPE_PUSH_RULES -> handlePushRules(realm, event) + UserAccountDataTypes.TYPE_IGNORED_USER_LIST -> handleIgnoredUsers(realm, event) + UserAccountDataTypes.TYPE_BREADCRUMBS -> handleBreadcrumbs(realm, event) + } + } + } + + // If we get some direct chat invites, we synchronize the user account data including those. + suspend fun synchronizeWithServerIfNeeded(invites: Map) { + if (invites.isNullOrEmpty()) return + val directChats = directChatsHelper.getLocalUserAccount() + var hasUpdate = false + monarchy.doWithRealm { realm -> + invites.forEach { (roomId, _) -> + val myUserStateEvent = RoomMemberHelper(realm, roomId).getLastStateEvent(userId) + val inviterId = myUserStateEvent?.sender + val myUserRoomMember: RoomMemberContent? = myUserStateEvent?.let { it.asDomain().content?.toModel() } + val isDirect = myUserRoomMember?.isDirect + if (inviterId != null && inviterId != userId && isDirect == true) { + directChats + .getOrPut(inviterId, { arrayListOf() }) + .apply { + if (contains(roomId)) { + Timber.v("Direct chats already include room $roomId with user $inviterId") + } else { + add(roomId) + hasUpdate = true + } + } + } + } + } + if (hasUpdate) { + val updateUserAccountParams = UpdateUserAccountDataTask.DirectChatParams( + directMessages = directChats + ) + updateUserAccountDataTask.execute(updateUserAccountParams) + } + } + + private fun handlePushRules(realm: Realm, event: UserAccountDataEvent) { + val pushRules = event.content.toModel() ?: return + realm.where(PushRulesEntity::class.java) + .findAll() + .deleteAllFromRealm() + + // Save only global rules for the moment + val globalRules = pushRules.global + + val content = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.CONTENT } + globalRules.content?.forEach { rule -> + content.pushRules.add(PushRulesMapper.map(rule)) + } + realm.insertOrUpdate(content) + + val override = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.OVERRIDE } + globalRules.override?.forEach { rule -> + PushRulesMapper.map(rule).also { + override.pushRules.add(it) + } + } + realm.insertOrUpdate(override) + + val rooms = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.ROOM } + globalRules.room?.forEach { rule -> + rooms.pushRules.add(PushRulesMapper.map(rule)) + } + realm.insertOrUpdate(rooms) + + val senders = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.SENDER } + globalRules.sender?.forEach { rule -> + senders.pushRules.add(PushRulesMapper.map(rule)) + } + realm.insertOrUpdate(senders) + + val underrides = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.UNDERRIDE } + globalRules.underride?.forEach { rule -> + underrides.pushRules.add(PushRulesMapper.map(rule)) + } + realm.insertOrUpdate(underrides) + } + + private fun handleDirectChatRooms(realm: Realm, event: UserAccountDataEvent) { + val oldDirectRooms = RoomSummaryEntity.getDirectRooms(realm) + oldDirectRooms.forEach { + it.isDirect = false + it.directUserId = null + } + val content = event.content.toModel() ?: return + content.forEach { + val userId = it.key + it.value.forEach { roomId -> + val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() + if (roomSummaryEntity != null) { + roomSummaryEntity.isDirect = true + roomSummaryEntity.directUserId = userId + realm.insertOrUpdate(roomSummaryEntity) + } + } + } + } + + private fun handleIgnoredUsers(realm: Realm, event: UserAccountDataEvent) { + val userIds = event.content.toModel()?.ignoredUsers?.keys ?: return + realm.where(IgnoredUserEntity::class.java) + .findAll() + .deleteAllFromRealm() + // And save the new received list + userIds.forEach { realm.createObject(IgnoredUserEntity::class.java).apply { userId = it } } + // TODO If not initial sync, we should execute a init sync + } + + private fun handleBreadcrumbs(realm: Realm, event: UserAccountDataEvent) { + val recentRoomIds = event.content.toModel()?.recentRoomIds ?: return + val entity = BreadcrumbsEntity.getOrCreate(realm) + + // And save the new received list + entity.recentRoomIds = RealmList().apply { addAll(recentRoomIds) } + + // Update the room summaries + // Reset all the indexes... + RoomSummaryEntity.where(realm) + .greaterThan(RoomSummaryEntityFields.BREADCRUMBS_INDEX, RoomSummary.NOT_IN_BREADCRUMBS) + .findAll() + .forEach { + it.breadcrumbsIndex = RoomSummary.NOT_IN_BREADCRUMBS + } + + // ...and apply new indexes + recentRoomIds.forEachIndexed { index, roomId -> + RoomSummaryEntity.where(realm, roomId) + .findFirst() + ?.breadcrumbsIndex = index + } + } + + fun handleGenericAccountData(realm: Realm, type: String, content: Content?) { + val existing = realm.where() + .equalTo(UserAccountDataEntityFields.TYPE, type) + .findFirst() + if (existing != null) { + // Update current value + existing.contentStr = ContentMapper.map(content) + } else { + realm.createObject(UserAccountDataEntity::class.java).let { accountDataEntity -> + accountDataEntity.type = type + accountDataEntity.contentStr = ContentMapper.map(content) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncService.kt new file mode 100644 index 0000000000..20aa409336 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncService.kt @@ -0,0 +1,163 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.sync.job + +import android.app.Service +import android.content.Intent +import android.os.IBinder +import org.matrix.android.sdk.api.Matrix +import org.matrix.android.sdk.api.failure.isTokenError +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.sync.SyncState +import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker +import org.matrix.android.sdk.internal.session.sync.SyncTask +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.launch +import timber.log.Timber +import java.util.concurrent.atomic.AtomicBoolean + +/** + * Can execute periodic sync task. + * An IntentService is used in conjunction with the AlarmManager and a Broadcast Receiver + * in order to be able to perform a sync even if the app is not running. + * The and must be declared in the Manifest or the app using the SDK + */ +abstract class SyncService : Service() { + + private var sessionId: String? = null + private var mIsSelfDestroyed: Boolean = false + + private var isInitialSync: Boolean = false + private lateinit var session: Session + private lateinit var syncTask: SyncTask + private lateinit var networkConnectivityChecker: NetworkConnectivityChecker + private lateinit var taskExecutor: TaskExecutor + private lateinit var coroutineDispatchers: MatrixCoroutineDispatchers + private lateinit var backgroundDetectionObserver: BackgroundDetectionObserver + + private val isRunning = AtomicBoolean(false) + + private val serviceScope = CoroutineScope(SupervisorJob()) + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + Timber.i("onStartCommand $intent") + val isInit = initialize(intent) + if (isInit) { + onStart(isInitialSync) + doSyncIfNotAlreadyRunning() + } else { + // We should start and stop as we have to ensure to call Service.startForeground() + onStart(isInitialSync) + stopMe() + } + // No intent just start the service, an alarm will should call with intent + return START_STICKY + } + + override fun onDestroy() { + Timber.i("## onDestroy() : $this") + if (!mIsSelfDestroyed) { + Timber.w("## Destroy by the system : $this") + } + serviceScope.coroutineContext.cancelChildren() + isRunning.set(false) + super.onDestroy() + } + + private fun stopMe() { + mIsSelfDestroyed = true + stopSelf() + } + + private fun doSyncIfNotAlreadyRunning() { + if (isRunning.get()) { + Timber.i("Received a start while was already syncing... ignore") + } else { + isRunning.set(true) + serviceScope.launch(coroutineDispatchers.io) { + doSync() + } + } + } + + private suspend fun doSync() { + Timber.v("Execute sync request with timeout 0") + val params = SyncTask.Params(TIME_OUT) + try { + syncTask.execute(params) + // Start sync if we were doing an initial sync and the syncThread is not launched yet + if (isInitialSync && session.getSyncState() == SyncState.Idle) { + val isForeground = !backgroundDetectionObserver.isInBackground + session.startSync(isForeground) + } + stopMe() + } catch (throwable: Throwable) { + Timber.e(throwable) + if (throwable.isTokenError()) { + stopMe() + } else { + Timber.v("Should be rescheduled to avoid wasting resources") + sessionId?.also { + onRescheduleAsked(it, isInitialSync, delay = 10_000L) + } + stopMe() + } + } + } + + private fun initialize(intent: Intent?): Boolean { + if (intent == null) { + return false + } + val matrix = Matrix.getInstance(applicationContext) + val safeSessionId = intent.getStringExtra(EXTRA_SESSION_ID) ?: return false + try { + val sessionComponent = matrix.sessionManager.getSessionComponent(safeSessionId) + ?: throw IllegalStateException("You should have a session to make it work") + session = sessionComponent.session() + sessionId = safeSessionId + syncTask = sessionComponent.syncTask() + isInitialSync = !session.hasAlreadySynced() + networkConnectivityChecker = sessionComponent.networkConnectivityChecker() + taskExecutor = sessionComponent.taskExecutor() + coroutineDispatchers = sessionComponent.coroutineDispatchers() + backgroundDetectionObserver = matrix.backgroundDetectionObserver + return true + } catch (exception: Exception) { + Timber.e(exception, "An exception occurred during initialisation") + return false + } + } + + abstract fun onStart(isInitialSync: Boolean) + + abstract fun onRescheduleAsked(sessionId: String, isInitialSync: Boolean, delay: Long) + + override fun onBind(intent: Intent?): IBinder? { + return null + } + + companion object { + const val EXTRA_SESSION_ID = "EXTRA_SESSION_ID" + private const val TIME_OUT = 0L + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt new file mode 100644 index 0000000000..1a2d6b1fd3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt @@ -0,0 +1,220 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.sync.job + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.squareup.moshi.JsonEncodingException +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.isTokenError +import org.matrix.android.sdk.api.session.sync.SyncState +import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker +import org.matrix.android.sdk.internal.session.sync.SyncTask +import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker +import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver +import org.matrix.android.sdk.internal.util.Debouncer +import org.matrix.android.sdk.internal.util.createUIHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import timber.log.Timber +import java.net.SocketTimeoutException +import java.util.Timer +import java.util.TimerTask +import javax.inject.Inject +import kotlin.concurrent.schedule + +private const val RETRY_WAIT_TIME_MS = 10_000L +private const val DEFAULT_LONG_POOL_TIMEOUT = 30_000L + +internal class SyncThread @Inject constructor(private val syncTask: SyncTask, + private val typingUsersTracker: DefaultTypingUsersTracker, + private val networkConnectivityChecker: NetworkConnectivityChecker, + private val backgroundDetectionObserver: BackgroundDetectionObserver) + : Thread(), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener { + + private var state: SyncState = SyncState.Idle + private var liveState = MutableLiveData(state) + private val lock = Object() + private val syncScope = CoroutineScope(SupervisorJob()) + private val debouncer = Debouncer(createUIHandler()) + + private var canReachServer = true + private var isStarted = false + private var isTokenValid = true + private var retryNoNetworkTask: TimerTask? = null + + init { + updateStateTo(SyncState.Idle) + } + + fun setInitialForeground(initialForeground: Boolean) { + val newState = if (initialForeground) SyncState.Idle else SyncState.Paused + updateStateTo(newState) + } + + fun restart() = synchronized(lock) { + if (!isStarted) { + Timber.v("Resume sync...") + isStarted = true + // Check again server availability and the token validity + canReachServer = true + isTokenValid = true + lock.notify() + } + } + + fun pause() = synchronized(lock) { + if (isStarted) { + Timber.v("Pause sync...") + isStarted = false + retryNoNetworkTask?.cancel() + syncScope.coroutineContext.cancelChildren() + } + } + + fun kill() = synchronized(lock) { + Timber.v("Kill sync...") + updateStateTo(SyncState.Killing) + retryNoNetworkTask?.cancel() + syncScope.coroutineContext.cancelChildren() + lock.notify() + } + + fun currentState() = state + + fun liveState(): LiveData { + return liveState + } + + override fun onConnectivityChanged() { + retryNoNetworkTask?.cancel() + synchronized(lock) { + canReachServer = true + lock.notify() + } + } + + override fun run() { + Timber.v("Start syncing...") + isStarted = true + networkConnectivityChecker.register(this) + backgroundDetectionObserver.register(this) + while (state != SyncState.Killing) { + Timber.v("Entering loop, state: $state") + if (!isStarted) { + Timber.v("Sync is Paused. Waiting...") + updateStateTo(SyncState.Paused) + synchronized(lock) { lock.wait() } + Timber.v("...unlocked") + } else if (!canReachServer) { + Timber.v("No network. Waiting...") + updateStateTo(SyncState.NoNetwork) + // We force retrying in RETRY_WAIT_TIME_MS maximum. Otherwise it will be unlocked by onConnectivityChanged() or restart() + retryNoNetworkTask = Timer(SyncState.NoNetwork.toString(), false).schedule(RETRY_WAIT_TIME_MS) { + synchronized(lock) { + canReachServer = true + lock.notify() + } + } + synchronized(lock) { lock.wait() } + Timber.v("...retry") + } else if (!isTokenValid) { + Timber.v("Token is invalid. Waiting...") + updateStateTo(SyncState.InvalidToken) + synchronized(lock) { lock.wait() } + Timber.v("...unlocked") + } else { + if (state !is SyncState.Running) { + updateStateTo(SyncState.Running(afterPause = true)) + } + // No timeout after a pause + val timeout = state.let { if (it is SyncState.Running && it.afterPause) 0 else DEFAULT_LONG_POOL_TIMEOUT } + Timber.v("Execute sync request with timeout $timeout") + val params = SyncTask.Params(timeout) + val sync = syncScope.launch { + doSync(params) + } + runBlocking { + sync.join() + } + Timber.v("...Continue") + } + } + Timber.v("Sync killed") + updateStateTo(SyncState.Killed) + backgroundDetectionObserver.unregister(this) + networkConnectivityChecker.unregister(this) + } + + private suspend fun doSync(params: SyncTask.Params) { + try { + syncTask.execute(params) + } catch (failure: Throwable) { + if (failure is Failure.NetworkConnection) { + canReachServer = false + } + if (failure is Failure.NetworkConnection && failure.cause is SocketTimeoutException) { + // Timeout are not critical + Timber.v("Timeout") + } else if (failure is Failure.Cancelled) { + Timber.v("Cancelled") + } else if (failure.isTokenError()) { + // No token or invalid token, stop the thread + Timber.w(failure, "Token error") + isStarted = false + isTokenValid = false + } else { + Timber.e(failure) + if (failure !is Failure.NetworkConnection || failure.cause is JsonEncodingException) { + // Wait 10s before retrying + Timber.v("Wait 10s") + delay(RETRY_WAIT_TIME_MS) + } + } + } finally { + state.let { + if (it is SyncState.Running && it.afterPause) { + updateStateTo(SyncState.Running(afterPause = false)) + } + } + } + } + + private fun updateStateTo(newState: SyncState) { + Timber.v("Update state from $state to $newState") + if (newState == state) { + return + } + state = newState + debouncer.debounce("post_state", Runnable { + liveState.value = newState + }, 150) + } + + override fun onMoveToForeground() { + restart() + } + + override fun onMoveToBackground() { + pause() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt new file mode 100644 index 0000000000..e702de3573 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt @@ -0,0 +1,116 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.sync.job + +import android.content.Context +import androidx.work.BackoffPolicy +import androidx.work.CoroutineWorker +import androidx.work.ExistingWorkPolicy +import androidx.work.WorkerParameters +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.failure.isTokenError +import org.matrix.android.sdk.internal.di.WorkManagerProvider +import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker +import org.matrix.android.sdk.internal.session.sync.SyncTask +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.worker.SessionWorkerParams +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory +import org.matrix.android.sdk.internal.worker.getSessionComponent +import timber.log.Timber +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +private const val DEFAULT_LONG_POOL_TIMEOUT = 0L + +/** + * Possible previous worker: None + * Possible next worker : None + */ +internal class SyncWorker(context: Context, + workerParameters: WorkerParameters +) : CoroutineWorker(context, workerParameters) { + + @JsonClass(generateAdapter = true) + internal data class Params( + override val sessionId: String, + val timeout: Long = DEFAULT_LONG_POOL_TIMEOUT, + val automaticallyRetry: Boolean = false, + override val lastFailureMessage: String? = null + ) : SessionWorkerParams + + @Inject lateinit var syncTask: SyncTask + @Inject lateinit var taskExecutor: TaskExecutor + @Inject lateinit var networkConnectivityChecker: NetworkConnectivityChecker + + override suspend fun doWork(): Result { + Timber.i("Sync work starting") + val params = WorkerParamsFactory.fromData(inputData) + ?: return Result.success() + .also { Timber.e("Unable to parse work parameters") } + + val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() + sessionComponent.inject(this) + return runCatching { + doSync(params.timeout) + }.fold( + { Result.success() }, + { failure -> + if (failure.isTokenError() || !params.automaticallyRetry) { + Result.failure() + } else { + Result.retry() + } + } + ) + } + + private suspend fun doSync(timeout: Long) { + val taskParams = SyncTask.Params(timeout) + syncTask.execute(taskParams) + } + + companion object { + private const val BG_SYNC_WORK_NAME = "BG_SYNCP" + + fun requireBackgroundSync(workManagerProvider: WorkManagerProvider, sessionId: String, serverTimeout: Long = 0) { + val data = WorkerParamsFactory.toData(Params(sessionId, serverTimeout, false)) + val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder() + .setConstraints(WorkManagerProvider.workConstraints) + .setBackoffCriteria(BackoffPolicy.LINEAR, 1_000, TimeUnit.MILLISECONDS) + .setInputData(data) + .build() + workManagerProvider.workManager + .enqueueUniqueWork(BG_SYNC_WORK_NAME, ExistingWorkPolicy.REPLACE, workRequest) + } + + fun automaticallyBackgroundSync(workManagerProvider: WorkManagerProvider, sessionId: String, serverTimeout: Long = 0, delay: Long = 30_000) { + val data = WorkerParamsFactory.toData(Params(sessionId, serverTimeout, true)) + val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder() + .setConstraints(WorkManagerProvider.workConstraints) + .setInputData(data) + .setBackoffCriteria(BackoffPolicy.LINEAR, delay, TimeUnit.MILLISECONDS) + .build() + workManagerProvider.workManager + .enqueueUniqueWork(BG_SYNC_WORK_NAME, ExistingWorkPolicy.REPLACE, workRequest) + } + + fun stopAnyBackgroundSync(workManagerProvider: WorkManagerProvider) { + workManagerProvider.workManager + .cancelUniqueWork(BG_SYNC_WORK_NAME) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DeviceInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DeviceInfo.kt new file mode 100644 index 0000000000..226486596e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DeviceInfo.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.JsonClass + +/** + * This class describes the device information + */ +@JsonClass(generateAdapter = true) +internal data class DeviceInfo( + /** + * The owner user id + */ + val user_id: String? = null, + + /** + * The device id + */ + val device_id: String? = null, + + /** + * The device display name + */ + val display_name: String? = null, + + /** + * The last time this device has been seen. + */ + val last_seen_ts: Long = 0, + + /** + * The last ip address + */ + val last_seen_ip: String? = null + +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DeviceListResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DeviceListResponse.kt new file mode 100644 index 0000000000..df79060192 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DeviceListResponse.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.JsonClass + +/** + * This class describes the device list response from a sync request + */ +@JsonClass(generateAdapter = true) +internal data class DeviceListResponse( + // user ids list which have new crypto devices + val changed: List = emptyList(), + // List of user ids who are no more tracked. + val left: List = emptyList() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DeviceOneTimeKeysCountSyncResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DeviceOneTimeKeysCountSyncResponse.kt new file mode 100644 index 0000000000..36fe4acfa1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DeviceOneTimeKeysCountSyncResponse.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class DeviceOneTimeKeysCountSyncResponse( + @Json(name = "signed_curve25519") val signedCurve25519: Int? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DevicesListResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DevicesListResponse.kt new file mode 100644 index 0000000000..ec59e8f2c8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DevicesListResponse.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.JsonClass + +/** + * This class describes the + */ +@JsonClass(generateAdapter = true) +internal data class DevicesListResponse( + val devices: List? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/GroupSyncProfile.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/GroupSyncProfile.kt new file mode 100644 index 0000000000..5a7ed53cae --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/GroupSyncProfile.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class GroupSyncProfile( + /** + * The name of the group, if any. May be nil. + */ + @Json(name = "name") val name: String? = null, + + /** + * The URL for the group's avatar. May be nil. + */ + @Json(name = "avatar_url") val avatarUrl: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/GroupsSyncResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/GroupsSyncResponse.kt new file mode 100644 index 0000000000..68557d1d9a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/GroupsSyncResponse.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class GroupsSyncResponse( + /** + * Joined groups: An array of groups ids. + */ + @Json(name = "join") val join: Map = emptyMap(), + + /** + * Invitations. The groups that the user has been invited to: keys are groups ids. + */ + @Json(name = "invite") val invite: Map = emptyMap(), + + /** + * Left groups. An array of groups ids: the groups that the user has left or been banned from. + */ + @Json(name = "leave") val leave: Map = emptyMap() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/InvitedGroupSync.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/InvitedGroupSync.kt new file mode 100644 index 0000000000..cae6bc36a4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/InvitedGroupSync.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class InvitedGroupSync( + /** + * The identifier of the inviter. + */ + @Json(name = "inviter") val inviter: String? = null, + + /** + * The group profile. + */ + @Json(name = "profile") val profile: GroupSyncProfile? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/InvitedRoomSync.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/InvitedRoomSync.kt new file mode 100644 index 0000000000..efd1b50b3b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/InvitedRoomSync.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +// InvitedRoomSync represents a room invitation during server sync v2. +@JsonClass(generateAdapter = true) +internal data class InvitedRoomSync( + + /** + * The state of a room that the user has been invited to. These state events may only have the 'sender', 'type', 'state_key' + * and 'content' keys present. These events do not replace any state that the client already has for the room, for example if + * the client has archived the room. Instead the client should keep two separate copies of the state: the one from the 'invite_state' + * and one from the archived 'state'. If the client joins the room then the current state will be given as a delta against the + * archived 'state' not the 'invite_state'. + */ + @Json(name = "invite_state") val inviteState: RoomInviteState? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/PresenceSyncResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/PresenceSyncResponse.kt new file mode 100644 index 0000000000..2c6057d152 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/PresenceSyncResponse.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Event + +// PresenceSyncResponse represents the updates to the presence status of other users during server sync v2. +@JsonClass(generateAdapter = true) +internal data class PresenceSyncResponse( + + /** + * List of presence events (array of Event with type m.presence). + */ + val events: List? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomInviteState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomInviteState.kt new file mode 100644 index 0000000000..e37d4f58c7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomInviteState.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Event + +// RoomInviteState represents the state of a room that the user has been invited to. +@JsonClass(generateAdapter = true) +internal data class RoomInviteState( + + /** + * List of state events (array of MXEvent). + */ + @Json(name = "events") val events: List = emptyList() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomResponse.kt new file mode 100644 index 0000000000..df53eabd80 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomResponse.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Event + +/** + * Class representing a room from a JSON response from room or global initial sync. + */ +@JsonClass(generateAdapter = true) +internal data class RoomResponse( + // The room identifier. + val roomId: String? = null, + + // The last recent messages of the room. + val messages: TokensChunkResponse? = null, + + // The state events. + val state: List? = null, + + // The private data that this user has attached to this room. + val accountData: List? = null, + + // The current user membership in this room. + val membership: String? = null, + + // The room visibility (public/private). + val visibility: String? = null, + + // The matrix id of the inviter in case of pending invitation. + val inviter: String? = null, + + // The invite event if membership is invite. + val invite: Event? = null, + + // The presence status of other users + // (Provided in case of room initial sync @see http://matrix.org/docs/api/client-server/#!/-rooms/get_room_sync_data)). + val presence: List? = null, + + // The read receipts (Provided in case of room initial sync). + val receipts: List? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSync.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSync.kt new file mode 100644 index 0000000000..08556e800d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSync.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +// RoomSync represents the response for a room during server sync v2. +@JsonClass(generateAdapter = true) +internal data class RoomSync( + /** + * The state updates for the room. + */ + @Json(name = "state") val state: RoomSyncState? = null, + + /** + * The timeline of messages and state changes in the room. + */ + @Json(name = "timeline") val timeline: RoomSyncTimeline? = null, + + /** + * The ephemeral events in the room that aren't recorded in the timeline or state of the room (e.g. typing, receipts). + */ + @Json(name = "ephemeral") val ephemeral: RoomSyncEphemeral? = null, + + /** + * The account data events for the room (e.g. tags). + */ + @Json(name = "account_data") val accountData: RoomSyncAccountData? = null, + + /** + * The notification counts for the room. + */ + @Json(name = "unread_notifications") val unreadNotifications: RoomSyncUnreadNotifications? = null, + + /** + * The room summary + */ + @Json(name = "summary") val summary: RoomSyncSummary? = null + +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncAccountData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncAccountData.kt new file mode 100644 index 0000000000..13ea47a505 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncAccountData.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Event + +@JsonClass(generateAdapter = true) +internal data class RoomSyncAccountData( + /** + * List of account data events (array of Event). + */ + @Json(name = "events") val events: List = emptyList() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncEphemeral.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncEphemeral.kt new file mode 100644 index 0000000000..6d0e9e825f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncEphemeral.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Event + +// RoomSyncEphemeral represents the ephemeral events in the room that aren't recorded in the timeline or state of the room (e.g. typing). +@JsonClass(generateAdapter = true) +internal data class RoomSyncEphemeral( + /** + * List of ephemeral events (array of Event). + */ + @Json(name = "events") val events: List = emptyList() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncState.kt new file mode 100644 index 0000000000..f30e5a082d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncState.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Event + +// RoomSyncState represents the state updates for a room during server sync v2. +@JsonClass(generateAdapter = true) +internal data class RoomSyncState( + + /** + * List of state events (array of Event). The resulting state corresponds to the *start* of the timeline. + */ + @Json(name = "events") val events: List = emptyList() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncSummary.kt new file mode 100644 index 0000000000..a2dddb9e8c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncSummary.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class RoomSyncSummary( + + /** + * Present only if the room has no m.room.name or m.room.canonical_alias. + * + * + * Lists the mxids of the first 5 members in the room who are currently joined or invited (ordered by stream ordering as seen on the server, + * to avoid it jumping around if/when topological order changes). As the heroes’ membership status changes, the list changes appropriately + * (sending the whole new list in the next /sync response). This list always excludes the current logged in user. If there are no joined or + * invited users, it lists the parted and banned ones instead. Servers can choose to send more or less than 5 members if they must, but 5 + * seems like a good enough number for most naming purposes. Clients should use all the provided members to name the room, but may truncate + * the list if helpful for UX + */ + @Json(name = "m.heroes") val heroes: List = emptyList(), + + /** + * The number of m.room.members in state 'joined' (including the syncing user) (can be null) + */ + @Json(name = "m.joined_member_count") val joinedMembersCount: Int? = null, + + /** + * The number of m.room.members in state 'invited' (can be null) + */ + @Json(name = "m.invited_member_count") val invitedMembersCount: Int? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncTimeline.kt new file mode 100644 index 0000000000..29e5d2089b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncTimeline.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Event + +// RoomSyncTimeline represents the timeline of messages and state changes for a room during server sync v2. +@JsonClass(generateAdapter = true) +internal data class RoomSyncTimeline( + + /** + * List of events (array of Event). + */ + @Json(name = "events") val events: List = emptyList(), + + /** + * Boolean which tells whether there are more events on the server + */ + @Json(name = "limited") val limited: Boolean = false, + + /** + * If the batch was limited then this is a token that can be supplied to the server to retrieve more events + */ + @Json(name = "prev_batch") val prevToken: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncUnreadNotifications.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncUnreadNotifications.kt new file mode 100644 index 0000000000..bbcec474e2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncUnreadNotifications.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Event + +/** + * `MXRoomSyncUnreadNotifications` represents the unread counts for a room. + */ +@JsonClass(generateAdapter = true) +internal data class RoomSyncUnreadNotifications( + /** + * List of account data events (array of Event). + */ + @Json(name = "events") val events: List? = null, + + /** + * The number of unread messages that match the push notification rules. + */ + @Json(name = "notification_count") val notificationCount: Int? = null, + + /** + * The number of highlighted unread messages (subset of notifications). + */ + @Json(name = "highlight_count") val highlightCount: Int? = null) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomsSyncResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomsSyncResponse.kt new file mode 100644 index 0000000000..79000edf40 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomsSyncResponse.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +// RoomsSyncResponse represents the rooms list in server sync v2 response. +@JsonClass(generateAdapter = true) +internal data class RoomsSyncResponse( + /** + * Joined rooms: keys are rooms ids. + */ + @Json(name = "join") val join: Map = emptyMap(), + + /** + * Invitations. The rooms that the user has been invited to: keys are rooms ids. + */ + @Json(name = "invite") val invite: Map = emptyMap(), + + /** + * Left rooms. The rooms that the user has left or been banned from: keys are rooms ids. + */ + @Json(name = "leave") val leave: Map = emptyMap() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/SyncResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/SyncResponse.kt new file mode 100644 index 0000000000..e57c6cd1f8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/SyncResponse.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.session.sync.model.accountdata.UserAccountDataSync + +// SyncResponse represents the request response for server sync v2. +@JsonClass(generateAdapter = true) +internal data class SyncResponse( + /** + * The user private data. + */ + @Json(name = "account_data") val accountData: UserAccountDataSync? = null, + + /** + * The opaque token for the end. + */ + @Json(name = "next_batch") val nextBatch: String? = null, + + /** + * The updates to the presence status of other users. + */ + @Json(name = "presence") val presence: PresenceSyncResponse? = null, + + /* + * Data directly sent to one of user's devices. + */ + @Json(name = "to_device") val toDevice: ToDeviceSyncResponse? = null, + + /** + * List of rooms. + */ + @Json(name = "rooms") val rooms: RoomsSyncResponse? = null, + + /** + * Devices list update + */ + @Json(name = "device_lists") val deviceLists: DeviceListResponse? = null, + + /** + * One time keys management + */ + @Json(name = "device_one_time_keys_count") + val deviceOneTimeKeysCount: DeviceOneTimeKeysCountSyncResponse? = null, + + /** + * List of groups. + */ + @Json(name = "groups") val groups: GroupsSyncResponse? = null + +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/ToDeviceSyncResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/ToDeviceSyncResponse.kt new file mode 100644 index 0000000000..1bc9f0a3fa --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/ToDeviceSyncResponse.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Event + +// ToDeviceSyncResponse represents the data directly sent to one of user's devices. +@JsonClass(generateAdapter = true) +internal data class ToDeviceSyncResponse( + + /** + * List of direct-to-device events. + */ + val events: List? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/TokensChunkResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/TokensChunkResponse.kt new file mode 100644 index 0000000000..813c300ec9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/TokensChunkResponse.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.sync.model + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class TokensChunkResponse( + val start: String? = null, + val end: String? = null, + val chunk: List? = null) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/AcceptedTermsContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/AcceptedTermsContent.kt new file mode 100644 index 0000000000..57cd387243 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/AcceptedTermsContent.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.sync.model.accountdata + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class AcceptedTermsContent( + @Json(name = "accepted") val acceptedTerms: List = emptyList() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/BreadcrumbsContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/BreadcrumbsContent.kt new file mode 100644 index 0000000000..54aa5cb0b9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/BreadcrumbsContent.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.sync.model.accountdata + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class BreadcrumbsContent( + @Json(name = "recent_rooms") val recentRoomIds: List = emptyList() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/DirectMessagesContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/DirectMessagesContent.kt new file mode 100644 index 0000000000..fbaccf08c6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/DirectMessagesContent.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.sync.model.accountdata + +typealias DirectMessagesContent = Map> diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/IdentityServerContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/IdentityServerContent.kt new file mode 100644 index 0000000000..5328525c53 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/IdentityServerContent.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.sync.model.accountdata + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class IdentityServerContent( + @Json(name = "base_url") val baseUrl: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/IgnoredUsersContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/IgnoredUsersContent.kt new file mode 100644 index 0000000000..1095d2e76d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/IgnoredUsersContent.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.sync.model.accountdata + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.util.emptyJsonDict + +@JsonClass(generateAdapter = true) +internal data class IgnoredUsersContent( + /** + * Required. The map of users to ignore. UserId -> empty object for future enhancement + */ + @Json(name = "ignored_users") val ignoredUsers: Map +) { + + companion object { + fun createWithUserIds(userIds: List): IgnoredUsersContent { + return IgnoredUsersContent( + ignoredUsers = userIds.associateWith { emptyJsonDict } + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/UserAccountDataSync.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/UserAccountDataSync.kt new file mode 100644 index 0000000000..358f090bbd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/UserAccountDataSync.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.sync.model.accountdata + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent + +@JsonClass(generateAdapter = true) +internal data class UserAccountDataSync( + @Json(name = "events") val list: List = emptyList() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/AcceptTermsBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/AcceptTermsBody.kt new file mode 100644 index 0000000000..497d30fdca --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/AcceptTermsBody.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.terms + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This class represent a list of urls of terms the user wants to accept + */ +@JsonClass(generateAdapter = true) +internal data class AcceptTermsBody( + @Json(name = "user_accepts") + val acceptedTermUrls: List +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt new file mode 100644 index 0000000000..f887754b6b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.terms + +import dagger.Lazy +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.terms.GetTermsResponse +import org.matrix.android.sdk.api.session.terms.TermsService +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificate +import org.matrix.android.sdk.internal.network.NetworkConstants +import org.matrix.android.sdk.internal.network.RetrofitFactory +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.identity.IdentityAuthAPI +import org.matrix.android.sdk.internal.session.identity.IdentityRegisterTask +import org.matrix.android.sdk.internal.session.openid.GetOpenIdTokenTask +import org.matrix.android.sdk.internal.session.sync.model.accountdata.AcceptedTermsContent +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes +import org.matrix.android.sdk.internal.session.user.accountdata.AccountDataDataSource +import org.matrix.android.sdk.internal.session.user.accountdata.UpdateUserAccountDataTask +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.launchToCallback +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import org.matrix.android.sdk.internal.util.ensureTrailingSlash +import okhttp3.OkHttpClient +import javax.inject.Inject + +internal class DefaultTermsService @Inject constructor( + @UnauthenticatedWithCertificate + private val unauthenticatedOkHttpClient: Lazy, + private val accountDataDataSource: AccountDataDataSource, + private val termsAPI: TermsAPI, + private val retrofitFactory: RetrofitFactory, + private val getOpenIdTokenTask: GetOpenIdTokenTask, + private val identityRegisterTask: IdentityRegisterTask, + private val updateUserAccountDataTask: UpdateUserAccountDataTask, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val taskExecutor: TaskExecutor +) : TermsService { + override fun getTerms(serviceType: TermsService.ServiceType, + baseUrl: String, + callback: MatrixCallback): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + val url = buildUrl(baseUrl, serviceType) + val termsResponse = executeRequest(null) { + apiCall = termsAPI.getTerms("${url}terms") + } + GetTermsResponse(termsResponse, getAlreadyAcceptedTermUrlsFromAccountData()) + } + } + + override fun agreeToTerms(serviceType: TermsService.ServiceType, + baseUrl: String, + agreedUrls: List, + token: String?, + callback: MatrixCallback): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + val url = buildUrl(baseUrl, serviceType) + val tokenToUse = token?.takeIf { it.isNotEmpty() } ?: getToken(baseUrl) + + executeRequest(null) { + apiCall = termsAPI.agreeToTerms("${url}terms", AcceptTermsBody(agreedUrls), "Bearer $tokenToUse") + } + + // client SHOULD update this account data section adding any the URLs + // of any additional documents that the user agreed to this list. + // Get current m.accepted_terms append new ones and update account data + val listOfAcceptedTerms = getAlreadyAcceptedTermUrlsFromAccountData() + + val newList = listOfAcceptedTerms.toMutableSet().apply { addAll(agreedUrls) }.toList() + + updateUserAccountDataTask.execute(UpdateUserAccountDataTask.AcceptedTermsParams( + acceptedTermsContent = AcceptedTermsContent(newList) + )) + } + } + + private suspend fun getToken(url: String): String { + // TODO This is duplicated code see DefaultIdentityService + val api = retrofitFactory.create(unauthenticatedOkHttpClient, url).create(IdentityAuthAPI::class.java) + + val openIdToken = getOpenIdTokenTask.execute(Unit) + val token = identityRegisterTask.execute(IdentityRegisterTask.Params(api, openIdToken)) + + return token.token + } + + private fun buildUrl(baseUrl: String, serviceType: TermsService.ServiceType): String { + val servicePath = when (serviceType) { + TermsService.ServiceType.IntegrationManager -> NetworkConstants.URI_INTEGRATION_MANAGER_PATH + TermsService.ServiceType.IdentityService -> NetworkConstants.URI_IDENTITY_PATH_V2 + } + return "${baseUrl.ensureTrailingSlash()}$servicePath" + } + + private fun getAlreadyAcceptedTermUrlsFromAccountData(): Set { + return accountDataDataSource.getAccountDataEvent(UserAccountDataTypes.TYPE_ACCEPTED_TERMS) + ?.content + ?.toModel() + ?.acceptedTerms + ?.toSet() + .orEmpty() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/TermsAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/TermsAPI.kt new file mode 100644 index 0000000000..950c0a151b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/TermsAPI.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.terms + +import org.matrix.android.sdk.internal.network.HttpHeaders +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.POST +import retrofit2.http.Url + +internal interface TermsAPI { + /** + * This request does not require authentication + */ + @GET + fun getTerms(@Url url: String): Call + + /** + * This request requires authentication + */ + @POST + fun agreeToTerms(@Url url: String, + @Body params: AcceptTermsBody, + @Header(HttpHeaders.Authorization) token: String): Call +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/TermsModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/TermsModule.kt new file mode 100644 index 0000000000..7aa97cd1cc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/TermsModule.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.terms + +import dagger.Binds +import dagger.Lazy +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.api.session.terms.TermsService +import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificate +import org.matrix.android.sdk.internal.network.RetrofitFactory +import org.matrix.android.sdk.internal.session.SessionScope +import okhttp3.OkHttpClient + +@Module +internal abstract class TermsModule { + + @Module + companion object { + @Provides + @JvmStatic + @SessionScope + fun providesTermsAPI(@UnauthenticatedWithCertificate unauthenticatedOkHttpClient: Lazy, + retrofitFactory: RetrofitFactory): TermsAPI { + val retrofit = retrofitFactory.create(unauthenticatedOkHttpClient, "https://foo.bar") + return retrofit.create(TermsAPI::class.java) + } + } + + @Binds + abstract fun bindTermsService(service: DefaultTermsService): TermsService +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/TermsResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/TermsResponse.kt new file mode 100644 index 0000000000..240291c09f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/TermsResponse.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.terms + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.auth.registration.LocalizedFlowDataLoginTerms + +/** + * This class represent a localized privacy policy for registration Flow. + */ +@JsonClass(generateAdapter = true) +data class TermsResponse( + @Json(name = "policies") + val policies: JsonDict? = null +) { + + fun getLocalizedTerms(userLanguage: String, + defaultLanguage: String = "en"): List { + return policies?.map { + val tos = policies[it.key] as? Map<*, *> ?: return@map null + ((tos[userLanguage] ?: tos[defaultLanguage]) as? Map<*, *>)?.let { termsMap -> + val name = termsMap[NAME] as? String + val url = termsMap[URL] as? String + LocalizedFlowDataLoginTerms( + policyName = it.key, + localizedUrl = url, + localizedName = name, + version = tos[VERSION] as? String + ) + } + }?.filterNotNull().orEmpty() + } + + private companion object { + const val VERSION = "version" + const val NAME = "name" + const val URL = "url" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/typing/DefaultTypingUsersTracker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/typing/DefaultTypingUsersTracker.kt new file mode 100644 index 0000000000..0fa557467c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/typing/DefaultTypingUsersTracker.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.typing + +import org.matrix.android.sdk.api.session.room.sender.SenderInfo +import org.matrix.android.sdk.api.session.typing.TypingUsersTracker +import org.matrix.android.sdk.internal.session.SessionScope +import javax.inject.Inject + +@SessionScope +internal class DefaultTypingUsersTracker @Inject constructor() : TypingUsersTracker { + + private val typingUsers = mutableMapOf>() + + /** + * Set all currently typing users for a room (excluding yourself) + */ + fun setTypingUsersFromRoom(roomId: String, senderInfoList: List) { + val hasNewValue = typingUsers[roomId] != senderInfoList + if (hasNewValue) { + typingUsers[roomId] = senderInfoList + } + } + + override fun getTypingUsers(roomId: String): List { + return typingUsers[roomId] ?: emptyList() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/DefaultUserService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/DefaultUserService.kt new file mode 100644 index 0000000000..e79893e752 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/DefaultUserService.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.user + +import androidx.lifecycle.LiveData +import androidx.paging.PagedList +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.user.UserService +import org.matrix.android.sdk.api.session.user.model.User +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.internal.session.user.accountdata.UpdateIgnoredUserIdsTask +import org.matrix.android.sdk.internal.session.user.model.SearchUserTask +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import javax.inject.Inject + +internal class DefaultUserService @Inject constructor(private val userDataSource: UserDataSource, + private val searchUserTask: SearchUserTask, + private val updateIgnoredUserIdsTask: UpdateIgnoredUserIdsTask, + private val taskExecutor: TaskExecutor) : UserService { + + override fun getUser(userId: String): User? { + return userDataSource.getUser(userId) + } + + override fun getUserLive(userId: String): LiveData> { + return userDataSource.getUserLive(userId) + } + + override fun getUsersLive(): LiveData> { + return userDataSource.getUsersLive() + } + + override fun getPagedUsersLive(filter: String?, excludedUserIds: Set?): LiveData> { + return userDataSource.getPagedUsersLive(filter, excludedUserIds) + } + + override fun getIgnoredUsersLive(): LiveData> { + return userDataSource.getIgnoredUsersLive() + } + + override fun searchUsersDirectory(search: String, + limit: Int, + excludedUserIds: Set, + callback: MatrixCallback>): Cancelable { + val params = SearchUserTask.Params(limit, search, excludedUserIds) + return searchUserTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun ignoreUserIds(userIds: List, callback: MatrixCallback): Cancelable { + val params = UpdateIgnoredUserIdsTask.Params(userIdsToIgnore = userIds.toList()) + return updateIgnoredUserIdsTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun unIgnoreUserIds(userIds: List, callback: MatrixCallback): Cancelable { + val params = UpdateIgnoredUserIdsTask.Params(userIdsToUnIgnore = userIds.toList()) + return updateIgnoredUserIdsTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/SearchUserAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/SearchUserAPI.kt new file mode 100644 index 0000000000..2b9a5f4aa4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/SearchUserAPI.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.user + +import org.matrix.android.sdk.internal.network.NetworkConstants +import org.matrix.android.sdk.internal.session.user.model.SearchUsersParams +import org.matrix.android.sdk.internal.session.user.model.SearchUsersResponse +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.POST + +internal interface SearchUserAPI { + + /** + * Perform a user search. + * + * @param searchUsersParams the search params. + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user_directory/search") + fun searchUsers(@Body searchUsersParams: SearchUsersParams): Call +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserDataSource.kt new file mode 100644 index 0000000000..dd3c6856c0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserDataSource.kt @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.user + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import androidx.paging.DataSource +import androidx.paging.LivePagedListBuilder +import androidx.paging.PagedList +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.user.model.User +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.toOptional +import org.matrix.android.sdk.internal.database.mapper.asDomain +import org.matrix.android.sdk.internal.database.model.IgnoredUserEntity +import org.matrix.android.sdk.internal.database.model.IgnoredUserEntityFields +import org.matrix.android.sdk.internal.database.model.UserEntity +import org.matrix.android.sdk.internal.database.model.UserEntityFields +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.util.fetchCopied +import io.realm.Case +import javax.inject.Inject + +internal class UserDataSource @Inject constructor(@SessionDatabase private val monarchy: Monarchy) { + + private val realmDataSourceFactory: Monarchy.RealmDataSourceFactory by lazy { + monarchy.createDataSourceFactory { realm -> + realm.where(UserEntity::class.java) + .isNotEmpty(UserEntityFields.USER_ID) + .sort(UserEntityFields.DISPLAY_NAME) + } + } + + private val domainDataSourceFactory: DataSource.Factory by lazy { + realmDataSourceFactory.map { + it.asDomain() + } + } + + private val livePagedListBuilder: LivePagedListBuilder by lazy { + LivePagedListBuilder(domainDataSourceFactory, PagedList.Config.Builder().setPageSize(100).setEnablePlaceholders(false).build()) + } + + fun getUser(userId: String): User? { + val userEntity = monarchy.fetchCopied { UserEntity.where(it, userId).findFirst() } + ?: return null + + return userEntity.asDomain() + } + + fun getUserLive(userId: String): LiveData> { + val liveData = monarchy.findAllMappedWithChanges( + { UserEntity.where(it, userId) }, + { it.asDomain() } + ) + return Transformations.map(liveData) { results -> + results.firstOrNull().toOptional() + } + } + + fun getUsersLive(): LiveData> { + return monarchy.findAllMappedWithChanges( + { realm -> + realm.where(UserEntity::class.java) + .isNotEmpty(UserEntityFields.USER_ID) + .sort(UserEntityFields.DISPLAY_NAME) + }, + { it.asDomain() } + ) + } + + fun getPagedUsersLive(filter: String?, excludedUserIds: Set?): LiveData> { + realmDataSourceFactory.updateQuery { realm -> + val query = realm.where(UserEntity::class.java) + if (filter.isNullOrEmpty()) { + query.isNotEmpty(UserEntityFields.USER_ID) + } else { + query + .beginGroup() + .contains(UserEntityFields.DISPLAY_NAME, filter, Case.INSENSITIVE) + .or() + .contains(UserEntityFields.USER_ID, filter) + .endGroup() + } + excludedUserIds + ?.takeIf { it.isNotEmpty() } + ?.let { + query.not().`in`(UserEntityFields.USER_ID, it.toTypedArray()) + } + query.sort(UserEntityFields.DISPLAY_NAME) + } + return monarchy.findAllPagedWithChanges(realmDataSourceFactory, livePagedListBuilder) + } + + fun getIgnoredUsersLive(): LiveData> { + return monarchy.findAllMappedWithChanges( + { realm -> + realm.where(IgnoredUserEntity::class.java) + .isNotEmpty(IgnoredUserEntityFields.USER_ID) + .sort(IgnoredUserEntityFields.USER_ID) + }, + { getUser(it.userId) ?: User(userId = it.userId) } + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserEntityFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserEntityFactory.kt new file mode 100644 index 0000000000..6333a87a0b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserEntityFactory.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.user + +import org.matrix.android.sdk.api.session.room.model.RoomMemberContent +import org.matrix.android.sdk.internal.database.model.UserEntity + +internal object UserEntityFactory { + + fun create(userId: String, roomMember: RoomMemberContent): UserEntity { + return UserEntity( + userId = userId, + displayName = roomMember.displayName ?: "", + avatarUrl = roomMember.avatarUrl ?: "" + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserModule.kt new file mode 100644 index 0000000000..51e4339061 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserModule.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.user + +import dagger.Binds +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.api.session.user.UserService +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.user.accountdata.DefaultSaveIgnoredUsersTask +import org.matrix.android.sdk.internal.session.user.accountdata.DefaultUpdateIgnoredUserIdsTask +import org.matrix.android.sdk.internal.session.user.accountdata.SaveIgnoredUsersTask +import org.matrix.android.sdk.internal.session.user.accountdata.UpdateIgnoredUserIdsTask +import org.matrix.android.sdk.internal.session.user.model.DefaultSearchUserTask +import org.matrix.android.sdk.internal.session.user.model.SearchUserTask +import retrofit2.Retrofit + +@Module +internal abstract class UserModule { + + @Module + companion object { + @Provides + @JvmStatic + @SessionScope + fun providesSearchUserAPI(retrofit: Retrofit): SearchUserAPI { + return retrofit.create(SearchUserAPI::class.java) + } + } + + @Binds + abstract fun bindUserService(service: DefaultUserService): UserService + + @Binds + abstract fun bindSearchUserTask(task: DefaultSearchUserTask): SearchUserTask + + @Binds + abstract fun bindSaveIgnoredUsersTask(task: DefaultSaveIgnoredUsersTask): SaveIgnoredUsersTask + + @Binds + abstract fun bindUpdateIgnoredUserIdsTask(task: DefaultUpdateIgnoredUserIdsTask): UpdateIgnoredUserIdsTask + + @Binds + abstract fun bindUserStore(store: RealmUserStore): UserStore +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserStore.kt new file mode 100644 index 0000000000..ea64cb9a2c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserStore.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.user + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.internal.database.model.UserEntity +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.util.awaitTransaction +import javax.inject.Inject + +internal interface UserStore { + suspend fun createOrUpdate(userId: String, displayName: String? = null, avatarUrl: String? = null) +} + +internal class RealmUserStore @Inject constructor(@SessionDatabase private val monarchy: Monarchy) : UserStore { + + override suspend fun createOrUpdate(userId: String, displayName: String?, avatarUrl: String?) { + monarchy.awaitTransaction { + val userEntity = UserEntity(userId, displayName ?: "", avatarUrl ?: "") + it.insertOrUpdate(userEntity) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataAPI.kt new file mode 100644 index 0000000000..25336cacf7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataAPI.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.user.accountdata + +import org.matrix.android.sdk.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.PUT +import retrofit2.http.Path + +interface AccountDataAPI { + + /** + * Set some account_data for the client. + * + * @param userId the user id + * @param type the type + * @param params the put params + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/account_data/{type}") + fun setAccountData(@Path("userId") userId: String, + @Path("type") type: String, + @Body params: Any): Call +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataContent.kt new file mode 100644 index 0000000000..5384a1ba9c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataContent.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.user.accountdata + +/** + * Tag class to identify every account data content + */ +internal interface AccountDataContent diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataDataSource.kt new file mode 100644 index 0000000000..d54bfdd63d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataDataSource.kt @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.user.accountdata + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.toOptional +import org.matrix.android.sdk.internal.database.mapper.AccountDataMapper +import org.matrix.android.sdk.internal.database.model.UserAccountDataEntity +import org.matrix.android.sdk.internal.database.model.UserAccountDataEntityFields +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent +import io.realm.Realm +import io.realm.RealmQuery +import javax.inject.Inject + +internal class AccountDataDataSource @Inject constructor(@SessionDatabase private val monarchy: Monarchy, + private val accountDataMapper: AccountDataMapper) { + + fun getAccountDataEvent(type: String): UserAccountDataEvent? { + return getAccountDataEvents(setOf(type)).firstOrNull() + } + + fun getLiveAccountDataEvent(type: String): LiveData> { + return Transformations.map(getLiveAccountDataEvents(setOf(type))) { + it.firstOrNull()?.toOptional() + } + } + + fun getAccountDataEvents(types: Set): List { + return monarchy.fetchAllMappedSync( + { accountDataEventsQuery(it, types) }, + accountDataMapper::map + ) + } + + fun getLiveAccountDataEvents(types: Set): LiveData> { + return monarchy.findAllMappedWithChanges( + { accountDataEventsQuery(it, types) }, + accountDataMapper::map + ) + } + + private fun accountDataEventsQuery(realm: Realm, types: Set): RealmQuery { + val query = realm.where(UserAccountDataEntity::class.java) + if (types.isNotEmpty()) { + query.`in`(UserAccountDataEntityFields.TYPE, types.toTypedArray()) + } + return query + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataModule.kt new file mode 100644 index 0000000000..291d0bfaf7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataModule.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.user.accountdata + +import dagger.Binds +import dagger.Module +import dagger.Provides +import retrofit2.Retrofit + +@Module +internal abstract class AccountDataModule { + + @Module + companion object { + + @JvmStatic + @Provides + fun providesAccountDataAPI(retrofit: Retrofit): AccountDataAPI { + return retrofit.create(AccountDataAPI::class.java) + } + } + + @Binds + abstract fun bindUpdateUserAccountDataTask(task: DefaultUpdateUserAccountDataTask): UpdateUserAccountDataTask + + @Binds + abstract fun bindSaveBreadcrumbsTask(task: DefaultSaveBreadcrumbsTask): SaveBreadcrumbsTask + + @Binds + abstract fun bindUpdateBreadcrumbsTask(task: DefaultUpdateBreadcrumbsTask): UpdateBreadcrumbsTask +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultAccountDataService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultAccountDataService.kt new file mode 100644 index 0000000000..2bf1705855 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultAccountDataService.kt @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.user.accountdata + +import androidx.lifecycle.LiveData +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.accountdata.AccountDataService +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.sync.UserAccountDataSyncHandler +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith +import javax.inject.Inject + +internal class DefaultAccountDataService @Inject constructor( + @SessionDatabase private val monarchy: Monarchy, + private val updateUserAccountDataTask: UpdateUserAccountDataTask, + private val userAccountDataSyncHandler: UserAccountDataSyncHandler, + private val accountDataDataSource: AccountDataDataSource, + private val taskExecutor: TaskExecutor +) : AccountDataService { + + override fun getAccountDataEvent(type: String): UserAccountDataEvent? { + return accountDataDataSource.getAccountDataEvent(type) + } + + override fun getLiveAccountDataEvent(type: String): LiveData> { + return accountDataDataSource.getLiveAccountDataEvent(type) + } + + override fun getAccountDataEvents(types: Set): List { + return accountDataDataSource.getAccountDataEvents(types) + } + + override fun getLiveAccountDataEvents(types: Set): LiveData> { + return accountDataDataSource.getLiveAccountDataEvents(types) + } + + override fun updateAccountData(type: String, content: Content, callback: MatrixCallback?): Cancelable { + return updateUserAccountDataTask.configureWith(UpdateUserAccountDataTask.AnyParams( + type = type, + any = content + )) { + this.retryCount = 5 + this.callback = object : MatrixCallback { + override fun onSuccess(data: Unit) { + // TODO Move that to the task (but it created a circular dependencies...) + monarchy.runTransactionSync { realm -> + userAccountDataSyncHandler.handleGenericAccountData(realm, type, content) + } + callback?.onSuccess(data) + } + + override fun onFailure(failure: Throwable) { + callback?.onFailure(failure) + } + } + } + .executeBy(taskExecutor) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DirectChatsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DirectChatsHelper.kt new file mode 100644 index 0000000000..8bec45a203 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DirectChatsHelper.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.user.accountdata + +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.query.getDirectRooms +import org.matrix.android.sdk.internal.di.SessionDatabase +import io.realm.Realm +import io.realm.RealmConfiguration +import javax.inject.Inject + +internal class DirectChatsHelper @Inject constructor(@SessionDatabase + private val realmConfiguration: RealmConfiguration) { + + /** + * @return a map of userId <-> list of roomId + */ + fun getLocalUserAccount(filterRoomId: String? = null): MutableMap> { + return Realm.getInstance(realmConfiguration).use { realm -> + RoomSummaryEntity.getDirectRooms(realm) + .asSequence() + .filter { it.roomId != filterRoomId && it.directUserId != null } + .groupByTo(mutableMapOf(), { it.directUserId!! }, { it.roomId }) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/SaveBreadcrumbsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/SaveBreadcrumbsTask.kt new file mode 100644 index 0000000000..6ef28954e0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/SaveBreadcrumbsTask.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.user.accountdata + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.internal.database.model.BreadcrumbsEntity +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields +import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import io.realm.RealmList +import javax.inject.Inject + +/** + * Save the Breadcrumbs roomId list in DB, either from the sync, or updated locally + */ +internal interface SaveBreadcrumbsTask : Task { + data class Params( + val recentRoomIds: List + ) +} + +internal class DefaultSaveBreadcrumbsTask @Inject constructor( + @SessionDatabase private val monarchy: Monarchy +) : SaveBreadcrumbsTask { + + override suspend fun execute(params: SaveBreadcrumbsTask.Params) { + monarchy.awaitTransaction { realm -> + // Get or create a breadcrumbs entity + val entity = BreadcrumbsEntity.getOrCreate(realm) + + // And save the new received list + entity.recentRoomIds = RealmList().apply { addAll(params.recentRoomIds) } + + // Update the room summaries + // Reset all the indexes... + RoomSummaryEntity.where(realm) + .greaterThan(RoomSummaryEntityFields.BREADCRUMBS_INDEX, RoomSummary.NOT_IN_BREADCRUMBS) + .findAll() + .forEach { + it.breadcrumbsIndex = RoomSummary.NOT_IN_BREADCRUMBS + } + + // ...and apply new indexes + params.recentRoomIds.forEachIndexed { index, roomId -> + RoomSummaryEntity.where(realm, roomId) + .findFirst() + ?.breadcrumbsIndex = index + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/SaveIgnoredUsersTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/SaveIgnoredUsersTask.kt new file mode 100644 index 0000000000..9141aabc80 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/SaveIgnoredUsersTask.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.user.accountdata + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.internal.database.model.IgnoredUserEntity +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import javax.inject.Inject + +/** + * Save the ignored users list in DB + */ +internal interface SaveIgnoredUsersTask : Task { + data class Params( + val userIds: List + ) +} + +internal class DefaultSaveIgnoredUsersTask @Inject constructor(@SessionDatabase private val monarchy: Monarchy) : SaveIgnoredUsersTask { + + override suspend fun execute(params: SaveIgnoredUsersTask.Params) { + monarchy.awaitTransaction { realm -> + // clear current ignored users + realm.where(IgnoredUserEntity::class.java) + .findAll() + .deleteAllFromRealm() + + // And save the new received list + params.userIds.forEach { realm.createObject(IgnoredUserEntity::class.java).apply { userId = it } } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UpdateBreadcrumbsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UpdateBreadcrumbsTask.kt new file mode 100644 index 0000000000..ab602dd603 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UpdateBreadcrumbsTask.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.user.accountdata + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.internal.database.model.BreadcrumbsEntity +import org.matrix.android.sdk.internal.database.query.get +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.sync.model.accountdata.BreadcrumbsContent +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.fetchCopied +import javax.inject.Inject + +// Use the same arbitrary value than Riot-Web +private const val MAX_BREADCRUMBS_ROOMS_NUMBER = 20 + +internal interface UpdateBreadcrumbsTask : Task { + data class Params( + val newTopRoomId: String + ) +} + +internal class DefaultUpdateBreadcrumbsTask @Inject constructor( + private val saveBreadcrumbsTask: SaveBreadcrumbsTask, + private val updateUserAccountDataTask: UpdateUserAccountDataTask, + @SessionDatabase private val monarchy: Monarchy +) : UpdateBreadcrumbsTask { + + override suspend fun execute(params: UpdateBreadcrumbsTask.Params) { + val newBreadcrumbs = + // Get the breadcrumbs entity, if any + monarchy.fetchCopied { BreadcrumbsEntity.get(it) } + ?.recentRoomIds + ?.apply { + // Modify the list to add the newTopRoomId first + // Ensure the newTopRoomId is not already in the list + remove(params.newTopRoomId) + // Add the newTopRoomId at first position + add(0, params.newTopRoomId) + } + ?.take(MAX_BREADCRUMBS_ROOMS_NUMBER) + ?: listOf(params.newTopRoomId) + + // Update the DB locally, do not wait for the sync + saveBreadcrumbsTask.execute(SaveBreadcrumbsTask.Params(newBreadcrumbs)) + + // FIXME It can remove the previous breadcrumbs, if not synced yet + // And update account data + updateUserAccountDataTask.execute(UpdateUserAccountDataTask.BreadcrumbsParams( + breadcrumbsContent = BreadcrumbsContent(newBreadcrumbs) + )) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UpdateIgnoredUserIdsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UpdateIgnoredUserIdsTask.kt new file mode 100644 index 0000000000..b47a25187b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UpdateIgnoredUserIdsTask.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.user.accountdata + +import com.zhuinden.monarchy.Monarchy +import org.greenrobot.eventbus.EventBus +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes +import org.matrix.android.sdk.internal.database.model.IgnoredUserEntity +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.sync.model.accountdata.IgnoredUsersContent +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal interface UpdateIgnoredUserIdsTask : Task { + + data class Params( + val userIdsToIgnore: List = emptyList(), + val userIdsToUnIgnore: List = emptyList() + ) +} + +internal class DefaultUpdateIgnoredUserIdsTask @Inject constructor( + private val accountDataApi: AccountDataAPI, + @SessionDatabase private val monarchy: Monarchy, + private val saveIgnoredUsersTask: SaveIgnoredUsersTask, + @UserId private val userId: String, + private val eventBus: EventBus +) : UpdateIgnoredUserIdsTask { + + override suspend fun execute(params: UpdateIgnoredUserIdsTask.Params) { + // Get current list + val ignoredUserIds = monarchy.fetchAllMappedSync( + { realm -> realm.where(IgnoredUserEntity::class.java) }, + { it.userId } + ).toMutableSet() + + val original = ignoredUserIds.toSet() + + ignoredUserIds.removeAll { it in params.userIdsToUnIgnore } + ignoredUserIds.addAll(params.userIdsToIgnore) + + if (original == ignoredUserIds) { + // No change + return + } + + val list = ignoredUserIds.toList() + val body = IgnoredUsersContent.createWithUserIds(list) + + executeRequest(eventBus) { + apiCall = accountDataApi.setAccountData(userId, UserAccountDataTypes.TYPE_IGNORED_USER_LIST, body) + } + + // Update the DB right now (do not wait for the sync to come back with updated data, for a faster UI update) + saveIgnoredUsersTask.execute(SaveIgnoredUsersTask.Params(list)) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UpdateUserAccountDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UpdateUserAccountDataTask.kt new file mode 100644 index 0000000000..a68d76f25b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UpdateUserAccountDataTask.kt @@ -0,0 +1,112 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.user.accountdata + +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.integrationmanager.AllowedWidgetsContent +import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationProvisioningContent +import org.matrix.android.sdk.internal.session.sync.model.accountdata.AcceptedTermsContent +import org.matrix.android.sdk.internal.session.sync.model.accountdata.BreadcrumbsContent +import org.matrix.android.sdk.internal.session.sync.model.accountdata.IdentityServerContent +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface UpdateUserAccountDataTask : Task { + + interface Params { + val type: String + fun getData(): Any + } + + data class IdentityParams(override val type: String = UserAccountDataTypes.TYPE_IDENTITY_SERVER, + private val identityContent: IdentityServerContent + ) : Params { + + override fun getData(): Any { + return identityContent + } + } + + data class AcceptedTermsParams(override val type: String = UserAccountDataTypes.TYPE_ACCEPTED_TERMS, + private val acceptedTermsContent: AcceptedTermsContent + ) : Params { + + override fun getData(): Any { + return acceptedTermsContent + } + } + + // TODO Use [UserAccountDataDirectMessages] class? + data class DirectChatParams(override val type: String = UserAccountDataTypes.TYPE_DIRECT_MESSAGES, + private val directMessages: Map> + ) : Params { + + override fun getData(): Any { + return directMessages + } + } + + data class BreadcrumbsParams(override val type: String = UserAccountDataTypes.TYPE_BREADCRUMBS, + private val breadcrumbsContent: BreadcrumbsContent + ) : Params { + + override fun getData(): Any { + return breadcrumbsContent + } + } + + data class AllowedWidgets(override val type: String = UserAccountDataTypes.TYPE_ALLOWED_WIDGETS, + private val allowedWidgetsContent: AllowedWidgetsContent) : Params { + + override fun getData(): Any { + return allowedWidgetsContent + } + } + + data class IntegrationProvisioning(override val type: String = UserAccountDataTypes.TYPE_INTEGRATION_PROVISIONING, + private val integrationProvisioningContent: IntegrationProvisioningContent) : Params { + + override fun getData(): Any { + return integrationProvisioningContent + } + } + + data class AnyParams(override val type: String, + private val any: Any + ) : Params { + override fun getData(): Any { + return any + } + } +} + +internal class DefaultUpdateUserAccountDataTask @Inject constructor( + private val accountDataApi: AccountDataAPI, + @UserId private val userId: String, + private val eventBus: EventBus +) : UpdateUserAccountDataTask { + + override suspend fun execute(params: UpdateUserAccountDataTask.Params) { + return executeRequest(eventBus) { + apiCall = accountDataApi.setAccountData(userId, params.type, params.getData()) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/model/SearchUser.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/model/SearchUser.kt new file mode 100644 index 0000000000..4299794c16 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/model/SearchUser.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.user.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class SearchUser( + @Json(name = "user_id") val userId: String, + @Json(name = "display_name") val displayName: String? = null, + @Json(name = "avatar_url") val avatarUrl: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/model/SearchUserTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/model/SearchUserTask.kt new file mode 100644 index 0000000000..5f587d7f8d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/model/SearchUserTask.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.user.model + +import org.matrix.android.sdk.api.session.user.model.User +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.user.SearchUserAPI +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface SearchUserTask : Task> { + + data class Params( + val limit: Int, + val search: String, + val excludedUserIds: Set + ) +} + +internal class DefaultSearchUserTask @Inject constructor( + private val searchUserAPI: SearchUserAPI, + private val eventBus: EventBus +) : SearchUserTask { + + override suspend fun execute(params: SearchUserTask.Params): List { + val response = executeRequest(eventBus) { + apiCall = searchUserAPI.searchUsers(SearchUsersParams(params.search, params.limit)) + } + return response.users.map { + User(it.userId, it.displayName, it.avatarUrl) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/model/SearchUsersParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/model/SearchUsersParams.kt new file mode 100644 index 0000000000..b8e855a004 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/model/SearchUsersParams.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.user.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing an user search parameters + */ +@JsonClass(generateAdapter = true) +internal data class SearchUsersParams( + // the searched term + @Json(name = "search_term") val searchTerm: String, + // set a limit to the request response + @Json(name = "limit") val limit: Int +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/model/SearchUsersResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/model/SearchUsersResponse.kt new file mode 100644 index 0000000000..646d8e63bf --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/model/SearchUsersResponse.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.user.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing an users search response + */ +@JsonClass(generateAdapter = true) +internal data class SearchUsersResponse( + @Json(name = "limited") val limited: Boolean = false, + @Json(name = "results") val users: List = emptyList() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/CreateWidgetTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/CreateWidgetTask.kt new file mode 100644 index 0000000000..9f7981a95b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/CreateWidgetTask.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.widgets + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.internal.database.awaitNotEmptyResult +import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity +import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields +import org.matrix.android.sdk.internal.database.query.whereStateKey +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface CreateWidgetTask : Task { + + data class Params( + val roomId: String, + val widgetId: String, + val content: Content + ) +} + +internal class DefaultCreateWidgetTask @Inject constructor(@SessionDatabase private val monarchy: Monarchy, + private val roomAPI: RoomAPI, + @UserId private val userId: String, + private val eventBus: EventBus) : CreateWidgetTask { + + override suspend fun execute(params: CreateWidgetTask.Params) { + executeRequest(eventBus) { + apiCall = roomAPI.sendStateEvent( + roomId = params.roomId, + stateEventType = EventType.STATE_ROOM_WIDGET_LEGACY, + stateKey = params.widgetId, + params = params.content + ) + } + awaitNotEmptyResult(monarchy.realmConfiguration, 30_000L) { + CurrentStateEventEntity + .whereStateKey(it, params.roomId, type = EventType.STATE_ROOM_WIDGET_LEGACY, stateKey = params.widgetId) + .and() + .equalTo(CurrentStateEventEntityFields.ROOT.SENDER, userId) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetPostAPIMediator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetPostAPIMediator.kt new file mode 100644 index 0000000000..0f8f2c1f94 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetPostAPIMediator.kt @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.widgets + +import android.os.Build +import android.webkit.JavascriptInterface +import android.webkit.WebView +import com.squareup.moshi.Moshi +import org.matrix.android.sdk.api.session.widgets.WidgetPostAPIMediator +import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.util.createUIHandler +import timber.log.Timber +import java.lang.reflect.Type +import java.util.HashMap +import javax.inject.Inject + +internal class DefaultWidgetPostAPIMediator @Inject constructor(private val moshi: Moshi, + private val widgetPostMessageAPIProvider: WidgetPostMessageAPIProvider) + : WidgetPostAPIMediator { + + private val jsonAdapter = moshi.adapter(JSON_DICT_PARAMETERIZED_TYPE) + + private var handler: WidgetPostAPIMediator.Handler? = null + private var webView: WebView? = null + + private val uiHandler = createUIHandler() + + override fun setWebView(webView: WebView) { + this.webView = webView + webView.addJavascriptInterface(this, "Android") + } + + override fun clearWebView() { + webView?.removeJavascriptInterface("Android") + webView = null + } + + override fun setHandler(handler: WidgetPostAPIMediator.Handler?) { + this.handler = handler + } + + override fun injectAPI() { + val js = widgetPostMessageAPIProvider.get() + if (js != null) { + uiHandler.post { + webView?.loadUrl("javascript:$js") + } + } + } + + @JavascriptInterface + fun onWidgetEvent(jsonEventData: String) { + Timber.d("BRIDGE onWidgetEvent : $jsonEventData") + try { + val dataAsDict = jsonAdapter.fromJson(jsonEventData) + @Suppress("UNCHECKED_CAST") + val eventData = (dataAsDict?.get("event.data") as? JsonDict) ?: return + onWidgetMessage(eventData) + } catch (e: Exception) { + Timber.e(e, "## onWidgetEvent() failed") + } + } + + private fun onWidgetMessage(eventData: JsonDict) { + try { + if (handler?.handleWidgetRequest(this, eventData) == false) { + sendError("", eventData) + } + } catch (e: Exception) { + Timber.e(e, "## onWidgetMessage() : failed") + sendError("", eventData) + } + } + + /* + * ********************************************************************************************* + * Message sending methods + * ********************************************************************************************* + */ + + /** + * Send a boolean response + * + * @param response the response + * @param eventData the modular data + */ + override fun sendBoolResponse(response: Boolean, eventData: JsonDict) { + val jsString = if (response) "true" else "false" + sendResponse(jsString, eventData) + } + + /** + * Send an integer response + * + * @param response the response + * @param eventData the modular data + */ + override fun sendIntegerResponse(response: Int, eventData: JsonDict) { + sendResponse(response.toString() + "", eventData) + } + + /** + * Send an object response + * + * @param response the response + * @param eventData the modular data + */ + override fun sendObjectResponse(type: Type, response: T?, eventData: JsonDict) { + var jsString: String? = null + if (response != null) { + val objectAdapter = moshi.adapter(type) + try { + jsString = "JSON.parse('${objectAdapter.toJson(response)}')" + } catch (e: Exception) { + Timber.e(e, "## sendObjectResponse() : toJson failed ") + } + } + sendResponse(jsString ?: "null", eventData) + } + + /** + * Send success + * + * @param eventData the modular data + */ + override fun sendSuccess(eventData: JsonDict) { + val successResponse = mapOf("success" to true) + sendObjectResponse(Map::class.java, successResponse, eventData) + } + + /** + * Send an error + * + * @param message the error message + * @param eventData the modular data + */ + override fun sendError(message: String, eventData: JsonDict) { + Timber.e("## sendError() : eventData $eventData failed $message") + + // TODO: JS has an additional optional parameter: nestedError + val params = HashMap>() + val subMap = HashMap() + subMap["message"] = message + params["error"] = subMap + sendObjectResponse(Map::class.java, params, eventData) + } + + /** + * Send the response to the javascript + * + * @param jsString the response data + * @param eventData the modular data + */ + private fun sendResponse(jsString: String, eventData: JsonDict) = uiHandler.post { + try { + val functionLine = "sendResponseFromRiotAndroid('" + eventData["_id"] + "' , " + jsString + ");" + Timber.v("BRIDGE sendResponse: $functionLine") + // call the javascript method + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + webView?.loadUrl("javascript:$functionLine") + } else { + webView?.evaluateJavascript(functionLine, null) + } + } catch (e: Exception) { + Timber.e(e, "## sendResponse() failed ") + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetService.kt new file mode 100644 index 0000000000..049b368fe5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetService.kt @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.widgets + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.widgets.WidgetPostAPIMediator +import org.matrix.android.sdk.api.session.widgets.WidgetService +import org.matrix.android.sdk.api.session.widgets.WidgetURLFormatter +import org.matrix.android.sdk.api.session.widgets.model.Widget +import org.matrix.android.sdk.api.util.Cancelable +import javax.inject.Inject +import javax.inject.Provider + +internal class DefaultWidgetService @Inject constructor(private val widgetManager: WidgetManager, + private val widgetURLFormatter: WidgetURLFormatter, + private val widgetPostAPIMediator: Provider) + : WidgetService { + + override fun getWidgetURLFormatter(): WidgetURLFormatter { + return widgetURLFormatter + } + + override fun getWidgetPostAPIMediator(): WidgetPostAPIMediator { + return widgetPostAPIMediator.get() + } + + override fun getRoomWidgets( + roomId: String, + widgetId: QueryStringValue, + widgetTypes: Set?, + excludedTypes: Set? + ): List { + return widgetManager.getRoomWidgets(roomId, widgetId, widgetTypes, excludedTypes) + } + + override fun getRoomWidgetsLive( + roomId: String, + widgetId: QueryStringValue, + widgetTypes: Set?, + excludedTypes: Set? + ): LiveData> { + return widgetManager.getRoomWidgetsLive(roomId, widgetId, widgetTypes, excludedTypes) + } + + override fun getUserWidgetsLive( + widgetTypes: Set?, + excludedTypes: Set? + ): LiveData> { + return widgetManager.getUserWidgetsLive(widgetTypes, excludedTypes) + } + + override fun getUserWidgets( + widgetTypes: Set?, + excludedTypes: Set? + ): List { + return widgetManager.getUserWidgets(widgetTypes, excludedTypes) + } + + override fun createRoomWidget( + roomId: String, + widgetId: String, + content: Content, + callback: MatrixCallback + ): Cancelable { + return widgetManager.createRoomWidget(roomId, widgetId, content, callback) + } + + override fun destroyRoomWidget( + roomId: String, + widgetId: String, + callback: MatrixCallback + ): Cancelable { + return widgetManager.destroyRoomWidget(roomId, widgetId, callback) + } + + override fun hasPermissionsToHandleWidgets(roomId: String): Boolean { + return widgetManager.hasPermissionsToHandleWidgets(roomId) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetURLFormatter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetURLFormatter.kt new file mode 100644 index 0000000000..28bcf0021c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetURLFormatter.kt @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.widgets + +import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerConfig +import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService +import org.matrix.android.sdk.api.session.widgets.WidgetURLFormatter +import org.matrix.android.sdk.internal.session.SessionLifecycleObserver +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManager +import org.matrix.android.sdk.internal.session.widgets.token.GetScalarTokenTask +import java.net.URLEncoder +import javax.inject.Inject + +@SessionScope +internal class DefaultWidgetURLFormatter @Inject constructor(private val integrationManager: IntegrationManager, + private val getScalarTokenTask: GetScalarTokenTask, + private val matrixConfiguration: MatrixConfiguration +) : IntegrationManagerService.Listener, WidgetURLFormatter, SessionLifecycleObserver { + + private lateinit var currentConfig: IntegrationManagerConfig + private var whiteListedUrls: List = emptyList() + + override fun onStart() { + setupWithConfiguration() + integrationManager.addListener(this) + } + + override fun onStop() { + integrationManager.removeListener(this) + } + + override fun onConfigurationChanged(configs: List) { + setupWithConfiguration() + } + + private fun setupWithConfiguration() { + val preferredConfig = integrationManager.getPreferredConfig() + if (!this::currentConfig.isInitialized || preferredConfig != currentConfig) { + currentConfig = preferredConfig + whiteListedUrls = if (matrixConfiguration.integrationWidgetUrls.isEmpty()) { + listOf(preferredConfig.restUrl) + } else { + matrixConfiguration.integrationWidgetUrls + } + } + } + + /** + * Takes care of fetching a scalar token if required and build the final url. + */ + override suspend fun format(baseUrl: String, params: Map, forceFetchScalarToken: Boolean, bypassWhitelist: Boolean): String { + return if (bypassWhitelist || isWhiteListed(baseUrl)) { + val taskParams = GetScalarTokenTask.Params(currentConfig.restUrl, forceFetchScalarToken) + val scalarToken = getScalarTokenTask.execute(taskParams) + buildString { + append(baseUrl) + appendParamToUrl("scalar_token", scalarToken) + appendParamsToUrl(params) + } + } else { + buildString { + append(baseUrl) + appendParamsToUrl(params) + } + } + } + + private fun isWhiteListed(url: String): Boolean { + val allowed: List = whiteListedUrls + for (allowedUrl in allowed) { + if (url.startsWith(allowedUrl)) { + return true + } + } + return false + } + + private fun StringBuilder.appendParamsToUrl(params: Map): StringBuilder { + params.forEach { (param, value) -> + appendParamToUrl(param, value) + } + return this + } + + private fun StringBuilder.appendParamToUrl(param: String, value: String): StringBuilder { + if (contains("?")) { + append("&") + } else { + append("?") + } + + append(param) + append("=") + append(URLEncoder.encode(value, "utf-8")) + + return this + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/RegisterWidgetResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/RegisterWidgetResponse.kt new file mode 100644 index 0000000000..b1d08ab295 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/RegisterWidgetResponse.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.widgets + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class RegisterWidgetResponse( + @Json(name = "scalar_token") val scalarToken: String? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt new file mode 100644 index 0000000000..cb9059b089 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt @@ -0,0 +1,207 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.widgets + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService +import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent +import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper +import org.matrix.android.sdk.api.session.widgets.WidgetManagementFailure +import org.matrix.android.sdk.api.session.widgets.model.Widget +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.SessionLifecycleObserver +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManager +import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource +import org.matrix.android.sdk.internal.session.user.accountdata.AccountDataDataSource +import org.matrix.android.sdk.internal.session.widgets.helper.WidgetFactory +import org.matrix.android.sdk.internal.session.widgets.helper.extractWidgetSequence +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.launchToCallback +import java.util.HashMap +import javax.inject.Inject + +@SessionScope +internal class WidgetManager @Inject constructor(private val integrationManager: IntegrationManager, + private val accountDataDataSource: AccountDataDataSource, + private val stateEventDataSource: StateEventDataSource, + private val taskExecutor: TaskExecutor, + private val createWidgetTask: CreateWidgetTask, + private val widgetFactory: WidgetFactory, + @UserId private val userId: String) + + : IntegrationManagerService.Listener, SessionLifecycleObserver { + + private val lifecycleOwner: LifecycleOwner = LifecycleOwner { lifecycleRegistry } + private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(lifecycleOwner) + + override fun onStart() { + lifecycleRegistry.currentState = Lifecycle.State.STARTED + integrationManager.addListener(this) + } + + override fun onStop() { + integrationManager.removeListener(this) + lifecycleRegistry.currentState = Lifecycle.State.DESTROYED + } + + fun getRoomWidgetsLive( + roomId: String, + widgetId: QueryStringValue = QueryStringValue.NoCondition, + widgetTypes: Set? = null, + excludedTypes: Set? = null + ): LiveData> { + // Get all im.vector.modular.widgets state events in the room + val liveWidgetEvents = stateEventDataSource.getStateEventsLive( + roomId = roomId, + eventTypes = setOf(EventType.STATE_ROOM_WIDGET, EventType.STATE_ROOM_WIDGET_LEGACY), + stateKey = widgetId + ) + return Transformations.map(liveWidgetEvents) { widgetEvents -> + widgetEvents.mapEventsToWidgets(widgetTypes, excludedTypes) + } + } + + fun getRoomWidgets( + roomId: String, + widgetId: QueryStringValue = QueryStringValue.NoCondition, + widgetTypes: Set? = null, + excludedTypes: Set? = null + ): List { + // Get all im.vector.modular.widgets state events in the room + val widgetEvents: List = stateEventDataSource.getStateEvents( + roomId = roomId, + eventTypes = setOf(EventType.STATE_ROOM_WIDGET, EventType.STATE_ROOM_WIDGET_LEGACY), + stateKey = widgetId + ) + return widgetEvents.mapEventsToWidgets(widgetTypes, excludedTypes) + } + + private fun List.mapEventsToWidgets(widgetTypes: Set? = null, + excludedTypes: Set? = null): List { + val widgetEvents = this + // Widget id -> widget + val widgets: MutableMap = HashMap() + // Order widgetEvents with the last event first + // There can be several im.vector.modular.widgets state events for a same widget but + // only the last one must be considered. + val sortedWidgetEvents = widgetEvents.sortedByDescending { + it.originServerTs + } + // Create each widget from its latest im.vector.modular.widgets state event + for (widgetEvent in sortedWidgetEvents) { // Filter widget types if required + val widget = widgetFactory.create(widgetEvent) ?: continue + val widgetType = widget.widgetContent.type ?: continue + if (widgetTypes != null && !widgetTypes.contains(widgetType)) { + continue + } + if (excludedTypes != null && excludedTypes.contains(widgetType)) { + continue + } + if (!widgets.containsKey(widget.widgetId)) { + widgets[widget.widgetId] = widget + } + } + return widgets.values.toList() + } + + fun getUserWidgetsLive( + widgetTypes: Set? = null, + excludedTypes: Set? = null + ): LiveData> { + val widgetsAccountData = accountDataDataSource.getLiveAccountDataEvent(UserAccountDataTypes.TYPE_WIDGETS) + return Transformations.map(widgetsAccountData) { + it.getOrNull()?.mapToWidgets(widgetTypes, excludedTypes) ?: emptyList() + } + } + + fun getUserWidgets( + widgetTypes: Set? = null, + excludedTypes: Set? = null + ): List { + val widgetsAccountData = accountDataDataSource.getAccountDataEvent(UserAccountDataTypes.TYPE_WIDGETS) ?: return emptyList() + return widgetsAccountData.mapToWidgets(widgetTypes, excludedTypes) + } + + private fun UserAccountDataEvent.mapToWidgets(widgetTypes: Set? = null, + excludedTypes: Set? = null): List { + return extractWidgetSequence(widgetFactory) + .filter { + val widgetType = it.widgetContent.type ?: return@filter false + (widgetTypes == null || widgetTypes.contains(widgetType)) + && (excludedTypes == null || !excludedTypes.contains(widgetType)) + } + .toList() + } + + fun createRoomWidget(roomId: String, widgetId: String, content: Content, callback: MatrixCallback): Cancelable { + return taskExecutor.executorScope.launchToCallback(callback = callback) { + if (!hasPermissionsToHandleWidgets(roomId)) { + throw WidgetManagementFailure.NotEnoughPower + } + val params = CreateWidgetTask.Params( + roomId = roomId, + widgetId = widgetId, + content = content + ) + createWidgetTask.execute(params) + try { + getRoomWidgets(roomId, widgetId = QueryStringValue.Equals(widgetId, QueryStringValue.Case.INSENSITIVE)).first() + } catch (failure: Throwable) { + throw WidgetManagementFailure.CreationFailed + } + } + } + + fun destroyRoomWidget(roomId: String, widgetId: String, callback: MatrixCallback): Cancelable { + return taskExecutor.executorScope.launchToCallback(callback = callback) { + if (!hasPermissionsToHandleWidgets(roomId)) { + throw WidgetManagementFailure.NotEnoughPower + } + val params = CreateWidgetTask.Params( + roomId = roomId, + widgetId = widgetId, + content = emptyMap() + ) + createWidgetTask.execute(params) + } + } + + fun hasPermissionsToHandleWidgets(roomId: String): Boolean { + val powerLevelsEvent = stateEventDataSource.getStateEvent( + roomId = roomId, + eventType = EventType.STATE_ROOM_POWER_LEVELS, + stateKey = QueryStringValue.NoCondition + ) + val powerLevelsContent = powerLevelsEvent?.content?.toModel() ?: return false + return PowerLevelsHelper(powerLevelsContent).isUserAllowedToSend(userId, true, null) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetModule.kt new file mode 100644 index 0000000000..56b2d94701 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetModule.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.widgets + +import dagger.Binds +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.api.session.widgets.WidgetPostAPIMediator +import org.matrix.android.sdk.api.session.widgets.WidgetService +import org.matrix.android.sdk.api.session.widgets.WidgetURLFormatter +import org.matrix.android.sdk.internal.session.widgets.token.DefaultGetScalarTokenTask +import org.matrix.android.sdk.internal.session.widgets.token.GetScalarTokenTask +import retrofit2.Retrofit + +@Module +internal abstract class WidgetModule { + + @Module + companion object { + @JvmStatic + @Provides + fun providesWidgetsAPI(retrofit: Retrofit): WidgetsAPI { + return retrofit.create(WidgetsAPI::class.java) + } + } + + @Binds + abstract fun bindWidgetService(service: DefaultWidgetService): WidgetService + + @Binds + abstract fun bindWidgetURLBuilder(formatter: DefaultWidgetURLFormatter): WidgetURLFormatter + + @Binds + abstract fun bindWidgetPostAPIMediator(mediator: DefaultWidgetPostAPIMediator): WidgetPostAPIMediator + + @Binds + abstract fun bindCreateWidgetTask(task: DefaultCreateWidgetTask): CreateWidgetTask + + @Binds + abstract fun bindGetScalarTokenTask(task: DefaultGetScalarTokenTask): GetScalarTokenTask +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetPostMessageAPIProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetPostMessageAPIProvider.kt new file mode 100644 index 0000000000..5b26415ce5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetPostMessageAPIProvider.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.widgets + +import android.content.Context +import timber.log.Timber +import javax.inject.Inject + +internal class WidgetPostMessageAPIProvider @Inject constructor(private val context: Context) { + + private var postMessageAPIString: String? = null + + fun get(): String? { + if (postMessageAPIString == null) { + postMessageAPIString = readFromAsset(context) + } + return postMessageAPIString + } + + private fun readFromAsset(context: Context): String? { + return try { + context.assets.open("postMessageAPI.js").bufferedReader().use { + it.readText() + } + } catch (failure: Throwable) { + Timber.e(failure, "Reading postMessageAPI.js asset failed") + null + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetsAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetsAPI.kt new file mode 100644 index 0000000000..bfcd27b28a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetsAPI.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.session.widgets + +import org.matrix.android.sdk.internal.session.openid.RequestOpenIdTokenResponse +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Query + +internal interface WidgetsAPI { + + /** + * register to the server + * + * @param body the body content (Ref: https://github.com/matrix-org/matrix-doc/pull/1961) + */ + @POST("register") + fun register(@Body body: RequestOpenIdTokenResponse, + @Query("v") version: String?): Call + + @GET("account") + fun validateToken(@Query("scalar_token") scalarToken: String?, + @Query("v") version: String?): Call +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetsAPIProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetsAPIProvider.kt new file mode 100644 index 0000000000..cbbc11bb93 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetsAPIProvider.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.widgets + +import dagger.Lazy +import org.matrix.android.sdk.internal.di.Unauthenticated +import org.matrix.android.sdk.internal.network.RetrofitFactory +import org.matrix.android.sdk.internal.session.SessionScope +import okhttp3.OkHttpClient +import javax.inject.Inject + +@SessionScope +internal class WidgetsAPIProvider @Inject constructor(@Unauthenticated private val okHttpClient: Lazy, + private val retrofitFactory: RetrofitFactory) { + + // Map to keep one WidgetAPI instance by serverUrl + private val widgetsAPIs = mutableMapOf() + + fun get(serverUrl: String): WidgetsAPI { + return widgetsAPIs.getOrPut(serverUrl) { + retrofitFactory.create(okHttpClient, serverUrl).create(WidgetsAPI::class.java) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/UserAccountWidgets.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/UserAccountWidgets.kt new file mode 100644 index 0000000000..f6dafd0553 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/UserAccountWidgets.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.widgets.helper + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent +import org.matrix.android.sdk.api.session.widgets.model.Widget + +internal fun UserAccountDataEvent.extractWidgetSequence(widgetFactory: WidgetFactory): Sequence { + return content.asSequence() + .mapNotNull { + @Suppress("UNCHECKED_CAST") + (it.value as? JsonDict)?.toModel() + }.mapNotNull { event -> + widgetFactory.create(event) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/WidgetFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/WidgetFactory.kt new file mode 100644 index 0000000000..2cbc9b23dc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/WidgetFactory.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.widgets.helper + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.sender.SenderInfo +import org.matrix.android.sdk.api.session.widgets.model.Widget +import org.matrix.android.sdk.api.session.widgets.model.WidgetContent +import org.matrix.android.sdk.api.session.widgets.model.WidgetType +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper +import org.matrix.android.sdk.internal.session.user.UserDataSource +import io.realm.Realm +import io.realm.RealmConfiguration +import java.net.URLEncoder +import javax.inject.Inject + +internal class WidgetFactory @Inject constructor(@SessionDatabase private val realmConfiguration: RealmConfiguration, + private val userDataSource: UserDataSource, + @UserId private val userId: String) { + + fun create(widgetEvent: Event): Widget? { + val widgetContent = widgetEvent.content.toModel() + if (widgetContent?.url == null) return null + val widgetId = widgetEvent.stateKey ?: return null + val type = widgetContent.type ?: return null + val senderInfo = if (widgetEvent.senderId == null || widgetEvent.roomId == null) { + null + } else { + Realm.getInstance(realmConfiguration).use { + val roomMemberHelper = RoomMemberHelper(it, widgetEvent.roomId) + val roomMemberSummaryEntity = roomMemberHelper.getLastRoomMember(widgetEvent.senderId) + SenderInfo( + userId = widgetEvent.senderId, + displayName = roomMemberSummaryEntity?.displayName, + isUniqueDisplayName = roomMemberHelper.isUniqueDisplayName(roomMemberSummaryEntity?.displayName), + avatarUrl = roomMemberSummaryEntity?.avatarUrl + ) + } + } + val isAddedByMe = widgetEvent.senderId == userId + val computedUrl = widgetContent.computeURL(widgetEvent.roomId) + return Widget( + widgetContent = widgetContent, + event = widgetEvent, + widgetId = widgetId, + senderInfo = senderInfo, + isAddedByMe = isAddedByMe, + computedUrl = computedUrl, + type = WidgetType.fromString(type) + ) + } + + private fun WidgetContent.computeURL(roomId: String?): String? { + var computedUrl = url ?: return null + val myUser = userDataSource.getUser(userId) + computedUrl = computedUrl + .replace("\$matrix_user_id", userId) + .replace("\$matrix_display_name", myUser?.displayName ?: userId) + .replace("\$matrix_avatar_url", myUser?.avatarUrl ?: "") + + if (roomId != null) { + computedUrl = computedUrl.replace("\$matrix_room_id", roomId) + } + for ((key, value) in data) { + if (value is String) { + computedUrl = computedUrl.replace("$$key", URLEncoder.encode(value, "utf-8")) + } + } + return computedUrl + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/token/GetScalarTokenTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/token/GetScalarTokenTask.kt new file mode 100644 index 0000000000..58b0c61060 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/token/GetScalarTokenTask.kt @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.widgets.token + +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.MatrixError +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.openid.GetOpenIdTokenTask +import org.matrix.android.sdk.internal.session.widgets.RegisterWidgetResponse +import org.matrix.android.sdk.api.session.widgets.WidgetManagementFailure +import org.matrix.android.sdk.internal.session.widgets.WidgetsAPI +import org.matrix.android.sdk.internal.session.widgets.WidgetsAPIProvider +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject +import javax.net.ssl.HttpsURLConnection + +internal interface GetScalarTokenTask : Task { + + data class Params( + val serverUrl: String, + val forceRefresh: Boolean = false + ) +} + +private const val WIDGET_API_VERSION = "1.1" + +internal class DefaultGetScalarTokenTask @Inject constructor(private val widgetsAPIProvider: WidgetsAPIProvider, + private val scalarTokenStore: ScalarTokenStore, + private val getOpenIdTokenTask: GetOpenIdTokenTask) : GetScalarTokenTask { + + override suspend fun execute(params: GetScalarTokenTask.Params): String { + val widgetsAPI = widgetsAPIProvider.get(params.serverUrl) + return if (params.forceRefresh) { + scalarTokenStore.clearToken(params.serverUrl) + getNewScalarToken(widgetsAPI, params.serverUrl) + } else { + val scalarToken = scalarTokenStore.getToken(params.serverUrl) + if (scalarToken == null) { + getNewScalarToken(widgetsAPI, params.serverUrl) + } else { + validateToken(widgetsAPI, params.serverUrl, scalarToken) + } + } + } + + private suspend fun getNewScalarToken(widgetsAPI: WidgetsAPI, serverUrl: String): String { + val openId = getOpenIdTokenTask.execute(Unit) + val registerWidgetResponse = executeRequest(null) { + apiCall = widgetsAPI.register(openId, WIDGET_API_VERSION) + } + if (registerWidgetResponse.scalarToken == null) { + // Should not happen + throw IllegalStateException("Scalar token is null") + } + scalarTokenStore.setToken(serverUrl, registerWidgetResponse.scalarToken) + return validateToken(widgetsAPI, serverUrl, registerWidgetResponse.scalarToken) + } + + private suspend fun validateToken(widgetsAPI: WidgetsAPI, serverUrl: String, scalarToken: String): String { + return try { + executeRequest(null) { + apiCall = widgetsAPI.validateToken(scalarToken, WIDGET_API_VERSION) + } + scalarToken + } catch (failure: Throwable) { + if (failure is Failure.ServerError && failure.httpCode == HttpsURLConnection.HTTP_FORBIDDEN) { + if (failure.error.code == MatrixError.M_TERMS_NOT_SIGNED) { + throw WidgetManagementFailure.TermsNotSignedException(serverUrl, scalarToken) + } else { + scalarTokenStore.clearToken(serverUrl) + getNewScalarToken(widgetsAPI, serverUrl) + } + } else { + throw failure + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/token/ScalarTokenStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/token/ScalarTokenStore.kt new file mode 100644 index 0000000000..a6f8b3c890 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/token/ScalarTokenStore.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.session.widgets.token + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.internal.database.model.ScalarTokenEntity +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.util.awaitTransaction +import org.matrix.android.sdk.internal.util.fetchCopyMap +import javax.inject.Inject + +internal class ScalarTokenStore @Inject constructor(@SessionDatabase private val monarchy: Monarchy) { + + fun getToken(apiUrl: String): String? { + return monarchy.fetchCopyMap({ realm -> + ScalarTokenEntity.where(realm, apiUrl).findFirst() + }, { scalarToken, _ -> + scalarToken.token + }) + } + + suspend fun setToken(apiUrl: String, token: String) { + monarchy.awaitTransaction { realm -> + val scalarTokenEntity = ScalarTokenEntity(apiUrl, token) + realm.insertOrUpdate(scalarTokenEntity) + } + } + + suspend fun clearToken(apiUrl: String) { + monarchy.awaitTransaction { realm -> + ScalarTokenEntity.where(realm, apiUrl).findFirst()?.deleteFromRealm() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/ConfigurableTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/ConfigurableTask.kt new file mode 100644 index 0000000000..050f0ba295 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/ConfigurableTask.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.task + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.NoOpMatrixCallback +import org.matrix.android.sdk.api.util.Cancelable +import java.util.UUID + +internal fun Task.configureWith(params: PARAMS, + init: (ConfigurableTask.Builder.() -> Unit) = {} +): ConfigurableTask { + return ConfigurableTask.Builder(this, params).apply(init).build() +} + +internal fun Task.configureWith(init: (ConfigurableTask.Builder.() -> Unit) = {}): ConfigurableTask { + return configureWith(Unit, init) +} + +internal data class ConfigurableTask( + val task: Task, + val params: PARAMS, + val id: UUID, + val callbackThread: TaskThread, + val executionThread: TaskThread, + val callback: MatrixCallback + +) : Task by task { + + class Builder( + private val task: Task, + private val params: PARAMS, + var id: UUID = UUID.randomUUID(), + var callbackThread: TaskThread = TaskThread.MAIN, + var executionThread: TaskThread = TaskThread.IO, + var retryCount: Int = 0, + var callback: MatrixCallback = NoOpMatrixCallback() + ) { + + fun build() = ConfigurableTask( + task = task, + params = params, + id = id, + callbackThread = callbackThread, + executionThread = executionThread, + callback = callback + ) + } + + fun executeBy(taskExecutor: TaskExecutor): Cancelable { + return taskExecutor.execute(this) + } + + override fun toString(): String { + return "${task.javaClass.name} with ID: $id" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/CoroutineSequencer.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/CoroutineSequencer.kt new file mode 100644 index 0000000000..2fde8478ec --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/CoroutineSequencer.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.task + +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withPermit + +/** + * This class intends to be used to ensure suspendable methods are played sequentially all the way long. + */ +internal interface CoroutineSequencer { + /** + * @param block the suspendable block to execute + * @return the result of the block + */ + suspend fun post(block: suspend () -> T): T +} + +internal open class SemaphoreCoroutineSequencer : CoroutineSequencer { + + // Permits 1 suspend function at a time. + private val semaphore = Semaphore(1) + + override suspend fun post(block: suspend () -> T): T { + return semaphore.withPermit { + block() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/CoroutineToCallback.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/CoroutineToCallback.kt new file mode 100644 index 0000000000..233d50c695 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/CoroutineToCallback.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.task + +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.extensions.foldToCallback +import org.matrix.android.sdk.internal.util.toCancelable +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext + +internal fun CoroutineScope.launchToCallback( + context: CoroutineContext = EmptyCoroutineContext, + callback: MatrixCallback, + block: suspend () -> T +): Cancelable = launch(context, CoroutineStart.DEFAULT) { + val result = runCatching { + block() + } + withContext(Dispatchers.Main) { + result.foldToCallback(callback) + } +}.toCancelable() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/Task.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/Task.kt new file mode 100644 index 0000000000..a9e7ab2d73 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/Task.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.task + +internal interface Task { + + suspend fun execute(params: PARAMS): RESULT +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/TaskExecutor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/TaskExecutor.kt new file mode 100644 index 0000000000..a3c815bbe8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/TaskExecutor.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.task + +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.di.MatrixScope +import org.matrix.android.sdk.internal.extensions.foldToCallback +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import org.matrix.android.sdk.internal.util.toCancelable +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import timber.log.Timber +import javax.inject.Inject +import kotlin.coroutines.EmptyCoroutineContext + +@MatrixScope +internal class TaskExecutor @Inject constructor(private val coroutineDispatchers: MatrixCoroutineDispatchers) { + + val executorScope = CoroutineScope(SupervisorJob()) + + fun execute(task: ConfigurableTask): Cancelable { + return executorScope + .launch(task.callbackThread.toDispatcher()) { + val resultOrFailure = runCatching { + withContext(task.executionThread.toDispatcher()) { + Timber.v("Enqueue task $task") + Timber.v("Execute task $task on ${Thread.currentThread().name}") + task.execute(task.params) + } + } + resultOrFailure + .onFailure { + Timber.e(it, "Task failed") + } + .foldToCallback(task.callback) + } + .toCancelable() + } + + fun cancelAll() = executorScope.coroutineContext.cancelChildren() + + private fun TaskThread.toDispatcher() = when (this) { + TaskThread.MAIN -> coroutineDispatchers.main + TaskThread.COMPUTATION -> coroutineDispatchers.computation + TaskThread.IO -> coroutineDispatchers.io + TaskThread.CALLER -> EmptyCoroutineContext + TaskThread.CRYPTO -> coroutineDispatchers.crypto + TaskThread.DM_VERIF -> coroutineDispatchers.dmVerif + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/TaskThread.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/TaskThread.kt new file mode 100644 index 0000000000..3b9c69bccb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/TaskThread.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.task + +internal enum class TaskThread { + MAIN, + COMPUTATION, + IO, + CALLER, + CRYPTO, + DM_VERIF +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/BackgroundDetectionObserver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/BackgroundDetectionObserver.kt new file mode 100644 index 0000000000..0a15f09719 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/BackgroundDetectionObserver.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.util + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.OnLifecycleEvent +import org.matrix.android.sdk.internal.di.MatrixScope +import timber.log.Timber +import javax.inject.Inject + +/** + * To be attached to ProcessLifecycleOwner lifecycle + */ +@MatrixScope +internal class BackgroundDetectionObserver @Inject constructor() : LifecycleObserver { + + var isInBackground: Boolean = false + private set + + private + val listeners = LinkedHashSet() + + fun register(listener: Listener) { + listeners.add(listener) + } + + fun unregister(listener: Listener) { + listeners.remove(listener) + } + + @OnLifecycleEvent(Lifecycle.Event.ON_START) + fun onMoveToForeground() { + Timber.v("App returning to foreground…") + isInBackground = false + listeners.forEach { it.onMoveToForeground() } + } + + @OnLifecycleEvent(Lifecycle.Event.ON_STOP) + fun onMoveToBackground() { + Timber.v("App going to background…") + isInBackground = true + listeners.forEach { it.onMoveToBackground() } + } + + interface Listener { + fun onMoveToForeground() + fun onMoveToBackground() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/CancelableCoroutine.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/CancelableCoroutine.kt new file mode 100644 index 0000000000..beede69759 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/CancelableCoroutine.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.util + +import org.matrix.android.sdk.api.util.Cancelable +import kotlinx.coroutines.Job + +internal fun Job.toCancelable(): Cancelable { + return CancelableCoroutine(this) +} + +/** + * Private, use the extension above + */ +private class CancelableCoroutine(private val job: Job) : Cancelable { + + override fun cancel() { + if (!job.isCancelled) { + job.cancel() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/CancelableWork.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/CancelableWork.kt new file mode 100644 index 0000000000..fccfda15e5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/CancelableWork.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.util + +import androidx.work.WorkManager +import org.matrix.android.sdk.api.util.Cancelable +import java.util.UUID + +internal class CancelableWork(private val workManager: WorkManager, + private val workId: UUID) : Cancelable { + + override fun cancel() { + workManager.cancelWorkById(workId) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/CompatUtil.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/CompatUtil.kt new file mode 100644 index 0000000000..0836d96af9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/CompatUtil.kt @@ -0,0 +1,328 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +@file:Suppress("DEPRECATION") + +package org.matrix.android.sdk.internal.util + +import android.content.Context +import android.content.SharedPreferences +import android.os.Build +import android.preference.PreferenceManager +import android.security.KeyPairGeneratorSpec +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import android.util.Base64 +import androidx.annotation.RequiresApi +import androidx.core.content.edit +import timber.log.Timber +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.math.BigInteger +import java.security.InvalidAlgorithmParameterException +import java.security.InvalidKeyException +import java.security.KeyPairGenerator +import java.security.KeyStore +import java.security.KeyStoreException +import java.security.NoSuchAlgorithmException +import java.security.NoSuchProviderException +import java.security.PrivateKey +import java.security.SecureRandom +import java.security.UnrecoverableKeyException +import java.security.cert.CertificateException +import java.security.spec.AlgorithmParameterSpec +import java.security.spec.RSAKeyGenParameterSpec +import java.util.Calendar +import java.util.zip.GZIPOutputStream +import javax.crypto.Cipher +import javax.crypto.CipherInputStream +import javax.crypto.CipherOutputStream +import javax.crypto.IllegalBlockSizeException +import javax.crypto.KeyGenerator +import javax.crypto.NoSuchPaddingException +import javax.crypto.SecretKey +import javax.crypto.spec.GCMParameterSpec +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec +import javax.security.auth.x500.X500Principal + +object CompatUtil { + private val TAG = CompatUtil::class.java.simpleName + private const val ANDROID_KEY_STORE_PROVIDER = "AndroidKeyStore" + private const val AES_GCM_CIPHER_TYPE = "AES/GCM/NoPadding" + private const val AES_GCM_KEY_SIZE_IN_BITS = 128 + private const val AES_GCM_IV_LENGTH = 12 + private const val AES_LOCAL_PROTECTION_KEY_ALIAS = "aes_local_protection" + + private const val RSA_WRAP_LOCAL_PROTECTION_KEY_ALIAS = "rsa_wrap_local_protection" + private const val RSA_WRAP_CIPHER_TYPE = "RSA/NONE/PKCS1Padding" + private const val AES_WRAPPED_PROTECTION_KEY_SHARED_PREFERENCE = "aes_wrapped_local_protection" + + private const val SHARED_KEY_ANDROID_VERSION_WHEN_KEY_HAS_BEEN_GENERATED = "android_version_when_key_has_been_generated" + + private var sSecretKeyAndVersion: SecretKeyAndVersion? = null + + /** + * Returns the unique SecureRandom instance shared for all local storage encryption operations. + */ + private val prng: SecureRandom by lazy(LazyThreadSafetyMode.NONE) { SecureRandom() } + + /** + * Create a GZIPOutputStream instance + * Special treatment on KitKat device, force the syncFlush param to false + * Before Kitkat, this param does not exist and after Kitkat it is set to false by default + * + * @param outputStream the output stream + */ + @Throws(IOException::class) + fun createGzipOutputStream(outputStream: OutputStream): GZIPOutputStream { + return if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) { + GZIPOutputStream(outputStream, false) + } else { + GZIPOutputStream(outputStream) + } + } + + /** + * Returns the AES key used for local storage encryption/decryption with AES/GCM. + * The key is created if it does not exist already in the keystore. + * From Marshmallow, this key is generated and operated directly from the android keystore. + * From KitKat and before Marshmallow, this key is stored in the application shared preferences + * wrapped by a RSA key generated and operated directly from the android keystore. + * + * @param context the context holding the application shared preferences + */ + @RequiresApi(Build.VERSION_CODES.KITKAT) + @Synchronized + @Throws(KeyStoreException::class, + CertificateException::class, + NoSuchAlgorithmException::class, + IOException::class, + NoSuchProviderException::class, + InvalidAlgorithmParameterException::class, + NoSuchPaddingException::class, + InvalidKeyException::class, + IllegalBlockSizeException::class, + UnrecoverableKeyException::class) + private fun getAesGcmLocalProtectionKey(context: Context): SecretKeyAndVersion { + if (sSecretKeyAndVersion == null) { + val keyStore = KeyStore.getInstance(ANDROID_KEY_STORE_PROVIDER) + keyStore.load(null) + + Timber.i(TAG, "Loading local protection key") + + var key: SecretKey? + + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) + // Get the version of Android when the key has been generated, default to the current version of the system. In this case, the + // key will be generated + val androidVersionWhenTheKeyHasBeenGenerated = sharedPreferences + .getInt(SHARED_KEY_ANDROID_VERSION_WHEN_KEY_HAS_BEEN_GENERATED, Build.VERSION.SDK_INT) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (keyStore.containsAlias(AES_LOCAL_PROTECTION_KEY_ALIAS)) { + Timber.i(TAG, "AES local protection key found in keystore") + key = keyStore.getKey(AES_LOCAL_PROTECTION_KEY_ALIAS, null) as SecretKey + } else { + // Check if a key has been created on version < M (in case of OS upgrade) + key = readKeyApiL(sharedPreferences, keyStore) + + if (key == null) { + Timber.i(TAG, "Generating AES key with keystore") + val generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE_PROVIDER) + generator.init( + KeyGenParameterSpec.Builder(AES_LOCAL_PROTECTION_KEY_ALIAS, + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setKeySize(AES_GCM_KEY_SIZE_IN_BITS) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .build()) + key = generator.generateKey() + + sharedPreferences.edit { + putInt(SHARED_KEY_ANDROID_VERSION_WHEN_KEY_HAS_BEEN_GENERATED, Build.VERSION.SDK_INT) + } + } + } + } else { + key = readKeyApiL(sharedPreferences, keyStore) + + if (key == null) { + Timber.i(TAG, "Generating RSA key pair with keystore") + val generator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, ANDROID_KEY_STORE_PROVIDER) + val start = Calendar.getInstance() + val end = Calendar.getInstance() + end.add(Calendar.YEAR, 10) + + generator.initialize( + KeyPairGeneratorSpec.Builder(context) + .setAlgorithmParameterSpec(RSAKeyGenParameterSpec(2048, RSAKeyGenParameterSpec.F4)) + .setAlias(RSA_WRAP_LOCAL_PROTECTION_KEY_ALIAS) + .setSubject(X500Principal("CN=matrix-android-sdk")) + .setStartDate(start.time) + .setEndDate(end.time) + .setSerialNumber(BigInteger.ONE) + .build()) + val keyPair = generator.generateKeyPair() + + Timber.i(TAG, "Generating wrapped AES key") + + val aesKeyRaw = ByteArray(AES_GCM_KEY_SIZE_IN_BITS / java.lang.Byte.SIZE) + prng.nextBytes(aesKeyRaw) + key = SecretKeySpec(aesKeyRaw, "AES") + + val cipher = Cipher.getInstance(RSA_WRAP_CIPHER_TYPE) + cipher.init(Cipher.WRAP_MODE, keyPair.public) + val wrappedAesKey = cipher.wrap(key) + + sharedPreferences.edit { + putString(AES_WRAPPED_PROTECTION_KEY_SHARED_PREFERENCE, Base64.encodeToString(wrappedAesKey, 0)) + putInt(SHARED_KEY_ANDROID_VERSION_WHEN_KEY_HAS_BEEN_GENERATED, Build.VERSION.SDK_INT) + } + } + } + + sSecretKeyAndVersion = SecretKeyAndVersion(key!!, androidVersionWhenTheKeyHasBeenGenerated) + } + + return sSecretKeyAndVersion!! + } + + /** + * Read the key, which may have been stored when the OS was < M + * + * @param sharedPreferences shared pref + * @param keyStore key store + * @return the key if it exists or null + */ + @Throws(KeyStoreException::class, + NoSuchPaddingException::class, + NoSuchAlgorithmException::class, + InvalidKeyException::class, + UnrecoverableKeyException::class) + private fun readKeyApiL(sharedPreferences: SharedPreferences, keyStore: KeyStore): SecretKey? { + val wrappedAesKeyString = sharedPreferences.getString(AES_WRAPPED_PROTECTION_KEY_SHARED_PREFERENCE, null) + if (wrappedAesKeyString != null && keyStore.containsAlias(RSA_WRAP_LOCAL_PROTECTION_KEY_ALIAS)) { + Timber.i(TAG, "RSA + wrapped AES local protection keys found in keystore") + val privateKey = keyStore.getKey(RSA_WRAP_LOCAL_PROTECTION_KEY_ALIAS, null) as PrivateKey + val wrappedAesKey = Base64.decode(wrappedAesKeyString, 0) + val cipher = Cipher.getInstance(RSA_WRAP_CIPHER_TYPE) + cipher.init(Cipher.UNWRAP_MODE, privateKey) + return cipher.unwrap(wrappedAesKey, "AES", Cipher.SECRET_KEY) as SecretKey + } + + // Key does not exist + return null + } + + /** + * Create a CipherOutputStream instance. + * Before Kitkat, this method will return out as local storage encryption is not implemented for + * devices before KitKat. + * + * @param out the output stream + * @param context the context holding the application shared preferences + */ + @Throws(IOException::class, + CertificateException::class, + NoSuchAlgorithmException::class, + UnrecoverableKeyException::class, + InvalidKeyException::class, + InvalidAlgorithmParameterException::class, + NoSuchPaddingException::class, + NoSuchProviderException::class, + KeyStoreException::class, + IllegalBlockSizeException::class) + fun createCipherOutputStream(out: OutputStream, context: Context): OutputStream? { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + return out + } + + val keyAndVersion = getAesGcmLocalProtectionKey(context) + + val cipher = Cipher.getInstance(AES_GCM_CIPHER_TYPE) + val iv: ByteArray + + if (keyAndVersion.androidVersionWhenTheKeyHasBeenGenerated >= Build.VERSION_CODES.M) { + cipher.init(Cipher.ENCRYPT_MODE, keyAndVersion.secretKey) + iv = cipher.iv + } else { + iv = ByteArray(AES_GCM_IV_LENGTH) + prng.nextBytes(iv) + cipher.init(Cipher.ENCRYPT_MODE, keyAndVersion.secretKey, IvParameterSpec(iv)) + } + + if (iv.size != AES_GCM_IV_LENGTH) { + Timber.e(TAG, "Invalid IV length ${iv.size}") + return null + } + + out.write(iv.size) + out.write(iv) + + return CipherOutputStream(out, cipher) + } + + /** + * Create a CipherInputStream instance. + * Before Kitkat, this method will return `in` because local storage encryption is not implemented for devices before KitKat. + * Warning, if `in` is not an encrypted stream, it's up to the caller to close and reopen `in`, because the stream has been read. + * + * @param in the input stream + * @param context the context holding the application shared preferences + * @return in, or the created InputStream, or null if the InputStream `in` does not contain encrypted data + */ + @Throws(NoSuchPaddingException::class, + NoSuchAlgorithmException::class, + CertificateException::class, + InvalidKeyException::class, + KeyStoreException::class, + UnrecoverableKeyException::class, + IllegalBlockSizeException::class, + NoSuchProviderException::class, + InvalidAlgorithmParameterException::class, + IOException::class) + fun createCipherInputStream(`in`: InputStream, context: Context): InputStream? { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + return `in` + } + + val iv_len = `in`.read() + if (iv_len != AES_GCM_IV_LENGTH) { + Timber.e(TAG, "Invalid IV length $iv_len") + return null + } + + val iv = ByteArray(AES_GCM_IV_LENGTH) + `in`.read(iv) + + val cipher = Cipher.getInstance(AES_GCM_CIPHER_TYPE) + + val keyAndVersion = getAesGcmLocalProtectionKey(context) + + val spec: AlgorithmParameterSpec = if (keyAndVersion.androidVersionWhenTheKeyHasBeenGenerated >= Build.VERSION_CODES.M) { + GCMParameterSpec(AES_GCM_KEY_SIZE_IN_BITS, iv) + } else { + IvParameterSpec(iv) + } + + cipher.init(Cipher.DECRYPT_MODE, keyAndVersion.secretKey, spec) + + return CipherInputStream(`in`, cipher) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Debouncer.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Debouncer.kt new file mode 100644 index 0000000000..46ba75968c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Debouncer.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.util + +import android.os.Handler + +internal class Debouncer(private val handler: Handler) { + + private val runnables = HashMap() + + fun debounce(identifier: String, r: Runnable, millis: Long): Boolean { + // debounce + runnables[identifier]?.let { runnable -> handler.removeCallbacks(runnable) } + + insertRunnable(identifier, r, millis) + return true + } + + private fun insertRunnable(identifier: String, r: Runnable, millis: Long) { + val chained = Runnable { + handler.post(r) + runnables.remove(identifier) + } + runnables[identifier] = chained + handler.postDelayed(chained, millis) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Exhaustive.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Exhaustive.kt new file mode 100644 index 0000000000..eaf17b9ae0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Exhaustive.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.util + +// Trick to ensure that when block is exhaustive +internal val T.exhaustive: T get() = this diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/FileSaver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/FileSaver.kt new file mode 100644 index 0000000000..27625d90bc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/FileSaver.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.util + +import androidx.annotation.WorkerThread +import java.io.File +import java.io.FileOutputStream +import java.io.InputStream + +/** + * Save an input stream to a file with Okio + */ +@WorkerThread +fun writeToFile(inputStream: InputStream, outputFile: File) { + FileOutputStream(outputFile).use { + inputStream.copyTo(it) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Handler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Handler.kt new file mode 100644 index 0000000000..7d103e1031 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Handler.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.util + +import android.os.Handler +import android.os.HandlerThread +import android.os.Looper + +internal fun createBackgroundHandler(name: String): Handler = Handler( + HandlerThread(name).apply { start() }.looper +) + +internal fun createUIHandler(): Handler = Handler( + Looper.getMainLooper() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Hash.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Hash.kt new file mode 100644 index 0000000000..f8c22afb30 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Hash.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.util + +import java.security.MessageDigest + +/** + * Compute a Hash of a String, using md5 algorithm + */ +fun String.md5() = try { + val digest = MessageDigest.getInstance("md5") + digest.update(toByteArray()) + digest.digest() + .joinToString("") { String.format("%02X", it) } + .toLowerCase() +} catch (exc: Exception) { + // Should not happen, but just in case + hashCode().toString() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/JsonCanonicalizer.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/JsonCanonicalizer.kt new file mode 100644 index 0000000000..4a15f2ff98 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/JsonCanonicalizer.kt @@ -0,0 +1,96 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.util + +import androidx.annotation.VisibleForTesting +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import timber.log.Timber +import java.util.TreeSet + +/** + * Build canonical Json + * Doc: https://matrix.org/docs/spec/appendices.html#canonical-json + */ +object JsonCanonicalizer { + + fun getCanonicalJson(type: Class, o: T): String { + val adapter = MoshiProvider.providesMoshi().adapter(type) + + // Canonicalize manually + return canonicalize(adapter.toJson(o)) + .replace("\\/", "/") + } + + @VisibleForTesting + fun canonicalize(jsonString: String): String { + return try { + val jsonObject = JSONObject(jsonString) + + canonicalizeRecursive(jsonObject) + } catch (e: JSONException) { + Timber.e(e, "Unable to canonicalize") + jsonString + } + } + + /** + * Canonicalize a JSON element + * + * @param src the src + * @return the canonicalize element + */ + private fun canonicalizeRecursive(any: Any): String { + when (any) { + is JSONArray -> { + // Canonicalize each element of the array + return (0 until any.length()).joinToString(separator = ",", prefix = "[", postfix = "]") { + canonicalizeRecursive(any.get(it)) + } + } + is JSONObject -> { + // Sort the attributes by name, and the canonicalize each element of the JSONObject + + val attributes = TreeSet() + for (entry in any.keys()) { + attributes.add(entry) + } + + return buildString { + append("{") + for ((index, value) in attributes.withIndex()) { + append("\"") + append(value) + append("\"") + append(":") + append(canonicalizeRecursive(any[value])) + + if (index < attributes.size - 1) { + append(",") + } + } + append("}") + } + } + is String -> return JSONObject.quote(any) + else -> return any.toString() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/LiveDataUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/LiveDataUtils.kt new file mode 100644 index 0000000000..9c38729fe4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/LiveDataUtils.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.util + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData + +object LiveDataUtils { + + fun combine(firstSource: LiveData, + secondSource: LiveData, + mapper: (FIRST, SECOND) -> OUT): LiveData { + return MediatorLiveData().apply { + var firstValue: FIRST? = null + var secondValue: SECOND? = null + + val valueDispatcher = { + firstValue?.let { safeFirst -> + secondValue?.let { safeSecond -> + val mappedValue = mapper(safeFirst, safeSecond) + postValue(mappedValue) + } + } + } + + addSource(firstSource) { + firstValue = it + valueDispatcher() + } + + addSource(secondSource) { + secondValue = it + valueDispatcher() + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/MatrixCoroutineDispatchers.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/MatrixCoroutineDispatchers.kt new file mode 100644 index 0000000000..d66a38d346 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/MatrixCoroutineDispatchers.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.util + +import kotlinx.coroutines.CoroutineDispatcher + +internal data class MatrixCoroutineDispatchers( + val io: CoroutineDispatcher, + val computation: CoroutineDispatcher, + val main: CoroutineDispatcher, + val crypto: CoroutineDispatcher, + val dmVerif: CoroutineDispatcher +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Monarchy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Monarchy.kt new file mode 100644 index 0000000000..81f5af9ac6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Monarchy.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.util + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.internal.database.awaitTransaction +import io.realm.Realm +import io.realm.RealmModel +import java.util.concurrent.atomic.AtomicReference + +internal suspend fun Monarchy.awaitTransaction(transaction: suspend (realm: Realm) -> T): T { + return awaitTransaction(realmConfiguration, transaction) +} + +fun Monarchy.fetchCopied(query: (Realm) -> T?): T? { + val ref = AtomicReference() + doWithRealm { realm -> + val result = query.invoke(realm)?.let { + realm.copyFromRealm(it) + } + ref.set(result) + } + return ref.get() +} + +fun Monarchy.fetchCopyMap(query: (Realm) -> T?, map: (T, realm: Realm) -> U): U? { + val ref = AtomicReference() + doWithRealm { realm -> + val result = query.invoke(realm)?.let { + map(realm.copyFromRealm(it), realm) + } + ref.set(result) + } + return ref.get() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/SecretKeyAndVersion.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/SecretKeyAndVersion.kt new file mode 100644 index 0000000000..d96be91618 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/SecretKeyAndVersion.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.util + +import javax.crypto.SecretKey + +/** + * Tuple which contains the secret key and the version of Android when the key has been generated + */ +internal data class SecretKeyAndVersion( + /** + * the key + */ + val secretKey: SecretKey, + /** + * The android version when the key has been generated + */ + val androidVersionWhenTheKeyHasBeenGenerated: Int) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringProvider.kt new file mode 100644 index 0000000000..902d7d3316 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringProvider.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.util + +import android.content.res.Resources +import androidx.annotation.ArrayRes +import androidx.annotation.NonNull +import androidx.annotation.StringRes +import dagger.Reusable +import javax.inject.Inject + +@Reusable +internal class StringProvider @Inject constructor(private val resources: Resources) { + + /** + * Returns a localized string from the application's package's + * default string table. + * + * @param resId Resource id for the string + * @return The string data associated with the resource, stripped of styled + * text information. + */ + @NonNull + fun getString(@StringRes resId: Int): String { + return resources.getString(resId) + } + + /** + * Returns a localized formatted string from the application's package's + * default string table, substituting the format arguments as defined in + * [java.util.Formatter] and [java.lang.String.format]. + * + * @param resId Resource id for the format string + * @param formatArgs The format arguments that will be used for + * substitution. + * @return The string data associated with the resource, formatted and + * stripped of styled text information. + */ + @NonNull + fun getString(@StringRes resId: Int, vararg formatArgs: Any?): String { + return resources.getString(resId, *formatArgs) + } + + @Throws(Resources.NotFoundException::class) + fun getStringArray(@ArrayRes id: Int): Array { + return resources.getStringArray(id) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt new file mode 100644 index 0000000000..a236771cd6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2018 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.util + +import timber.log.Timber + +/** + * Convert a string to an UTF8 String + * + * @param s the string to convert + * @return the utf-8 string + */ +fun convertToUTF8(s: String): String { + return try { + val bytes = s.toByteArray(Charsets.UTF_8) + String(bytes) + } catch (e: Exception) { + Timber.e(e, "## convertToUTF8() failed") + s + } +} + +/** + * Convert a string from an UTF8 String + * + * @param s the string to convert + * @return the utf-16 string + */ +fun convertFromUTF8(s: String): String { + return try { + val bytes = s.toByteArray() + String(bytes, Charsets.UTF_8) + } catch (e: Exception) { + Timber.e(e, "## convertFromUTF8() failed") + s + } +} + +fun String.withoutPrefix(prefix: String) = if (startsWith(prefix)) substringAfter(prefix) else this diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/SuspendMatrixCallback.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/SuspendMatrixCallback.kt new file mode 100644 index 0000000000..0595d68c3b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/SuspendMatrixCallback.kt @@ -0,0 +1,36 @@ +/* + + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + + */ +package org.matrix.android.sdk.internal.util + +import org.matrix.android.sdk.api.MatrixCallback +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +suspend inline fun awaitCallback(crossinline callback: (MatrixCallback) -> Unit) = suspendCoroutine { cont -> + callback(object : MatrixCallback { + override fun onFailure(failure: Throwable) { + cont.resumeWithException(failure) + } + + override fun onSuccess(data: T) { + cont.resume(data) + } + }) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/UrlUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/UrlUtils.kt new file mode 100644 index 0000000000..da155c8bdd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/UrlUtils.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.util + +import java.net.URL + +internal fun String.isValidUrl(): Boolean { + return try { + URL(this) + true + } catch (t: Throwable) { + false + } +} + +/** + * Ensure string starts with "http". If it is not the case, "https://" is added, only if the String is not empty + */ +internal fun String.ensureProtocol(): String { + return when { + isEmpty() -> this + !startsWith("http") -> "https://$this" + else -> this + } +} + +/** + * Ensure string has trailing / + */ +internal fun String.ensureTrailingSlash(): String { + return when { + isEmpty() -> this + !endsWith("/") -> "$this/" + else -> this + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/GetWellknownTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/GetWellknownTask.kt new file mode 100644 index 0000000000..e20fe9a304 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/GetWellknownTask.kt @@ -0,0 +1,222 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.wellknown + +import android.util.MalformedJsonException +import dagger.Lazy +import org.matrix.android.sdk.api.MatrixPatterns +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import org.matrix.android.sdk.api.auth.data.WellKnown +import org.matrix.android.sdk.api.auth.wellknown.WellknownResult +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.internal.di.Unauthenticated +import org.matrix.android.sdk.internal.network.RetrofitFactory +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.network.httpclient.addSocketFactory +import org.matrix.android.sdk.internal.network.ssl.UnrecognizedCertificateException +import org.matrix.android.sdk.internal.session.homeserver.CapabilitiesAPI +import org.matrix.android.sdk.internal.session.identity.IdentityAuthAPI +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.isValidUrl +import okhttp3.OkHttpClient +import java.io.EOFException +import javax.inject.Inject +import javax.net.ssl.HttpsURLConnection + +internal interface GetWellknownTask : Task { + data class Params( + val matrixId: String, + val homeServerConnectionConfig: HomeServerConnectionConfig? + ) +} + +/** + * Inspired from AutoDiscovery class from legacy Matrix Android SDK + */ +internal class DefaultGetWellknownTask @Inject constructor( + @Unauthenticated + private val okHttpClient: Lazy, + private val retrofitFactory: RetrofitFactory +) : GetWellknownTask { + + override suspend fun execute(params: GetWellknownTask.Params): WellknownResult { + if (!MatrixPatterns.isUserId(params.matrixId)) { + return WellknownResult.InvalidMatrixId + } + + val homeServerDomain = params.matrixId.substringAfter(":") + + val client = buildClient(params.homeServerConnectionConfig) + return findClientConfig(homeServerDomain, client) + } + + private fun buildClient(homeServerConnectionConfig: HomeServerConnectionConfig?): OkHttpClient { + return if (homeServerConnectionConfig != null) { + okHttpClient.get() + .newBuilder() + .addSocketFactory(homeServerConnectionConfig) + .build() + } else { + okHttpClient.get() + } + } + + /** + * Find client config + * + * - Do the .well-known request + * - validate homeserver url and identity server url if provide in .well-known result + * - return action and .well-known data + * + * @param domain: homeserver domain, deduced from mx userId (ex: "matrix.org" from userId "@user:matrix.org") + */ + private suspend fun findClientConfig(domain: String, client: OkHttpClient): WellknownResult { + val wellKnownAPI = retrofitFactory.create(client, "https://dummy.org") + .create(WellKnownAPI::class.java) + + return try { + val wellKnown = executeRequest(null) { + apiCall = wellKnownAPI.getWellKnown(domain) + } + + // Success + val homeServerBaseUrl = wellKnown.homeServer?.baseURL + if (homeServerBaseUrl.isNullOrBlank()) { + WellknownResult.FailPrompt + } else { + if (homeServerBaseUrl.isValidUrl()) { + // Check that HS is a real one + validateHomeServer(homeServerBaseUrl, wellKnown, client) + } else { + WellknownResult.FailError + } + } + } catch (throwable: Throwable) { + when (throwable) { + is UnrecognizedCertificateException -> { + throw Failure.UnrecognizedCertificateFailure( + "https://$domain", + throwable.fingerprint + ) + } + is Failure.NetworkConnection -> { + WellknownResult.Ignore + } + is Failure.OtherServerError -> { + when (throwable.httpCode) { + HttpsURLConnection.HTTP_NOT_FOUND -> WellknownResult.Ignore + else -> WellknownResult.FailPrompt + } + } + is MalformedJsonException, is EOFException -> { + WellknownResult.FailPrompt + } + else -> { + throw throwable + } + } + } + } + + /** + * Return true if home server is valid, and (if applicable) if identity server is pingable + */ + private suspend fun validateHomeServer(homeServerBaseUrl: String, wellKnown: WellKnown, client: OkHttpClient): WellknownResult { + val capabilitiesAPI = retrofitFactory.create(client, homeServerBaseUrl) + .create(CapabilitiesAPI::class.java) + + try { + executeRequest(null) { + apiCall = capabilitiesAPI.ping() + } + } catch (throwable: Throwable) { + return WellknownResult.FailError + } + + return if (wellKnown.identityServer == null) { + // No identity server + WellknownResult.Prompt(homeServerBaseUrl, null, wellKnown) + } else { + // if m.identity_server is present it must be valid + val identityServerBaseUrl = wellKnown.identityServer.baseURL + if (identityServerBaseUrl.isNullOrBlank()) { + WellknownResult.FailError + } else { + if (identityServerBaseUrl.isValidUrl()) { + if (validateIdentityServer(identityServerBaseUrl, client)) { + // All is ok + WellknownResult.Prompt(homeServerBaseUrl, identityServerBaseUrl, wellKnown) + } else { + WellknownResult.FailError + } + } else { + WellknownResult.FailError + } + } + } + } + + /** + * Return true if identity server is pingable + */ + private suspend fun validateIdentityServer(identityServerBaseUrl: String, client: OkHttpClient): Boolean { + val identityPingApi = retrofitFactory.create(client, identityServerBaseUrl) + .create(IdentityAuthAPI::class.java) + + return try { + executeRequest(null) { + apiCall = identityPingApi.ping() + } + + true + } catch (throwable: Throwable) { + false + } + } + + /** + * Try to get an identity server URL from a home server URL, using a .wellknown request + */ + /* + fun getIdentityServer(homeServerUrl: String, callback: ApiCallback) { + if (homeServerUrl.startsWith("https://")) { + wellKnownRestClient.getWellKnown(homeServerUrl.substring("https://".length), + object : SimpleApiCallback(callback) { + override fun onSuccess(info: WellKnown) { + callback.onSuccess(info.identityServer?.baseURL) + } + }) + } else { + callback.onUnexpectedError(InvalidParameterException("malformed url")) + } + } + + fun getServerPreferredIntegrationManagers(homeServerUrl: String, callback: ApiCallback>) { + if (homeServerUrl.startsWith("https://")) { + wellKnownRestClient.getWellKnown(homeServerUrl.substring("https://".length), + object : SimpleApiCallback(callback) { + override fun onSuccess(info: WellKnown) { + callback.onSuccess(info.getIntegrationManagers()) + } + }) + } else { + callback.onUnexpectedError(InvalidParameterException("malformed url")) + } + } + */ +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/WellKnownAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/WellKnownAPI.kt new file mode 100644 index 0000000000..7d88614809 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/WellKnownAPI.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.wellknown + +import org.matrix.android.sdk.api.auth.data.WellKnown +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Path + +internal interface WellKnownAPI { + @GET("https://{domain}/.well-known/matrix/client") + fun getWellKnown(@Path("domain") domain: String): Call +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/WellknownModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/WellknownModule.kt new file mode 100644 index 0000000000..ab42223755 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/WellknownModule.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.wellknown + +import dagger.Binds +import dagger.Module + +@Module +internal abstract class WellknownModule { + + @Binds + abstract fun bindGetWellknownTask(task: DefaultGetWellknownTask): GetWellknownTask +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/AlwaysSuccessfulWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/AlwaysSuccessfulWorker.kt new file mode 100644 index 0000000000..d0fc7df098 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/AlwaysSuccessfulWorker.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ +package org.matrix.android.sdk.internal.worker + +import android.content.Context +import androidx.work.Worker +import androidx.work.WorkerParameters + +internal class AlwaysSuccessfulWorker(context: Context, params: WorkerParameters) + : Worker(context, params) { + + override fun doWork(): Result { + return Result.success() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/DelegateWorkerFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/DelegateWorkerFactory.kt new file mode 100644 index 0000000000..e711b0d686 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/DelegateWorkerFactory.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.worker + +import android.content.Context +import androidx.work.ListenableWorker +import androidx.work.WorkerParameters + +interface DelegateWorkerFactory { + + fun create(context: Context, params: WorkerParameters): ListenableWorker +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/Extensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/Extensions.kt new file mode 100644 index 0000000000..8f824a76b8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/Extensions.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.worker + +import androidx.work.OneTimeWorkRequest +import org.matrix.android.sdk.internal.session.room.send.NoMerger + +/** + * If startChain parameter is true, the builder will have a inputMerger set to [NoMerger] + */ +internal fun OneTimeWorkRequest.Builder.startChain(startChain: Boolean): OneTimeWorkRequest.Builder { + if (startChain) { + setInputMerger(NoMerger::class.java) + } + return this +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/MatrixWorkerFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/MatrixWorkerFactory.kt new file mode 100644 index 0000000000..509cecf022 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/MatrixWorkerFactory.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.worker + +import android.content.Context +import androidx.work.ListenableWorker +import androidx.work.WorkerFactory +import androidx.work.WorkerParameters +import javax.inject.Inject +import javax.inject.Provider + +class MatrixWorkerFactory @Inject constructor( + private val workerFactories: Map, @JvmSuppressWildcards Provider> +) : WorkerFactory() { + + override fun createWorker( + appContext: Context, + workerClassName: String, + workerParameters: WorkerParameters + ): ListenableWorker? { + val foundEntry = + workerFactories.entries.find { Class.forName(workerClassName).isAssignableFrom(it.key) } + val factoryProvider = foundEntry?.value + ?: throw IllegalArgumentException("unknown worker class name: $workerClassName") + return factoryProvider.get().create(appContext, workerParameters) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/SessionWorkerParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/SessionWorkerParams.kt new file mode 100644 index 0000000000..840cda3dec --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/SessionWorkerParams.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.worker + +/** + * Note about the Worker usage: + * The workers we chain, or when using the append strategy, should never return Result.Failure(), else the chain will be broken forever + */ +interface SessionWorkerParams { + val sessionId: String + + /** + * Null when no error occurs. When chaining Workers, first step is to check that there is no lastFailureMessage from the previous workers + * If it is the case, the worker should just transmit the error and shouldn't do anything else + */ + val lastFailureMessage: String? +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/Worker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/Worker.kt new file mode 100644 index 0000000000..9f40d6aa05 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/Worker.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.worker + +import androidx.work.ListenableWorker +import org.matrix.android.sdk.api.Matrix +import org.matrix.android.sdk.internal.session.SessionComponent + +internal fun ListenableWorker.getSessionComponent(sessionId: String): SessionComponent? { + return Matrix.getInstance(applicationContext).sessionManager.getSessionComponent(sessionId) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/WorkerParamsFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/WorkerParamsFactory.kt new file mode 100644 index 0000000000..2b7cba0b0c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/WorkerParamsFactory.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.android.sdk.internal.worker + +import androidx.work.Data +import org.matrix.android.sdk.internal.di.MoshiProvider + +object WorkerParamsFactory { + + const val KEY = "WORKER_PARAMS_JSON" + + inline fun toData(params: T): Data { + val moshi = MoshiProvider.providesMoshi() + val adapter = moshi.adapter(T::class.java) + val json = adapter.toJson(params) + return Data.Builder().putString(KEY, json).build() + } + + inline fun fromData(data: Data): T? { + val json = data.getString(KEY) + return if (json == null) { + null + } else { + val moshi = MoshiProvider.providesMoshi() + val adapter = moshi.adapter(T::class.java) + adapter.fromJson(json) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/androidsdk/crypto/data/MXDeviceInfo.java b/matrix-sdk-android/src/main/java/org/matrix/androidsdk/crypto/data/MXDeviceInfo.java new file mode 100755 index 0000000000..3811cf65cd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/androidsdk/crypto/data/MXDeviceInfo.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.androidsdk.crypto.data; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +/* + * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose + */ + +public class MXDeviceInfo implements Serializable { + private static final long serialVersionUID = 20129670646382964L; + + // This device is a new device and the user was not warned it has been added. + public static final int DEVICE_VERIFICATION_UNKNOWN = -1; + + // The user has not yet verified this device. + public static final int DEVICE_VERIFICATION_UNVERIFIED = 0; + + // The user has verified this device. + public static final int DEVICE_VERIFICATION_VERIFIED = 1; + + // The user has blocked this device. + public static final int DEVICE_VERIFICATION_BLOCKED = 2; + + /** + * The id of this device. + */ + public String deviceId; + + /** + * the user id + */ + public String userId; + + /** + * The list of algorithms supported by this device. + */ + public List algorithms; + + /** + * A map from : to >. + */ + public Map keys; + + /** + * The signature of this MXDeviceInfo. + * A map from : to >. + */ + public Map> signatures; + + /* + * Additional data from the home server. + */ + public Map unsigned; + + /** + * Verification state of this device. + */ + public int mVerified; + + /** + * Constructor + */ + public MXDeviceInfo() { + mVerified = DEVICE_VERIFICATION_UNKNOWN; + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/org/matrix/androidsdk/crypto/data/MXOlmInboundGroupSession2.java b/matrix-sdk-android/src/main/java/org/matrix/androidsdk/crypto/data/MXOlmInboundGroupSession2.java new file mode 100755 index 0000000000..51c1fe2f1e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/androidsdk/crypto/data/MXOlmInboundGroupSession2.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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. + */ + +package org.matrix.androidsdk.crypto.data; + +import org.matrix.olm.OlmInboundGroupSession; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/* + * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose + */ + +/** + * This class adds more context to a OLMInboundGroupSession object. + * This allows additional checks. The class implements NSCoding so that the context can be stored. + */ +public class MXOlmInboundGroupSession2 implements Serializable { + // define a serialVersionUID to avoid having to redefine the class after updates + private static final long serialVersionUID = 201702011617L; + + // The associated olm inbound group session. + public OlmInboundGroupSession mSession; + + // The room in which this session is used. + public String mRoomId; + + // The base64-encoded curve25519 key of the sender. + public String mSenderKey; + + // Other keys the sender claims. + public Map mKeysClaimed; + + // Devices which forwarded this session to us (normally empty). + public List mForwardingCurve25519KeyChain = new ArrayList<>(); +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_airplane.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_airplane.xml new file mode 100644 index 0000000000..72026cd7a0 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_airplane.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_anchor.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_anchor.xml new file mode 100644 index 0000000000..b89d033b9e --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_anchor.xml @@ -0,0 +1,12 @@ + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_apple.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_apple.xml new file mode 100644 index 0000000000..54e0f9a3c0 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_apple.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_ball.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_ball.xml new file mode 100644 index 0000000000..b12c6d245b --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_ball.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_banana.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_banana.xml new file mode 100644 index 0000000000..cdd3cb1b9f --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_banana.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_bell.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_bell.xml new file mode 100644 index 0000000000..2f29828bcf --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_bell.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_bicycle.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_bicycle.xml new file mode 100644 index 0000000000..1427e793c5 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_bicycle.xml @@ -0,0 +1,27 @@ + + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_book.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_book.xml new file mode 100644 index 0000000000..8e3ecc00c0 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_book.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_butterfly.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_butterfly.xml new file mode 100644 index 0000000000..d4b557a7ed --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_butterfly.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_cactus.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_cactus.xml new file mode 100644 index 0000000000..ce8aff0657 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_cactus.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_cake.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_cake.xml new file mode 100644 index 0000000000..9ebb3c0904 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_cake.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_cat.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_cat.xml new file mode 100644 index 0000000000..b34cf63d98 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_cat.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_clock.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_clock.xml new file mode 100644 index 0000000000..48d7150c36 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_clock.xml @@ -0,0 +1,27 @@ + + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_cloud.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_cloud.xml new file mode 100644 index 0000000000..d390bd6e87 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_cloud.xml @@ -0,0 +1,12 @@ + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_corn.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_corn.xml new file mode 100644 index 0000000000..d863d03c2a --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_corn.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_dog.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_dog.xml new file mode 100644 index 0000000000..8346a5ebee --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_dog.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_elephant.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_elephant.xml new file mode 100644 index 0000000000..d0a2de42cb --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_elephant.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_fire.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_fire.xml new file mode 100644 index 0000000000..ebf42039b1 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_fire.xml @@ -0,0 +1,12 @@ + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_fish.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_fish.xml new file mode 100644 index 0000000000..30907f2496 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_fish.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_flag.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_flag.xml new file mode 100644 index 0000000000..250388dc4a --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_flag.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_flower.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_flower.xml new file mode 100644 index 0000000000..8a91221a80 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_flower.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_folder.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_folder.xml new file mode 100644 index 0000000000..9320766492 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_folder.xml @@ -0,0 +1,12 @@ + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_gift.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_gift.xml new file mode 100644 index 0000000000..d18c6e860a --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_gift.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_glasses.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_glasses.xml new file mode 100644 index 0000000000..8913d1ffd7 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_glasses.xml @@ -0,0 +1,12 @@ + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_globe.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_globe.xml new file mode 100644 index 0000000000..2a07829cb3 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_globe.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_guitar.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_guitar.xml new file mode 100644 index 0000000000..2622fbe416 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_guitar.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_hammer.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_hammer.xml new file mode 100644 index 0000000000..7b70654d52 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_hammer.xml @@ -0,0 +1,12 @@ + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_hat.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_hat.xml new file mode 100644 index 0000000000..15f980bdb1 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_hat.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_headphone.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_headphone.xml new file mode 100644 index 0000000000..cbc43e7601 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_headphone.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_heart.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_heart.xml new file mode 100644 index 0000000000..d37bcc33d1 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_heart.xml @@ -0,0 +1,9 @@ + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_horse.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_horse.xml new file mode 100644 index 0000000000..bedf0f6f46 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_horse.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_hourglass.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_hourglass.xml new file mode 100644 index 0000000000..8bb37a35bb --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_hourglass.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_key.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_key.xml new file mode 100644 index 0000000000..4cd1d033f7 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_key.xml @@ -0,0 +1,9 @@ + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_light_bulb.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_light_bulb.xml new file mode 100644 index 0000000000..18f3149500 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_light_bulb.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_lion.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_lion.xml new file mode 100644 index 0000000000..b97a508fc2 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_lion.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_lock.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_lock.xml new file mode 100644 index 0000000000..de3979434f --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_lock.xml @@ -0,0 +1,12 @@ + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_moon.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_moon.xml new file mode 100644 index 0000000000..3f5abe6ae3 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_moon.xml @@ -0,0 +1,12 @@ + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_mushroom.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_mushroom.xml new file mode 100644 index 0000000000..72f7036856 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_mushroom.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_octopus.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_octopus.xml new file mode 100644 index 0000000000..054760f3b8 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_octopus.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_panda.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_panda.xml new file mode 100644 index 0000000000..ab1e718c44 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_panda.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_paperclip.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_paperclip.xml new file mode 100644 index 0000000000..e8f89859d6 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_paperclip.xml @@ -0,0 +1,9 @@ + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_pencil.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_pencil.xml new file mode 100644 index 0000000000..3b9f51fca5 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_pencil.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_penguin.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_penguin.xml new file mode 100644 index 0000000000..fb2e05760f --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_penguin.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_phone.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_phone.xml new file mode 100644 index 0000000000..7beda09c4e --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_phone.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_pig.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_pig.xml new file mode 100644 index 0000000000..c31bd06c52 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_pig.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_pin.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_pin.xml new file mode 100644 index 0000000000..f10e4606a9 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_pin.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_pizza.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_pizza.xml new file mode 100644 index 0000000000..a514aeb3d6 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_pizza.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_rabbit.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_rabbit.xml new file mode 100644 index 0000000000..c8ff75c999 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_rabbit.xml @@ -0,0 +1,27 @@ + + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_robot.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_robot.xml new file mode 100644 index 0000000000..a53cfe99c0 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_robot.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_rocket.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_rocket.xml new file mode 100644 index 0000000000..4097ed9030 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_rocket.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_rooster.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_rooster.xml new file mode 100644 index 0000000000..cb7ad563f0 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_rooster.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_santa.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_santa.xml new file mode 100644 index 0000000000..4f7bc1a24f --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_santa.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_scissors.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_scissors.xml new file mode 100644 index 0000000000..98e68c2071 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_scissors.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_smiley.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_smiley.xml new file mode 100644 index 0000000000..087adc8c6d --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_smiley.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_strawberry.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_strawberry.xml new file mode 100644 index 0000000000..0eeb290d9d --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_strawberry.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_thumbs_up.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_thumbs_up.xml new file mode 100644 index 0000000000..9761204ab6 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_thumbs_up.xml @@ -0,0 +1,12 @@ + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_train.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_train.xml new file mode 100644 index 0000000000..e317ce1642 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_train.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_tree.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_tree.xml new file mode 100644 index 0000000000..c5acc19a72 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_tree.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_trophy.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_trophy.xml new file mode 100644 index 0000000000..631da7320d --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_trophy.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_trumpet.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_trumpet.xml new file mode 100644 index 0000000000..84f95a8592 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_trumpet.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_turtle.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_turtle.xml new file mode 100644 index 0000000000..1cedc1b6ad --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_turtle.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_umbrella.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_umbrella.xml new file mode 100644 index 0000000000..ac1267cd3b --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_umbrella.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_unicorn.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_unicorn.xml new file mode 100644 index 0000000000..19cef5d339 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_unicorn.xml @@ -0,0 +1,30 @@ + + + + + + + + + + diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_wrench.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_wrench.xml new file mode 100644 index 0000000000..ba3c4313a3 --- /dev/null +++ b/matrix-sdk-android/src/main/res/drawable/ic_verification_wrench.xml @@ -0,0 +1,9 @@ + + + diff --git a/matrix-sdk-android/src/main/res/values-ar/strings.xml b/matrix-sdk-android/src/main/res/values-ar/strings.xml new file mode 100644 index 0000000000..e9aba1721a --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-ar/strings.xml @@ -0,0 +1,82 @@ + + + + أرسل ⁨%1$s⁩ صورة. + + دعوة من ⁨%s⁩ + دعى ⁨%1$s⁩ ⁨%2$s⁩ + دعاك ⁨%1$s⁩ + انضمّ ⁨%1$s⁩ + غادر ⁨%1$s⁩ + رفض ⁨%1$s⁩ الدعوة + طرد ⁨%1$s⁩ ⁨%2$s⁩ + رفع ⁨%1$s⁩ الحظر عن ⁨%2$s⁩ + منع ⁨%1$s⁩ ⁨%2$s⁩ + غيّر ⁨%1$s⁩ صورته + ضبط ⁨%1$s⁩ اسم العرض على ⁨%2$s⁩ + غيّر ⁨%1$s⁩ اسم الحساب المعروض من %2$s⁩ إلى %3$s⁩ + أزال ⁨%1$s⁩ اسم الحساب المعروض (⁨%2$s⁩) + غيّر ⁨%1$s⁩ الموضوع إلى: ⁨%2$s⁩ + غيّر ⁨%1$s⁩ اسم الغرفة إلى: ⁨%2$s⁩ + ردّ ⁨%s⁩ على المكالمة. + أنهى ⁨%s⁩ المكالمة. + جعل ⁨%1$s⁩ تأريخ الغرفة مستقبلًا ظاهرا على %2$s + كل أعضاء الغرفة من لحظة دعوتهم. + كل أعضاء الغرفة من لحظة انضمامهم. + كل أعضاء الغرفة. + الكل. + المجهول (⁨%s⁩). + فعّل ⁨%1$s⁩ تعمية الطرفين (⁨%2$s⁩) + + طلب ⁨%1$s⁩ اجتماع VoIP + بدأ اجتماع VoIP + انتهى اجتماع VoIP + + أزال ⁨%1$s⁩ اسم الغرفة + أزال ⁨%1$s⁩ موضوع الغرفة + حدّث ⁨%1$s⁩ اللاحة ⁨%2$s⁩ + أرسل ⁨%1$s⁩ دعوة إلى ⁨%2$s⁩ للانضمام إلى الغرفة + ** تعذّر فك التعمية: ⁨%s⁩ ** + لم يُرسل جهاز المرسل مفاتيح هذه الرسالة. + + تعذّر إرسال الرسالة + + فشل رفع الصورة + + خطأ في الشبكة + خطأ في «ماترِكس» + + ليس ممكنا الانضمام ثانيةً إلى غرفة فارغة. + + رسالة معمّاة + + عنوان البريد الإلكتروني + رقم الهاتف + + ‏‏⁨%1$s⁩: ‏⁨%2$s⁩ + انسحب ⁨%1$s⁩ من الدعوة ⁨%2$s⁩ + أجرى ⁨%s⁩ مكالمة مرئية. + أجرى ⁨%s⁩ مكالمة صوتية. + قبل ⁨%1$s⁩ دعوة ⁨%2$s⁩ + + تعذر التهذيب + أرسل ⁨%1$s⁩ ملصقا. + + (تغيّرت الصورة أيضا) + + دعوة من ⁨%s⁩ + غرفة فارغة + + ‏⁨%1$s⁩ و ⁨%2$s⁩ + دعوة إلى غرفة + + + صفر + واحد + اثنان + قليل + كثير + اخرى + + + diff --git a/matrix-sdk-android/src/main/res/values-az/strings.xml b/matrix-sdk-android/src/main/res/values-az/strings.xml new file mode 100644 index 0000000000..9c60dfafa7 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-az/strings.xml @@ -0,0 +1,182 @@ + + + %1$s: %2$s + %1$s şəkil göndərdi. + %1$s stiker göndərdi. + + %s-nin dəvəti + %1$s dəvət etdi %2$s + %1$s sizi dəvət etdi + %1$s qoşuldu + %1$s qalıb + %1$s dəvəti rədd etdi + %1$s %2$s-i xaric etdi + %1$s %2$s-i blokdan açdı + %1$s %2$s-i blokladı + %1$s %2$s-in dəvətini geri götürdü + %1$s avatarı dəyişdi + %1$s ekran adını %2$s olaraq təyin etdi + %1$s ekran adını %2$s-dan %3$s-ya dəyişdi + %1$s onların göstərilən adlarını sildi (%2$s) + %1$s mövzunu dəyişdi: %2$s + %1$s otaq adını dəyişdirdi: %2$s + %s video zəng etdi. + %s səsli zəng etdi. + %s zəngə cavab verdi. + %s zəng başa çatdı. + "%1$s gələcək otaq tarixçəsini %2$s-ə görünən etdi" + bütün otaq üzvləri, dəvət olunduğu andan. + bütün otaq üzvləri, qoşulduğu andan. + bütün otaq üzvləri. + hər kəs. + naməlum (%s). + %1$s sondan-sona şifrələmə açdı (%2$s) + %s bu otağı təkmilləşdirdi. + + %1$s VoIP konfrans istədi + VoIP konfransı başladı + VoIP konfransı başa çatdı + + (avatar da dəyişdirilib) + %1$s otaq adını sildi + %1$s otaq mövzusunu sildi + Mesaj silindi + Mesaj %1$s tərəfindən silindi + Mesaj silindi [səbəb: %1$s] + Mesaj %1$s tərəfindən qaldırıldı [səbəb: %2$s] + %1$s profilini %2$s yenilədi + %1$s otağa qoşulmaq üçün %2$s dəvətnamə göndərdi + %1$s otağa qoşulmaq üçün %2$s dəvətini ləğv etdi + %1$s %2$s üçün dəvəti qəbul etdi + + ** Şifrəni aça bilmir: %s ** + Göndərənin cihazı bu mesaj üçün açarları bizə göndərməyib. + + Redaktə etmək olmur + Mesaj göndərmək olmur + + Şəkil yükləmək olmur + + Şəbəkə xətası + Matris xətası + + Boş bir otağa yenidən qoşulmaq hazırda mümkün deyil. + + Şifrəli mesaj + + Elektron poçt ünvanı + Telefon nömrəsi + + %s-dən dəvət + Otağa dəvət + + %1$s və %2$s + + + %1$s və 1 digər + %1$s və %2$d digərləri + + + Boş otaq + + + It + Pişik + Aslan + At + Kərgədan + Donuz + Fil + Dovşan + Panda + Xoruz + Pinqvin + Tısbağa + Balıq + Ahtapot + Kəpənək + Çiçək + Ağac + Kaktus + Göbələk + Qlobus + Ay + Bulud + Atəş + Banan + Alma + Çiyələk + Qarğıdalı + Pizza + Tort + Ürək + Təbəssüm + Robot + Papaq + Eynəklər + Açar + Santa + Baş barmaqlar yuxarı + Çətir + Qum saatı + Saat + Hədiyyə + Lampa + Kitab + Qələm + Kağız sancağı + Qayçı + Qıfıl + Açar + Çəkic + Telefon + Bayraq + Qatar + Velosiped + Təyyarə + Raket + Kubok + Top + Gitara + Saz + Zəng + Anker + Qulaqlıqlar + Qovluq + Sancaq + + İlkin sinxronizasiya: +\nHesab idxal olunur… + İlkin sinxronizasiya: +\nKriptografiyanın idxalı + İlkin sinxronizasiya: +\nOtaqlar idxalı + İlkin sinxronizasiya: +\nOtaqlara daxil olmaq + İlkin sinxronizasiya: +\nDəvət olunmuş otaqların idxalı + İlkin sinxronizasiya: +\nTərk olunmuş otaqların idxalı + İlkin sinxronizasiya: +\nİcmaların idxalı + İlkin sinxronizasiya: +\nHesab məlumatlarının idxalı + + Mesaj göndərilir… + Göndərmə növbəsini təmizləyin + + %1$s-nin dəvəti. Səbəb: %2$s + %1$s dəvət olunmuş %2$s. Səbəb: %3$s + %1$s sizi dəvət etdi. Səbəb: %2$s + %1$s qoşuldu. Səbəb: %2$s + %1$s qalıb. Səbəb: %2$s + %1$s dəvəti rədd etdi. Səbəb: %2$s + %1$s %2$s-i xaric etdi. Səbəb: %3$s + %1$s blokdan açdı %2$s. Səbəb: %3$s + %1$s blokladı %2$s. Səbəb: %3$s + %1$s otağa qoşulmaq üçün %2$s dəvətnamə göndərdi. Səbəb: %3$s + %1$s otağa qoşulmaq üçün %2$s dəvətini ləğv etdi. Səbəb: %3$s + %1$s %2$s üçün dəvəti qəbul etdi. Səbəb: %3$s + %1$s %2$s dəvətini geri götürdü. Səbəb: %3$s + + diff --git a/matrix-sdk-android/src/main/res/values-bg/strings.xml b/matrix-sdk-android/src/main/res/values-bg/strings.xml new file mode 100644 index 0000000000..07d59852f3 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-bg/strings.xml @@ -0,0 +1,207 @@ + + + + %1$s: %2$s + %1$s изпрати снимка. + + Поканата на %s + %1$s покани %2$s + %1$s Ви покани + %1$s се присъедини в стаята + %1$s напусна стаята + %1$s отхвърли поканата + %1$s изгони %2$s + %1$s отблокира %2$s + %1$s блокира %2$s + %1$s оттегли поканата си за %2$s + %1$s смени своята профилна снимка + %1$s си сложи име %2$s + %1$s смени своето име от %2$s на %3$s + %1$s премахна своето име (%2$s) + %1$s смени темата на: %2$s + %1$s смени името на стаята на: %2$s + %s започна видео разговор. + %s започна гласов разговор. + %s отговори на повикването. + %s прекрати разговора. + %1$s направи бъдещата история на стаята видима за %2$s + всички членове, от момента на поканването им в нея. + всички членове, от момента на присъединяването им в нея. + всички членове в нея. + всеки. + непозната (%s). + %1$s включи шифроване от край до край (%2$s) + + %1$s заяви VoIP групов разговор + Започна VoIP групов разговор + Груповият разговор приключи + + (профилната снимка също беше сменена) + %1$s премахна името на стаята + %1$s премахна темата на стаята + %1$s обнови своя профил %2$s + %1$s изпрати покана на %2$s да се присъедини към стаята + %1$s прие поканата за %2$s + + ** Неуспешно разшифроване: %s ** + Неуспешно премахване + Неуспешно изпращане на съобщението + + Неуспешно качване на снимката + + Грешка в мрежата + Matrix грешка + + В момента не е възможно да се присъедините отново към празна стая. + + Шифровано съобщение + + Имейл адрес + Телефонен номер + + Устройството на подателя не изпрати ключовете за това съобщение. + + %1$s изпрати стикер. + + Покана от %s + Покана за стая + %1$s и %2$s + + + %1$s и 1 друг + %1$s и %2$d други + + + Празна стая + + Премахнато съобщение + Съобщение премахнато от %1$s + Премахнато съобщение [причина: %1$s] + Съобщение премахнато от %1$s [причина: %2$s] + Куче + Котка + Лъв + Кон + Еднорог + Прасе + Слон + Заек + Панда + Петел + Пингвин + Костенурка + Риба + Октопод + Пеперуда + Цвете + Дърво + Кактус + Гъба + Глобус + Луна + Облак + Огън + Банан + Ябълка + Ягода + Царевица + Пица + Торта + Сърце + Усмивка + Робот + Шапка + Очила + Гаечен ключ + Дядо Коледа + Палец нагоре + Чадър + Пясъчен часовник + Часовник + Подарък + Лампа + Книга + Молив + Кламер + Ножици + Катинар + Ключ + Чук + Телефон + Знаме + Влак + Колело + Самолет + Ракета + Трофей + Топка + Китара + Тромпет + Звънец + Котва + Слушалки + Папка + Карфица + + Начална синхронизация: +\nИмпортиране на профил… + Начална синхронизация: +\nИмпортиране на данни за шифроване + Начална синхронизация: +\nИмпортиране на стаи + Начална синхронизация: +\nИмпортиране на стаи, от които съм част + Начална синхронизация: +\nИмпортиране на стаи, към които съм поканен + Начална синхронизация: +\nИмпортиране на стаи, които съм напуснал + Начална синхронизация: +\nИмпортиране на общности + Начална синхронизация: +\nИмпортиране на данни за профила + + %s обнови тази стая. + + Изпращане на съобщение… + Изчисти опашката за изпращане + + %1$s оттегли поканата за присъединяване на %2$s към стаята + поканата на %1$s. Причина: %2$s + %1$s покани %2$s. Причина: %3$s + %1$s ви покани. Причина: %2$s + %1$s се присъедини в стаята. Причина: %2$s + %1$s напусна стаята. Причина: %2$s + %1$s отхвърли поканата. Причина: %2$s + %1$s изгони %2$s. Причина: %3$s + %1$s блокира %2$s. Причина: %3$s + %1$s блокира %2$s. Причина: %3$s + %1$s изпрати покана до %2$s да се присъедини в стаята. Причина: %3$s + %1$s премахна поканата за присъединяване на %2$s в стаята. Причина: %3$s + %1$s прие поканата за %2$s. Причина: %3$s + %1$s оттегли поканата на %2$s. Причина: %3$s + + + %1$s добави %2$s като адрес за тази стая. + %1$s добави %2$s като адреси за тази стая. + + + + %1$s премахна %2$s като адрес за тази стая. + %1$s премахна %2$s като адреси за тази стая. + + + %1$s добави %2$s и премахна %3$s като адреси за тази стая. + + %1$s настрой %2$s като основен адрес за тази стая. + %1$s премахна основния адрес за тази стая. + + %1$s разреши на гости да се присъединяват в стаята. + %1$s предотврати присъединяването на гости в стаята. + + %1$s включи шифроване от-край-до-край. + %1$s включи шифроване от-край-до-край (неразпознат алгоритъм %2$s). + + %s изпрати запитване за потвърждение на ключа ви, но клиентът ви не поддържа верифициране посредством чат. Ще трябва да използвате стария метод за верифициране на ключове. + + %1$s създаде стаята + diff --git a/matrix-sdk-android/src/main/res/values-bn-rIN/strings.xml b/matrix-sdk-android/src/main/res/values-bn-rIN/strings.xml new file mode 100644 index 0000000000..805d13a62a --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-bn-rIN/strings.xml @@ -0,0 +1,295 @@ + + + %1$s একটি ফটো পাঠিয়েছে। + %1$s একটি স্তিকার পাঠিয়েছে। + + %s এর আমন্ত্রণ + %1$s %2$s কে আমন্ত্রণ করেছে + %1$s আপনাকে আমন্ত্রণ করেছে + %1$s রুম এ যোগ দিয়েছে + %1$s রুম ছেড়ে দিয়েছে + %1$s আমন্ত্রণ টি বাতিল করেছে + %1$s %2$s কে কিক করেছে + %1$s %2$s কে নিষিদ্ধ তালিকা থেকে মুক্ত করেছে + %1$s %2$s কে নিষিদ্ধ করেছে + %1$s %2$s এর আমন্ত্রণ ফেরত নিয়েছে + %1$s নিজের অবতার পরিবর্তন করেছে + %1$s নিজের প্রদর্শন নাম %2$s রেখেছে + %1$s নিজের প্রদর্শন নাম %2$s থেকে %3$s তে পরিবর্তন করেছে + %1$s নিজের প্রদর্শন নাম মুছে দিয়েছে (%2$s) + %1$s বিষয় টি এতে পরিবর্তন করেছে: %2$s + %1$s রুম এর নাম এতে পরিবর্তন করেছে: %2$s + %s একটি ভিডিও কল স্থাপন করেছিল। + %s একটি ভয়েস কল দিয়েছে। + %1$s: %2$s + আপনি একটি ছবি প্রেরণ করেছেন। + আপনি একটি স্তিকার পাঠিয়েছেন। + + আপনার আমন্ত্রণ + %1$s কক্ষটি তৈরি করেছেন + আপনি কক্ষটি তৈরি করেছেন + আপনি %1$s কে আমন্ত্রিত করেছেন + আপনি কক্ষে যোগ দিয়েছেন + আপনি কক্ষ ছেড়ে দিয়েছেন + আপনি আমন্ত্রণটি বাতিল করেছেন + আপনি %1$s কে কীক করেছেন + আপনি %1$s কে নিষিদ্ধ মুক্ত করেছেন + আপনি %1$s কে নিষিদ্ধ করেছেন + আপনি %1$s এর আমন্ত্রণ প্রত্যাহার করেছেন + আপনি আপনার অবতারটি পরিবর্তন করেছেন + আপনি আপনার প্রদর্শনের নামটি %1$s তে সেট করেছেন + আপনি আপনার প্রদর্শনের নামটি %1$s থেকে %2$s এ পরিবর্তন করেছেন + আপনি আপনার প্রদর্শনের নামটি সরিয়ে দিয়েছেন (যেটা ছিল %1$s) + আপনি বিষয়টিকে এতে পরিবর্তন করেছেন: %1$s + %1$s কক্ষের অবতারটি পরিবর্তন করেছে + আপনি কক্ষের অবতারটি পরিবর্তন করেছেন + আপনি কক্ষের নাম এতে পরিবর্তন করেছেন:%1$s + আপনি একটি ভিডিও কল করেছেন। + আপনি একটি ভয়েস কল দিয়েছেন। + কল সেটআপ করার জন্য %s ডেটা প্রেরণ করেছে। + আপনি কল সেটআপ করার জন্য ডেটা প্রেরণ করেছেন। + %s কলটির উত্তর দিয়েছে। + আপনি কলটি উত্তর দিয়েছেন। + %s কলটি শেষ করেছেন। + আপনি কলটি শেষ করেছেন। + %1$s ভবিষ্যতের ঘরের ইতিহাস %2$s এর কাছে দৃশ্যমান করে তুলেছে + আপনি ভবিষ্যতের কক্ষ ইতিহাস %1$s এর কাছে দৃশ্যমান করেছেন + কক্ষের সমস্ত সদস্য, যখন থেকে তারা আমন্ত্রিত। + কক্ষের সমস্ত সদস্য, যখন থেকে তারা যোগদান করেছিল। + সমস্ত কক্ষের সদস্য। + যে কেউ। + অজানা (%s)। + %1$s এন্ড-টু-এন্ড এনক্রিপশন চালু করেছে (%2$s) + আপনি শেষ-থেকে-শেষ এনক্রিপশন চালু করেছেন (%1$s) + %s এই কক্ষটিকে আপগ্রেড করেছে। + আপনি এই কক্ষটি আপগ্রেড করেছেন। + + %1$s একটি ভিওআইপি সম্মেলনের জন্য অনুরোধ করেছে + আপনি একটি ভিওআইপি সম্মেলনের অনুরোধ করেছেন + ভিওআইপি সম্মেলন শুরু হয়েছে + ভিওআইপি সম্মেলন শেষ হয়েছে + + (আবতারটিও পরিবর্তন করা হয়েছিল) + %1$s কক্ষের নাম সরিয়েছে + আপনি কক্ষের নাম সরিয়েছেন + %1$s কক্ষের বিষয় মুছে ফেলেছে + আপনি কক্ষের বিষয়টিকে সরিয়ে দিয়েছেন + %1$s কক্ষের অবতার সরিয়ে নিয়েছে + আপনি কক্ষের অবতার সরিয়েছেন + বার্তা সরানো হয়েছে + %1$s দ্বারা বার্তা সরানো হয়েছে + বার্তা সরানো হয়েছে [কারণ:%1$s] + %1$s দ্বারা বার্তা সরানো হয়েছে [কারণ: %2$s] + %1$s তাদের প্রোফাইল %2$ আপডেট করেছে + আপনি আপনার প্রোফাইল %1$s আপডেট করেছেন + %1$s %2$s কে ঘরে যোগদানের জন্য একটি আমন্ত্রণ পাঠিয়েছে + আপনি %1$s কে ঘরে যোগদানের জন্য একটি আমন্ত্রণ প্রেরণ করেছেন + %1$s %2$s এর কক্ষে যোগদানের আমন্ত্রণ বাতিল করে দিয়েছিল + আপনি %1$s এর কক্ষে যোগদানের জন্য আমন্ত্রণটি বাতিল করেছেন + %1$s %2$s এর জন্য আমন্ত্রণটি গ্রহণ করেছে + আপনি %1$s এর জন্য আমন্ত্রণটি গ্রহণ করেছেন + + %1$s %2$s উইজেট যুক্ত করেছে + আপনি %1$s উইজেট যুক্ত করেছেন + %1$s %2$s উইজেট সরিয়ে দিয়েছেন + আপনি %1$s উইজেট সরিয়েছেন + %1$s %2$s উইজেট পরিবর্তন করেছেন + আপনি %1$s উইজেট পরিবর্তন করেছেন + + অ্যাডমিন + নিয়ামক + ডিফল্ট + কাস্টম (%1$d) + কাস্টম + + আপনি %1$s এর পাওয়ার স্তর পরিবর্তন করেছেন। + %1$s %2$s এর পাওয়ার স্তর পরিবর্তন করেছে। + %1$s %2$s থেকে %3$s পর্যন্ত + + ** ডিক্রিপ্ট করতে অক্ষম: %s ** + প্রেরকের ডিভাইস আমাদের এই বার্তার জন্য কীগুলি প্রেরণ করেনি। + + পুনরায় প্রতিক্রিয়া করতে পারেনি + বার্তা পাঠাতে অক্ষম + + চিত্র আপলোড করতে ব্যর্থ + + নেটওয়ার্ক ত্রুটি + ম্যাট্রিক্স ত্রুটি + + খালি কক্ষে পুনরায় যোগদান করা বর্তমানে সম্ভব নয়। + + এনক্রিপ্ট করা বার্তা + + ইমেল ঠিকানা + ফোন নম্বর + + %s থেকে আমন্ত্রণ করুন + কক্ষ আমন্ত্রণ + + %1$s এবং %2$s + + + %1$s এবং অন্য ১ জন + %1$s এবং অন্যান্য %2$d জন + + + খালি কক্ষ + + + কুকুর + বেড়াল + সিংহ + ঘোড়া + ইউনিকর্ন + শূকর + হাতি + খরগোশ + পান্ডা + গৃহপালিত মোরগ + পেংগুইন + কচ্ছপ + মাছ + অক্টোপাস + প্রজাপতি + ফুল + গাছ + ফণীমনসা + মাশরুম + পৃথিবী + চন্দ্র + মেঘ + আগুন + কলা + আপেল + স্ট্রবেরি + ভূট্টা + পিজা + কেক + হৃদয় + স্মাইলি + রোবট + টুপি + চশমা + রেঞ্চ + সান্তা + থাম্বস আপ + ছাতা + বালিঘড়ি + ঘড়ি + উপহার + আলো বালব + বই + পেন্সিল + পেপার ক্লিপ + কাঁচি + লক + চাবি + হাতুড়ি + টেলিফোন + পতাকা + রেলগাড়ি + সাইকেল + বিমান + রকেট + ট্রফি + বল + গিটার + ট্রাম্পেট + ঘণ্টা + নোঙ্গর + হেডফোন + ফোল্ডার + পিন + + প্রাথমিক সিঙ্ক: +\nঅ্যাকাউন্ট আমদানি করা হচ্ছে… + প্রাথমিক সিঙ্ক: +\nক্রিপ্টো আমদানি হচ্ছে + প্রাথমিক সিঙ্ক: +\nকক্ষগুলি আমদানি করা হচ্ছে + প্রাথমিক সিঙ্ক: +\nযোগ করা কক্ষগুলিতে আমদানি করা হিচ্ছে + প্রাথমিক সিঙ্ক: +\nআমন্ত্রিত করা কক্ষগুলিতে আমদানি করা হিচ্ছে + প্রাথমিক সিঙ্ক: +\nছেড়ে দেওয়া কক্ষগুলিতে আমদানি করা হিচ্ছে + প্রাথমিক সিঙ্ক: +\nসম্প্রদায়গুলি আমদানি করা হচ্ছে + প্রাথমিক সিঙ্ক: +\nঅ্যাকাউন্ট ডেটা আমদানি করা হচ্ছে + + বার্তা প্রেরণ করা হচ্ছে … + প্রেরণ সারি পরিষ্কার করুন + + %1$s এর আমন্ত্রণ। কারণ: %2$s + আপনার আমন্ত্রণ। কারণ: %1$s + %1$s আমন্ত্রিত করেছেন %2$s কে। কারণ: %3$s + আপনি %1$s কে আমন্ত্রিত করেছেন। কারণ: %2$s + %1$s আপনাকে আমন্ত্রণ করেছে। কারণ: %2$s + %1$s রুম এ যোগ দিয়েছে। কারণ: %2$s + আপনি কক্ষে যোগ দিয়েছেন। কারণ: %1$s + %1$s রুম ছেড়ে দিয়েছে। কারণ: %2$s + আপনি কক্ষ ছেড়ে দিয়েছেন। কারণ: %1$s + %1$s আমন্ত্রণ বাতিল করেছেন। কারণ: %2$s + আপনি আমন্ত্রণটি বাতিল করেছেন। কারণ: %1$s + %1$s %2$s কে কিক করেছে। কারণ: %2$s + আপনি %1$s কে কীক করেছেন। কারণ: %2$s + %1$s %2$s কে নিষিদ্ধ তালিকা থেকে মুক্ত করেছে। কারণ: %3$s + আপনি %1$s কে নিষিদ্ধ মুক্ত করেছেন। কারণ: %2$s + %1$s %2$s কে নিষিদ্ধ করেছে। কারণ: %3$s + আপনি %1$s কে নিষিদ্ধ করেছেন। কারণ: %2$s + %1$s রুমের সাথে যোগ দিতে %2$s কে একটি আমন্ত্রণ পাঠিয়েছেন। কারণ: %3$s + আপনি %1$s কে ঘরে যোগদানের জন্য একটি আমন্ত্রণ প্রেরণ করেছেন। কারণ: %2$s + %1$s %2$s এর কক্ষে যোগদানের আমন্ত্রণ বাতিল করে দিয়েছিল। কারণ: %3$s + আপনি %1$s এর কক্ষে যোগদানের জন্য আমন্ত্রণটি বাতিল করেছেন। কারণ: %2$s + %1$s %2$s এর জন্য আমন্ত্রণ গ্রহণ করেছেন। কারণ: %3$s + আপনি %1$s এর জন্য আমন্ত্রণটি গ্রহণ করেছেন। কারণ: %2$s + %1$s %2$s এর আমন্ত্রণ ফেরত নিয়েছে। কারণ: %3$s + আপনি %1$s এর আমন্ত্রণ প্রত্যাহার করেছেন। কারণ: %2$s + + + %1$s এই ঘরের ঠিকানা হিসাবে %2$s যুক্ত করেছে। + %1$s এই ঘরের ঠিকানাগুলি হিসাবে %2$s যুক্ত করেছে। + + + + আপনি এই কক্ষের জন্য ঠিকানা হিসাবে %1$s যুক্ত করেছেন। + আপনি এই কক্ষের ঠিকানা হিসাবে %1$s যুক্ত করেছেন। + + + + %1$s এই ঘরের ঠিকানা হিসাবে %2$s সরানো হয়েছে। + %1$s %3$s কে এই ঘরের ঠিকানা হিসাবে সরানো হয়েছে। + + + + আপনি এই ঘরের ঠিকানা হিসাবে %1$s সরিয়েছেন। + আপনি এই ঘরের ঠিকানা হিসাবে %2$s গুলি সরিয়েছেন। + + + %1$s %2$s যোগ করেছে এবং %3$s গুলি এই ঘরের ঠিকানা হিসাবে সরানো হয়েছে। + আপনি %1$s যোগ করেছেন এবং %2$s কে এই ঘরের ঠিকানা হিসাবে সরিয়ে দিয়েছেন। + + %1$s এই ঘরের মূল ঠিকানাটি %2$s তে সেট করে। + আপনি এই ঘরের মূল ঠিকানাটি %1$s তে সেট করেছেন। + %1$s এই ঘরের মূল ঠিকানা সরিয়ে নিয়েছে। + আপনি এই ঘরের মূল ঠিকানা সরিয়েছেন। + + %1$s অতিথিদের ঘরে যোগদানের অনুমতি দিয়েছে। + আপনি অতিথিদের ঘরে যোগদানের অনুমতি দিয়েছেন। + %1$s অতিথিদের ঘরে যোগদান করতে বাধা দিয়েছে। + আপনি অতিথিদের ঘরে যোগদান করতে বাধা দিয়েছেন। + + %1$s এন্ড-টু-এন্ড এনক্রিপশন চালু করেছে। + আপনি শেষ থেকে শেষ এনক্রিপশন চালু করেছেন। + %1$s এন্ড-টু-এন্ড এনক্রিপশন চালু করেছে (অজানা অ্যালগরিদম %2$s)। + আপনি শেষ-থেকে-শেষ এনক্রিপশন চালু করেছেন (অজানা অ্যালগরিদম %1$s )। + + %s আপনার কীটি যাচাই করার জন্য অনুরোধ করছে, তবে আপনার ক্লায়েন্ট ইন-চ্যাট কী যাচাইকরণ সমর্থন করে না। কীগুলি যাচাই করতে আপনাকে লিগ্যাসি কী যাচাইকরণ ব্যবহার করতে হবে। + + গ্রহণ + পতন + বন্ধ করুন + + diff --git a/matrix-sdk-android/src/main/res/values-bs/strings.xml b/matrix-sdk-android/src/main/res/values-bs/strings.xml new file mode 100644 index 0000000000..6a6ee46d32 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-bs/strings.xml @@ -0,0 +1,7 @@ + + + Pozovite iz %s + Poziv u Sobu + %1$s i %2$s + Prazna soba + \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-ca/strings.xml b/matrix-sdk-android/src/main/res/values-ca/strings.xml new file mode 100644 index 0000000000..2dc2206c8c --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-ca/strings.xml @@ -0,0 +1,79 @@ + + + %1$s: %2$s + %1$s ha enviat una imatge. + + %1s ha sortit + %1s ha entrat + Número de telèfon + + Correu electrònic + Missatge encriptat + + la invitació de %s + %1$s ha convidat a %2$s + %1$s us ha convidat + %1$s ha rebutjat la invitació + %1$s ha fet fora a %2$s + + + %1$s ha canviat el seu nom visible de %2$s a %3$s + %1$s ha eliminat el seu nom visible (%2$s) + %1$s ha canviat el tema a: %2$s + %1$s ha canviat el nom de la sala a: %2$s + %s ha contestat la trucada. + %s ha finalitzat la trucada. + tots el membres de la sala, des del punt en què són convidats. + tots els membres de la sala. + desconegut (%s). + %1$s ha activat l\'encriptació d\'extrem a extrem (%2$s) + + %1$s ha sol·licitat una conferència VoIP + %1$s ha readmès a %2$s + %1$s ha vetat a %2$s + %1$s ha retirat la invitació de %2$s + %1$s ha canviat el seu avatar + %1$s ha permès a %2$s veure l\'historial que es generi a partir d\'ara + tots els membres de la sala, des del punt en què hi entrin. + qualsevol. + S\'ha iniciat la conferència VoIP + S\'ha finalitzat la conferència de veu IP + + (s\'ha canviat també l\'avatar) + %1$s ha eliminat el nom de la sala + %1$s ha eliminat el tema de la sala + %1$s ha actualitzat el seu perfil %2$s + %1$s ha enviat una invitació a %2$s per a entrar a la sala + %1$s ha acceptat la invitació per a %2$s + + ** No s\'ha pogut desencriptar: %s ** + El dispositiu del remitent no ens ha enviat les claus per aquest missatge. + + No s\'ha pogut redactar + No s\'ha pogut enviar el missatge + + No s\'ha pogut pujar la imatge + + S\'ha produït un error de xarxa + S\'ha produït un error de Matrix + + Actualment no es pot tornar a entrar a una sala buida. + + %1$s a canviat el seu nom visible a %2$s + %s ha iniciat una trucada de vídeo. + %s ha iniciat una trucada de veu. + + + Convidat per %s + Convideu a la sala + %1$s i %2$s + Sala buida + + %1$s i 1 altre + %1$s i %2$d altres + + + + %1$s ha enviat un adhesiu. + + diff --git a/matrix-sdk-android/src/main/res/values-cs/strings.xml b/matrix-sdk-android/src/main/res/values-cs/strings.xml new file mode 100644 index 0000000000..44908c38f7 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-cs/strings.xml @@ -0,0 +1,171 @@ + + + %1$s: %2$s + Uživatel %1$s poslal obrázek. + Uživatel %1$s poslal nálepku. + + Pozvánka od uživatele %s + Uživatel %1$s pozval uživatele %2$s + Uživatel %1$s vás pozval + Uživatel %1$s se připojil + Uživatel %1$s odešel + Uživatel %1$s odmítl pozvání + Uživatel %1$s vykopl uživatele %2$s + Uživatel %1$s znovu povolil vstup uživateli %2$s + Uživatel %1$s vykázal uživatele %2$s + Uživatel %1$s zrušil pozvání pro uživatele %2$s + Uživatel %1$s změnil svůj profilový obrázek + Uživatel %1$s nastavil své zobrazované jméno na %2$s + Uživatel %1$s změnil své zobrazované jméno z %2$s na %3$s + Uživatel %1$s odstranil své zobrazované jméno (%2$s) + Uživatel %1$s změnil téma na: %2$s + Uživatel %1$s změnil název místnosti na: %2$s + Uživatel %s uskutečnil videohovor. + Uživatel %s uskutečnil hlasový hovor. + Uživatel %s přijal hovor. + Uživatel %s ukončil hovor. + Uživatel %1$s nastavit viditelnost budoucích zpráv v místnosti pro %2$s + všechny členy místnosti od chvíle, kdy budou pozváni. + všechny členy místnosti od chvíle, kdy se připojí. + všechny členy místnosti. + kohokoliv. + neznámým (%s). + Uživatel %1$s zapnul end-to-end šifrování (%2$s) + + Uživatel %1$s požádal o VoIP konferenci + Začala VoIP konference + VoIP konference skončila + + (profilový obrázek byl také změněn) + Uživatel %1$s odstranil název místnosti + Uživatel %1$s odstranil téma místnosti + Uživatel %1$s aktualizoval svůj profil %2$s + Uživatel %1$s do této místnosti pozval uživatele %2$s + Uživatel %1$s přijal pozvání pro %2$s + + ** Nelze dešifrovat: %s ** + Odesílatelovo zařízení neposlalo klíče pro tuto zprávu. + + Nelze vymazat + Zprávu nelze odeslat + + Obrázek nelze nahrát + + Chyba sítě + Chyba v Matrixu + + V současnosti není možné se znovu připojit do prázdné místnosti. + + Šifrovaná zpráva + + E-mailová adresa + Telefonní číslo + + Pozvání od %s + Pozvání do místnosti + + %1$s a %2$s + + + %1$s a jeden další + %1$s a %2$d další + %1$s a %2$d dalších + + + Prázdná místnost + + Uživatel %s upgradoval tuto místnost. + + Zpráva byla smazána [důvod: %1$s] + Zpráva smazána uživatelem %1$s [důvod: %2$s] + Uživatel %1$s obnovil pozvánku do místnosti pro uživatele %2$s + Kočka + Lev + Kůň + Jednorožec + Prase + Slon + Králík + Panda + Kohout + Tučňák + Želva + Ryba + Chobotnice + Motýl + Květina + Strom + Kaktus + Houba + Zeměkoule + Měsíc + Mrak + Oheň + Banán + Jablko + Jahoda + Kukuřice + Pizza + Dort + Srdce + Smajlík + Robot + Klobouk + Brýle + Santa Klaus + Zvednutý palec + Deštník + Přesípací hodiny + Hodiny + Dárek + Žárovka + Kniha + Tužka + Sponka + Nůžky + Zámek + Klíč + Kladivo + Telefon + Vlajka + Vlak + Jízdní kolo + Letadlo + Raketa + Trofej + Míč + Kytara + Trumpeta + Zvon + Kotva + Sluchátka + Desky + Úvodní synchronizace: +\nImport účtu… + Úvodní synchronizace: +\nImport klíčů + Úvodní synchronizace: +\nImport místností + Úvodní synchronizace: +\nImport místností, kterými jste členy + Úvodní synchronizace: +\nImport opuštěných místností + Úvodní synchronizace: +\nImport skupin + Úvodní synchronizace: +\nImport dat účtu + + Odesílání zprávy… + Maticový klíč + Připínáček + + Úvodní synchronizace: +\nImport pozvánek + Vymazat frontu neodeslaných zpráv + + Uživatel %1$s pozval uživatele %2$s. Důvod: %3$s + Uživatel %1$s váš pozval. Důvod: %2$s + Uživatel %1$s odešel. Důvod: %2$s + Zpráva odstraněna + Zprávu odstranil/a %1$s + diff --git a/matrix-sdk-android/src/main/res/values-da/strings.xml b/matrix-sdk-android/src/main/res/values-da/strings.xml new file mode 100644 index 0000000000..510fa231af --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-da/strings.xml @@ -0,0 +1,75 @@ + + + + %1$s: %2$s + %1$s sendte et billede. + + %ss invitation + %1$s inviterede %2$s + %1$s inviterede dig + %1$s forbandt + %1$s forlod rummet + %1$s afviste invitationen + %1$s kickede %2$s + %1$s unbannede %2$s + %1$s bannede %2$s + %1$s trak %2$ss invitation tilbage + %1$s skiftede sin avatar + %1$s satte sit viste navn til %2$s + %1$s ændrede sit viste navn fra %2$s til %3$s + %1$s fjernede sit viste navn (%2$s) + %1$s ændrede emnet til: %2$s + %1$s ændrede rumnavnet til: %2$s + %s startede et videoopkald. + %s startede et stemmeopkald. + %s svarede opkaldet. + %s stoppede opkaldet. + %1$s gjorde den fremtidige rum historik synlig for %2$s + alle medlemmer af rummet, fra det tidspunkt de er inviteret. + alle medlemmer af rummet, fra det tidspunkt de er forbundede. + Alle medlemmer af rummet. + alle. + ukendt (%s). + %1$s slog ende-til-ende kryptering til (%2$s) + + %1$s forespurgte en VoIP konference + VoIP konference startet + VoIP konference afsluttet + + (avatar blev også ændret) + %1$s fjernede navnet på rummet + %1$s fjernede emnet for rummet + %1$s opdaterede sin profil %2$s + %1$s inviterede %2$s til rummet + %1$s accepterede invitationen til %2$s + + ** Kunne ikke dekryptere: %s ** + Afsenderens enhed har ikke sendt os nøglerne til denne besked. + + Kunne ikke hemmeligholde + Kunne ikke sende besked + + Kunne ikke uploade billede + + Netværks fejl + Matrix fejl + + Det er i øjeblikket ikke muligt at genforbinde til et tomt rum. + + Krypteret besked + + mailadresse + Telefonnummer + + Invitation fra %s + Invitation til rum + %1$s og %2$s + + + %1$s og 1 anden + %1$s og %2$d andre + + + Tomt rum + + diff --git a/matrix-sdk-android/src/main/res/values-de/strings.xml b/matrix-sdk-android/src/main/res/values-de/strings.xml new file mode 100644 index 0000000000..7ec9240067 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-de/strings.xml @@ -0,0 +1,306 @@ + + + + %1$s: %2$s + %1$s hat ein Bild gesendet. + + Einladung von %s + %1$s hat %2$s eingeladen + %1$s hat dich eingeladen + %1$s hat den Raum betreten + %1$s hat den Raum verlassen + %1$s hat die Einladung abgelehnt + %1$s hat %2$s gekickt + %1$s hat die Sperre von %2$s aufgehoben + %1$s hat %2$s verbannt + %1$s hat die Einladung für %2$s zurückgezogen + %1$s hat das Profilbild geändert + %1$s hat den Anzeigenamen geändert in %2$s + %1$s hat den Anzeigenamen von %2$s auf %3$s geändert + %1$s hat den Anzeigenamen gelöscht (%2$s) + %1$s hat das Raumthema geändert auf: %2$s + %1$s hat den Raumnamen geändert in: %2$s + %s hat einen Videoanruf durchgeführt. + %s hat einen Sprachanruf getätigt. + %s hat den Anruf angenommen. + %s hat den Anruf beendet. + %1$s hat den zukünftigen Chatverlauf sichtbar gemacht für %2$s + Alle Mitglieder (ab dem Zeitpunkt, an dem sie eingeladen wurden). + Alle Mitglieder (ab dem Zeitpunkt, an dem sie den Raum betreten haben). + alle Raum-Mitglieder. + Jeder. + Unbekannt (%s). + %1$s hat die Ende-zu-Ende-Verschlüsselung aktiviert (%2$s) + + %1$s möchte eine VoIP-Konferenz beginnen + VoIP-Konferenz gestartet + VoIP-Konferenz beendet + + (Profilbild wurde ebenfalls geändert) + %1$s hat den Raumnamen entfernt + %1$s hat das Raum-Thema entfernt + %1$s hat das Benutzerprofil aktualisiert %2$s + %1$s hat eine Einladung an %2$s gesendet + %1$s hat die Einladung in %2$s akzeptiert + + ** Nicht entschlüsselbar: %s ** + Das absendende Gerät hat uns keine Schlüssel für diese Nachricht übermittelt. + + + Entfernen nicht möglich + Nachricht kann nicht gesendet werden + + Bild konnte nicht hochgeladen werden + + + Netzwerk-Fehler + Matrix-Fehler + + + + + + + + + Es ist aktuell nicht möglich, einen leeren Raum erneut zu betreten. + + Verschlüsselte Nachricht + + + E-Mail-Adresse + Telefonnummer + + %1$s sandte einen Sticker. + + + Einladung von %s + Raumeinladung + %1$s und %2$s + Leerer Raum + + + %1$s und 1 andere(r) + %1$s und %2$d andere + + + + Nachricht entfernt + Nachricht entfernt von %1$s + Nachricht entfernt [Grund: %1$s] + Nachricht entfernt von %1$s [Grund: %2$s] + Pizza + Hund + Katze + Löwe + Pferd + Einhorn + Schwein + Elefant + Kaninchen + %s hat diesen Raum aufgewertet. + + Panda + Hahn + Pinguin + Schildkröte + Fisch + Oktopus + Schmetterling + Blume + Baum + Kaktus + Pilz + Globus + Mond + Wolke + Feuer + Banane + Apfel + Erdbeere + Mais + Kuchen + Herz + Smiley + Roboter + Hut + Brille + Schraubenschlüssel + Weihnachtsmann + Daumen hoch + Regenschirm + Sanduhr + Uhr + Geschenk + Glühbirne + Buch + Bleistift + Büroklammer + Schere + Schloss + Schlüssel + Hammer + Telefon + Flagge + Zug + Fahrrad + Flugzeug + Rakete + Pokal + Ball + Gitarre + Trompete + Glocke + Anker + Kopfhörer + Ordner + Stecknadel + + Sende eine Nachricht… + Sendewarteschlange leeren + + Erste Synchronisation: Importiere Benutzerkonto… + Erste Synchronisation: Importiere Cryptoschlüssel + Erste Synchronisation: Importiere Räume + Erste Synchronisation: Importiere betretene Räume + Erste Synchronisation: Importiere eingeladene Räume + Erste Synchronisation: Importiere verlassene Räume + Erste Synchronisation: Importiere Gemeinschaften + Erste Synchronisation: Importiere Benutzerdaten + + %1$s hat die Einladung an %2$s, den Raum zu betreten, zurückgezogen + %1$s\'s Einladung. Grund: %2$s + %1$s hat %2$s eingeladen. Grund: %3$s + %1$s hat dich eingeladen. Grund: %2$s + %1$s ist dem Raum beigetreten. Grund: %2$s + %1$s hat den Raum verlassen. Grund: %2$s + %1$s hat die Einladung abgelehnt. Grund: %2$s + %1$s hat %2$s gekickt. Grund: %3$s + %1$s hat Sperre von %2$s aufgehoben. Grund: %3$s + %1$s hat %2$s verbannt. Grund: %3$s + %1$s hat eine Einladung an %2$s gesandt um diesem Raum beizutreten. Grund: %3$s + %1$s hat Einladung an %2$s zu Betreten dieses Raumes zurückgezogen. Grund: %3$s + %1$s hat die Einladung für %2$s angenommen. Grund: %3$s + %1$s hat Einladung für %2$s verworfen. Grund: %3$s + + + %1$s fügt %2$s als eine Adresse für diesen Raum hinzu. + %1$s fügt %2$s als Adressen für diesen Raum hinzu. + + + + %1$s entfernt %2$s als eine Adresse für diesen Raum. + %1$s entfernt %2$s als Adressen für diesen Raum. + + + %1$s fügt %2$s als Adresse für diesen Raum hinzu und entfernt %3$s. + + %1$s legt die Hauptadresse fest für diesen Raum als %2$s fest. + %1$s entfernt die Hauptadresse für diesen Raum. + + %1$s hat Gästen erlaubt den Raum zu betreten. + %1$s hat Gäste unterbunden den Raum zu betreten. + + %1$s aktivierte Ende-zu-Ende-Verschlüsselung. + %1$s aktivierte Ende-zu-Ende-Verschlüsselung (unbekannter Algorithmus %2$s). + + %s fordert zur Überprüfung deines Schlüssels auf, jedoch unterstützt dein Client nicht die Schlüsselüberprüfung im Chat. Du musst die herkömmliche Schlüsselüberprüfung verwenden, um die Schlüssel zu überprüfen. + + Du hast ein Bild gesendet. + Du hast einen Sticker gesendet. + + Deine Einladung + %1$s hat den Raum erstellt + Du hast den Raum erstellt + Du hast $1$s eingeladen + Du bist dem Raum beigetreten + Du hast den Raum verlassen + Du hast die Einladung abgelehnt + Du hast %1$s aus dem Raum entfernt + Du hast den Bann von %1$s aufgehoben + Du hast %1$s gebannt + Du hast die Einladung von %1$s zurückgenommen + Du hast dein Profilbild geändert + Du hast deinen Anzeigenamen zu %1$s geändert + Du hast deinen Anzeigenamen von %1$s zu %2$s geändert + Du hast deinen Anzeigenamen entfernt (er war %1$s) + Du hast das Thema geändert auf: %1$s + %1$s hat das Bild des Raumes geändert + Du hast das Bild des Raumes geändert + Du hast den Raumnamen zu %1$s geändert + Du hast einen Videoanruf gestartet. + Du hast einen Audioanruf gestartet. + Du hast den Anruf angenommen. + Du hast den Anruf beendet. + Du hast den zukünftigen Nachrichtenverlauf für %1$s sichtbar gemacht + Du hast Ende-zu-Ende-Verschlüsselung aktiviert (%1$s) + Du hast den Raum aufgwertet. + + Du hast eine VoIP-Konferenz angefordert + Du hast den Raumnamen entfernt + Du hast das Raumthema entfernt + %1$s hat das Bild des Raumes entfernt + Du hast das Bild des Raumes entfernt + Du hast dein Profil %1$s aktualisiert + Du hast %1$s in den Raum eingeladen + Du hast die Einladung für %1$s zurückgenommen + Du hast die Einladung für %1$s akzeptiert + + %1$s hat das %2$s Widget hinzugefügt + Du hast das %1$s Widget hinzugefügt + %1$s hat das %2$s Widget entfernt + Du hast das %1$s Widget entfernt + %1$s hat das %2$s Widget modifiziert + Du hast das %1$s Widget modifiziert + + Administrator + Moderator + Standard + Benutzerdefiniert (%1$d) + Benutzerdefiniert + + Du hast die Berechtigungsstufe von %1$s geändert. + %1$s hat die Berechtigungsstufe von %2$s geändert. + %1$s von %2$s zu %3$s + + Deine Einladung. Grund: %1$s + Du hast %1$s eingeladen. Grund: %2$s + Du bist dem Raum beigetreten. Grund: %1$s + Du hast den Raum verlassen. Grund: %1$s + Du hast die Einladung abgelehnt. Grund: %1$s + Du hast %1$s aus dem Raum entfernt. Grund %2$s + Du hast den Bann von %1$s aufgehoben. Grund: %2$s + Du hast %1$s gebannt. Grund: %2$s + Du hast %1$s in den Raum eingeladen. Grund: %2$s + Du hast die Einladung für %1$s zurückgenommen. Grund: %2$s + Du hast die Einladung von %1$s angenommen. Grund: %2$s + Du hast die Einladung von %1$s abgelehnt. Grund: %2$s + + + Du hast die Raumaddresse %1$s hinzugefügt. + Du hast die Raumaddressen %1$s hinzugefügt. + + + + Du hast die Raumaddresse %1$s vom Raum entfernt. + Du hast die Raumaddressen %1$s vom Raum entfernt. + + + Du hast den Raumaddressen %1$s hinzugefügt und %2$s entfernt. + + Du hast die Hauptaddresse für diesen Raum auf %1$s gesetzt. + Du hast die Hauptaddresse des Raums entfernt. + + Du hast Gästen erlaubt dem Raum beizutreten. + Du hast Gästen untersagt dem Raum beizutreten. + + Du hast Ende-zu-Ende-Verschlüsselung aktiviert. + Du hast Ende-zu-Ende-Verschlüsselung aktiviert (unbekannter Algorithmus %1$s). + + Akzeptiere + Ablehnen + Anruf beenden + + %s hat Daten gesendet, um einen Anruf zu starten. + Du hast Daten geschickt, um eine Anruf zu starten. + diff --git a/matrix-sdk-android/src/main/res/values-el/strings.xml b/matrix-sdk-android/src/main/res/values-el/strings.xml new file mode 100644 index 0000000000..9db4e91849 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-el/strings.xml @@ -0,0 +1,76 @@ + + + Ηλεκτρονική διεύθυνση + %1$s: %2$s + Ο/Η %1$s έστειλε μια εικόνα. + Ο/Η %1$s έστειλε ένα αυτοκόλλητο. + + Ο/Η %1$s σας προσκάλεσε + Ο/Η %1$s αποχώρησε + Ο/Η %1$s απέρριψε την πρόσκληση + Ο/Η %1$s έδιωξε τον/την %2$s + Ο/Η %1$s προσκάλεσε τον/την %2$s + Η πρόσκληση του/της %s + Αριθμός τηλεφώνου + + Ο/Η %1$s απέκλεισε τον/την %2$s + Ο/Η %1$s απέσυρε την πρόσκληση του/της %2$s + Ο/Η %1$s άλλαξε εικονίδιο χρήστη + Ο/Η %1$s άλλαξε το εμφανιζόμενό του/της όνομα σε %2$s + Ο/Η %1$s άλλαξε το εμφανιζόμενό του/της όνομα από %2$s σε %3$s + Ο/Η %1$s αφαίρεσε το εμφανιζόμενό του/της όνομα (%2$s) + Ο/Η %1$s άλλαξε το θέμα σε: %2$s + Ο/Η %1$s άλλαξε το όνομα του δωματίου σε: %2$s + Ο/Η %s απάντησε στην κλήση. + Ο/Η %s τερμάτισε την κλήση. + + Ο/Η %s πραγματοποίησε μια κλήση βίντεο. + Ο/Η %s πραγματοποίησε μια κλήση ήχου. + Ο/Η %1$s κατέστησε το μελλοντικό ιστορικό του δωματίου ορατό στον/στην %2$s + όλα τα μέλη του δωματίου, από την στιγμή που προσκλήθηκαν. + όλα τα μέλη του δωματίου. + οποιοσδήποτε. + άγνωστος/η (%s). + (έγινε αλλαγή και του εικονιδίου χρήστη) + Ο/Η %1$s αφαίρεσε το όνομα του δωματίου + Ο/Η %1$s αφαίρεσε το θέμα του δωματίου + Ο/Η %1$s ανανέωσε το προφίλ του/της %2$s + Ο/Η %1$s δέχτηκε την πρόσκληση για το %2$s + + ** Αδυναμία αποκρυπτογράφησης: %s ** + Η συσκευή του/της αποστολέα δεν μας έχει στείλει τα κλειδιά για αυτό το μήνυμα. + + Αποτυχία αποστολής μηνύματος + + Αποτυχία αναφόρτωσης εικόνας + + Σφάλμα δικτύου + Σφάλμα του Matrix + + Κρυπτογραφημένο μήνυμα + + Ο/Η %1$s ζήτησε μια VoIP διάσκεψη + Η VoIP διάσκεψη ξεκίνησε + Η VoIP διάσκεψη έληξε + + Ο/Η %1$s εισήλθε στο δωμάτιο + + Πρόσκληση από %s + Πρόσκληση στο δωμάτιο + + %1$s και %2$s + + + %1$s και 1 ακόμα + %1$s και %2$d ακόμα + + + Άδειο δωμάτιο + + όλα τα μέλη του δωματίου από την στιγμή που εισήλθαν. + Ο/Η %1$s ενεργοποίησε την κρυπτογράφηση απ\'άκρη σ\'άκρη (%2$s) + + Ο/Η %1$s έστειλε μία πρόσκληση στον/στην %2$s για να εισέλθει στο δωμάτιο + Δεν είναι δυνατή ακόμα η επανείσοδος σε ένα άδειο δωμάτιο. + + diff --git a/matrix-sdk-android/src/main/res/values-en-rGB/strings.xml b/matrix-sdk-android/src/main/res/values-en-rGB/strings.xml new file mode 100644 index 0000000000..f457e30ed0 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-en-rGB/strings.xml @@ -0,0 +1,5 @@ + + + Spanner + Aeroplane + diff --git a/matrix-sdk-android/src/main/res/values-eo/strings.xml b/matrix-sdk-android/src/main/res/values-eo/strings.xml new file mode 100644 index 0000000000..4a1e2c4c65 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-eo/strings.xml @@ -0,0 +1,205 @@ + + + %1$s sendis bildon. + %1$s sendis glumarkon. + + Invito de %s + %1$s invitis uzanton %2$s + %1$s invitis vin + %1$s alvenis + %1$s foriris + %1$s malakceptis la inviton + %1$s forpelis uzanton %2$s + %1$s malforbaris uzanton %2$s + %1$s forbaris uzanton %2$s + %1$s nuligis inviton por %2$s + %1$s ŝanĝis sian profilbildon + ** Ne eblas malĉifri: %s ** + La aparato de la sendanto ne sendis al ni la ŝlosilojn por tiu mesaĝo. + + %1$s: %2$s + %1$s ŝanĝis sian vidigan nomon al %2$s + %1$s ŝanĝis sian vidigan nomon de %2$s al %3$s + %1$s forigis sian vidigan nomon (%2$s) + %1$s ŝanĝis la temon al: %2$s + %1$s ŝanĝis nomon de la ĉambro al: %2$s + %s vidvokis. + %s voĉvokis. + %s respondis la vokon. + %s finis la vokon. + %1$s videbligis estontan historion de ĉambro al %2$s + ĉiuj ĉambranoj, ekde iliaj invitoj. + ĉiuj ĉambranoj, ekde iliaj aliĝoj. + ĉiuj ĉambranoj. + ĉiu ajn. + nekonata (%s). + %1$s ŝaltis tutvojan ĉifradon (%2$s) + %s gradaltigis la ĉambron. + + Mesaĝo foriĝis + Mesaĝo foriĝis de %1$s + Mesaĝo foriĝis [kialo: %1$s] + Mesaĝo foriĝis de %1$s [kialo: %2$s] + %1$s ĝisdatigis sian profilon %2$s + %1$s sendis aliĝan inviton al %2$s + %1$s nuligis la aliĝan inviton por %2$s + %1$s akceptis la inviton por %2$s + + Ne povis redakti + Ne povas sendi mesaĝon + + Malsukcesis alŝuti bildon + + Reta eraro + Matrix-eraro + + Nun ne eblas re-aliĝi al malplena ĉambro + + Ĉifrita mesaĝo + + Retpoŝtadreso + Telefonnumero + + Invito de %s + Ĉambra invito + + %1$s kaj %2$s + + + %1$s kaj 1 alia + %1$s kaj %2$d aliaj + + + Malplena ĉambro + + + Hundo + Kato + Leono + Ĉevalo + Unukorno + Porko + Elefanto + Kuniklo + Pando + Koko + Pingveno + Testudo + Fiŝo + Polpo + Papilio + Floro + Arbo + Kakto + Fungo + Globo + Luno + Nubo + Fajro + Banano + Pomo + Frago + Maizo + Pico + Kuko + Koro + Mieneto + Roboto + Ĉapelo + Okulvitroj + Boltilo + Kristnaska viro + Dikfingro supren + Ombrelo + Sablohorloĝo + Horloĝo + Donaco + Lampo + Libro + Grifelo + Paperkuntenilo + Tondilo + Seruro + Ŝlosilo + Martelo + Telefono + Flago + Vagonaro + Biciklo + Aviadilo + Raketo + Trofeo + Pilko + Gitaro + Trumpeto + Sonorilo + Ankro + Kapaŭdilo + Dosierujo + Pinglo + + Komenca spegulado: +\nEnportante konton… + Komenca spegulado: +\nEnportante ĉifrilojn + Komenca spegulado: +\nEnportante ĉambrojn + Komenca spegulado: +\nEnportante aliĝitajn ĉambrojn + Komenca spegulado: +\nEnportante ĉambrojn de invitoj + Komenca spegulado: +\nEnportante forlasitajn ĉambrojn + Komenca spegulado: +\nEnportante komunumojn + Komenca spegulado: +\nEnportante datumojn de konto + + Sendante mesaĝon… + Vakigi sendan atendovicon + + %1$s petis grupan vokon + Grupa voko komenciĝis + Grupa voko finiĝis + + (ankaŭ profilbildo ŝanĝiĝis) + %1$s forigis nomon de la ĉambro + %1$s forigis temon de la ĉambro + Invito de %1$s. Kialo: %2$s + %1$s invitis uzanton %2$s. Kialo: %3$s + %1$s invitis vin. Kialo: %2$s + %1$s aliĝis al la ĉambro. Kialo: %2$s + %1$s foriris de la ĉambro. Kialo: %2$s + %1$s rifuzis la inviton. Kialo: %2$s + %1$s forpelis uzanton %2$s. Kialo: %3$s + %1$s malforbaris uzanton %2$s. Kialo: %3$s + %1$s forbaris uzanton %2$s. Kialo: %3$s + %1$s sendis inviton al la ĉambro al %2$s. Kialo: %3$s + %1$s nuligis la inviton al la ĉambro al %2$s. Kialo: %3$s + %1$s akceptis la inviton por %2$s. Kialo: %3$s + %1$s nuligis la inviton al %2$s. Kialo: %3$s + + + %1$s aldonis %2$s kiel adreson por ĉi tiu ĉambro. + %1$s aldonis %2$s kiel adresojn por ĉi tiu ĉambro. + + + + %1$s forigis %2$s kiel adreson por ĉi tiu ĉambro. + %1$s forigis %2$s kiel adresojn por ĉi tiu ĉambro. + + + %1$s aldonis %2$s kaj forigis %3$s kiel adresojn por ĉi tiu ĉambro. + + %1$s agordis la ĉefadreson por ĉi tiu ĉambro al %2$s. + %1$s forigis la ĉefadreson de ĉi tiu ĉambro. + + %1$s permesis al gastoj aliĝi al la ĉambro. + %1$s malpermesis al gastoj aliĝi al la ĉambro. + + %1$s ŝaltis tutvojan ĉifradon. + %1$s ŝaltis tutvojan ĉifradon (kun nerekonita algoritmo %2$s). + + %s petas kontrolon de via ŝlosilo, sed via kliento ne subtenas kontrolon de ŝlosiloj en la babilujo. Vi devos uzi malnovecan kontrolon de ŝlosiloj. + + diff --git a/matrix-sdk-android/src/main/res/values-es-rMX/strings.xml b/matrix-sdk-android/src/main/res/values-es-rMX/strings.xml new file mode 100644 index 0000000000..35b7bfc829 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-es-rMX/strings.xml @@ -0,0 +1,96 @@ + + + + %1$s: %2$s + %1$s envió una imagen. + + la invitación de %s + %1$s invitó a %2$s + %1$s te invitó + %1$s se unió + %1$s salió + %1$s rechazó la invitación + %1$s quitó a %2$s + %1$s desprohibió a %2$s + %1$s prohibió %2$s + %1$s retiró la invitación de %2$s + %1$s cambió su foto de perfil + %1$s estableció %2$s como su nombre visible + %1$s cambió su nombre visible de %2$s a %3$s + %1$s eliminó su nombre visible (%2$s) + %1$s cambió el tema a: %2$s + %1$s cambió el nombre de la sala a: %2$s + %s comenzó una llamada de video. + %s comenzó una llamada de voz. + %s recibió la llamada. + %s terminó la llamada. + %1$s dejó que %2$s vea el historial del futuro + todos los miembros de la sala, desde su invitación. + todos los miembros de la sala, desde cuando entraron. + todos los miembros de la sala. + todos. + desconocido (%s). + %1$s encendió el cifrado de extremo a extremo (%2$s) + + %1$s solicitó una conferencia VoIP + conferencia VoIP comenzó + conferencia VoIP finalizó + + (foto de perfil también se cambió) + %1$s eliminó el nombre de la sala + %1$s retiró el tema de la sala + %1$s actualizó su perfil %2$s + %1$s envió una invitación a %2$s para entrar a la sala + %1$s aceptó la invitación de %2$s + + ** No se puede descifrar: %s ** + El dispositivo del remitente no nos ha enviado las claves de este mensaje. + + + No se pudo redactar + No se puede enviar el mensaje + + La subida de la imagen falló + + + Error de la red + Error de Matrix + + + + + + + + + No es posible volver a unirse a una sala vacía. + + Mensaje cifrado + + + Correo electrónico + Número telefónico + + %1$s envió una calcomanía. + + + Invitación de %s + Invitación de Sala + %1$s y %2$s + Sala vacía + + + + %1$s y otro + %1$s y %2$d otros + + + Mensaje eliminado + Mensaje eliminado por %1$s + Mensaje eliminado [motivo: %1$s] + Mensaje eliminado por %1$s [motivo: %2$s] + Perro + Gato + León + Caballo + diff --git a/matrix-sdk-android/src/main/res/values-es/strings.xml b/matrix-sdk-android/src/main/res/values-es/strings.xml new file mode 100644 index 0000000000..3c019b3b80 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-es/strings.xml @@ -0,0 +1,215 @@ + + + + %1$s: %2$s + %1$s envió una imagen. + + la invitación de %s + %1$s invitó a %2$s + %1$s te ha invitado + %1$s se ha unido + %1$s salió + %1$s rechazó la invitación + %1$s expulsó a %2$s + %1$s le quitó el veto a %2$s + %1$s vetó a %2$s + %1$s retiró la invitación de %2$s + %1$s cambió su avatar + %1$s estableció %2$s como su nombre público + %1$s cambió su nombre público de %2$s a %3$s + %1$s eliminó su nombre público (%2$s) + %1$s cambió el tema a: %2$s + %1$s cambió el nombre de la sala a: %2$s + %s realizó una llamada de vídeo. + %s realizó una llamada de voz. + %s contestó la llamada. + %s finalizó la llamada. + %1$s hizo visible el historial futuro de la sala para %2$s + todos los miembros de la sala, desde su invitación. + todos los miembros de la sala, desde el momento en que se unieron. + todos los miembros de la sala. + todos. + desconocido (%s). + %1$s activó el cifrado de extremo a extremo (%2$s) + + %1$s solicitó una conferencia de vozIP + conferencia de vozIP iniciada + conferencia de vozIP finalizada + + (el avatar también se cambió) + %1$s eliminó el nombre de la sala + %1$s eliminó el tema de la sala + %1$s actualizó su perfil %2$s + %1$s invitó a %2$s a unirse a la sala + %1$s aceptó la invitación para %2$s + + ** No es posible descifrar: %s ** + El dispositivo emisor no nos ha enviado las claves para este mensaje. + + + No se pudo redactar + No es posible enviar el mensaje + + No se pudo cargar la imagen + + + Error de red + Error de Matrix + + + + + + + + + Actualmente no es posible volver a unirse a una sala vacía. + + Mensaje cifrado + + + Dirección de correo electrónico + Número telefónico + + %1$s envió una pegatina. + + + Invitación de %s + Invitación a Sala + %1$s y %2$s + Sala vacía + + + %1$s y 1 otro + %1$s y %2$d otros + + + + Mensaje eliminado + Mensaje eliminado por %1$s + Mensaje eliminado [motivo: %1$s] + Mensaje eliminado por %1$s [motivo: %2$s] + %1$s ha revocado la invitación a unirse a la sala para %2$s + Perro + Gato + León + Caballo + Unicornio + Cerdo + Elefante + Conejo + Panda + Gallo + Pingüino + Tortuga + Pez + Pulpo + Mariposa + Flor + Árbol + Cactus + Seta + Luna + Nube + Fuego + Plátano + Manzana + Fresa + Maíz + Pizza + Pastel + Corazón + Sombrero + Gafas + Llave inglesa + Pulgares arriba + Paraguas + Reloj de arena + Reloj + Regalo + Bombilla + Libro + Lápiz + Clip + Tijeras + Candado + Llave + Martillo + Teléfono + Bandera + Tren + Bicicleta + Avión + Cohete + Trofeo + Pelota + Guitarra + Trompeta + Campana + Ancla + Auriculares + Carpeta + Sincronización Inicial +\nImportando cuenta… + Sincronización Inicial: +\nImportando Salas + Sincronización Inicial: +\nImportando Comunidades + Sincronización Inicial: +\nImportando Datos de la Cuenta + + Enviando mensaje… + Borrar cola de envío + + %1$s ha invitado a %2$s. Razón: %3$s + %1$s te ha invitado. Razón: %2$s + %1$s se ha unido. Razón: %2$s + %1$s se ha ido. Razón: %2$s + %1$s ha rechadazo la invitación. Razón: %2$s + %1$s expulsó a %2$s. Razón: %3$s + %1$s ha baneado a %2$s. Razón: %3$s + %1$s ha aceptado la invitación para %2$s. Razón: %3$s + %1$s ha eliminado la dirección principal para esta sala. + + %s ha actualizado la sala. + + Globo Terráqueo + Cara sonriente + Robot + Papá Noel + Pin + + Sincronización Inicial: +\nImportando criptografía + Sincronización Inicial: +\nImportando Salas a las que te has unido + Sincronización Inicial: +\nImportando Salas a las que has sido invitada + Sincronización Inicial: +\nImportando Salas Abandonadas + Invitación de %1$s. Razón: %2$s + %1$s ha desbaneado a %2$s. Razón: %3$s + %1$s envió una invitación a %2$s para que se una a la sala. Razón: %3$s + %1$s revocó la invitación de %2$s para unirse a la sala. Razón: %3$s + %1$s ha retirado la invitación de %2$s. Razón: %3$s + + + %1$s ha añadido %2$s como alias de esta sala. + %1$s ha añadido %2$s como alias de esta sala. + + + + %1$s ha quitado %2$s como alias de esta habitación. + %1$s ha quitado %2$s como alias de esta habitación. + + + %1$s ha establecido la dirección principal de esta sala a %2$s. + %1$s ha permitido que los invitados se unan a la sala. + %1$s ha impedido que los invitados se unan a la sala. + + %1$s ha activado la encriptación extremo a extremo. + %1$s ha activado la encriptación de extremo a extremo (algoritmo no reconocido %2$s). + + %s solicita verificar su clave, pero su cliente no soporta la verificación de la clave en chat. Necesitará usar la verificación de claves clásica para poder verificar las claves. + + diff --git a/matrix-sdk-android/src/main/res/values-et/strings.xml b/matrix-sdk-android/src/main/res/values-et/strings.xml new file mode 100644 index 0000000000..657d5446eb --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-et/strings.xml @@ -0,0 +1,302 @@ + + + %1$s: %2$s + %1$s saatis pildi. + %1$s saatis kleepsu. + + Kasutaja %s kutse + %1$s kutsus kasutajat %2$s + %1$s kutsus sind + %1$s liitus jututoaga + %1$s lahkus jututoast + %1$s lükkas tagasi kutse + %1$s müksas kasutajat %2$s + %1$s võttis tagasi kutse kasutajale %2$s + %1$s muutis oma avatari + %1$s määras oma kuvatavaks nimeks %2$s + %1$s muutis senise kuvatava nime %2$s uueks nimeks %3$s + %1$s eemaldas oma kuvatava nime (%2$s) + %1$s muutis uueks teemaks %2$s + %1$s muutis jututoa uueks nimeks %2$s + %s alustas videokõnet. + %s alustas häälkõnet. + %s vastas kõnele. + %s lõpetas kõne. + %1$s seadistas, et tulevane jututoa ajalugu on nähtav kasutajale %2$s + kõikidele jututoa liikmetele alates kutsumise hetkest. + kõikidele jututoa liikmetele alates liitumise hetkest. + kõikidele jututoa liikmetele. + kõikidele. + teadmata (%s). + %1$s lülitas sisse läbiva krüptimise (%2$s) + %s uuendas seda jututuba. + + %1$s saatis VoIP konverentsi kutse + VoIP-konverents algas + VoIP-konverents lõppes + + (samuti sai avatar muudetud) + %1$s eemaldas jututoa nime + %1$s eemaldas jututoa teema + Sõnum on eemaldatud + Sõnum on eemaldatud %1$s poolt + Sõnum on eemaldatud [põhjus: %1$s] + Sõnum on eemaldatud %1$s poolt [põhjus: %2$s] + %1$s uuendas oma profiili %2$s + %1$s saatis jututoaga liitumiseks kutse kasutajale %2$s + %1$s võttis tagasi jututoaga liitumise kutse kasutajalt %2$s + %1$s võttis vastu kutse %2$s nimel + + ** Ei õnnestu dekrüptida: %s ** + Sõnumi saatja seade ei ole selle sõnumi jaoks saatnud dekrüptimisvõtmeid. + + Ei saanud muuta sõnumit + Sõnumi saatmine ei õnnestunud + + Faili üles laadimine ei õnnestunud + + Võrguühenduse viga + Matrix\'i viga + + Hetkel ei ole võimalik uuesti liituda tühja jututoaga. + + Krüptitud sõnum + + E-posti aadress + Telefoninumber + + Kutse kasutajalt %s + Kutse jututuppa + + %1$s ja %2$s + + + %1$s ja üks muu + %1$s ja %2$d muud + + + Tühi jututuba + + + Koer + Kass + Lõvi + Hobune + Ükssarvik + Siga + Elevant + Jänes + Panda + Kukk + Pingviin + Kilpkonn + Kala + Kaheksajalg + Liblikas + Lill + Puu + Kaktus + Seen + Maakera + Kuu + Pilv + Tuli + Banaan + Õun + Maasikas + Mais + Pitsa + Kook + Süda + Smaili + Robot + Müts + Prillid + Mutrivõti + Jõuluvana + Pöidlad püsti + Vihmavari + Liivakell + Kell + Kingitus + Lambipirn + Raamat + Pliiats + Kirjaklamber + Käärid + Lukk + Võti + Haamer + Telefon + Lipp + Rong + Jalgratas + Lennuk + Rakett + Auhind + Pall + Kitarr + Trompet + Kelluke + Ankur + Kõrvaklapid + Kaust + Knopka + + Alglaadimine: +\nImpordin kontot… + Alglaadimine: +\nImpordin krüptoseadistusi + Alglaadimine: +\nImpordin jututubasid + Alglaadimine: +\nImpordin liitutud jututubasid + Alglaadimine: +\nImpordin kutsutud jututubasid + Alglaadimine: +\nImpordin lahkutud jututubasid + Alglaadimine: +\nImpordin kogukondi + Alglaadimine: +\nImpordin kontoandmeid + + Saadan sõnumit… + Tühjenda saatmisjärjekord + + Kasutaja %1$s kutse. Põhjus: %2$s + %1$s kutsus kasutajat %2$s. Põhjus: %3$s + %1$s kutsus sind. Põhjus: %2$s + %1$s liitus jututoaga. Põhjus: %2$s + %1$s lahkus jututoast. Põhjus: %2$s + %1$s lükkas kutse tagasi. Põhjus: %2$s + %1$s müksas välja kasutaja %2$s. Põhjus: %3$s + %1$s saatis kasutajale %2$s kutse jututoaga liitumiseks. Põhjus: %3$s + %1$s tühistas kasutajale %2$s saadetud kutse jututoaga liitumiseks. Põhjus: %3$s + %1$s võttis vastu kutse %2$s jututoaga liitumiseks. Põhjus: %3$s + %1$s võttis tagasi kasutajale %2$s saadetud kutse. Põhjus: %3$s + + %1$s lülitas sisse läbiva krüptimise. + %1$s lülitas sisse läbiva krüptimise (tundmatu algoritm %2$s). + + + %1$s lisas %2$s selle jututoa aadressiks. + %1$s lisas %2$s selle jututoa aadressideks. + + + + %1$s eemaldas %2$s kui selle jututoa aadressi. + %1$s eemaldas %2$s selle jututoa aadresside hulgast. + + + %1$s lisas %2$s ja eemaldas %3$s selle jututoa aadresside loendist. + + %1$s seadistas selle jututoa põhiaadressiks %2$s. + %1$s eemaldas selle jututoa põhiaadressi. + + %1$s lubas külalistel selle jututoaga liituda. + %1$s seadistas, et külalised ei või selle jututoaga liituda. + + %s soovib verifitseerida sinu võtmeid, kuid sinu kasutatav klient ei oska vestluse-sisest verifitseerimist teha. Sa pead kasutama traditsioonilist verifitseerimislahendust. + + Kasutaja %1$s lõi jututoa + Sina saatsid pildi. + Sina saatsid kleepsu. + + Sinu kutse + Sa lõid jututoa + Sina kutsusid kasutajat %1$s + Sina liitusid jututoaga + Sina lahkusid jututoast + Sina lükkasid kutse tagasi + Sina müksasid %1$s välja + %1$s taastas %2$s ligipääsu + Sina taastasid %1$s ligipääsu + %1$s keelas %1$s ligipääsu + Sina keelasid %1$s ligipääsu + Sina võtsid tagasi %1$s kutse + Sina muutsid oma tunnuspilti + Sina määrasid oma kuvatavaks nimeks %1$s + Sina muutsid senise kuvatava nime %1$s uueks nimeks %2$s + Sina eemaldasid oma kuvatava nime (oli %1$s) + Sina muutsid uueks teemaks %1$s + %1$s muutis jututoa tunnuspilti + Sina muutsid jututoa tunnuspilti + Sina muutsid jututoa uueks nimeks %1$s + Sa alustasid videokõnet. + Sa alustasid häälkõnet. + %s saatis info kõne algatamiseks. + Sa saatsid info kõne algatamiseks. + Sa vastasid kõnele. + Sa lõpetasid kõne. + Sa seadistasid, et tulevane jututoa ajalugu on nähtav kasutajale %1$s + Sa lülitasid sisse läbiva krüptimise (%1$s) + Sa uuendasid seda jututuba. + + Sa algatasid VoIP rühmakõne + Sa eemaldasid jututoa nime + Sa eemaldasid jututoa teema + %1$s eemaldas jututoa tunnuspildi + Sa eemaldasid jututoa tunnuspildi + Sa uuendasid oma profiili %1$s + Sina saatsid kasutajale %1$s kutse jututoaga liitumiseks + Sina võtsid tagasi jututoaga liitumise kutse kasutajalt %1$s + Sina võtsid vastu kutse %1$s nimel + + %1$s lisas %2$s vidina + Sina lisasid %1$s vidina + %1$s eemaldas %2$s vidina + Sina eemdaldasid %1$s vidina + %1$s muutis %2$s vidinat + Sa muutsid %1$s vidinat + + Peakasutaja + Moderaator + Tavakasutaja + Kohandatud kasutajaõigused (%1$s) + Kohandatud õigused + + Sina muutsid kasutaja %1$s õigusi. + %1$s muutis kasutaja %2$s õigusi. + %1$s õiguste muutus %2$s -> %3$s + + Sinu kutse. Põhjus %1$s + Sina kutsusid kasutajat %1$s. Põhjus: %1$s + Sina liitusid jututoaga. Põhjus: %1$s + Sina lahkusid jututoast. Põhjus: %1$s + Sina lükkasid kutse tagasi. Põhjus: %1$s + Sina müksasid kasutaja %1$s välja. Põhjus: %2$s + %1$s taastas ligipääsu kasutajale %2$s. Põhjus: %3$s + Sina taastasid kasutaja %1$s ligipääsu. Põhjus: %2$s + %1$s keelas kasutaja %2$s ligipääsu. Põhjus: %3$s + Sina keelasid kasutaja %1$s ligipääsu. Põhjus: %2$s + Sina saatsid kasutajale %1$s kutse jututoaga liitumiseks. Põhjus: %2$s + Sina võtsid tagasi jututoaga liitumise kutse kasutajalt %1$s. Põhjus: %2$s + Sina võtsid vastu kutse %1$s nimel. Põhjus: %2$s + Sina võtsid tagasi kasutaja %1$s kutse. Põhjus: %2$s + + + Sina lisasid %1$s selle jututoa aadressiks. + Sina lisasid %1$s selle jututoa aadressideks. + + + + Sina eemaldasid %1$s, kui selle jututoa aadressi. + Sina eemaldasid %1$s selle jututoa aadresside hulgast. + + + Sina lisasid %1$s selle jututoa aadressiks ning eemaldasid %2$s aadresside hulgast. + + Sina seadistasid selle jututoa põhiaadressiks %1$s. + Sina eemaldasid selle jututoa põhiaadressi. + + Sina lubasid külalistel selle jututoaga liituda. + Sina seadistasid, et külalised ei või selle jututoaga liituda. + + Sa lülitasid sisse läbiva krüptimise. + Sa lülitasid sisse läbiva krüptimise (kasutusel on tundmatu algoritm %1$s). + + Võta vastu + Keeldu + Lõpeta kõne + + diff --git a/matrix-sdk-android/src/main/res/values-eu/strings.xml b/matrix-sdk-android/src/main/res/values-eu/strings.xml new file mode 100644 index 0000000000..1a5c81fe5e --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-eu/strings.xml @@ -0,0 +1,207 @@ + + + + %1$s: %2$s + %1$s erabiltzaileak irudi bat bidali du. + + %s erabiltzailearen gonbidapena + %1$s erabiltzaileak %2$s gonbidatu du + %1$s erabiltzaileak gonbidatu zaitu + %1$s gelara elkartu da + %1$s gelatik atera da + %1$s erabiltzaileak gonbidapena baztertu du + %1$s erabiltzaileak %2$s kanporatu du + %1$s erabiltzaileak debekua kendu dio %2$s erabiltzaileari + %1$s erabiltzaileak %2$s debekatu du + %1$s erabiltzaileak %2$s erabiltzailearen gonbidapena atzera bota du + %1$s erabiltzaileak abatarra aldatu du + %1$s erabiltzaileak bere pantaila-izena aldatu du beste honetara: %2$s + %1$s erabiltzaileak bere pantaila-izena aldatu du, honetatik: %2$s honetara: %3$s + %1$s erabiltzaileak bere pantaila-izena kendu du (%2$s) + %1$s erabiltzaileak mintzagaia honetara aldatu du: %2$s + %1$s erabiltzaileak gelaren izena honetara aldatu du: %2$s + %s erabiltzaileak bideo deia hasi du. + %s erabiltzaileak ahots deia hasi du. + %s erabiltzaileak deia erantzun du. + %s erabiltzaileak deia amaitu du. + %1$s erabiltzaileak gelaren historiala ikusgai jarri du hauentzat: %2$s + gelako kide guztiak, gonbidatu zitzaienetik. + gelako kide guztiak, elkartu zirenetik. + gelako kide guztiak. + edonor. + ezezaguna (%s). + %1$s erabiltzaileak muturretik muturrera zifratzea aktibatu du (%2$s) + + %1$s erabiltzaileak VoIP konferentzia bat eskatu du + VoIP konferentzia hasita + VoIP konferentzia amaituta + + (abatarra ere aldatu da) + %1$s erabiltzaileak gelaren izena kendu du + %1$s erabiltzaileak gelaren mintzagaia kendu du + %1$s erabiltzaileak bere profila eguneratu du %2$s + %1$s erabiltzaileak gelara elkartzeko gonbidapen bat bidali dio %2$s erabiltzaileari + %1$s erabiltzaileak %2$s gelarako gonbidapena onartu du + + ** Ezin izan da deszifratu: %s ** + Igorlearen gailuak ez dizkigu mezu honetarako gakoak bidali. + + Ezin izan da kendu + Ezin izan da mezua bidali + + Huts egin du irudia igotzean + + Sare errorea + Matrix errorea + + Ezin da oraingoz hutsik dagoen gela batetara berriro sartu. + + Zifratutako mezua + + E-mail helbidea + Telefono zenbakia + + %1$s erabiltzaileak eranskailu bat bidali du. + + %s gelarako gonbidapena + Gela gonbidapena + %1$s eta %2$s + Gela hutsa + + + %1$s eta beste bat + %1$s eta beste %2$d + + + + Mezua kendu da + %1$s erabiltzaileak mezua kendu du + Mezua kendu da [arrazoia: %1$s] + %1$s erabiltzaileak mezua kendu du [arrazoia: %2$s] + Txakurra + Katua + Lehoia + Zaldia + Unikornioa + Zerria + Elefantea + Untxia + Panda + Oilarra + Pinguinoa + Dortoka + Arraina + Olagarroa + Tximeleta + Lorea + Zuhaitza + Kaktusa + Perretxikoa + Lurra + Ilargia + Hodeia + Sua + Banana + Sagarra + Marrubia + Artoa + Pizza + Pastela + Bihotza + Irrifartxoa + Robota + Txanoa + Betaurrekoak + Giltza + Santa + Ederto + Aterkia + Harea-erlojua + Erlojua + Oparia + Bonbilla + Liburua + Arkatza + Klipa + Artaziak + Giltzarrapoa + Giltza + Mailua + Telefonoa + Bandera + Trena + Bizikleta + Hegazkina + Kohetea + Saria + Baloia + Gitarra + Tronpeta + Kanpaia + Aingura + Aurikularrak + Karpeta + Txintxeta + + Hasierako sinkronizazioa: +\nKontua inportatzen… + Hasierako sinkronizazioa: +\nZifratzea inportatzen + Hasierako sinkronizazioa: +\nGelak inportatzen + Hasierako sinkronizazioa: +\nElkartutako gelak inportatzen + Hasierako sinkronizazioa: +\nGonbidatutako gelak inportatzen + Hasierako sinkronizazioa: +\nUtzitako gelak inportatzen + Hasierako sinkronizazioa: +\nKomunitateak inportatzen + Hasierako sinkronizazioa: +\nKontuaren datuak inportatzen + + %s erabiltzaileak gela hau eguneratu du. + + Mezua bidaltzen… + Garbitu bidalketa-ilara + + %1$s erabiltzaileak %2$s gelara elkartzeko gonbidapena indargabetu du + %1$s erabiltzailearen gonbidapena. Arrazoia: %2$s + %1$s erabiltzaileak %2$s gonbidatu du. Arrazoia: %3$s + %1$s erabiltzaileak gonbidatu zaitu. Arrazoia: %2$s + %1$s gelara elkartu da. Arrazoia: %2$s + %1$s gelatik atera da. Arrazoia: %2$s + %1$s erabiltzaileak gonbidapena baztertu du. Arrazoia: %2$s + %1$s erabiltzaileak %2$s kanporatu du. Arrazoia: %3$s + %1$s erabiltzaileak debekua kendu dio %2$s erabiltzaileari. Arrazoia: %3$s + %1$s erabiltzaileak %2$s debekatu du. Arrazoia: %3$s + "%1$s erabiltzaileak gelara elkartzeko gonbidapen bat bidali dio %2$s erabiltzaileari. Arrazoia: %3$s" + "%1$s erabiltzaileak %2$s gelara elkartzeko gonbidapena indargabetu du. Arrazoia: %3$s" + "%1$s erabiltzaileak %2$s gelarako gonbidapena onartu du. Arrazoia: %3$s" + "%1$s erabiltzaileak %2$s erabiltzailearen gonbidapena indargabetu du. Arrazoia: %3$s" + + + %1$s erabiltzaileak %2$s gehitu du gela honen helbide gisa. + %1$s erabiltzaileak %2$s gehitu ditu gela honen helbide gisa. + + + + %1$s erabiltzaileak %2$s kendu du gela honen helbide gisa. + %1$s erabiltzaileak %3$s kendu ditu gela honen helbide gisa. + + + %1$s erabiltzaileak %2$s gehitu %3$s eta kendu ditu gela honen helbide gisa. + + %1$s erabiltzaileak %2$s ezarri du gela honen helbide nagusi gisa. + %1$s erabiltzaileak gela honen helbide nagusia kendu du. + + %1$k gonbidatuak gelara sartzea onartu du. + %1%k gonbidatuak gelara sartzea galerazi du. + + %1$s erabiltzaileak muturretik muturrerako zifratzea gaitu du. + %1$s erabiltzaileak muturretik muturrerako zifratzea gaitu du. (%2$s algoritmo ezezaguna). + + %s(e)k zure gakoa egiaztatzea eskatu du, baina zure bezeroak ez du txatean gakoa egiaztatzea onartzen. Gako egiaztaketa zaharra erabili beharko duzu. + + %1$s erabiltzaileak gela sortu du + diff --git a/matrix-sdk-android/src/main/res/values-fa/strings.xml b/matrix-sdk-android/src/main/res/values-fa/strings.xml new file mode 100644 index 0000000000..18d8578e54 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-fa/strings.xml @@ -0,0 +1,206 @@ + + + %1$s: %2$s + %1$s تصویری فرستاد. + %1$s برچسبی فرستاد. + + دعوت %s + ‫%1$s، %2$s را دعوت کرد + %1$s دعوتتان کرد + %1$s به اتاق پیوست + %1$s اتاق را ترک کرد + %1$s دعوت را رد کرد + %1$s، %2$s را اخراج کرد + %1$s، انسداد %2$s را رفع کرد + %1$s، %2$s را مسدود کرد + %1$s دعوت %2$s را نپذیرفت + %1$s تصویرش را عوض کرد + %1$s نام نمایشی خود را به %2$s تنظیم کرد + %1$s نام نمایشیش را از %2$s به %3$s تغییر داد + %1$s نام نمایشیش (%2$s) را پاک کرد + %1$s موضوع را به %2$s تغییر داد + %1$s نام اتاق را به %2$s تغییر داد + %s یک تماس تصویری برقرار کرد. + %s یک تماس صوتی برقرار کرد. + %s تماس را پاسخ داد. + %s به تماس پایان داد. + %1$s تاریخچهٔ آیندهٔ اتاق را برای %2$s نمایان کرد + همهٔ اعضای اتاق، از زمان دعوت شدنشان. + همهٔ اعضای اتاق، از زمان پیوستنشان. + همهٔ اعضای اتاق. + هرکسی. + ناشناخته (%s). + %1$s رمزنگاری سرتاسری را روشن کرد (%2$s) + %s این اتاق را ارتقا داد. + + %1$s درخواست یک گردهمایی صوتی داد + گردهمایی صوتی آغاز شد + گردهمایی صوتی پایان یافت + + (تصویر هم عوض شد) + %1$s نام اتاق را پاک کرد + %1$s موضوع اتاق را پاک کرد + پیام پاک شد + پیام به دست %1$s پاک شد + پیام پاک شد [دلیل: %1$s] + پیام به دست %1$s پاک شد [دلیل: %2$s] + %1$s دعوتی برای پیوستن %2$s به اتاق فرستاد + %1$s دعوت پیوستن به اتاق %2$s را باطل کرد + %1$s دعوت برای %2$s را پذیرفت + + ** ناتوان در رمزگشایی: %s ** + دستگاه فرستنده، کلیدهای این پیام را برایمان نفرستاده است. + + ناتوان در فرستادن پیام + + شکست در بارگذاری تصویر + + خطای شبکه + خطای ماتریس + + در حال حاضر امکان بازپیوست به اتاقی خالی وجود ندارد‌‌. + + پیام رمزنگاری شده + + نشانی رایانامه + شماره تلفن + + دعوت از %s + دعوت اتاق + + %1$s و %2$s + + + %1$s و ۱ نفر دیگر + %1$s و %2$d نفر دیگر + + + اتاق خالی + + + سگ + گربه + شیر + اسب + تک‌شاخ + خوک + فیل + خرگوش + پاندا + خروس + پنگوئن + لاک‌پشت + ماهی + هشت‌پا + پروانه + گل + درخت + کاکتوس + قارچ + جهان + ماه + ابر + آتش + موز + سیب + توت‌فرنگی + بلال + پیتزا + کیک + قلب + لبخند + آدم‌آهنی + کلاه + عینک + آچار + بابانوئل + شست + چتر + ساعت شنی + ساعت + هدیه + لامپ + کتاب + مداد + گیره کاغذ + قیچی + قفل + کلید + چکّش + تلفن + پرچم + قطار + دوچرخه + هواپیما + موشک + جام + توپ + گیتار + ترومپت + زنگ + لنگر + هدفون + پوشه + پونز + + همگام‌سازی نخستین: +\nدر حال درون‌ریزی حساب… + همگام‌سازی نخستین: +\nدر حال درون‌ریزی رمزنگاری + همگام‌سازی نخستین: +\nدر حال درون‌ریزی اتاق‌ها + همگام‌سازی نخستین: +\nدر حال درون‌ریزی اتاق‌های پیوسته + همگام‌سازی نخستین: +\nدر حال درون‌ریزی اتاق‌های دعوت‌شده + همگام‌سازی نخستین: +\nدر حال درون‌ریزی اتاق‌های ترک‌شده + همگام‌سازی نخستین: +\nدر حال درون‌ریزی انجمن‌ها + همگام‌سازی نخستین: +\nدر حال درون‌ریزی داده‌های حساب + + در حال فرستادن پیام… + پاک‌سازی صفِ در حال ارسال + + دعوت %1$s. دلیل: %2$s + %1$s، %2$s را دعوت کرد. دلیل: %3$s + %1$s دعوتتان کرد. دلیل: %2$s + %1$s به اتاق پیوست. دلیل: %2$s + %1$s اتاق را ترک کرد. دلیل: %2$s + %1$s دعوت را رد کرد. دلیل: %2$s + %1$s، %2$s را اخراج کرد. دلیل: %3$s + %1$s انسداد %2$s را رفع کرد. دلیل: %3$s + %1$s، %2$s را مسدود کرد. دلیل: %3$s + %1$s دعوتی برای پیوستن %2$s به اتاق فرستاد. دلیل: %3$s + %1$s دعوت %2$s برای پیوستن به اتاق را باطل کرد. دلیل: %3$s + %1$s دعوت برای %2$s را پذیرفت. دلیل: %3$s + %1$s دعوت %2$s را نپذیرفت. دلیل: %3$s + + + %1$s، %2$s را به عنوان نشانی‌ای برای این اتاق افزود. + %1$s، %2$s را به عنوان نشانی‌هایی برای این اتاق افزود. + + + + %1$s، %2$s را به عنوان نشانی‌ای برای این اتاق پاک کرد. + %1$s، %3$s را به عنوان نشانی‌هایی برای این اتاق پاک کرد. + + + %1$s برای نشانی این اتاق، %2$s را افزود و %3$s را پاک کرد. + + %1$s نشانی اصلی این اتاق را به %2$s تنظیم کرد. + %1$s نشانی اصلی را برای این اتاق پاک کرد. + + %1$s اجازه داد میمهانان به گروه بپیوندند. + %1$s جلوی پیوستن میمهانان به گروه را گرفت. + + %1$s رمزنگاری سرتاسری را روشن کرد. + %1$s رمزنگاری سرتاسری را روشن کرد (الگوریتم تشخیص‌داده‌نشده %2$s ). + + %s درخواست تأیید کلیدتان را دارد، ولی کارخواهتان تأیید کلید درون گپ را پشتیبانی نمی‌کند. برای تأیید کلیدها لازم است از تأییدیهٔ کلید قدیمی استفاده کنید. + + %1$s اتاق را ایجاد کرد + %1$s نمایه خود را به‌روز کرد %2$s + نمی‌توان ویرایش کرد + diff --git a/matrix-sdk-android/src/main/res/values-fi/strings.xml b/matrix-sdk-android/src/main/res/values-fi/strings.xml new file mode 100644 index 0000000000..078769942c --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-fi/strings.xml @@ -0,0 +1,207 @@ + + + %1$s lähetti kuvan. + + Käyttäjän %s kutsu + %1$s kutsui käyttäjän %2$s + %1$s kutsui sinut + %1$s liittyi huoneeseen + %1$s poistui huoneesta + %1$s hylkäsi kutsun + %1$s poisti käyttäjän %2$s + %1$s poisti porttikiellon käyttäjältä %2$s + %1$s antoi porttikiellon käyttäjälle %2$s + %1$s veti takaisin kutsun käyttäjälle %2$s + %1$s vaihtoi profiilikuvaansa + %1$s asetti näyttönimekseen %2$s + %1$s muutti näyttönimensä nimestä %2$s nimeen %3$s + %1$s poisti näyttönimensä (%2$s) + %1$s vaihtoi aiheeksi: %2$s + %1$s vaihtoi huoneen nimeksi %2$s + %s soitti videopuhelun. + %s soitti äänipuhelun. + %s vastasi puheluun. + %s lopetti puhelun. + %1$s muutti tulevan huonehistorian näkyväksi seuraaville: %2$s + kaikki huoneen jäsenet, kutsumisestaan asti. + kaikki huoneen jäsenet, liittymisestään asti. + kaikki huoneen jäsenet. + kaikki. + tuntematon (%s). + %1$s otti käyttöön osapuolten välisen salauksen (%2$s) + + %1$s lähetti VoIP-konferenssipyynnön + VoIP-konferenssi alkoi + VoIP-konferenssi päättyi + + (myös kuva vaihdettiin) + %1$s poisti huoneen nimen + %1$s poisti huoneen aiheen + %1$s päivitti profiilinsa %2$s + %1$s lähetti liittymiskutsun huoneeseen käyttäjälle %2$s + %1$s hyväksyi kutsun käyttäjän %2$s puolesta + ** Salauksen purku epäonnistui: %s ** + Lähettäjän laite ei ole lähettänyt avaimia tähän viestiin. + + Viestin lähetys epäonnistui + + Kuvan lataaminen epäonnistui + + Verkkovirhe + Matrix-virhe + + Tällä hetkellä ei ole mahdollista liittyä uudelleen tyhjään huoneeseen. + + Salattu viesti + + Sähköpostiosoite + Puhelinnumero + + + Takaisinveto epäonnistui + %1$s: %2$s + + + Kutsu käyttäjältä %s + Huonekutsu + %1$s ja %2$s + Tyhjä huone + + + %1$s lähetti tarran. + + + %1$s ja yksi muu + %1$s ja %2$d muuta + + + Viesti poistettu + %1$s poisti viestin + Viesti poistettu [syy: %1$s] + %1$s poisti viestin [syy: %2$s] + Koira + Kissa + Leijona + Hevonen + Yksisarvinen + Sika + Norsu + Kani + Panda + Kukko + Pingviini + Kilpikonna + Kala + Tursas + Perhonen + Kukka + Puu + Kaktus + Sieni + Maapallo + Kuu + Pilvi + Tuli + Banaani + Omena + Mansikka + Maissi + Pizza + Kakku + Sydän + Hymiö + Robotti + Hattu + Silmälasit + Jakoavain + Joulupukki + Peukut ylös + Sateenvarjo + Tiimalasi + Kello + Lahja + Hehkulamppu + Kirja + Lyijykynä + Klemmari + Sakset + Lukko + Avain + Vasara + Puhelin + Lippu + Juna + Polkupyörä + Lentokone + Raketti + Palkinto + Pallo + Kitara + Trumpetti + Soittokello + Ankkuri + Kuulokkeet + Kansio + Nuppineula + + Alkusynkronointi: +\nTuodaan tiliä… + Alkusynkronointi: +\nTuodaan kryptoa + Alkusynkronointi: +\nTuodaan huoneita + Alkusynkronointi: +\nTuodaan liityttyjä huoneita + Alkusynkronointi: +\nTuodaan kutsuttuja huoneita + Alkusynkronointi: +\nTuodaan poistuttuja huoneita + Alkusynkronointi: +\nTuodaan yhteisöjä + Alkusynkronointi: +\nTuodaan tilin tietoja + + %s päivitti tämän huoneen. + + Lähetetään viestiä… + Tyhjennä lähetysjono + + %1$s veti takaisin käyttäjän %2$s liittymiskutsun huoneeseen + Henkilön %1$s kutsu. Syy: %2$s + %1$s kutsui henkilön %2$s. Syy: %3$s + %1$s kutsui sinut. Syy: %2$s + %1$s liittyi huoneeseen. Syy: %2$s + %1$s poistui huoneesta. Syy: %2$s + %1$s hylkäsi kutsun. Syy: %2$s + %1$s poisti käyttäjän %2$s huoneesta. Syy: %3$s + %1$s poisti porttikiellon käyttäjältä %2$s. Syy: %3$s + %1$s antoi porttikiellon käyttäjälle %2$s. Syy: %3$s + %1$s lähetti kutsun liittyä huoneeseen käyttäjälle %2$s. Syy: %3$s + %1$s kumosi kutsun liittyä huoneeseen käyttäjälle %2$s. Syy: %3$s + %1$s hyväksyi kutsun liityäkseen huoneeseen %2$s. Syy: %3$s + %1$s veti takaisin käyttäjän %2$s kutsun. Syy: %3$s + + + %1$s lisäsi tälle huoneelle osoitteen %2$s. + %1$s lisäsi tälle huoneelle osoitteet %2$s. + + + + %1$s poisti tältä huoneelta osoitteen %2$s. + %1$s poisti tältä huoneelta osoitteet %3$s. + + + %1$s lisäsi tälle huoneelle osoitteen %2$s ja poisti osoitteen %3$s. + + %1$s asetti tämän huoneen pääosoitteeksi %2$s. + %1$s poisti tämän huoneen pääosoitteen. + + %1$s salli vieraiden liittyä huoneeseen. + %1$s esti vieraita liittymästä huoneeseen. + + %1$s laittoi päälle osapuolten välisen salauksen. + %1$s laittoi päälle osapuolisten välisen salauksen (tuntematon algoritmi %2$s). + + %s haluaa varmentaa salausavaimesi, mutta asiakasohjelmasi ei tue keskustelun aikana tapahtuvaa avainten varmennusta. Joudut käyttämään perinteistä varmennustapaa. + + diff --git a/matrix-sdk-android/src/main/res/values-fr/strings.xml b/matrix-sdk-android/src/main/res/values-fr/strings.xml new file mode 100644 index 0000000000..aad3bd1afb --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-fr/strings.xml @@ -0,0 +1,207 @@ + + + + %1$s : %2$s + %1$s a envoyé une image. + + invitation de %s + %1$s a invité %2$s + %1$s vous a invité + %1$s a rejoint le salon + %1$s est parti du salon + %1$s a rejeté l’invitation + %1$s a expulsé %2$s + %1$s a révoqué le bannissement de %2$s + %1$s a banni %2$s + %1$s a annulé l’invitation de %2$s + %1$s a changé d’avatar + %1$s a modifié son nom affiché en %2$s + %1$s a modifié son nom affiché %2$s en %3$s + %1$s a supprimé son nom affiché (%2$s) + %1$s a changé le sujet en : %2$s + %1$s a changé le nom du salon en : %2$s + %s a passé un appel vidéo. + %s a passé un appel vocal. + %s a répondu à l’appel. + %s a raccroché. + %1$s a rendu l’historique futur du salon visible pour %2$s + tous les membres du salon, depuis qu’ils ont été invités. + tous les membres du salon, depuis qu’ils l’ont rejoint. + tous les membres du salon. + n’importe qui. + inconnu (%s). + %1$s a activé le chiffrement de bout en bout (%2$s) + + %1$s a demandé une téléconférence VoIP + Téléconférence VoIP démarrée + Téléconférence VoIP terminée + + (l’avatar a aussi changé) + %1$s a supprimé le nom du salon + %1$s a supprimé le sujet du salon + %1$s a mis à jour son profil %2$s + %1$s a envoyé une invitation à %2$s pour rejoindre le salon + %1$s a accepté l’invitation pour %2$s + + ** Déchiffrement impossible : %s ** + L’appareil de l’expéditeur ne nous a pas envoyé les clés pour ce message. + + Effacement impossible + Envoi du message impossible + + L’envoi de l’image a échoué + + Erreur de réseau + Erreur de Matrix + + Il est impossible pour le moment de revenir dans un salon vide. + + Message chiffré + + Adresse e-mail + Numéro de téléphone + + %1$s a envoyé un sticker. + + Invitation de %s + Invitation au salon + Salon vide + %1$s et %2$s + + + %1$s et 1 autre + %1$s et %2$d autres + + + + Message supprimé + Message supprimé par %1$s + Message supprimé [motif : %1$s] + Message supprimé par %1$s [motif : %2$s] + Chien + Chat + Lion + Cheval + Licorne + Cochon + Éléphant + Lapin + Panda + Coq + Manchot + Tortue + Poisson + Pieuvre + Papillon + Fleur + Arbre + Cactus + Champignon + Terre + Lune + Nuage + Feu + Banane + Pomme + Fraise + Maïs + Pizza + Gâteau + Cœur + Smiley + Robot + Chapeau + Lunettes + Clé plate + Père Noël + Pouce levé + Parapluie + Sablier + Horloge + Cadeau + Ampoule + Livre + Crayon + Trombone + Ciseaux + Cadenas + Clé + Marteau + Téléphone + Drapeau + Train + Vélo + Avion + Fusée + Trophée + Balle + Guitare + Trompette + Cloche + Ancre + Écouteurs + Dossier + Épingle + + Synchronisation initiale : +\nImportation du compte… + Synchronisation initiale : +\nImportation de la cryptographie + Synchronisation initiale : +\nImportation des salons + Synchronisation initiale : +\nImportation des salons que vous avez rejoints + Synchronisation initiale : +\nImportation des salons où vous avez été invités + Synchronisation initiale : +\nImportation des salons que vous avez quittés + Synchronisation initiale : +\nImportation des communautés + Synchronisation initiale : +\nImportation des données du compte + + %s a mis à niveau ce salon. + + Envoi du message… + Vider la file d’envoi + + %1$s a révoqué l’invitation pour %2$s à rejoindre le salon + Invitation de %1$s. Raison : %2$s + %1$s a invité %2$s. Raison : %3$s + %1$s vous a invité. Raison : %2$s + %1$s a rejoint le salon. Raison : %2$s + %1$s est parti du salon. Raison : %2$s + %1$s a refusé l’invitation. Raison : %2$s + %1$s a expulsé %2$s. Raison : %3$s + %1$s a révoqué le bannissement de %2$s. Raison : %3$s + %1$s a banni %2$s. Raison : %3$s + %1$s a envoyé une invitation à %2$s pour rejoindre le salon. Raison : %3$s + %1$s a révoqué l’invitation de %2$s à rejoindre le salon. Raison : %3$s + %1$s a accepté l’invitation pour %2$s. Raison : %3$s + %1$s a annulé l’invitation de %2$s. Raison : %3$s + + + %1$s a ajouté %2$s comme adresse pour ce salon. + %1$s a ajouté %2$s comme adresses pour ce salon. + + + + %1$s a supprimé %2$s comme adresse pour ce salon. + %1$s a supprimé %3$s comme adresses pour ce salon. + + + %1$s a ajouté %2$s et supprimé %3$s comme adresses pour ce salon. + + %1$s a défini %2$s comme adresse principale pour ce salon. + %1$s a supprimé l’adresse principale de ce salon. + + %1$s a autorisé les visiteurs à rejoindre le salon. + %1$s a empêché les visiteurs de rejoindre le salon. + + %1$s a activé le chiffrement de bout en bout. + %1$s a activé le chiffrement de bout en bout (algorithme %2$s inconnu). + + %s demande à vérifier votre clé, mais votre client ne supporte pas la vérification de clés dans les discussions. Vous devrez utiliser l’ancienne vérification de clés pour vérifier les clés. + + %1$s a créé le salon + diff --git a/matrix-sdk-android/src/main/res/values-gl/strings.xml b/matrix-sdk-android/src/main/res/values-gl/strings.xml new file mode 100644 index 0000000000..77868e7df3 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-gl/strings.xml @@ -0,0 +1,68 @@ + + + Enderezo de correo + Fallo ao subir a páxina + + + %1$s: %2$s + %1$s enviou unha imaxe. + %1$s enviou unha icona. + + Convite de %s + %1$s convidou a %2$s + %1$s convidouno + %1$s entrou + %1$s saíu + %1$s rexeitou o convite + %1$s expulsou a %2$s + %1$s desbloqueou a %2$s + %1$s bloqueou a %2$s + %1$s cancelou o convite de %2$s + %1$s cambiou o seu avatar + %1$s cambiou o seu nome a %2$s + %1$s cambiou o seu nome de %2$s a %3$s + %1$s borrou o seu nome público (%2$s) + %1$s cambiou o tema desta sala para: %2$s + %1$s cambiou o nome desta sala para: %2$s + %s iniciou unha chamada de vídeo. + %s iniciou unha chamada de voz. + %s respondeu á chamada. + %s terminou a chamada. + %1$s fixo visible os próximos históricos para %2$s + toda a xente que integran esta sala, desde o momento en que foron convidados. + todas a xente da sala, desde o momento en que entraron. + todas os membros da sala. + todos. + descoñecido (%s). + %1$s activou a criptografía par-a-par (%2$s) + + %1$s solicitou unha conferencia VoIP + A conferencia VoIP comenzou + A conferencia VoIP terminou + + (o avatar tamén foi cambiado) + %1$s borrou o nome da sala + %1$s removeu o tema da sala + %1$s actualizou o seu perfil %2$s + %1$s envioulle un convite a %2$s para que entre na sala + %1$s aceptou o convite para %2$s + + ** Imposíbel descifrar: %s ** + O dispositivo do que envía non enviou as chaves desta mensaxe. + + Non se puido redactar + Non foi posíbel enviar a mensaxe + + Erro da conexión + Erro de Matrix + + Aínda non é posíbel volver a entrar nunha sala baleira. + + Mensaxe cifrada + + Número de teléfono + + %1$s e %2$s + + + diff --git a/matrix-sdk-android/src/main/res/values-hu/strings.xml b/matrix-sdk-android/src/main/res/values-hu/strings.xml new file mode 100644 index 0000000000..35f35eaecd --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-hu/strings.xml @@ -0,0 +1,206 @@ + + + %1$s: %2$s + %1$s küldött egy képet. + + %s meghívója + %1$s meghívta: %2$s + %1$s meghívott + %1$s belépett a szobába + %1$s kilépett a szobából + %1$s elutasította a meghívást + %1$s kidobta: %2$s + %1$s feloldotta %2$s tiltását + %1$s kitiltotta: %2$s + %1$s visszavonta %2$s meghívását + %1$s megváltoztatta a profilképét + %1$s megváltoztatta a megjelenő nevét erre: %2$s + %1$s megváltoztatta a megjelenítendő nevét erről: %2$s, erre: %3$s + %1$s eltávolította a megjelenítendő nevét (%2$s) + %1$s megváltoztatta a témát erre: %2$s + %1$s megváltoztatta a szoba nevét erre: %2$s + %s videóhívást kezdeményezett. + %s hanghívást kezdeményezett. + %s fogadta a hívást. + %s befejezte a hívást. + %1$s láthatóvá tette a jövőbeli előzményeket %2$s számára + az összes szobatag, onnantól, hogy meg lettek hívva. + az összes szobatag, onnantól, hogy csatlakoztak. + az összes szobatag. + bárki. + ismeretlen (%s). + %1$s bekapcsolta a végpontok közötti titkosítást (%2$s) + + %1$s hanghívás konferenciát kérelmezett + Hanghívás konferencia elindult + Hanghívás konferencia befejeződött + + (a profilkép is megváltozott) + %1$s eltávolította a szoba nevét + %1$s eltávolította a szoba témáját + %1$s megváltoztatta a(z) %2$s profilját + %1$s meghívót küldött %2$s számára, hogy csatlakozzon a szobához + %1$s elfogadta a meghívót ebbe: %2$s + + ** Visszafejtés sikertelen: %s ** + A küldő eszköze nem küldte el a kulcsokat ehhez az üzenethez. + + Kitakarás sikertelen + Üzenet küldése sikertelen + + Kép feltöltése sikertelen + + Hálózati hiba + Matrix hiba + + Jelenleg nem lehetséges újracsatlakozni egy üres szobához. + + Titkosított üzenet + + E-mail cím + Telefonszám + + %1$s küldött egy matricát. + + Meghívó tőle: %s + Meghívó egy szobába + %1$s és %2$s + Üres szoba + + + %1$s és 1 másik + %1$s és %2$d másik + + + + Üzenet eltávolítva + Üzenetet eltávolította: %1$s + Üzenet eltávolítva [ok: %1$s] + Üzenetet eltávolította: %1$s [ok: %2$s] + Kutya + Macska + Oroszlán + + Egyszarvú + Malac + Elefánt + Nyúl + Panda + Kakas + Pingvin + Teknős + Hal + Polip + Pillangó + Virág + Fa + Kaktusz + Gomba + Föld + Hold + Felhő + Tűz + Banán + Alma + Eper + Kukorica + Pizza + Süti + Szív + Smiley + Robot + Kalap + Szemüveg + Csavarkulcs + Télapó + Hüvelykujj fel + Esernyő + Homokóra + Óra + Ajándék + Égő + Könyv + Ceruza + Gémkapocs + Olló + Zár + Kulcs + Kalapács + Telefon + Zászló + Vonat + Kerékpár + Repülő + Rakéta + Trófea + Labda + Gitár + Trombita + Harang + Vasmacska + Fejhallgató + Mappa + + + Induló szinkronizáció: +\nFiók betöltése… + Induló szinkronizáció: +\nTitkosítás betöltése + Induló szinkronizáció: +\nSzobák betöltése + Induló szinkronizáció: +\nCsatlakozott szobák betöltése + Induló szinkronizáció: +\nMeghívott szobák betöltése + Induló szinkronizáció: +\nElhagyott szobák betöltése + Induló szinkronizáció: +\nKözösségek betöltése + Induló szinkronizáció: +\nFiók adatok betöltése + + %s frissítette ezt a szobát. + + Üzenet küldése… + Küldő sor ürítése + + %1$s visszavonta %2$s meghívását, hogy csatlakozzon a szobához + %1$s meghívója. Ok: %2$s + %1$s meghívta őt: %2$s. Ok: %3$s + %1$s meghívott. Ok: %2$s + %1$s belépett a szobába. Mert: %2$s + %1$s kilépett a szobából. Ok: %2$s + %1$s visszautasította a meghívót. Ok: %2$s + %1$s kirúgta őt: %2$s. Ok: %3$s + %1$s visszaengedte őt: %2$s. Ok: %3$s + %1$s kitiltotta őt: %2$s. Ok: %3$s + %1$s meghívót küldött neki: %2$s, hogy lépjen be a szobába. Ok: %3$s + %1$s visszavonta %2$s meghívóját a szobába való belépéshez. Ok: %3$s + %1$s elfogadta a meghívót ide: %2$s. Ok: %3$s + %1$s visszavonta %2$s meghívóját. Ok: %3$s + + + %1$s ezt a címet adta a szobához: %2$s. + %1$s ezeket a címeket adta a szobához: %2$s. + + + + %1$s ezt a címet törölte a szobából: %3$s. + %1$s ezeket a címeket törölte a szobából: %3$s. + + + %1$s a szobához adta ezeket:%2$s és törölte ezeket: %3$s. + + %1$s a szoba elsődleges címét erre állította be: %2$s. + %1$s eltávolította a szoba elsődleges címét. + + %1$s megengedte a vendégeknek, hogy belépjenek ebbe a szobába. + %1$s megtiltotta a vendégeknek, hogy belépjenek ebbe a szobába. + + %1$s bekapcsolta a végpontok közötti titkosítást. + %1$s bekapcsolta a végpontok közötti titkosítást (ismeretlen algoritmus %2$s). + + %s kéri a kulcsok ellenőrzését de a kliens nem támogatja a szobán belüli kulcs ellenőrzést. A hagyományos módon kell ellenőrizned a kulcsokat. + + %1$s szobát készített + diff --git a/matrix-sdk-android/src/main/res/values-id/strings.xml b/matrix-sdk-android/src/main/res/values-id/strings.xml new file mode 100644 index 0000000000..157b23d401 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-id/strings.xml @@ -0,0 +1,12 @@ + + + Undang dari %s + Undangan Ruang + %1$s dan %2$s + + Ruang kosong + + + %1$s dan %2$d yang lain + + \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-in/strings.xml b/matrix-sdk-android/src/main/res/values-in/strings.xml new file mode 100644 index 0000000000..157b23d401 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-in/strings.xml @@ -0,0 +1,12 @@ + + + Undang dari %s + Undangan Ruang + %1$s dan %2$s + + Ruang kosong + + + %1$s dan %2$d yang lain + + \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-is/strings.xml b/matrix-sdk-android/src/main/res/values-is/strings.xml new file mode 100644 index 0000000000..ecf19edb8a --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-is/strings.xml @@ -0,0 +1,76 @@ + + + %1$s: %2$s + %1$s sendi mynd. + %1$s sendi límmerki. + + %s sendi boð um þátttöku + %1$s bauð %2$s + %1$s bauð þér + %1$s gekk í hópinn + %1$s hætti + %1$s hafnaði boðinu + %1$s sparkaði %2$s + %1$s afbannaði %2$s + %1$s bannaði %2$s + %1$s breyttu auðkennismynd sinni + allir meðlimir spjallrásar, síðan þeim var boðið. + allir meðlimir spjallrásar, síðan þeir skráðu sig. + allir meðlimir spjallrásar. + hver sem er. + óþekktur (%s). + VoIP-símafundur hafinn + VoIP-símafundi lokið + + (einnig var skipt um auðkennismynd) + ** Mistókst að afkóða: %s ** + + Gat ekki sent skilaboð + + Gat ekki sent inn mynd + + Villa í netkerfi + Villa í Matrix + + Dulrituð skilaboð + + Tölvupóstfang + Símanúmer + + %1$s tók til baka boð frá %2$s + %1$s setti birtingarnafn sitt sem %2$s + %1$s breytti birtingarnafni sínu úr %2$s í %3$s + %1$s fjarlægði birtingarnafn sitt (%2$s) + %1$s breytti umræðuefninu í: %2$s + %1$s breytti heiti spjallrásarinnar í: %2$s + %s hringdi myndsamtal. + %s hringdi raddsamtal. + %s svaraði símtalinu. + %s lauk símtalinu. + %1$s kveikti á enda-í-enda dulritun (%2$s) + + %1$s bað um VoIP-símafund + %1$s fjarlægði heiti spjallrásar + %1$s fjarlægði umfjöllunarefni spjallrásar + %1$s gerði ferilskrá spjallrásar héðan í frá sýnilega fyrir %2$s + %1$s uppfærði notandasniðið sitt %2$s + %1$s sendi boð til %2$s um þátttöku í spjallrásinni + %1$s samþykkti boð um að taka þátt í %2$s + + Tæki sendandans hefur ekki sent okkur dulritunarlyklana fyrir þessi skilaboð. + + Gat ekki ritstýrt + Ekki er í augnablikinu hægt að taka aftur þátt í spjallrás sem er tóm. + + Boð á spjallrás + %1$s og %2$s + + + %1$s og 1 annar + %1$s og %2$d aðrir + + + Tóm spjallrás + Boð frá %s + + diff --git a/matrix-sdk-android/src/main/res/values-it/strings.xml b/matrix-sdk-android/src/main/res/values-it/strings.xml new file mode 100644 index 0000000000..2b2a097f13 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-it/strings.xml @@ -0,0 +1,303 @@ + + + %1$s: %2$s + %1$s ha inviato un\'immagine. + + Invito di %s + %1$s ha invitato %2$s + %1$s ti ha invitato + %1$s è entrato nella stanza + %1$s è uscito dalla stanza + %1$s ha rifiutato l\'invito + %1$s ha buttato fuori %2$s + %1$s ha tolto il bando a %2$s + %1$s ha bandito %2$s + %1$s ha revocato l\'invito per %2$s + %1$s ha modificato il suo avatar + %1$s hanno cambiato il nome visualizzato con %2$s + %1$s ha cambiato il nome visualizzato da %2$s a %3$s + %1$s ha rimosso il nome visibile (%2$s) + %1$s ha cambiato l\'argomento con: %2$s + %1$s ha cambiato il nome della stanza con: %2$s + %s ha iniziato una chiamata video. + %s ha iniziato una chiamata vocale. + %s ha risposto alla chiamata. + %s ha terminato la chiamata. + %1$s ha reso la futura cronologia della stanza visibile a %2$s + tutti i membri della stanza, dal momento del loro invito. + tutti i membri della stanza, dal momento in cui sono entrati. + tutti i membri della stanza. + chiunque. + sconosciuto (%s). + %1$s ha attivato la crittografia end-to-end (%2$s) + + %1$s ha richiesto una conferenza VoIP + Conferenza VoIP iniziata + Conferenza VoIP terminata + + (anche l\'avatar è cambiato) + %1$s ha rimosso il nome della stanza + %1$s ha rimosso l\'argomento della stanza + %1$s ha aggiornato il profilo %2$s + %1$s ha mandato un invito a %2$s per unirsi alla stanza + %1$s ha accettato l\'invito per %2$s + + ** Impossibile decriptare: %s ** + Il dispositivo del mittente non ci ha inviato le chiavi per questo messaggio. + + Impossibile revisionare + Impossibile inviare il messaggio + + Invio dell\'immagine fallito + + Errore di rete + Errore di Matrix + + Al momento non è possibile rientrare in una stanza vuota. + + Messaggio criptato + + Indirizzo email + Numero di telefono + + %1$s ha inviato un adesivo. + + + Invito da %s + Invito nella stanza + %1$s e %2$s + Stanza vuota + + + %1$s e 1 altro + %1$s e %2$d altri + + + + Messaggio rimosso + Messaggio rimosso da %1$s + Messaggio rimosso [motivo: %1$s] + Messaggio rimosso da %1$s [motivo: %2$s] + Cane + Gatto + Leone + Cavallo + Unicorno + Maiale + Elefante + Coniglio + Panda + Gallo + Pinguino + Tartaruga + Pesce + Piovra + Farfalla + Fiore + Albero + Cactus + Fungo + Globo + Luna + Nuvola + Fuoco + Banana + Mela + Fragola + Mais + Pizza + Torta + Cuore + Sorriso + Robot + Cappello + Occhiali + Chiave inglese + Babbo Natale + Pollice in su + Ombrello + Clessidra + Orologio + Regalo + Lampadina + Libro + Matita + Graffetta + Forbici + Lucchetto + Chiave + Martello + Telefono + Bandiera + Treno + Bicicletta + Aeroplano + Razzo + Trofeo + Palla + Chitarra + Tromba + Campana + Ancora + Cuffie + Cartella + Spillo + + Sync iniziale: +\nImportazione account… + Sync iniziale: +\nImportazione cifratura + Sync iniziale: +\nImportazione stanze + Sync iniziale: +\nImportazione stanze partecipate + Sync iniziale: +\nImportazione stanze invitate + Sync iniziale: +\nImportazione stanze lasciate + Sync iniziale: +\nImportazione comunità + Sync iniziale: +\nImportazione dati account + + %s ha aggiornato questa stanza. + + Invio messaggio in corso … + Cancella la coda di invio + + %1$s ha revocato l\'invito a %2$s di unirsi alla stanza + Invito di %1$s. Motivo: %2$s + %1$s ha invitato %2$s. Motivo: %3$s + %1$s ti ha invitato. Motivo: %2$s + %1$s è entrato nella stanza. Motivo: %2$s + %1$s è uscito dalla stanza. Motivo: %2$s + %1$s ha rifiutato l\'invito. Motivo: %2$s + %1$s ha buttato fuori %2$s. Motivo: %3$s + %1$s ha riammesso %2$s. Motivo: %3$s + %1$s ha bandito %2$s. Motivo: %3$s + %1$s ha inviato un invito a %2$s di unirsi alla stanza. Motivo: %3$s + %1$s ha revocato l\'invito a %2$s di unirsi alla stanza. Motivo: %3$s + %1$s ha accettato l\'invito per %2$s. Motivo: %3$s + %1$s ha rifiutato l\'invito di %2$s. Motivo: %3$s + + + %1$s ha aggiunto %2$s come indirizzo per questa stanza. + %1$s ha aggiunto %2$s come indirizzi per questa stanza. + + + + %1$s ha rimosso %2$s come indirizzo per questa stanza. + %1$s ha rimosso %3$s come indirizzi per questa stanza. + + + %1$s ha aggiunto %2$s e rimosso %3$s come indirizzi per questa stanza. + + %1$s ha impostato l\'indirizzo principale per questa stanza a %2$s. + %1$s ha rimosso l\'indirizzo principale per questa stanza. + + %1$s ha permesso l\'accesso alla stanza per gli ospiti. + %1$s ha impedito l\'accesso alla stanza per gli ospiti. + + %1$s ha attivato la cifratura end-to-end. + %1$s ha attivato la cifratura end-to-end (algoritmo %2$s non riconosciuto). + + %s sta chiedendo di verificare la tua chiave, ma il tuo client non supporta la verifica in-chat. Dovrai usare il metodo di verifica obsoleto per verificare le chiavi. + + %1$s ha creato la stanza + Hai inviato un\'immagine. + Hai inviato un adesivo. + + Il tuo invito + Hai creato la stanza + Hai invitato %1$s + Sei entrato nella stanza + Sei uscito dalla stanza + Hai rifiutato l\'invito + Hai buttato fuori %1$s + Hai riammesso %1$s + Hai bandito %1$s + Hai ritirato l\'invito di %1$s + Hai cambiato il tuo avatar + Hai impostato il tuo nome visualizzato a %1$s + Hai cambiato il tuo nome visualizzato da %1$s a %2$s + Hai rimosso il tuo nome visibile (era %1$s) + Hai cambiato l\'argomento a: %1$s + %1$s ha modificato l\'avatar della stanza + Hai modificato l\'avatar della stanza + Hai cambiato il nome della stanza a: %1$s + Hai iniziato una videochiamata. + Hai iniziato una telefonata. + %s ha inviato dati per impostare la chiamata. + Hai inviato dati per impostare la chiamata. + Hai risposto alla chiamata. + Hai terminato la chiamata. + Hai reso visibile la futura cronologia della stanza a %1$s + Hai attivato la crittografia end-to-end (%1$s) + Hai aggiornato questa stanza. + + Hai richiesto una conferenza VoIP + Hai rimosso il nome della stanza + Hai rimosso l\'argomento della stanza + %1$s ha rimosso l\'avatar della stanza + Hai rimosso l\'avatar della stanza + Hai aggiornato il tuo profilo %1$s + Hai mandato un invito a %1$s a unirsi alla stanza + Hai revocato l\'invito per %1$s a unirsi alla stanza + Hai accettato l\'invito per %1$s + + %1$s ha aggiunto il widget %2$s + Hai aggiunto il widget %1$s + %1$s ha rimosso il widget %2$s + Hai rimosso il widget %1$s + %1$s ha modificato il widget %2$s + Hai modificato il widget %1$s + + Amministratore + Moderatore + Predefinito + Personalizzato (%1$d) + Personalizzato + + Hai cambiato il livello di potere di %1$s. + %1$s ha cambiato il livello di potere di %2$s. + %1$s da %2$s a %3$s + + Il tuo invito. Motivo: %1$s + Hai invitato %1$s. Motivo: %2$s + Sei entrato nella stanza. Motivo: %1$s + Sei uscito dalla stanza. Motivo: %1$s + Hai rifiutato l\'invito. Motivo: %1$s + Hai buttato fuori %1$s. Motivo: %2$s + Hai riammesso %1$s. Motivo: %2$s + Hai bandito %1$s. Motivo: %2$s + Hai mandato un invito a %1$s a unirsi alla stanza. Motivo: %2$s + Hai revocato l\'invito a %1$s a unirsi alla stanza. Motivo: %2$s + Hai accettato l\'invito per %1$s. Motivo: %2$s + Hai ritirato l\'invito di %2$s. Motivo: %2$s + + + Hai aggiunto %1$s come indirizzo per questa stanza. + Hai aggiunto %1$s come indirizzi per questa stanza. + + + + Hai rimosso %1$s come indirizzo per questa stanza. + Hai rimosso %2$s come indirizzi per questa stanza. + + + Hai aggiunto %1$s e rimosso %2$s come indirizzi per questa stanza. + + Hai impostato l\'indirizzo principale per questa stanza a %1$s. + Hai rimosso l\'indirizzo principale per questa stanza. + + Hai permesso l\'accesso alla stanza per gli ospiti. + Hai impedito l\'accesso alla stanza per gli ospiti. + + Hai attivato la crittografia end-to-end. + Hai attivato la crittografia end-to-end (algoritmo %1$s sconosciuto). + + Accetta + Rifiuta + Riaggancia + + diff --git a/matrix-sdk-android/src/main/res/values-ja/strings.xml b/matrix-sdk-android/src/main/res/values-ja/strings.xml new file mode 100644 index 0000000000..366c743494 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-ja/strings.xml @@ -0,0 +1,74 @@ + + + + %1$s: %2$s + %1$sが画像を送信しました。 + %1$sがスタンプを送信しました。 + + %sの招待 + %1$sが%2$sを招待しました + %1$sがあなたを招待しました + %1$sが参加しました + %1$sが退出しました + %1$sが招待を断りました + %1$sが%2$sを追放しました + %1$sが%2$sをブロック解除しました + %1$sが%2$sをブロックしました + %1$sが%2$sの招待を撤回しました + %1$sがアバターを変更しました + %1$sが表示名を%2$sに設定しました + %1$sが表示名を%2$sから%3$sに変更しました + %1$sが表示名 (%2$s) を削除しました + %1$sがテーマを%2$sに変更しました + %1$sが部屋名を%2$sに変更しました + %sがビデオ通話を開始しました。 + %sが音声通話を開始しました。 + %sが電話に出ました。 + %sが通話を終了しました。 + %sさんからの招待 + 部屋への招待 + %1$sと%2$s + 空の部屋 + + + %1$sと他%2$d名 + + + %1$sは、今後の部屋履歴を%2$sに表示させました + 部屋のメンバー全員、招待された時点から。 + 部屋のメンバー全員、参加した時点から。 + 部屋のメンバー全員。 + 誰でも。 + 不明 (%s)。 + %1$s がエンドツーエンド暗号化を有効にしました (%2$s) + + %1$s がVoIP会議をリクエストしました + VoIP会議が開始されました + VoIP会議が終了しました + + (アバターも変更された) + %1$s が部屋名を削除しました + %1$s がルームトピックを削除しました + %1$s がプロフィール %2$s を更新しました + %1$s は %2$s に部屋に参加するよう招待状を送りました + %1$sは%2$sの招待を受け入れました + + ** 解読できません: %s ** + 送信者の端末からこのメッセージのキーが送信されていません。 + + 修正できませんでした + メッセージを送信できません + + 画像のアップロードに失敗しました + + ネットワークエラー + Matrixエラー + + 現在空の部屋に再参加することはできません。 + + 暗号化されたメッセージ + + メールアドレス + 電話番号 + + diff --git a/matrix-sdk-android/src/main/res/values-ko/strings.xml b/matrix-sdk-android/src/main/res/values-ko/strings.xml new file mode 100644 index 0000000000..88c5e7d618 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-ko/strings.xml @@ -0,0 +1,167 @@ + + + %1$s: %2$s + %s님의 초대 + 헤드폰 + %1$s님이 사진을 보냈습니다. + %1$s님이 스티커를 보냈습니다. + + %1$s님이 %2$s님을 초대했습니다 + %1$s님이 당신을 초대했습니다 + %1$s님이 참가했습니다 + %1$s님이 떠났습니다 + %1$s님이 초대를 거부했습니다 + %1$s님이 %2$s님을 추방했습니다 + %1$s님이 %2$s님의 출입 금지를 풀었습니다 + %1$s님이 %2$s님을 출입 금지했습니다 + %1$s님이 %2$s님의 초대를 취소했습니다 + %1$s님이 아바타를 변경했습니다 + %1$s님이 표시 이름을 %2$s(으)로 설정했습니다 + %1$s님이 표시 이름을 %2$s에서 %3$s(으)로 변경했습니다 + %1$s님이 표시 이름을 삭제했습니다 (%2$s) + %1$s님이 주제를 다음으로 변경했습니다: %2$s + %1$s님이 방 이름을 다음으로 변경했습니다: %2$s + %s님이 영상 통화를 걸었습니다. + %s님이 음성 통화를 걸었습니다. + %s님이 전화를 받았습니다. + %s님이 전화를 끊었습니다. + %1$s님이 이후 %2$s에게 방 기록을 공개했습니다 + 초대된 시점부터 모든 방 구성원 + 들어온 시점부터 모든 방 구성원 + 모든 방 구성원 + 누구나. + 알 수 없음 (%s). + %1$s님이 종단간 암호화를 켰습니다 (%2$s) + %s님이 방을 업그레이드했습니다. + + %1$s님이 VoIP 회의를 요청했습니다 + VoIP 회의가 시작했습니다 + VoIP 회의가 끝났습니다 + + (아바타도 변경됨) + %1$s님이 방 이름을 삭제했습니다 + %1$s님이 방 주제를 삭제했습니다 + 메시지가 삭제되었습니다 + 메시지가 %1$s님에 의해 삭제되었습니다 + 메시지가 삭제되었습니다 [이유: %1$s] + 메시지가 %1$s님에 의해 삭제되었습니다 [이유: %2$s] + %1$s님이 프로필 %2$s을(를) 업데이트했습니다 + %1$s님이 %2$s님에게 방 초대를 보냈습니다 + %1$s님이 %2$s의 초대를 수락했습니다 + + ** 암호를 복호화할 수 없음: %s ** + 발신인의 기기에서 이 메시지의 키를 보내지 않았습니다. + + 검열할 수 없습니다 + 메시지를 보낼 수 없습니다 + + 사진 업로드에 실패했습니다 + + 네트워크 오류 + Matrix 오류 + + 현재 빈 방에 다시 들어갈 수 없습니다. + + 암호화된 메시지 + + 이메일 주소 + 전화번호 + + %s에서 초대함 + 방 초대 + + %1$s님과 %2$s님 + + + %1$s님 외 %2$d명 + + + 빈 방 + + + + 고양이 + 사자 + + 유니콘 + 돼지 + 코끼리 + 토끼 + 판다 + 수탉 + 펭귄 + 거북 + 물고기 + 문어 + 나비 + + 나무 + 선인장 + 버섯 + 지구본 + + 구름 + + 바나나 + 사과 + 딸기 + 옥수수 + 피자 + 케이크 + 하트 + 웃음 + 로봇 + 모자 + 안경 + 스패너 + 산타클로스 + 좋아요 + 우산 + 모래시계 + 시계 + 선물 + 전구 + + 연필 + 클립 + 가위 + 자물쇠 + 열쇠 + 망치 + 전화기 + 깃발 + 기차 + 자전거 + 비행기 + 로켓 + 트로피 + + 기타 + 트럼펫 + + + 폴더 + + + 초기 동기화: +\n계정 가져오는 중… + 초기 동기화: +\n암호 가져오는 중 + 초기 동기화: +\n방 가져오는 중 + 초기 동기화: +\n들어간 방 가져오는 중 + 초기 동기화: +\n초대받은 방 가져오는 중 + 초기 동기화: +\n떠난 방 가져오는 중 + 초기 동기화: +\n커뮤니티 가져오는 중 + 초기 동기화: +\n계정 데이터 가져오는 중 + + 메시지 보내는 중… + 전송 대기 열 지우기 + + %1$s님이 %2$s님에게 방에 참가하라고 보낸 초대를 취소했습니다 + diff --git a/matrix-sdk-android/src/main/res/values-lt/strings.xml b/matrix-sdk-android/src/main/res/values-lt/strings.xml new file mode 100644 index 0000000000..b867219408 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-lt/strings.xml @@ -0,0 +1,8 @@ + + + %1$s: %2$s + %1$s išsiuntė atvaizdą. + %1$s išsiuntė lipduką. + + %s pakvietimas + diff --git a/matrix-sdk-android/src/main/res/values-lv/strings.xml b/matrix-sdk-android/src/main/res/values-lv/strings.xml new file mode 100644 index 0000000000..b14cbb4b00 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-lv/strings.xml @@ -0,0 +1,75 @@ + + + %1$s: %2$s + %1$s nosūtīja attēlu. + + %s\'s uzaicinājums + %1$s uzaicināja %2$s + %1$s uzaicināja tevi + %1$s pievienojās + %1$s atstāja + %1$s noraidīja uzaicinājumu + %1$s \"izspēra\" ārā %2$s + %1$s atbanoja (atcēla pieejas liegumu) %2$s + %1$s liedza pieeju (banoja) %2$s + %1$s atsauca %2$s uzaicinājumu + %1$s nomainīja profila attēlu + %1$s uzstādīja redzamo vārdu uz %2$s + %1$s nomainīja redzamo vārdu no %2$s uz %3$s + %1$s dzēsa savu redzamo vārdu (%2$s) + %1$s nomainīja tēmas nosaukumu uz: %2$s + %1$s nomainīja istabas nosaukumu uz: %2$s + %s veica video zvanu. + %s veica audio zvanu. + %s atbildēja zvanam. + %s beidza zvanu. + %1$s padarīja istabas nākamo ziņu vēsturi redzamu %2$s + visi istabas biedri no brīža, kad tika uzaicināti. + visi istabas biedri no brīža, kad tika pievienojušies. + visi istabas biedri. + ikviens. + nezināms (%s). + %1$s ieslēdza ierīce-ierīce šifrēšanu (%2$s) + + %1$s vēlas VoIP konferenci + VoIP konference sākusies + VoIP konference ir beigusies + + (arī profila attēls mainījās) + %1$s dzēsa istabas nosaukumu + %1$s dzēsa istabas tēmas nosaukumu + %1$s atjaunoja profila informāciju %2$s + %1$s nosūtīja uzaicinājumu %2$s pievienoties istabai + %1$s apstiprināja uzaicinājumu priekš %2$s + + ** Nav iespējams atkodēt: %s ** + Sūtītāja ierīce mums nenosūtīja atslēgas priekš šīs ziņas. + + Nevarēja rediģēt + Nav iespējams nosūtīt ziņu + + Neizdevās augšuplādēt attēlu + + Tīkla kļūda + Matrix kļūda + + Šobrīd nav iespējams atkārtoti pievienoties tukšai istabai. + + Šifrēta ziņa + + Epasta adrese + Telefona numurs + + Uzaicinājums no %s + Uzaicinājums uz istabu + %1$s un %2$s + Tukša istaba + + + %1$s un 1 cits + %1$s un %2$d citi + %1$s un %2$d citu + + + + diff --git a/matrix-sdk-android/src/main/res/values-nl/strings.xml b/matrix-sdk-android/src/main/res/values-nl/strings.xml new file mode 100644 index 0000000000..22eb61f109 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-nl/strings.xml @@ -0,0 +1,215 @@ + + + + %1$s: %2$s + %1$s heeft een afbeelding gestuurd. + + Uitnodiging van %s + %1$s heeft %2$s uitgenodigd + %1$s heeft u uitgenodigd + %1$s neemt nu deel aan het gesprek + %1$s heeft het gesprek verlaten + %1$s heeft de uitnodiging geweigerd + %1$s heeft %2$s uit het gesprek verwijderd + %1$s heeft %2$s ontbannen + %1$s heeft %2$s verbannen + %1$s heeft de uitnodiging van %2$s ingetrokken + %1$s heeft zijn/haar avatar aangepast + %1$s heeft zijn/haar naam aangepast naar %2$s + %1$s heeft zijn/haar naam aangepast van %2$s naar %3$s + %1$s heeft zijn/haar naam verwijderd (%2$s) + %1$s heeft het onderwerp veranderd naar: %2$s + %1$s heeft de gespreksnaam veranderd naar: %2$s + %s heeft een video-oproep gemaakt. + %s heeft een spraakoproep gemaakt. + %s heeft de oproep beantwoord. + %s heeft opgehangen. + %1$s heeft de toekomstige gespreksgeschiedenis zichtbaar gemaakt voor %2$s + alle deelnemers aan het gesprek, vanaf het punt dat ze zijn uitgenodigd. + alle deelnemers aan het gesprek, vanaf het punt dat ze zijn toegetreden. + alle deelnemers aan het gesprek. + iedereen. + onbekend (%s). + %1$s heeft eind-tot-eind-versleuteling aangezet (%2$s) + + %1$s heeft een VoIP-vergadering aangevraagd + VoIP-vergadering gestart + VoIP-vergadering gestopt + + (avatar is ook veranderd) + %1$s heeft de gespreksnaam verwijderd + %1$s heeft het gespreksonderwerp verwijderd + %1$s heeft zijn/haar profiel %2$s bijgewerkt + %1$s heeft een uitnodiging naar %2$s gestuurd om het gesprek toe te treden + %1$s heeft de uitnodiging voor %2$s aanvaard + + ** Kan niet ontsleutelen: %s ** + Het apparaat van de afzender heeft geen sleutels voor dit bericht gestuurd. + + + Kon niet verwijderd worden + Kan bericht niet verzenden + + Uploaden van de afbeelding mislukt + + + Netwerkfout + Matrix-fout + + + + + + + Het is momenteel niet mogelijk om een leeg gesprek opnieuw toe te treden. + + Versleuteld bericht + + + E-mailadres + Telefoonnummer + + %1$s heeft een sticker gestuurd. + + + Uitnodiging van %s + Gespreksuitnodiging + %1$s en %2$s + Leeg gesprek + + + %1$s en 1 andere + %1$s en %2$d anderen + + + + Bericht verwijderd + Bericht verwijderd door %1$s + Bericht verwijderd [reden: %1$s] + Bericht verwijderd door %1$s [reden: %2$s] + Hond + Kat + Leeuw + Paard + Eenhoorn + Varken + Olifant + Konijn + Panda + Haan + Pinguïn + Schildpad + Vis + Octopus + Vlinder + Bloem + Boom + Cactus + Paddenstoel + Aardbol + Maan + Wolk + Vuur + Banaan + Appel + Aardbei + Maïs + Pizza + Taart + Hart + Smiley + Robot + Hoed + Bril + Moersleutel + Kerstman + Duim omhoog + Paraplu + Zandloper + Klok + Cadeau + Gloeilamp + Boek + Potlood + Paperclip + Schaar + Slot + Sleutel + Hamer + Telefoon + Vlag + Trein + Fiets + Vliegtuig + Raket + Trofee + Bal + Gitaar + Trompet + Bel + Anker + Koptelefoon + Map + Speld + + Initiële synchronisatie: +\nAccount wordt geïmporteerd… + Initiële synchronisatie: +\nCrypto wordt geïmporteerd + Initiële synchronisatie: +\nGesprekken worden geïmporteerd + Initiële synchronisatie: +\nDeelgenomen gesprekken worden geïmporteerd + Initiële synchronisatie: +\nUitgenodigde gesprekken worden geïmporteerd + Initiële synchronisatie: +\nVerlaten gesprekken worden geïmporteerd + Initiële synchronisatie: +\nGemeenschappen worden geïmporteerd + Initiële synchronisatie: +\nAccountgegevens worden geïmporteerd + + %s heeft dit gesprek opgewaardeerd. + + Bericht wordt verstuurd… + Uitgaande wachtrij legen + + %1$s heeft de uitnodiging voor %2$s om het gesprek toe te treden ingetrokken + Uitnodiging van %1$s. Reden: %2$s + %1$s heeft %2$s uitgenodigd. Reden: %3$s + %1$s heeft u uitgenodigd. Reden: %2$s + %1$s neemt nu deel. Reden: %2$s + %1$s is weggegaan. Reden: %2$s + %1$s heeft de uitnodiging geweigerd. Reden: %2$s + %1$s heeft %2$s verwijderd. Reden: %3$s + %1$s heeft %2$s ontbannen. Reden: %3$s + %1$s heeft %2$s verbannen. Reden: %3$s + %1$s heeft %2$s een uitnodiging voor het gesprek gestuurd. Reden: %3$s + %1$s heeft de uitnodiging voor %2$s ingetrokken. Reden: %3$s + %1$s heeft de uitnodiging voor %2$s aanvaard. Reden: %3$s + %1$s heeft de uitnodiging van %2$s ingetrokken. Reden: %3$s + + + %1$s heeft %2$s als gespreksadres toegevoegd. + %1$s heeft %2$s als gespreksadressen toegevoegd. + + + + %1$s heeft %2$s als gespreksadres verwijderd. + %1$s heeft %3$s als gespreksadressen verwijderd. + + + %1$s heeft %2$s als gespreksadres toegevoegd en %3$s verwijderd. + + %1$s heeft het hoofdadres voor dit gesprek ingesteld op %2$s. + %1$s heeft het hoofdadres voor dit gesprek verwijderd. + + %1$s heeft gasten de toegang tot het gesprek verleend. + %1$s heeft gasten de toegang tot het gesprek verhinderd. + + %1$s heeft eind-tot-eind-versleuteling ingeschakeld. + %1$s heeft eind-tot-eind-versleuteling ingeschakeld (onbekend algoritme %2$s). + + %s vraagt om uw sleutel te verifiëren, maar uw cliënt biedt geen ondersteuning voor verificatie in het gesprek. U zult de verouderde sleutelverificatie moeten gebruiken om de sleutels te verifiëren. + + diff --git a/matrix-sdk-android/src/main/res/values-nn/strings.xml b/matrix-sdk-android/src/main/res/values-nn/strings.xml new file mode 100644 index 0000000000..601cf4c9df --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-nn/strings.xml @@ -0,0 +1,150 @@ + + + Kryptert melding + + + %1$s: %2$s + %1$s sende eit bilæte. + %1$s sende eit klistremerke. + + %s si innbjoding + %1$s inviterte %2$s + %1$s inviterte deg + %1$s kom inn + %1$s forlot rommet + %1$s sa nei til innbjodingi + %1$s sparka %2$s + %1$s slapp %2$s inn att + %1$s stengde %2$s ute + %1$s tok attende %2$s si innbjoding + %1$s byta avataren sin + %1$s sette visingsnamnet sitt som %2$s + %1$s byta visingsnamnet sitt frå %2$s til %3$s + %1$s tok burt visingsnamnet sitt (%2$s) + %1$s gjorde emnet til: %2$s + %1$s gjorde romnamnet til: %2$s + %s starta ei videosamtala. + %s starta ein talesamtale. + %s tok røyret. + %s la på røyret. + %1$s gjorde den framtidige romsoga synleg for %2$s + alle rommedlemmar, frå då dei vart invitert inn. + alle rommedlemmar, frå då dei kom inn. + alle rommedlemmar. + kven som heldst. + uvisst (%s). + %1$s skrudde ende-til-ende-kryptering på (%2$s) + + %1$s bad um ei VoIP-gruppasamtala + VoIP-gruppasamtala er starta + VoIP-gruppasamtala er ferdug + + (avataren vart au byta) + %1$s tok burt romnamnet + %1$s tok burt romemnet + %1$s gjorde um på skildringi si %2$s + %1$s inviterte %2$s til rommet + %1$s sa ja til innbjodingi til %2$s + + ** Fekk ikkje til å dekryptera: %s ** + Avsendareiningi hev ikkje sendt oss nyklane fyr denna meldingi. + + Kunde ikkje gjera um + Fekk ikkje å senda meldingi + + Fekk ikkje til å lasta biletet upp + + Noko gjekk gale med netverket + Noko gjekk gale med Matrix + + Det lèt seg fyrebils ikkje gjera å fara inn att i eit tomt rom. + + Epostadresse + Telefonnummer + + Innbjoding frå %s + Rominnbjoding + %1$s og %2$s + + + %1$s og 1 til + %1$s og %2$d til + + + Tomt rom + + Ei melding vart stroki + %1$s strauk meldingi + Meldingi vart stroki [av di: %1$s] + %1$s strauk meldingi [av di: %2$s] + Hund + Katt + Løva + Hest + Einhyrning + Gris + Elefant + Hare + Panda + Hane + Pingvin + Skjoldpadda + Fisk + Blekksprut + Fivrelde + Blome + Tre + Kaktus + Sopp + Klote + Måne + Sky + Eld + Banan + Eple + Jordbær + Mais + Pizza + Kaka + Hjarta + Smilandlit + Robot + Hatt + Brillor + Skiftenykel + Nissen + Tumalen Upp + Regnskjold + Timeglas + Ur + Gåva + Ljospera + Bok + Blyant + Binders + Saks + Lås + Nykel + Hamar + Telefon + Flagg + Tog + Sykkel + Flyg + Rakett + Pokal + Ball + Gitar + Trompet + Klokka + Ankar + Hodetelefon + Mappa + Nål + + %s oppgraderte rommet. + + Nullstill sendingskø + + %1$s forlot rommet. Grunn: %2$s + diff --git a/matrix-sdk-android/src/main/res/values-pl/strings.xml b/matrix-sdk-android/src/main/res/values-pl/strings.xml new file mode 100644 index 0000000000..dc380516b7 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-pl/strings.xml @@ -0,0 +1,169 @@ + + + %1$s: %2$s + %1$s wysłał(a) zdjęcie. + + Zaproszenie od %s + %1$s zaprosił(a) %2$s + %1$s zaprosił(a) Cię + %1$s dołączył(a) + %1$s opuścił(a) + %1$s odrzucił(a) zaproszenie + %1$s wyrzucił(a) %2$s + %1$s odblokował(a) %2$s + %1$s zablokował(a) %2$s + %1$s zmienił(a) awatar + %1$s zmienił(a) wyświetlaną nazwę na %2$s + %1$s zmienił(a) wyświetlaną nazwę z %2$s na %3$s + %1$s usunął(-ęła) swoją wyświetlaną nazwę (%2$s) + %1$s zmienił(a) temat na: %2$s + Nie można wysłać wiadomości + + Przesyłanie zdjęcia nie powiodło się + + Błąd sieci + Błąd Matrixa + + Wiadomość zaszyfrowana + + Adres e-mail + Numer telefonu + + wszyscy członkowie pokoju. + wszyscy. + %1$s zmienił(a) nazwę pokoju na: %2$s + %s zakończył(a) rozmowę. + %1$s usunął(-ęła) nazwę pokoju + %1$s usunął(-ęła) temat pokoju + %1$s wysłał(a) naklejkę. + + %1$s włączył(a) szyfrowanie end-to-end (%2$s) + + %1$s wycofał(a) zaproszenie %2$s + %s odebrał(a) połączenie. + (awatar też został zmieniony) + + Zaproszenie od %s + Zaproszenie do pokoju + %1$s i %2$s + Pusty pokój + + + %1$s i jeden inny + %1$s i kilku innych + %1$s i %2$d innych + + + + ** Nie można odszyfrować: %s ** + %s wykonał(a) rozmowę wideo. + %s wykonał(a) połączenie głosowe. + %1$s uczynił(a) przyszłą historię pokoju widoczną dla %2$s + wszyscy członkowie pokoju, od momentu w którym zostali zaproszeni. + wszyscy członkowie pokoju, od momentu w którym dołączyli. + nieznane (%s). + %1$s zażądał(a) grupowego połączenia VoIP + Rozpoczęto grupowe połączenie głosowe VoIP + Zakończono grupowe połączenie głosowe VoIP + + %1$s zaktualizował swój profil %2$s + %1$s wysłał(a) zaproszenie do %2$s aby dołączył(a) do tego pokoju + %1$s zaakceptował(a) zaproszenie dla %2$s + + Urządzenie nadawcy nie wysłało nam kluczy do tej wiadomości. + + Nie można zredagować + Obecnie nie jest możliwe ponowne dołączenie do pustego pokoju. + + Wiadomość usunięta + Wiadomość usunięta przez %1$s + Wiadomość usunięta [powód: %1$s] + Wiadomość usunięta przez %1$s [powód: %2$s] + Pies + Kot + Lew + Koń + Jednorożec + Świnia + Słoń + Królik + Panda + Kogut + Pingwin + Żółw + Ryba + Ośmiornica + Motyl + Kwiat + Drzewo + Kaktus + Grzyb + Księżyc + Chmura + Ogień + Banan + Jabłko + Truskawka + Kukurydza + Pizza + Ciasto + Serce + Robot + Kapelusz + Okulary + Parasol + Klepsydra + Zegar + Żarówka + Książka + Ołówek + Spinacz + Nożyczki + Klucz + Telefon + Flaga + Pociąg + Rower + Samolot + Rakieta + Trofeum + Gitara + Trąbka + Dzwonek + Kotwica + Słuchawki + Folder + Pinezka + + Ziemia + Uśmiech + Klucz francuski + Mikołaj + Prezent + Młotek + %s zakutalizował(a) ten pokój. + + Kciuk w górę + Zamek + Piłka + Synchronizacja początkowa: +\nImportowanie konta… + Synchronizacja początkowa: +\nImportowanie kryptografii + Synchronizacja początkowa: +\nImportowanie Pokoi + Synchronizacja początkowa: +\nImportowanie dołączonych Pokoi + Synchronizacja początkowa: +\nImportowanie zaproszonych Pokoi + Synchronizacja początkowa: +\nImportowanie opuszczonych Pokoi + Synchronizacja początkowa: +\nImportowanie Społeczności + Synchronizacja początkowa: +\nImportowanie danych Konta + + Wysyłanie wiadomości… + Wyczyść kolejkę wysyłania + + diff --git a/matrix-sdk-android/src/main/res/values-pt-rBR/strings.xml b/matrix-sdk-android/src/main/res/values-pt-rBR/strings.xml new file mode 100644 index 0000000000..7c5d6fe583 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-pt-rBR/strings.xml @@ -0,0 +1,309 @@ + + + + %1$s: %2$s + %1$s enviou uma foto. + + convite de %s + %1$s convidou %2$s + %1$s convidou você + %1$s entrou na sala + %1$s saiu da sala + %1$s recusou o convite + %1$s removeu %2$s + %1$s removeu o banimento de %2$s + %1$s baniu %2$s + %1$s desfez o convite a %2$s + %1$s alterou a foto de perfil + %1$s definiu o nome e sobrenome como %2$s + %1$s alterou o nome e sobrenome de %2$s para %3$s + %1$s removeu o nome e sobrenome (era %2$s) + %1$s alterou a descrição para: %2$s + %1$s alterou o nome da sala para: %2$s + %s iniciou uma chamada de vídeo. + %s iniciou uma chamada de voz. + %s aceitou a chamada. + %s encerrou a chamada. + %1$s deixou o histórico futuro da sala visível para %2$s + todos os membros da sala, a partir do momento em que foram convidados. + todos os membros da sala, a partir do momento em que entraram nela. + todos os membros da sala. + qualquer pessoa. + desconhecido (%s). + %1$s ativou a criptografia de ponta a ponta (%2$s) + + %1$s deseja iniciar uma chamada em grupo + Chamada em grupo iniciada + Chamada em grupo encerrada + + (a foto de perfil também foi alterada) + %1$s removeu o nome da sala + %1$s removeu a descrição da sala + %1$s atualizou o perfil %2$s + %1$s enviou um convite para %2$s entrar na sala + %1$s aceitou o convite para %2$s + + ** Não foi possível descriptografar: %s ** + O aparelho do remetente não nos enviou as chaves para esta mensagem. + + + Não foi possível redigir + Não foi possível enviar a mensagem + + O envio da imagem falhou + + + Erro de conexão à internet + Erro no servidor Matrix + + + + + + + + + Atualmente, não é possível entrar novamente em uma sala vazia. + + Mensagem criptografada + + + Endereço de e-mail + Número de telefone + + + %1$s enviou uma figurinha. + + + Convite de %s + Convite para sala + %1$s e %2$s + Sala vazia + + + %1$s e 1 outro + %1$s e %2$d outros + + + + Você enviou uma foto. + Você enviou uma figurinha. + + Seu convite + %1$s criou a sala + Você criou a sala + Você convidou %1$s + Você entrou na sala + Você saiu da sala + Você recusou o convite + Você removeu %1$s + Você removeu o banimento de %1$s + Você baniu %1$s + Você desfez o convite a %1$s + Você alterou a sua foto de perfil + Você definiu o seu nome e sobrenome como %1$s + Você alterou o seu nome e sobrenome de %1$s para %2$s + Você removeu o seu nome e sobrenome (era %1$s) + Você alterou a descrição para: %1$s + %1$s alterou a foto da sala + Você alterou a foto da sala + Você alterou o nome da sala para: %1$s + Você iniciou uma chamada de vídeo. + Você iniciou uma chamada de voz. + %s enviou dados para configurar a chamada. + Você enviou dados para configurar a chamada. + Você aceitou a chamada. + Você encerrou a chamada. + Você deixou o histórico futuro da sala visível para %1$s + Você ativou a criptografia de ponta a ponta (%1$s) + %s atualizou esta sala. + Você atualizou esta sala. + + Você solicitou uma chamada em grupo + Você removeu o nome da sala + Você removeu a descrição da sala + %1$s removeu a foto da sala + Você removeu a foto da sala + Mensagem removida + Mensagem removida por %1$s + Mensagem removida [motivo: %1$s] + Mensagem removida por %1$s [motivo: %2$s] + Você atualizou o seu perfil %1$s + Você enviou um convite para %1$s entrar na sala + %1$s cancelou o convite a %2$s para entrar na sala + Você cancelou o convite a %1$s para entrar na sala + Você aceitou o convite para %1$s + + %1$s adicionou o widget %2$s + Você adicionou o widget %1$s + %1$s removeu o widget %2$s + Você removeu o widget %1$s + %1$s editou o widget %2$s + Você editou o widget %1$s + + Administrador + Moderador + Padrão + Personalizado (%1$d) + Personalizado + + Você alterou o nível de permissão de %1$s. + %1$s alterou o nível de permissão de %2$s. + %1$s de %2$s para %3$s + + Cachorro + Gato + Leão + Cavalo + Unicórnio + Porco + Elefante + Coelho + Panda + Galo + Pinguim + Tartaruga + Peixe + Polvo + Borboleta + Flor + Árvore + Cacto + Cogumelo + Globo + Lua + Nuvem + Fogo + Banana + Maçã + Morango + Milho + Pizza + Bolo + Coração + Sorriso + Robô + Chapéu + Óculos + Chave inglesa + Papai-noel + Joinha + Guarda-chuva + Ampulheta + Relógio + Presente + Lâmpada + Livro + Lápis + Clipe de papel + Tesoura + Cadeado + Chave + Martelo + Telefone + Bandeira + Trem + Bicicleta + Avião + Foguete + Troféu + Bola + Guitarra + Trombeta + Sino + Âncora + Fones de ouvido + Pasta + Alfinete + + Primeira sincronização:↵ +\nImportando a conta… + Primeira sincronização:↵ +\nImportando as chaves de criptografia + Primeira sincronização:↵ +\nImportando as salas + Primeira sincronização:↵ +\nImportando as salas em que você entrou + Primeira sincronização:↵ +\nImportando as salas em que você foi convidado + Primeira sincronização:↵ +\nImportando as salas em que você saiu + Primeira sincronização:↵ +\nImportando as comunidades + Primeira sincronização:↵ +\nImportando os dados da conta + + Enviando mensagem… + Limpar a fila de envio + + Convite de %1$s. Motivo: %2$s + O seu convite. Motivo: %1$s + %1$s convidou %2$s. Motivo: %3$s + Você convidou %1$s. Motivo: %2$s + %1$s convidou você. Motivo: %2$s + %1$s entrou na sala. Motivo: %2$s + Você entrou na sala. Motivo: %1$s + %1$s saiu da sala. Motivo: %2$s + Você saiu da sala. Motivo: %1$s + %1$s recusou o convite. Motivo: %2$s + Você recusou o convite. Motivo: %1$s + %1$s removeu %2$s. Motivo: %3$s + Você removeu %1$s. Motivo: %2$s + %1$s removeu o banimento de %2$s. Motivo: %3$s + Você removeu o banimento de %1$s. Motivo: %2$s + %1$s baniu %2$s. Motivo: %3$s + Você baniu %1$s. Motivo: %2$s + %1$s enviou um convite para %2$s entrar na sala. Motivo: %3$s + Você enviou um convite para %1$s entrar na sala. Motivo: %2$s + %1$s revogou o convite para %2$s entrar na sala. Motivo: %3$s + Você revogou o convite para %1$s entrar na sala. Motivo: %2$s + %1$s aceitou o convite para %2$s. Motivo: %3$s + Você aceitou o convite para %1$s. Motivo: %2$s + %1$s desfez o convite de %2$s. Motivo: %3$s + Você desfez o convite de %1$s. Motivo: %2$s + + + %1$s adicionou %2$s como um endereço desta sala. + %1$s adicionou %2$s como endereços desta sala. + + + + Você adicionou %1$s como um endereço desta sala. + Você adicionou %1$s como endereços desta sala. + + + + %1$s removeu %2$s como um endereço desta sala. + %1$s removeu %3$s como endereços desta sala. + + + + Você removeu %1$s como um endereço desta sala. + Você removeu %2$s como endereços desta sala. + + + %1$s adicionou %2$s e removeu %3$s como endereços desta sala. + Você adicionou %1$s e removeu %2$s como endereços desta sala. + + %1$s definiu o endereço principal desta sala como %2$s. + Você definiu o endereço principal desta sala como %1$s. + %1$s removeu o endereço principal desta sala. + Você removeu o endereço principal desta sala. + + %1$s permitiu que convidados entrem na sala. + Você permitiu que convidados entrem na sala. + %1$s impediu que convidados entrem na sala. + Você impediu que convidados entrem na sala. + + %1$s ativou a criptografia de ponta a ponta. + Você ativou a criptografia de ponta a ponta. + %1$s ativou a criptografia de ponta a ponta (algoritmo não reconhecido %2$s). + Você ativou a criptografia de ponta a ponta (algoritmo não reconhecido %1$s). + + %s deseja verificar a sua chave, mas o seu aplicativo não suporta a verificação da chave da conversa. Você precisará usar a verificação tradicional de chaves para verificar chaves. + + Aceitar + Recusar + Encerrar + + diff --git a/matrix-sdk-android/src/main/res/values-pt/strings.xml b/matrix-sdk-android/src/main/res/values-pt/strings.xml new file mode 100644 index 0000000000..4bc90cf0cb --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-pt/strings.xml @@ -0,0 +1,87 @@ + + + + %1$s: %2$s + %1$s enviou uma imagem. + + convite de %s + %1$s convidou %2$s + %1$s convidou-o + %1$s entrou + %1$s saiu + %1$s recusou o convite + %1$s expulsou %2$s + %1$s des-baniu %2$s + %1$s baniu %2$s + %1$s cancelou o convite de %2$s + %1$s mudou o seu avatar + %1$s definiu seu nome público como %2$s + %1$s alterou seu nome público de %2$s para %3$s + %1$s apagou o seu nome público (%2$s) + %1$s alterou o tópico desta sala para: %2$s + %1$s alterou o nome desta sala para: %2$s + %s iniciou uma chamada de vídeo. + %s iniciou uma chamada de voz. + %s respondeu à chamada. + %s terminou a chamada. + %1$s tornou o histórico futuro desta sala visível para %2$s + todas os membros que integram esta sala, a partir do momento em que foram convidados. + todas os membros da sala, a partir do momento em que entraram. + todas os membros da sala. + todos. + desconhecida (%s). + %1$s ativou a criptografia ponta-a-ponta (%2$s) + + %1$s solicitou uma conferência VoIP + A conferência VoIP começou + A conferência VoIP terminou + + (o avatar também foi alterado) + %1$s removeu o nome da sala + %1$s removeu o tópico da sala + %1$s atualizou o seu perfil %2$s + %1$s enviou um convite para que %2$s se junte à sala + %1$s aceitou o convite para %2$s + + ** Impossível decifrar: %s ** + O dispositivo de quem enviou a mensagem não nos enviou as chaves para esta mensagem. + + + Não foi possível apagar + Não foi possível enviar a mensagem + + O envio da imagem falhou + + + Erro de conexão à Internet + Erro do Matrix + + + + + + + + + Ainda não é possível voltar a entrar numa sala vazia. + + Mensagem cifrada + + + Endereço de e-mail + Número de telefone + + + Convite de %s + Convite para sala + %1$s e %2$s + Sala vazia + + + %1$s enviou um sticker. + + %s fez o upgrade da sala. + + Mensagem removida + Mensagem removida por %1$s + diff --git a/matrix-sdk-android/src/main/res/values-ru/strings.xml b/matrix-sdk-android/src/main/res/values-ru/strings.xml new file mode 100644 index 0000000000..1657d80f1c --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-ru/strings.xml @@ -0,0 +1,320 @@ + + + + %1$s: %2$s + %1$s отправил(а) изображение. + + %s приглашение + %1$s пригласил(а) %2$s + %1$s пригласил(а) вас + %1$s вошёл(ла) в комнату + %1$s покинул(а) комнату + %1$s отклонил(а) приглашение + %1$s выгнан %2$s + %1$s разблокировал(а) %2$s + %1$s заблокировал(а) %2$s + %1$s отозвал(а) приглашение %2$s + %1$s изменил(а) свой аватар + %1$s установил(а) имя %2$s + %1$s изменил(а) имя с %2$s на %3$s + %1$s удалил(а) свое имя (%2$s) + %1$s изменил(а) тему на: %2$s + %1$s изменил(а) название комнаты: %2$s + %s начал(а) видеовызов. + %s начал(а) голосовой вызов. + %s ответил(а) на звонок. + %s завершил(а) вызов. + %1$s сделал(а) будущую историю комнаты видимой %2$s + всем членам, с момента их приглашения. + всем членам, с момента присоединения. + всем членам. + всем. + неизвестно (%s). + %1$s включил(а) сквозное шифрование (%2$s) + + %1$s запросил(а) VoIP конференцию + VoIP-конференция начата + VoIP-конференция завершена + + (аватар также был изменен) + %1$s удалил(а) название комнаты + %1$s удалил(а) тему комнаты + %1$s обновил(а) свой профиль %2$s + %1$s отправил(а) приглашение %2$s присоединиться к комнате + %1$s принял(а) приглашение от %2$s + + ** Невозможно расшифровать: %s ** + Устройство отправителя не предоставило нам ключ для расшифровки этого сообщения. + + + Не удалось изменить + Не удалось отправить сообщение + + Не удалось загрузить изображение + + + Сетевая ошибка + Ошибка Matrix + + + + + + + + + В настоящее время невозможно вновь присоединиться к пустой комнате. + + Зашифрованное сообщение + + + Адрес электронной почты + Номер телефона + + %1$s отправил стикер. + + + Приглашение от %s + Приглашение в комнату + %1$s и %2$s + Пустая комната + + + %1$s и 1 другой + %1$s и %2$d другие + %1$s и %2$d других + + + + + Сообщение удалено + %1$s удалил(а) сообщение + Сообщение удалено [причина: %1$s] + %1$s удалил(а) сообщение [причина: %2$s] + Собака + Кошка + Лев + Лошадь + Единорог + Поросёнок + Слон + Кролик + Панда + Петух + Пингвин + Черепаха + Рыба + Осьминог + Бабочка + Цветок + Дерево + Кактус + Гриб + Земля + Луна + Облако + Огонь + Банан + Яблоко + Клубника + Кукуруза + Пицца + Пирожное + Сердце + Смайлик + Робот + Шляпа + Очки + Гаечный ключ + Санта + Большой палец вверх + Зонтик + Песочные часы + Часы + Подарок + Лампочка + Книга + Карандаш + Скрепка для бумаг + Ножницы + Замок + Ключ + Молоток + Телефон + Флаг + Поезд + Велосипед + Самолёт + Ракета + Трофей + Мяч + Гитара + Труба + Колокол + Якорь + Наушники + Папка + Булавка + + Начальная синхронизация: +\nИмпорт учетной записи… + Начальная синхронизация: +\nИмпорт криптографии + Начальная синхронизация: +\nИмпорт комнат + Синхронизация начата: +\nИмпорт присоединенных комнат + Синхронизация начата: +\nИмпорт приглашенных комнат + Начальная синхронизация: +\nИмпорт покинутых комнат + Начальная синхронизация: +\nИмпорт сообществ + Начальная синхронизация: +\nИмпорт данных учетной записи + + %s обновил эту комнату. + + Отправка сообщения… + Очистить очередь отправки + + %1$s отозвал приглашение %2$s присоединиться к комнате + Приглашение %1$s. Причина: %2$s + %1$s приглашен %2$s. Причина: %3$s + %1$s пригласил вас. Причина: %2$s + %1$s вошёл(ла) в комнату. Причина: %2$s + %1$s покинул(а) комнату. Причина: %2$s + %1$s отклонил приглашение. Причина: %2$s + %1$s выгнали %2$s. Причина: %3$s + %1$s разблокировано %2$s. Причина: %3$s + %1$s забанен %2$s. Причина: %3$s + %1$s отправил приглашение %2$s в комнату. Причина: %3$s + %1$s отозвал приглашение %2$s присоединиться к комнате. Причина: %3$s + %1$s принял приглашение для %2$s. Причина: %3$s + %1$s отозвал приглашение %2$s. Причина: %3$s + + %1$s создал(а) комнату + + %1$s добавил(а) %2$s в качестве адреса для этой комнаты. + %1$s добавил(а) %2$s в качестве адресов для этой комнаты. + %1$s добавил(а) %2$s в качестве адресов для этой комнаты. + + + + %1$s удалил(а) адрес %2$s для комнаты. + %1$s удалил(а) адреса %2$s для комнаты. + %1$s удалил(а) адреса %2$s для комнаты. + + + %1$s добавил(а) адреса %2$s и удалил(а) %3$s для комнаты. + + %1$s сделал(а) %2$s главным адресом комнаты. + %1$s удалил(а) главный адрес комнаты. + + %1$s разрешил(а) гостям входить в комнату. + %1$s запретил(а) гостям входить в комнату. + + %1$s включил(а) сквозное шифрование. + %1$s включил(а) сквозное шифрование (неизвестный алгоритм %2$s). + + %s запрашивает подтверждение вашего ключа, но ваш клиент не поддерживает подтверждение в чате. Используйте устаревшую проверку для сверки ключей. + + Вы отправили изображение. + Вы отправили стикер. + + Ваше приглашение + Вы создали комнату + Вы пригласили %1$s + Вы вошли в комнату + Вы покинули комнату + Вы отклонили приглашение + Вы выгнали %1$s + Вы разбанили %1$s + Вы забанили %1$s + Вы отозвали приглашение %1$s + Вы сменили свой аватар + Вы сменили своё отображаемое имя на %1$s + Вы сменили своё отображаемое имя с %1$s на %2$s + Вы удалили своё отображаемое имя (%1$s) + Вы сменили тему на: %1$s + Вы сменили название комнаты на: %1$s + Вы начали видеозвонок. + Вы начали звонок. + Вы ответили на звонок. + Вы закончили звонок. + Вы сделали будущую историю комнаты видимой для %1$s + Вы включили сквозное шифрование (%1$s) + Вы обновили эту комнату. + + Вы начали групповой звонок + Вы удалили название комнаты + Вы удалили тему комнаты + Вы обновили свой профиль %1$s + Вы отправили %1$s приглашение в эту комнату + Вы отозвали у %1$s приглашение в эту комнату + Вы приняли приглашение для %1$s + + %1$s добавил(а) виджет %2$s + Вы добавили виджет %1$s + %1$s удалил(а) виджет %2$s + Вы удалили виджет %1$s + %1$s изменил(а) виджет %2$s + Вы изменили виджет %1$s + + Администратор + Модератор + По умолчанию + Пользовательский (%1$d) + Пользовательский + + Вы изменили уровни доступа %1$s. + %1$s изменил(а) уровни доступа %2$s. + %1$s с %2$s на %3$s + + Ваше приглашение. Причина: %1$s + Вы пригласили %1$s. Причина: %2$s + Вы вошли в комнату. Причина: %1$s + Вы покинули комнату. Причина: %1$s + Вы отклонили приглашение. Причина: %1$s + Вы выгнали %1$s. Причина: %2$s + Вы разбанили %1$s. Причина: %2$s + Вы забанили %1$s. Причина: %2$s + Вы отправили %1$s приглашение в эту комнату. Причина: %2$s + Вы отозвали у %1$s приглашение в эту комнату. Причина: %2$s + Вы приняли приглашение для %1$s. Причина: %2$s + Вы отозвали приглашение %1$s. Причина: %2$s + + + Вы добавили адрес %1$s для этой комнаты. + Вы добавили %1$s в качестве адресов для этой комнаты. + Вы добавили %1$s в качестве адресов для этой комнаты. + + + + Вы удалили адрес этой комнаты: %1$s. + Вы удалили адреса этой комнаты: %1$s. + Вы удалили адреса этой комнаты: %1$s. + + + Вы добавили адреса %1$s и удалили %2$s для этой комнаты. + + Вы задали главный адрес этой комнаты %1$s. + Вы удалили главный адрес этой комнаты. + + Вы разрешили гостям входить в комнату. + Вы запретили гостям входить в комнату. + + Вы включили сквозное шифрование. + Вы включили сквозное шифрование (неизвестный алгоритм %1$s). + + %1$s изменил(а) аватар комнаты + Вы изменили аватар комнаты + %s отправил(а) данные для начала звонка. + Вы отправили данные для начала звонка. + %1$s удалил(а) аватар комнаты + Вы удалили аватар комнаты + Принять + Отклонить + Завершить звонок + + diff --git a/matrix-sdk-android/src/main/res/values-sk/strings.xml b/matrix-sdk-android/src/main/res/values-sk/strings.xml new file mode 100644 index 0000000000..8aec8fccf9 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-sk/strings.xml @@ -0,0 +1,202 @@ + + + %1$s: %2$s + %1$s poslal obrázok. + + Pozvanie %s + %1$s pozval %2$s + %1$s vás pozval + %1$s sa pripojil/a do miestnosti + %1$s opustil/a miestnosť + %1$s odmietol pozvanie + %1$s vykázal %2$s + %1$s povolil vstup %2$s + %1$s zakázal vstup %2$s + %1$s stiahol pozvanie %2$s + %1$s si zmenil obrázok v profile + %1$s si nastavil zobrazované meno %2$s + %1$s si zmenil zobrazované meno z %2$s na %3$s + %1$s odstránil svoje zobrazované meno (%2$s) + %1$s zmenil tému na: %2$s + %1$s zmenil názov miestnosti na: %2$s + %s uskutočnil video hovor. + %s uskutočnil audio hovor. + %s prijal hovor. + %s ukončil hovor. + %1$s sprístupnil budúcu históriu miestnosti %2$s + pre všetkých členov, od kedy boli pozvaní. + pre všetkých členov, od kedy vstúpili. + pre všetkých členov. + pre každého. + neznámym (%s). + %1$s povolil E2E šifrovanie (%2$s) + + %1$s požiadal o VoIP konferenciu + Začala VoIP konferencia + Skončila VoIP konferencia + + (a tiež obrázok v profile) + %1$s odstránil názov miestnosti + %1$s odstránil tému miestnosti + %1$s aktualizoval svoj profil %2$s + %1$s pozval %2$s vstúpiť do miestnosti + %1$s prijal pozvanie do %2$s + + ** Nie je možné dešifrovať: %s ** + Zo zariadenia odosieľateľa nebolo možné získať kľúče potrebné na dešifrovanie tejto správy. + + Nie je ožné vymazať + Nie je možné odoslať správu + + Nepodarilo sa nahrať obrázok + + Chyba siete + Chyba Matrix + + V súčasnosti nie je možné znovu vstúpiť do prázdnej miestnosti. + + Šifrovaná správa + + Emailová adresa + Telefónne číslo + + %1$s poslal nálepku. + + Pozvanie od %s + Pozvanie do miestnosti + %1$s a %2$s + Prázdna miestnosť + + + %1$s a 1 ďalší + %1$s a %2$d ďalší + %1$s a %2$d ďalších + + + + + %s aktualizoval túto miestnosť. + + Správa odstránená + Správa odstránená používateľom %1$s + Správa odstránená [dôvod: %1$s] + Správa odstránená používateľom %1$s [dôvod: %2$s] + Hlava psa + Hlava mačky + Hlava leva + Kôň + Hlava jednorožca + Hlava prasaťa + Slon + Hlava zajaca + Hlava pandy + Kohút + Tučniak + Korytnačka + Ryba + Chobotnica + Motýľ + Tulipán + Listnatý strom + Kaktus + Huba + Zemeguľa + Polmesiac + Oblak + Oheň + Banán + Červené jablko + Jahoda + Kukuričný klas + Pizza + Narodeninová torta + Červené + Škeriaca sa tvár + Robot + Cylinder + Okuliare + Francúzsky kľúč + Santa Claus + Palec nahor + Dáždnik + Presýpacie hodiny + Budík + Zabalený darček + Žiarovka + Zatvorená kniha + Ceruzka + Sponka na papier + Nožnice + Zatvorená zámka + Kľúč + Kladivo + Telefón + Kockovaná zástava + Rušeň + Bicykel + Lietadlo + Raketa + Trofej + Futbal + Gitara + Trúbka + Zvon + Kotva + Slúchadlá + Fascikel + Špendlík + + Úvodná synchronizácia: +\nPrebieha import účtu… + Úvodná synchronizácia: +\nPrebieha import šifrovacích kľúčov + Úvodná synchronizácia: +\nPrebieha import miestností + Úvodná synchronizácia: +\nPrebieha import miestností, do ktorých ste vstúpili + Úvodná synchronizácia: +\nPrebieha import pozvánok + Úvodná synchronizácia: +\nPrebieha import opustených miestností + Úvodná synchronizácia: +\nPrebieha import komunít + Úvodná synchronizácia: +\nPrebieha import údajov účtu + + Odosielanie správy… + Vymazať správy na odoslanie + + %1$s zamietol pozvanie používateľa %2$s vstúpiť do miestnosti + Pozvanie od používateľa %1$s. Dôvod: %2$s + %1$s pozval používateľa %2$s. Dôvod: %3$s + %1$s vás pozval. Dôvod: %2$s + %1$s sa pripojil/a do miestnosti. Dôvod: %2$s + Používateľ %1$s odišiel z miestnosti. Dôvod: %2$s + %1$s odmietol pozvanie. Dôvod: %2$s + %1$s vyhodil používateľa %2$s. Dôvod: %3$s + %1$s znovu pridaný používateľom %2$s. Dôvod: %3$s + %1$s vyhodil %2$s. Dôvod: %3$s + %1$s poslal pozvánku používateľovi %2$s, aby sa pripojil na miestnosť. Dôvod: %3$s + %1$s odvolal/a pozvánku pre používateľa %2$s na pripojenie sa na miestnosť. Dôvod: %3$s + %1$s prijal pozvanie od používateľa %2$s. Dôvod: %3$s + %1$s odoprel/a pozvánku používateľa %2$s. Dôvod: %3$s + + + %1$s pridal/a adresu %2$s pre túto miestnosť. + %1$s pridal/a adresy %2$s pre túto miestnosť. + %1$s pridal/a adresy %2$s pre túto miestnosť. + + + + %1$s odstránil/a adresu %2$s pre túto miestnosť. + %1$s odstránil/a adresy %2$s pre túto miestnosť. + %1$s odstránil/a adresy %2$s pre túto miestnosť. + + + %1$s pridal/a adresu %2$s a odstránil/a adresu %3$s pre túto miestnosť. + + %1$s nastavil/a hlavnú adresu tejto miestnosti na %2$s. + %1$s odstránil/a hlavnú adresu pre túto miestnosť. + + %1$s povolil/a hosťom///návštevníkom prístup do tejto miestnosti. + diff --git a/matrix-sdk-android/src/main/res/values-sq/strings.xml b/matrix-sdk-android/src/main/res/values-sq/strings.xml new file mode 100644 index 0000000000..e63e28288f --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-sq/strings.xml @@ -0,0 +1,205 @@ + + + %1$s: %2$s + %1$s dërgoi një figurë. + %1$s ftoi %2$s + %1$s ju ftoi + %1$s hyri në dhomë + %1$s doli nga dhoma + %1$s hodhi tej ftesën + %1$s përzuri %2$s + %1$s dëboi %2$s + %1$s ndryshoi avatarin e vet + %1$s ndryshoi temën në: %2$s + %1$s ndryshoi emrin e dhomës në: %2$s + %s bëri një thirrje video. + %s bëri një thirrje zanore. + %s iu përgjigj thirrjes. + %s e përfundoi thirrjen. + %1$s e bëri historikun e ardhshëm të dhomës të dukshëm për %2$s + për krejt anëtarët e dhomës, prej çastit kur janë ftuar. + për krejt anëtarët e dhomës, prej çastit kur morën pjesë. + krejt anëtarët e dhomës. + cilido. + e panjohur (%s). + %1$s kërkoi një konferencë VoIP + Konferenca VoIP filloi + Konferenca VoIP përfundoi + + (u ndryshua edhe avatari) + %1$s hoqi emrin e dhomës + %1$s përditësoi profilin e tij %2$s + %1$s pranoi ftesën tuaj për %2$s + + ** S’arrihet të shfshehtëzohet: %s ** + Pajisja e dërguesit nuk na ka dërguar kyçet për këtë mesazh. + + S’u redaktua dot + S’arrihet të dërgohet mesazh + + Ngarkimi i figurës dështoi + + Gabim rrjeti + Gabim Matrix + + Hëpërhë s’është e mundur të rihyhet në një dhomë të zbrazët. + + U fshehtëzua mesazhi + + Adresë email + Numër telefoni + + Ftesë nga %s + Ftesë Dhome + + %1$s dhe %2$s + + Dhomë e zbrazët + + %1$s dërgoi një ngjitës. + + Ftesë e %s + %1$s hoqi dëbimin për %2$s + %1$s tërhoqi mbrapsht ftesën për %2$s + %1$s caktoi për veten emër ekrani %2$s + %1$s ndryshoi emrin e tyre në ekran nga %2$s në %3$s + %1$s hoqi emrin e tij në ekran (%2$s) + %1$s aktivizoi fshehtëzim skaj-më-skaj (%2$s) + + %1$s hoqi temën e dhomës + %1$s dërgoi një ftesë për %2$s që të marrë pjesë në dhomë + + %1$s dhe 1 tjetër + %1$s dhe %2$d të tjerë + + + Mesazhi u hoq + Mesazhi u hoq nga %1$s + Mesazh i hequr [arsye: %1$s] + Mesazh i hequr nga %1$s [arsye: %2$s] + Qen + Mace + Luan + Kalë + Njëbrirësh + Derr + Elefant + Lepur + Panda + Këndes + Pinguin + Breshkë + Peshk + Oktapod + Flutur + Lule + Pemë + Kaktus + Kërpudhë + Rruzull + Hëna + Re + Zjarr + Banane + Mollë + Luleshtrydhe + Misër + Picë + Tortë + Zemër + Emotikon + Robot + Kapë + Syze + Çelës + Babagjyshi i Vitit të Ri + Ombrellë + Klepsidër + Sahat + Dhuratë + Llambë + Libër + Laps + Kapëse + Gërshërë + Dry + Kyç + Çekiç + Telefon + Flamur + Tren + Biçikletë + Aeroplan + Raketë + Trofe + Top + Kitarë + Trombë + Kambanë + Spirancë + Kufje + Dosje + %s e përmirësoi këtë dhomë. + + Njëkohësimi Fillestar: +\nPo importohet llogaria… + Njëkohësimi Fillestar: +\nPo importohet kriptografi + Njëkohësimi Fillestar: +\nPo importohen Dhoma + Njëkohësimi Fillestar: +\nPo importohen Dhoma Ku Është Bërë Hyrje + Njëkohësimi Fillestar: +\nPo importohen Dhoma Me Ftesë + Njëkohësimi Fillestar: +\nPo importohen Dhoma të Braktisura + Njëkohësimi Fillestar: +\nPo importohen Bashkësi + Njëkohësimi Fillestar: +\nPo importohet të Dhëna Llogarie + + Po dërgohet mesazh… + Spastro radhë pritjeje + + %1$s shfuqizoi ftesën për %2$s për pjesëmarrje te dhoma + Ftesë e %1$s. Arsye: %2$s + %1$s ftoi %2$s. Arsye: %3$s + %1$s ju ftoi. Arsye: %2$s + %1$s erdhi në dhomë. Arsye: %2$s + %1$s doli nga dhoma. Arsye: %2$s + %1$s hodhi poshtë ftesën. Arsye: %2$s + %1$s përzuri %2$s. Arsye: %3$s + %1$s hoqi dëbimin për %2$s. Arsye: %3$s + %1$s dëboi %2$s. Arsye: %3$s + %1$s dërgoi një ftesë për %2$s për të ardhur në dhomë. Arsye: %3$s + %1$s shfuqizoi ftesën për %2$s për të ardhur në dhomë. Arsye: %3$s + %1$s pranoi ftesën për %2$s. Arsye: %3$s + %1$s tërhoqi mbrapsht ftesën për %2$s. Arsye: %3$s + + + %1$s shtoi %2$s si një adresë për këtë dhomë. + %1$s shtoi %2$s si adresa për këtë dhomë. + + + + %1$s hoqi %2$s si adresë për këtë dhomë. + %1$s hoqi %3$s si adresa për këtë dhomë. + + + %1$s shtoi %2$s dhe hoqi %3$s si adresa për këtë dhomë. + + %1$s caktoi %2$s si adresë kryesore për këtë dhomë. + %1$s hoqi adresën kryesore për këtë dhomë. + + %1$s ka lejuar vizitorë të marrin pjesë në dhomë. + %1$s ka penguar vizitorë të marrin pjesë në dhomë. + + %1$s aktivizoi fshehtëzim skaj-më-skaj. + %1$s aktivizoi fshehtëzim skaj-më-skaj (algoritëm i papranuar %2$s). + + %s po kërkon të verifikojë kyçin tuaj, por klienti juaj nuk mbulon verifikim kyçesh brenda fjalosjeje. Që të verifikoni kyça, do t’ju duhet të përdorni verifikim të dikurshëm kyçesh. + + %1$s krijo dhomën + Fiksoje + + diff --git a/matrix-sdk-android/src/main/res/values-te/strings.xml b/matrix-sdk-android/src/main/res/values-te/strings.xml new file mode 100644 index 0000000000..62f58c9e26 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-te/strings.xml @@ -0,0 +1,71 @@ + + + %s\'s ఆహ్వానం + %1$s ఆహ్వానించారు %2$s + %1$s వదిలి వెళారు + %1$s ఆహ్వానాన్ని తిరస్కరించారు + %1$s తన్నాడు %2$s + %1$s నిషేధాన్ని %2$s + %1$s నిషేధించారు %2$s + %1$s ఉపసంహరించుకుంది %2$s\'s ఆహ్వానం + %1$s వారి అవతార్ను మార్చారు + %1$s వారి డిస్ప్లే పేరును ని సెట్ చేసారు %2$s + %1$s వారి ప్రదర్శన పేరును %2$s నుండి %3$s మార్చారు + %1$s వారి ప్రదర్శన పేరుని తీసివేసారు (%2$s) + %1$s అంశం మార్చబడింది:%2$s + %1$s గది పెరు మార్చబడింది %2$s + %s ఒక వీడియో కాల్ని ఉంచింది. + %s వాయిస్ కాల్ని ఉంచారు. + %s కాల్కి సమాధానం ఇచ్చారు. + %s కాల్ ముగిసింది. + %1$s భవిష్యత్ గది చరిత్రను %2$s కి కనిపించేలా చేసింది + పాయింట్నుండి, అన్ని గది సభ్యుల వారు ఆహ్వానించబడ్డారు. + పాయింట్ నుండి, అన్ని గదుల సభ్యుల వారు చేరారు. + అన్ని గదుల సభ్యులు. + ఎవరైనా. + తెలియని (%s). + %1$s ఎండ్-టు-ఎండ్ ఎన్క్రిప్షన్ ఆన్ చెయ్యబడింది (%2$s) + + %1$s వి ఓ ఇ పి సమావేశాన్ని అభ్యర్థించారు + వి ఓ ఇ పి సమావేశం ప్రారంభమైంది + వి ఓ ఇ పి సమావేశం ముగిసింది + + (అవతార్ మార్చబడింది) + %1$s గది పేరు తొలగించబడింది + %1$s గది అంశాన్ని తీసివేసారు + %1$s వారి ప్రొఫైల్ నవీకరించబడింది %2$s + %1$s గదిలో చేరడానికి %2$s కు ఆహ్వానాన్ని పంపారు + %2$sకోసం %1$s ఆహ్వానాన్ని అంగీకరించారు + + ** వ్యక్తీకరించడానికి సాధ్యం కాలేదు: %s ** + ఈ సందేశానికి పంపేవారి పరికరం మాకు కీలను పంపలేదు. + + గది స్క్రీన్ + సందేశం పంపడం సాధ్యం కాలేదు + + చిత్రాన్ని అప్లోడ్ చేయడంలో విఫలమైంది + + సాధారణ లోపాలు + మాట్రిక్స్ లోపం + + మళ్లీ ఖాళీ గది ని చేరడానికి ప్రస్తుతం ఇది సాధ్యం కాదు. + + ఎన్క్రిప్టెడ్ సందేశం + + ఇమెయిల్ చిరునామా + ఫోను నంబరు + + + %1$s: %2$s + %1$s ఒక చిత్రం పంపారు. + + %1$s మిమ్మల్ని ఆహ్వానించారు + %1$s చేరారు + + %s నుండి ఆహ్వానించు + %1$s మరియు %2$s + గదికి ఆహ్వానం + ఖాళీ గది + + + diff --git a/matrix-sdk-android/src/main/res/values-th/strings.xml b/matrix-sdk-android/src/main/res/values-th/strings.xml new file mode 100644 index 0000000000..3abd948f77 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-th/strings.xml @@ -0,0 +1,4 @@ + + + %1$s: %2$s + diff --git a/matrix-sdk-android/src/main/res/values-uk/strings.xml b/matrix-sdk-android/src/main/res/values-uk/strings.xml new file mode 100644 index 0000000000..eb5071f190 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-uk/strings.xml @@ -0,0 +1,84 @@ + + + %1$s: %2$s + %1$s надіслав(ла) зображення. + + %s запрошення + %1$s запросив(ла) %2$s + Зашифроване повідомлення + + + Запрошення від %s + Запрошення до кімнати + %1$s і %2$s + Порожня кімната + + + %1$s надіслав(ла) наліпку. + + %1$s запросив(ла) Вас + %1$s приєднався(лась) + %1$s покинув(ла) + %1$s відхилив(ла) запрошення + %1$s копнув(ла) %2$s + %1$s розблокував(ла) %2$s + %1$s заблокував(ла) %2$s + %1$s відкликав(ла) запрошення для %2$s + %1$s змінив(ла) свій аватар + %1$s встановив(ла) собі ім’я %2$s + %1$s змінив(ла) своє ім’я з %2$s на %3$s + %1$s прибрав(ла) своє ім’я (%2$s) + %1$s змінив(ла) тему на: %2$s + %1$s змінив(ла) назву кімнати на: %2$s + %s розпочав(ла) відеодзвінок. + %s розпочав(ла) голосовий дзвінок. + %s відповів(ла) на дзвінок. + %s завершив(ла) дзвінок. + %1$s зробив(ла) майбутню історію кімнати видимою для %2$s + усіх співрозмовників, з моменту їх запрошення. + усіх співрозмовників, з моменту їх приєднання. + усіх співрозмовників. + будь-кого. + невідомо (%s). + %1$s увімкнув(ла) наскрізне шифрування (%2$s) + + %1$s запросив(ла) VoIP конференцію + VoIP конференція розпочалась + VoIP конференція завершилась + + (аватар також змінено) + %1$s прибрав(ла) назву кімнати + %1$s прибрав(ла) тему кімнати + %1$s оновив(ла) свій профіль %2$s + %1$s надіслав(ла) запрошення %2$s приєднатися до кімнати + %1$s прийняв(ла) запрошення у %2$s + + ** Неможливо розшифрувати: %s ** + Пристрій відправника не надіслав нам ключ для цього повідомлення. + + Неможливо відредагувати + Не вдалося надіслати повідомлення + + Не вдалося завантажити зображення + + Помилка мережі + Помилка Matrix + + Наразі неможливо переприєднатися до порожньої кімнати. + + Адреса електронної пошти + Номер телефону + + + %1$s та 1 інший + %1$s та %2$d інші + %1$s та %2$d інших + + + + %s вдосконалили цю кімнату. + + Повідомлення видалено + %1$s видалили повідомлення + Повідомлення видалено [причина: %1$s] + diff --git a/matrix-sdk-android/src/main/res/values-vls/strings.xml b/matrix-sdk-android/src/main/res/values-vls/strings.xml new file mode 100644 index 0000000000..5c9132ed35 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-vls/strings.xml @@ -0,0 +1,169 @@ + + + %1$s: %2$s + %1$s èt e fotootje gesteurd. + %1$s èt e sticker gesteurd. + + Uutnodigienge van %s + %1$s èt %2$s uutgenodigd + %1$s èt joun uitgenodigd + %1$s neemt nu deel an ’t gesprek + %1$s èt ’t gesprek verloatn + %1$s èt d’uitnodigienge geweigerd + %1$s èt %2$s uut ’t gesprek verwyderd + %1$s èt %2$s ountbann + %1$s èt %2$s verbann + %1$s èt d’uutnodigienge van %2$s ingetrokkn + %1$s èt zyn/heur avatar angepast + %1$s èt zyn/heur noame angepast noa %2$s + %1$s èt zyn/heur noame angepast van %2$s noa %3$s + %1$s èt zyn/heur noame verwyderd (%2$s) + %1$s èt ’t ounderwerp veranderd noa: %2$s + %1$s èt de gespreksnoame veranderd noa: %2$s + %s èt e video-iproep gemakt. + %s èt e sproakiproep gemakt. + %s èt den iproep beantwoord. + %s èt ipgehangn. + %1$s èt de toekomstige gespreksgeschiedenisse zichtboar gemakt vo %2$s + alle deelnemers an ’t gesprek, vanaf ’t punt dan ze zyn uutgenodigd. + alle deelnemers an ’t gesprek, vanaf ’t punt dan ze zyn toegetreedn. + alle deelnemers an ’t gesprek. + iedereen. + ounbekend (%s). + %1$s èt eind-tout-eind-versleutelienge angezet (%2$s) + + %1$s èt e VoIP-vergoaderienge angevroagd + VoIP-vergoaderienge begunn + VoIP-vergoaderienge gestopt + + (avatar es ook veranderd) + %1$s èt de gespreksnoame verwyderd + %1$s èt ’t gespreksounderwerp verwyderd + Bericht verwyderd + Bericht verwyderd deur %1$s + Bericht verwyderd [reden: %1$s] + Bericht verwyderd deur %1$s [reden: %2$s] + %1$s èt zyn/heur profiel %2$s bygewerkt + %1$s èt een uutnodigienge noa %2$s gesteurd vo ’t gesprek toe te treedn + %1$s èt d’uutnodigienge vo %2$s anveird + + ** Kun nie ountsleuteln: %s ** + ’t Toestel van den afzender èt geen sleutels vo da bericht hier gesteurd. + + Kosteg nie verwyderd wordn + Kosteg ’t bericht nie verzendn + + Iploadn van ’t fotootje es mislukt + + Netwerkfout + Matrix-fout + + ’t Es vo de moment nie meuglik van e leeg gesprek were toe te treedn. + + Versleuteld bericht + + E-mailadresse + Telefongnumero + + Uutnodigienge van %s + Gespreksuutnodigienge + + %1$s en %2$s + + + %1$s en 1 andere + %1$s en %2$d anderen + + + Leeg gesprek + + + Hound + Katte + Leeuw + Peird + Eenhoorn + Zwyn + Olifant + Keun + Panda + Hoane + Pinguin + Schildpadde + Vis + Octopus + Beutervlieg + Bloem + Boom + Cactus + Paddestoel + Eirdbol + Moane + Wolk + Vier + Banoan + Appel + Freize + Mais + Pizza + Toarte + Erte + Smiley + Robot + Hoed + Bril + Moersleutel + Kestman + Duum omhooge + Paraplu + Zandloper + Klok + Cadeau + Gloeilampe + Boek + Potlood + Paperclip + Schoar + Hangslot + Sleutel + Oamer + Telefong + Vlagge + Tring + Veloo + Vlieger + Rakette + Trofee + Bolle + Gitoar + Trompette + Belle + Anker + Koptelefong + Mappe + Pinne + + Initiële synchronisoasje: +\nAccount wor geïmporteerd… + Initiële synchronisoasje: +\nCrypto wor geïmporteerd + Initiële synchronisoasje: +\nGesprekkn wordn geïmporteerd + Initiële synchronisoasje: +\nDeelgenoomn gesprekken wordn geïmporteerd + Initiële synchronisoasje: +\nUutgenodigde gesprekkn wordn geïmporteerd + Initiële synchronisoasje: +\nVerloatn gesprekkn wordn geïmporteerd + Initiële synchronisoasje: +\nGemeenschappn wordn geïmporteerd + Initiële synchronisoasje: +\nAccountgegeevns wordn geïmporteerd + + %s èt da gesprek hier ipgewoardeerd. + + Bericht wor verstuurd… + Uutgoande wachtreeke leegn + + %1$s èt d’uutnodigienge vo %2$s vo ’t gesprek toe te treedn ingetrokkn + diff --git a/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml b/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml new file mode 100644 index 0000000000..ef080e8357 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml @@ -0,0 +1,296 @@ + + + %1$s 发送了一张图片。 + + %s 的邀请 + %1$s 邀请了 %2$s + %1$s 邀请了您 + %1$s 加入了聊天室 + %1$s 离开了聊天室 + %1$s 拒绝了邀请 + %1$s 移除了 %2$s + %1$s 解封了 %2$s + %1$s 封禁了 %2$s + %1$s 更换了他们的头像 + %1$s 将他们的昵称设置为 %2$s + %1$s 把他们的昵称从 %2$s 改为 %3$s + %1$s 移除了他们的昵称 (%2$s) + %1$s 把主题改为: %2$s + %1$s 把聊天室名称改为: %2$s + %s 发起了一次视频通话。 + %s 发起了一次语音通话。 + %s 已接听通话。 + %s 已结束通话。 + 所有聊天室成员,从他们被邀请开始。 + 所有聊天室成员,从他们加入开始。 + 所有聊天室成员。 + 任何人。 + 未知(%s)。 + %1$s 开启了端到端加密(%2$s) + + %1$s 请求了一次 VoIP 会议 + VoIP 会议已开始 + VoIP 会议已结束 + + (头像也被更改) + %1$s 移除了聊天室名称 + %1$s 移除了聊天室主题 + ** 无法解密:%s ** + 发送者的设备没有向我们发送此消息的密钥。 + + 无法发送消息 + + 上传图像失败 + + 网络错误 + Matrix 错误 + + 目前无法重新加入一个空的聊天室。 + + 已加密消息 + + 电子邮箱地址 + 手机号码 + + %1$s 撤回了对 %2$s 的邀请 + %1$s 让未来的聊天室历史记录对 %2$s 可见 + %1$s 更新了他的个人档案 %2$s + %1$s 向 %2$s 发送了加入聊天室的邀请 + %1$s 接受了 %2$s 的邀请 + + 无法撤回 + + %1$s:%2$s + %1$s 发送了一张贴纸。 + + 空聊天室 + 来自 %s 的邀请 + 聊天室邀请 + %1$s 和 %2$s + + %1$s 与其他 %2$d 位 + + + 消息已被移除 + 消息已被 %1$s 移除 + 消息已被移除 [原因: %1$s] + 消息已被 %1$s 移除 [原因: %2$s] + + + 狮子 + + 独角兽 + + 大象 + 兔子 + 熊猫 + 公鸡 + 企鹅 + 乌龟 + + 章鱼 + 蝴蝶 + + + 仙人掌 + 蘑菇 + 地球 + 月亮 + + + 香蕉 + 苹果 + 草莓 + 玉米 + 披萨 + 蛋糕 + + 微笑 + 机器人 + 帽子 + 眼镜 + 扳手 + 圣诞老人 + 点赞 + 雨伞 + 沙漏 + + 礼物 + 灯泡 + + 铅笔 + 回形针 + 剪刀 + + 钥匙 + 锤子 + 电话 + 旗子 + 火车 + 自行车 + 飞机 + 火箭 + 奖杯 + + 吉他 + 喇叭 + 铃铛 + + 耳机 + 文件夹 + 初始化同步: +\n正在导入账号… + 初始化同步: +\n正在导入加密数据 + 初始化同步: +\n正在导入聊天室 + 初始化同步: +\n正在导入已加入的聊天室 + 初始化同步: +\n正在导入已邀请的聊天室 + 初始化同步: +\n正在导入已离开的聊天室 + 初始化同步: +\n正在导入社区 + 初始化同步: +\n正在导入账号数据 + + %s 升级了此聊天室。 + + 正在发送消息… + 清除正在发送队列 + + %1$s 撤回了对 %2$s 加入聊天室的邀请 + 置顶 + + %1$s 的邀请。理由:%2$s + %1$s 邀请了 %2$s。理由:%3$s + %1$s 邀请了您。理由:%2$s + %1$s 加入了聊天室。理由:%2$s + %1$s 离开了聊天室。理由:%2$s + %1$s 已拒绝邀请。理由:%2$s + %1$s 踢走了 %2$s。理由:%3$s + %1$s 解封了 %2$s。理由:%3$s + %1$s 封禁了 %2$s。理由:%3$s + %1$s 已发送邀请给 %2$s 来加入聊天室。理由:%3$s + %1$s 撤销了 %2$s 加入聊天室的邀請。理由:%3$s + %1$s 接受 %2$s 的邀請。理由:%3$s + %1$s 撤回了对 %2$s 的邀请。理由:%3$s + + + %1$s 新增了 %2$s 为此聊天室的地址。 + + + + %1$s 移除了此聊天室的 %3$s 地址。 + + + %1$s 为此聊天室新增了 %2$s 并移除 %3$s 地址。 + + %1$s 将此聊天室的主地址设为了 %2$s。 + %1$s 为此聊天室移除了主地址。 + + %1$s 已允许访客加入聊天室。 + %1$s 已禁止访客加入聊天室。 + + %1$s 已开启端到端加密。 + %1$s 已开启端到端加密(无法识别的演算法 %2$s)。 + + %s 正在请求验证您的密钥,但您的客户端不支援聊天中密钥验证。 您将必须使用旧版的密钥验证来验证金钥。 + + %1$s 创建了这个聊天室 + 您发送了一张图片。 + 您发送了一张贴纸。 + + 您的邀请 + 您创建了这个聊天室 + 您邀请了 %1$s + 您加入了聊天室 + 您离开了聊天室 + 您拒绝了邀请 + 您移除了 %1$s + 您解封了 %1$s + 您封禁了 %1$s + 您撤回了对 %1$s 的邀请 + 您更换了您的头像 + 您将您的昵称设置为 %1$s + 您将您的昵称从 %1$s 改为 %2$s + 您移除了您的昵称 (%1$s) + 您把主题改为:%1$s + %1$s 变更了聊天室头像 + 您变更了聊天室头像 + 您把聊天室名称改为:%1$s + 您发起了一次视频通话。 + 您发起了一次语音通话。 + %s 发送了数据以建立通话。 + 您发送了数据以建立通话。 + 您接听了通话。 + 您结束了通话。 + 您已让未来的聊天室记录对 %1$s 可见 + 您开启了端到端加密(%1$s) + 您升级了此聊天室。 + + 您请求了 VoIP 会议 + 您移除了聊天室名称 + 您移除了聊天室主题 + %1$s 移除了聊天室头像 + 您移除了聊天室头像 + 您更新了您的个人档案 %1$s + 您向 %1$s 发送了加入聊天室的邀请 + 您已撤回了对 %1$s 加入聊天室的邀请 + 您接受了 %1$s 的邀请 + + %1$s 添加了 %2$s 小部件 + 您添加了 %1$s 小部件 + %1$s 移除了 %2$s 小部件 + 您移除了 %1$s 小部件 + %1$s 修改了 %2$s 小部件 + 您修改了 %1$s 小部件 + + 管理员 + 审核员 + 默认 + 自定义(%1$d) + 自定义 + + 您更改了%1$s 的权力等级。 + %1$s 更改了 %2$s 的权力等级。 + %1$s 从 %2$s 到 %3$s + + 您的邀请。理由:%1$s + 您邀请了 %1$s。理由:%2$s + 您加入了聊天室。理由:%1$s + 您离开了聊天室。理由:%1$s + 您拒绝了邀请。理由:%1$s + 您踢走了 %1$s。理由:%2$s + 您解封了 %1$s。理由:%2$s + 您封禁了 %1$s。理由:%2$s + 您已发送邀请给 %1$s 来加入聊天室。理由:%2$s + 您撤销了 %1$s 加入聊天室的邀请。理由:%2$s + 您接受了 %1$s 的邀请。理由:%2$s + 您撤回了 %1$s 的邀请。理由:%2$s + + + 您新增了 %1$s 为此聊天室的地址。 + + + + 您移除了此聊天室的 %2$s 地址。 + + + 您为此聊天室新增了 %1$s 并移除了 %2$s 地址。 + + 您将此聊天室的主地址设为了 %1$s。 + 您移除了此聊天室的主地址。 + + 您已允许访客加入聊天室。 + 您已禁止访客加入聊天室。 + + 您已开启端到端加密。 + 您已开启端到端加密(无法识别的算法 %1$s)。 + + 接受 + 拒绝 + 挂断 + + diff --git a/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml b/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml new file mode 100644 index 0000000000..ee2662f143 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml @@ -0,0 +1,297 @@ + + + %1$s:%2$s + %1$s 傳送了一張圖片。 + + %s 的邀請 + %1$s 邀請了 %2$s + %1$s 邀請您 + %1$s 已加入聊天室 + %1$s 已離開聊天室 + %1$s 拒絕邀請 + %1$s 踢出 %2$s + %1$s 解除禁止 %2$s + %1$s 禁止 %2$s + %1$s 收回了對 %2$s 的邀請 + %1$s 變更了他們的大頭貼 + %1$s 設定了他們的顯示名稱為 %2$s + %1$s 變更了他們的顯示名稱從 %2$s 到 %3$s + %1$s 移除了他們的顯示名稱 (%2$s) + %1$s 變更主題為:%2$s + %1$s 變更房間名稱為:%2$s + %s 撥出了視訊通話。 + %s 撥出了語音通話。 + %s 回覆了通話。 + %s 結束通話。 + %1$s 讓房間未來可讓 %2$s 看到歷史紀錄 + 所有的房間成員,從他們被邀請的時間開始。 + 所有的房間成員,從他們加入的時間開始。 + 所有的房間成員。 + 任何人。 + 未知 (%s)。 + %1$s 開啟了端對端加密 (%2$s) + + %1$s 請求了 VoIP 會議通話 + VoIP 會議通話已開始 + VoIP 會議通話已結束 + + (大頭貼也變更了) + %1$s 移除了房間名稱 + %1$s 移除了房間主題 + %1$s 更新了他們的基本資料 %2$s + %1$s 傳送加入房間的邀請給 %2$s + %1$s 接受 %2$s 的邀請 + + ** 無法解密:%s ** + 傳送者的裝置並未在此訊息傳送他們的金鑰。 + + 無法編輯 + 無法傳送訊息 + + 上傳圖片失敗 + + 網路錯誤 + Matrix 錯誤 + + 目前無法重新加入空房間。 + + 已加密的訊息 + + 電子郵件 + 電話號碼 + + %1$s 傳送了一張貼圖。 + + 來自%s 的邀請 + 聊天室邀請 + %1$s 和 %2$s + + 空聊天室 + + %1$s 和 和其他 %2$d 個人 + + + + 訊息已移除 + 訊息已被 %1$s 移除 + 訊息已移除 [理由:%1$s] + 訊息已被 %1$s 移除 [理由:%2$s] + + + + + 獨角獸 + + + + 貓熊 + 公雞 + 企鵝 + + + 章魚 + + + + 仙人掌 + 蘑菇 + 地球 + 月亮 + + + 香蕉 + 蘋果 + 草莓 + 玉米 + 披薩 + 蛋糕 + + 微笑 + 機器人 + 帽子 + 眼鏡 + 扳手 + 聖誕老人 + + 雨傘 + 沙漏 + 時鐘 + 禮物 + 燈泡 + + 鉛筆 + 迴紋針 + 剪刀 + + 鑰匙 + 鎚子 + 電話 + 旗子 + 火車 + 腳踏車 + 飛機 + 火箭 + 獎盃 + + 吉他 + 喇叭 + + + 耳機 + 資料夾 + 別針 + + 初始化同步: +\n正在匯入帳號…… + 初始化同步: +\n正在匯入 crypto + 初始化同步: +\n正在匯入聊天室 + 初始化同步: +\n正在匯入已加入的聊天室 + 初始化同步: +\n正在匯入已邀請的聊天室 + 初始化同步: +\n正在匯入已離開的聊天室 + 初始化同步: +\n正在匯入社群 + 初始化同步: +\n正在匯入帳號資料 + + %s 已升級此聊天室。 + + 正在傳送訊息…… + 清除傳送佇列 + + %1$s 撤銷了 %2$s 加入聊天室的邀請 + %1$s 的邀請。理由:%2$s + %1$s 邀請了 %2$s。理由:%3$s + %1$s 邀請了您。理由:%2$s + %1$s 已加入聊天室。理由:%2$s + %1$s 已離開聊天室。理由:%2$s + %1$s 已回絕邀請。理由:%2$s + %1$s 踢走了 %2$s。理由:%3$s + %1$s 取消封鎖了 %2$s。理由:%3$s + %1$s 封鎖了 %2$s。理由:%3$s + %1$s 已傳送邀請給 %2$s 來加入聊天室。理由:%3$s + %1$s 撤銷了 %2$s 加入聊天室的邀請。理由:%3$s + %1$s 接受 %2$s 的邀請。理由:%3$s + %1$s 撤回了對 %2$s 的邀請。理由:%3$s + + + %1$s 新增了 %2$s 為此聊天室的地址。 + + + + %1$s 移除了此聊天室的 %3$s 地址。 + + + %1$s 為此聊天室新增 %2$s 並移除 %3$s 地址。 + + %1$s 為此聊天室設定了 %2$s 為主地址。 + %1$s 為此聊天室移除了主要地址。 + + %1$s 已允許訪客加入聊天室。 + %1$s 已禁止訪客加入聊天室。 + + %1$s 已開啟端到端加密。 + %1$s 已開啟端到端加密(無法識別的演算法 %2$s)。 + + %s 正在請求驗證您的金鑰,但您的客戶端不支援聊天中金鑰驗證。您將必須使用舊版的金鑰驗證來驗證金鑰。 + + %1$s 建立了聊天室 + 您傳送了圖片。 + 您傳送了貼圖。 + + 您的邀請 + 您建立了聊天室 + 您邀請了 %1$s + 您加入了聊天室 + 您離開的聊天室 + 您回絕了邀請 + 您踢除了 %1$s + 您取消封鎖了 %1$s + 您封鎖了 %1$s + 您撤銷了 %1$s 的邀請 + 您變更了您的大頭貼 + 您將您的顯示名稱設定為 %1$s + 您將您的顯示名稱從 %1$s 變更為 %2$s + 您移除了您的顯示名稱(其曾為 %1$s) + 您將主題變更為:%1$s + %1$s 變更了聊天室大頭貼 + 您變更了聊天室大頭貼 + 您將聊天室名稱變更為:%1$s + 您發起了視訊通話。 + 您發起了音訊通話。 + %s 傳送了資料以建立通話。 + 您傳送了資料以建立通話。 + 您接了通話。 + 您結束了通話。 + 您已將未來的聊天室歷史設定為對 %1$s 可見 + 您開啟了端到端加密 (%1$s) + 您升級了此聊天室。 + + 您請求了 VoIP 會議 + 您移除了聊天室名稱 + 您移除了聊天室主題 + %1$s 移除了聊天室大頭貼 + 您移除了聊天室大頭貼 + 您更新了您的個人檔案 %1$s + 您傳送了邀請給 %1$s 以加入聊天室 + 您已撤銷對 %1$s 加入聊天室的邀請 + 您接受了 %1$s 的邀請 + + %1$s 新增了 %2$s 小工具 + 您新增了 %1$s 小工具 + %1$s 移除了 %2$s 小工具 + 您移除了 %1$s 小工具 + %1$s 修改了 %2$s 小工具 + 您修改了 %1$s 小工具 + + 管理員 + 板主 + 預設 + 自訂 (%1$d) + 自訂 + + 您變更了 %1$s 的權力等級。 + %1$s 變更了 %2$s 的權力等級。 + %1$s 從 %2$s 到 %3$s + + 您的邀請。理由:%1$s + 您邀請了 %1$s。理由:%2$s + 您加入了聊天室。理由:%1$s + 您離開了聊天室。理由:%1$s + 您回絕了邀請。理由:%1$s + 您踢除了 %1$s。理由:%2$s + 您取消封鎖了 %1$s。理由:%2$s + 您封鎖了 %1$s。理由:%2$s + 您傳甕了邀請給 %1$s 以加入聊天室。理由:%2$s + 您撤銷了 %1$s 加入聊天室的邀請。理由:%2$s + 您接受了 %1$s 的邀請。理由:%2$s + 您撤回了 %1$s 的邀請。理由:%2$s + + + 您為此聊天室新增了 %1$s 作為地址。 + + + + 您為此聊天室移除了 %2$s 作為地址。 + + + 您為此聊天室新增了 %1$s 並移除了 %2$s 作為地址。 + + 您將此聊天室的主要地址設定為 %1$s。 + 您將此聊天室的主要地址移除。 + + 您已允許訪客加入聊天室。 + 您已阻止訪客加入聊天室。 + + 您開啟了端到端加密。 + 您開啟了端到端加密(無法識別的演算法 %1$s)。 + + 接受 + 拒絕 + 掛斷 + + diff --git a/matrix-sdk-android/src/main/res/values/strings.xml b/matrix-sdk-android/src/main/res/values/strings.xml new file mode 100644 index 0000000000..0dc64c1b4b --- /dev/null +++ b/matrix-sdk-android/src/main/res/values/strings.xml @@ -0,0 +1,368 @@ + + + %1$s: %2$s + %1$s sent an image. + You sent an image. + %1$s sent a sticker. + You sent a sticker. + + %s\'s invitation + Your invitation + %1$s created the room + You created the room + %1$s invited %2$s + You invited %1$s + %1$s invited you + %1$s joined the room + You joined the room + %1$s left the room + You left the room + %1$s rejected the invitation + You rejected the invitation + %1$s kicked %2$s + You kicked %1$s + %1$s unbanned %2$s + You unbanned %1$s + %1$s banned %2$s + You banned %1$s + %1$s withdrew %2$s\'s invitation + You withdrew %1$s\'s invitation + %1$s changed their avatar + You changed your avatar + %1$s set their display name to %2$s + You set your display name to %1$s + %1$s changed their display name from %2$s to %3$s + You changed your display name from %1$s to %2$s + %1$s removed their display name (it was %2$s) + You removed your display name (it was %1$s) + %1$s changed the topic to: %2$s + You changed the topic to: %1$s + %1$s changed the room avatar + You changed the room avatar + %1$s changed the room name to: %2$s + You changed the room name to: %1$s + %s placed a video call. + You placed a video call. + %s placed a voice call. + You placed a voice call. + %s sent data to setup the call. + You sent data to setup the call. + %s answered the call. + You answered the call. + %s ended the call. + You ended the call. + %1$s made future room history visible to %2$s + You made future room history visible to %1$s + all room members, from the point they are invited. + all room members, from the point they joined. + all room members. + anyone. + unknown (%s). + %1$s turned on end-to-end encryption (%2$s) + You turned on end-to-end encryption (%1$s) + %s upgraded this room. + You upgraded this room. + + %1$s requested a VoIP conference + You requested a VoIP conference + VoIP conference started + VoIP conference finished + + (avatar was changed too) + %1$s removed the room name + You removed the room name + %1$s removed the room topic + You removed the room topic + %1$s removed the room avatar + You removed the room avatar + Message removed + Message removed by %1$s + Message removed [reason: %1$s] + Message removed by %1$s [reason: %2$s] + %1$s updated their profile %2$s + You updated your profile %1$s + %1$s sent an invitation to %2$s to join the room + You sent an invitation to %1$s to join the room + %1$s revoked the invitation for %2$s to join the room + You revoked the invitation for %1$s to join the room + %1$s accepted the invitation for %2$s + You accepted the invitation for %1$s + + %1$s added %2$s widget + You added %1$s widget + %1$s removed %2$s widget + You removed %1$s widget + %1$s modified %2$s widget + You modified %1$s widget + + Admin + Moderator + Default + Custom (%1$d) + Custom + + + You changed the power level of %1$s. + + %1$s changed the power level of %2$s. + + %1$s from %2$s to %3$s + + ** Unable to decrypt: %s ** + The sender\'s device has not sent us the keys for this message. + + + + + Could not redact + Unable to send message + + Failed to upload image + + + Network error + Matrix error + + + + + + + + + It is not currently possible to re-join an empty room. + + Encrypted message + + + Email address + Phone number + + + Invite from %s + Room Invite + + + %1$s and %2$s + + + %1$s and 1 other + %1$s and %2$d others + + + Empty room + + + + Dog + + Cat + + Lion + + Horse + + Unicorn + + Pig + + Elephant + + Rabbit + + Panda + + Rooster + + Penguin + + Turtle + + Fish + + Octopus + + Butterfly + + Flower + + Tree + + Cactus + + Mushroom + + Globe + + Moon + + Cloud + + Fire + + Banana + + Apple + + Strawberry + + Corn + + Pizza + + Cake + + Heart + + Smiley + + Robot + + Hat + + Glasses + + Wrench + + Santa + + Thumbs Up + + Umbrella + + Hourglass + + Clock + + Gift + + Light Bulb + + Book + + Pencil + + Paperclip + + Scissors + + Lock + + Key + + Hammer + + Telephone + + Flag + + Train + + Bicycle + + Airplane + + Rocket + + Trophy + + Ball + + Guitar + + Trumpet + + Bell + + Anchor + + Headphones + + Folder + + Pin + + + Initial Sync:\nImporting account… + Initial Sync:\nImporting crypto + Initial Sync:\nImporting Rooms + Initial Sync:\nImporting Joined Rooms + Initial Sync:\nImporting Invited Rooms + Initial Sync:\nImporting Left Rooms + Initial Sync:\nImporting Communities + Initial Sync:\nImporting Account Data + + Sending message… + Clear sending queue + + %1$s\'s invitation. Reason: %2$s + Your invitation. Reason: %1$s + %1$s invited %2$s. Reason: %3$s + You invited %1$s. Reason: %2$s + %1$s invited you. Reason: %2$s + %1$s joined the room. Reason: %2$s + You joined the room. Reason: %1$s + %1$s left the room. Reason: %2$s + You left the room. Reason: %1$s + %1$s rejected the invitation. Reason: %2$s + You rejected the invitation. Reason: %1$s + %1$s kicked %2$s. Reason: %3$s + You kicked %1$s. Reason: %2$s + %1$s unbanned %2$s. Reason: %3$s + You unbanned %1$s. Reason: %2$s + %1$s banned %2$s. Reason: %3$s + You banned %1$s. Reason: %2$s + %1$s sent an invitation to %2$s to join the room. Reason: %3$s + You sent an invitation to %1$s to join the room. Reason: %2$s + %1$s revoked the invitation for %2$s to join the room. Reason: %3$s + You revoked the invitation for %1$s to join the room. Reason: %2$s + %1$s accepted the invitation for %2$s. Reason: %3$s + You accepted the invitation for %1$s. Reason: %2$s + %1$s withdrew %2$s\'s invitation. Reason: %3$s + You withdrew %1$s\'s invitation. Reason: %2$s + + + %1$s added %2$s as an address for this room. + %1$s added %2$s as addresses for this room. + + + + You added %1$s as an address for this room. + You added %1$s as addresses for this room. + + + + %1$s removed %2$s as an address for this room. + %1$s removed %3$s as addresses for this room. + + + + You removed %1$s as an address for this room. + You removed %2$s as addresses for this room. + + + %1$s added %2$s and removed %3$s as addresses for this room. + You added %1$s and removed %2$s as addresses for this room. + + "%1$s set the main address for this room to %2$s." + "You set the main address for this room to %1$s." + "%1$s removed the main address for this room." + "You removed the main address for this room." + + "%1$s has allowed guests to join the room." + "You have allowed guests to join the room." + "%1$s has prevented guests from joining the room." + "You have prevented guests from joining the room." + + %1$s turned on end-to-end encryption. + You turned on end-to-end encryption. + %1$s turned on end-to-end encryption (unrecognised algorithm %2$s). + You turned on end-to-end encryption (unrecognised algorithm %1$s). + + %s is requesting to verify your key, but your client does not support in-chat key verification. You will need to use legacy key verification to verify keys. + + Accept + Decline + Hang Up + + diff --git a/matrix-sdk-android/src/main/res/xml/network_security_config.xml b/matrix-sdk-android/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000000..e40c61c229 --- /dev/null +++ b/matrix-sdk-android/src/main/res/xml/network_security_config.xml @@ -0,0 +1,16 @@ + + + + + + + + + + localhost + 127.0.0.1 + + 10.0.2.2 + + + diff --git a/matrix-sdk-android/src/main/res/xml/sdk_provider_paths.xml b/matrix-sdk-android/src/main/res/xml/sdk_provider_paths.xml new file mode 100644 index 0000000000..7c15e41df3 --- /dev/null +++ b/matrix-sdk-android/src/main/res/xml/sdk_provider_paths.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/matrix-sdk-android/src/release/java/org/matrix/android/sdk/internal/network/interceptors/CurlLoggingInterceptor.kt b/matrix-sdk-android/src/release/java/org/matrix/android/sdk/internal/network/interceptors/CurlLoggingInterceptor.kt new file mode 100644 index 0000000000..c4fed36216 --- /dev/null +++ b/matrix-sdk-android/src/release/java/org/matrix/android/sdk/internal/network/interceptors/CurlLoggingInterceptor.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.internal.network.interceptors + +import okhttp3.Interceptor +import okhttp3.Response +import org.matrix.android.sdk.internal.di.MatrixScope +import java.io.IOException +import javax.inject.Inject + +/** + * No op interceptor + */ +@MatrixScope +internal class CurlLoggingInterceptor @Inject constructor() + : Interceptor { + + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + return chain.proceed(chain.request()) + } +} diff --git a/matrix-sdk-android/src/release/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt b/matrix-sdk-android/src/release/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt new file mode 100644 index 0000000000..69b15a1fa5 --- /dev/null +++ b/matrix-sdk-android/src/release/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.internal.network.interceptors + +import androidx.annotation.NonNull +import okhttp3.logging.HttpLoggingInterceptor + +/** + * No op logger + */ +internal class FormattedJsonHttpLogger : HttpLoggingInterceptor.Logger { + + @Synchronized + override fun log(@NonNull message: String) { + } +} diff --git a/matrix-sdk-android/src/sharedTest/java/org/matrix/android/sdk/test/shared/TestRules.kt b/matrix-sdk-android/src/sharedTest/java/org/matrix/android/sdk/test/shared/TestRules.kt new file mode 100644 index 0000000000..52aa7ea0c7 --- /dev/null +++ b/matrix-sdk-android/src/sharedTest/java/org/matrix/android/sdk/test/shared/TestRules.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.test.shared + +import net.lachlanmckee.timberjunit.TimberTestRule + +internal fun createTimberTestRule(): TimberTestRule { + return TimberTestRule.builder() + .showThread(false) + .showTimestamp(false) + .onlyLogWhenTestFails(false) + .build() +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/MatrixTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/MatrixTest.kt new file mode 100644 index 0000000000..b0933c7106 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/MatrixTest.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk + +import org.matrix.android.sdk.test.shared.createTimberTestRule +import org.junit.Rule + +interface MatrixTest { + + @Rule + fun timberTestRule() = createTimberTestRule() +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/auth/data/VersionsKtTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/auth/data/VersionsKtTest.kt new file mode 100644 index 0000000000..69e2f12eb7 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/auth/data/VersionsKtTest.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.api.auth.data + +import org.matrix.android.sdk.internal.auth.version.Versions +import org.matrix.android.sdk.internal.auth.version.isSupportedBySdk +import org.amshove.kluent.shouldBe +import org.junit.Test + +class VersionsKtTest { + + @Test + fun isSupportedBySdkTooLow() { + Versions(supportedVersions = listOf("r0.4.0")).isSupportedBySdk() shouldBe false + Versions(supportedVersions = listOf("r0.4.1")).isSupportedBySdk() shouldBe false + } + + @Test + fun isSupportedBySdkUnstable() { + Versions(supportedVersions = listOf("r0.4.0"), unstableFeatures = mapOf("m.lazy_load_members" to true)).isSupportedBySdk() shouldBe true + } + + @Test + fun isSupportedBySdkOk() { + Versions(supportedVersions = listOf("r0.5.0")).isSupportedBySdk() shouldBe true + Versions(supportedVersions = listOf("r0.5.1")).isSupportedBySdk() shouldBe true + } + + // Was not working + @Test + fun isSupportedBySdkLater() { + Versions(supportedVersions = listOf("r0.6.0")).isSupportedBySdk() shouldBe true + Versions(supportedVersions = listOf("r0.6.1")).isSupportedBySdk() shouldBe true + } + + // Cover cases of issue #1442 + @Test + fun isSupportedBySdk1442() { + Versions(supportedVersions = listOf("r0.5.0", "r0.6.0")).isSupportedBySdk() shouldBe true + Versions(supportedVersions = listOf("r0.5.0", "r0.6.1")).isSupportedBySdk() shouldBe true + Versions(supportedVersions = listOf("r0.6.0")).isSupportedBySdk() shouldBe true + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/pushrules/PushRuleActionsTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/pushrules/PushRuleActionsTest.kt new file mode 100644 index 0000000000..f213e1b1c1 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/pushrules/PushRuleActionsTest.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2019 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.api.pushrules + +import org.matrix.android.sdk.MatrixTest +import org.matrix.android.sdk.api.pushrules.rest.PushRule +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Test + +class PushRuleActionsTest: MatrixTest { + + @Test + fun test_action_parsing() { + val rawPushRule = """ + { + "rule_id": ".m.rule.invite_for_me", + "default": true, + "enabled": true, + "conditions": [ + { + "key": "type", + "kind": "event_match", + "pattern": "m.room.member" + }, + { + "key": "content.membership", + "kind": "event_match", + "pattern": "invite" + }, + { + "key": "state_key", + "kind": "event_match", + "pattern": "[the user's Matrix ID]" + } + ], + "actions": [ + "notify", + { + "set_tweak": "sound", + "value": "default" + }, + { + "set_tweak": "highlight", + "value": false + } + ] + } + """.trimIndent() + + val pushRule = MoshiProvider.providesMoshi().adapter(PushRule::class.java).fromJson(rawPushRule) + + assertNotNull("Should have parsed the rule", pushRule) + + val actions = pushRule!!.getActions() + assertEquals(3, actions.size) + + assertTrue("First action should be notify", actions[0] is Action.Notify) + + assertTrue("Second action should be sound", actions[1] is Action.Sound) + assertEquals("Second action should have default sound", "default", (actions[1] as Action.Sound).sound) + + assertTrue("Third action should be highlight", actions[2] is Action.Highlight) + assertEquals("Third action tweak param should be false", false, (actions[2] as Action.Highlight).highlight) + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/pushrules/PushrulesConditionTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/pushrules/PushrulesConditionTest.kt new file mode 100644 index 0000000000..be5aeaaf0f --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/pushrules/PushrulesConditionTest.kt @@ -0,0 +1,218 @@ +/* + * Copyright 2019 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.api.pushrules + +import org.matrix.android.sdk.MatrixTest +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.Room +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.RoomMemberContent +import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent +import org.matrix.android.sdk.internal.session.room.RoomGetter +import io.mockk.every +import io.mockk.mockk +import org.amshove.kluent.shouldBe +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class PushrulesConditionTest: MatrixTest { + + /* ========================================================================================== + * Test EventMatchCondition + * ========================================================================================== */ + + @Test + fun test_eventmatch_type_condition() { + val condition = EventMatchCondition("type", "m.room.message") + + val simpleTextEvent = Event( + type = "m.room.message", + eventId = "mx0", + content = MessageTextContent("m.text", "Yo wtf?").toContent(), + originServerTs = 0) + + val rm = RoomMemberContent( + Membership.INVITE, + displayName = "Foo", + avatarUrl = "mxc://matrix.org/EqMZYbREvHXvYFyfxOlkf" + ) + val simpleRoomMemberEvent = Event( + type = "m.room.member", + eventId = "mx0", + stateKey = "@foo:matrix.org", + content = rm.toContent(), + originServerTs = 0) + + assert(condition.isSatisfied(simpleTextEvent)) + assert(!condition.isSatisfied(simpleRoomMemberEvent)) + } + + @Test + fun test_eventmatch_path_condition() { + val condition = EventMatchCondition("content.msgtype", "m.text") + + val simpleTextEvent = Event( + type = "m.room.message", + eventId = "mx0", + content = MessageTextContent("m.text", "Yo wtf?").toContent(), + originServerTs = 0) + + assert(condition.isSatisfied(simpleTextEvent)) + + Event( + type = "m.room.member", + eventId = "mx0", + stateKey = "@foo:matrix.org", + content = RoomMemberContent( + Membership.INVITE, + displayName = "Foo", + avatarUrl = "mxc://matrix.org/EqMZYbREvHXvYFyfxOlkf" + ).toContent(), + originServerTs = 0 + ).apply { + assert(EventMatchCondition("content.membership", "invite").isSatisfied(this)) + } + } + + @Test + fun test_eventmatch_cake_condition() { + val condition = EventMatchCondition("content.body", "cake") + + Event( + type = "m.room.message", + eventId = "mx0", + content = MessageTextContent("m.text", "How was the cake?").toContent(), + originServerTs = 0 + ).apply { + assert(condition.isSatisfied(this)) + } + + Event( + type = "m.room.message", + eventId = "mx0", + content = MessageTextContent("m.text", "Howwasthecake?").toContent(), + originServerTs = 0 + ).apply { + assert(condition.isSatisfied(this)) + } + } + + @Test + fun test_eventmatch_cakelie_condition() { + val condition = EventMatchCondition("content.body", "cake*lie") + + val simpleTextEvent = Event( + type = "m.room.message", + eventId = "mx0", + content = MessageTextContent("m.text", "How was the cakeisalie?").toContent(), + originServerTs = 0) + + assert(condition.isSatisfied(simpleTextEvent)) + } + + @Test + fun test_notice_condition() { + val conditionEqual = EventMatchCondition("content.msgtype", "m.notice") + + Event( + type = "m.room.message", + eventId = "mx0", + content = MessageTextContent("m.notice", "A").toContent(), + originServerTs = 0, + roomId = "2joined").also { + assertTrue("Notice", conditionEqual.isSatisfied(it)) + } + } + + /* ========================================================================================== + * Test RoomMemberCountCondition + * ========================================================================================== */ + + @Test + fun test_roommember_condition() { + val conditionEqual3 = RoomMemberCountCondition("3") + val conditionEqual3Bis = RoomMemberCountCondition("==3") + val conditionLessThan3 = RoomMemberCountCondition("<3") + + val room2JoinedId = "2joined" + val room3JoinedId = "3joined" + + val roomStub2Joined = mockk { + every { getNumberOfJoinedMembers() } returns 2 + } + + val roomStub3Joined = mockk { + every { getNumberOfJoinedMembers() } returns 3 + } + + val roomGetterStub = mockk { + every { getRoom(room2JoinedId) } returns roomStub2Joined + every { getRoom(room3JoinedId) } returns roomStub3Joined + } + + Event( + type = "m.room.message", + eventId = "mx0", + content = MessageTextContent("m.text", "A").toContent(), + originServerTs = 0, + roomId = room2JoinedId).also { + assertFalse("This room does not have 3 members", conditionEqual3.isSatisfied(it, roomGetterStub)) + assertFalse("This room does not have 3 members", conditionEqual3Bis.isSatisfied(it, roomGetterStub)) + assertTrue("This room has less than 3 members", conditionLessThan3.isSatisfied(it, roomGetterStub)) + } + + Event( + type = "m.room.message", + eventId = "mx0", + content = MessageTextContent("m.text", "A").toContent(), + originServerTs = 0, + roomId = room3JoinedId).also { + assertTrue("This room has 3 members", conditionEqual3.isSatisfied(it, roomGetterStub)) + assertTrue("This room has 3 members", conditionEqual3Bis.isSatisfied(it, roomGetterStub)) + assertFalse("This room has more than 3 members", conditionLessThan3.isSatisfied(it, roomGetterStub)) + } + } + + /* ========================================================================================== + * Test ContainsDisplayNameCondition + * ========================================================================================== */ + + @Test + fun test_displayName_condition() { + val condition = ContainsDisplayNameCondition() + + val event = Event( + type = "m.room.message", + eventId = "mx0", + content = MessageTextContent("m.text", "How was the cake benoit?").toContent(), + originServerTs = 0, + roomId = "2joined") + + condition.isSatisfied(event, "how") shouldBe true + condition.isSatisfied(event, "How") shouldBe true + condition.isSatisfied(event, "benoit") shouldBe true + condition.isSatisfied(event, "Benoit") shouldBe true + condition.isSatisfied(event, "cake") shouldBe true + + condition.isSatisfied(event, "ben") shouldBe false + condition.isSatisfied(event, "oit") shouldBe false + condition.isSatisfied(event, "enoi") shouldBe false + condition.isSatisfied(event, "H") shouldBe false + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/Base58Test.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/Base58Test.kt new file mode 100644 index 0000000000..b2d10968b6 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/Base58Test.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2018 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.util + +import org.matrix.android.sdk.MatrixTest +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runners.MethodSorters + +@FixMethodOrder(MethodSorters.JVM) +class Base58Test: MatrixTest { + + @Test + fun encode() { + // Example comes from https://github.com/keis/base58 + assertEquals("StV1DL6CwTryKyV", base58encode("hello world".toByteArray())) + } + + @Test + fun decode() { + // Example comes from https://github.com/keis/base58 + assertArrayEquals("hello world".toByteArray(), base58decode("StV1DL6CwTryKyV")) + } + + @Test + fun encode_curve25519() { + // Encode a 32 bytes key + assertEquals("4F85ZySpwyY6FuH7mQYyyr5b8nV9zFRBLj92AJa37sMr", + base58encode(("0123456789" + "0123456789" + "0123456789" + "01").toByteArray())) + } + + @Test + fun decode_curve25519() { + assertArrayEquals(("0123456789" + "0123456789" + "0123456789" + "01").toByteArray(), + base58decode("4F85ZySpwyY6FuH7mQYyyr5b8nV9zFRBLj92AJa37sMr")) + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/RecoveryKeyTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/RecoveryKeyTest.kt new file mode 100644 index 0000000000..6b9d388623 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/RecoveryKeyTest.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2018 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.util + +import org.matrix.android.sdk.MatrixTest +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class RecoveryKeyTest: MatrixTest { + + private val curve25519Key = byteArrayOf( + 0x77.toByte(), 0x07.toByte(), 0x6D.toByte(), 0x0A.toByte(), 0x73.toByte(), 0x18.toByte(), 0xA5.toByte(), 0x7D.toByte(), + 0x3C.toByte(), 0x16.toByte(), 0xC1.toByte(), 0x72.toByte(), 0x51.toByte(), 0xB2.toByte(), 0x66.toByte(), 0x45.toByte(), + 0xDF.toByte(), 0x4C.toByte(), 0x2F.toByte(), 0x87.toByte(), 0xEB.toByte(), 0xC0.toByte(), 0x99.toByte(), 0x2A.toByte(), + 0xB1.toByte(), 0x77.toByte(), 0xFB.toByte(), 0xA5.toByte(), 0x1D.toByte(), 0xB9.toByte(), 0x2C.toByte(), 0x2A.toByte()) + + @Test + fun isValidRecoveryKey_valid_true() { + assertTrue(isValidRecoveryKey("EsTcLW2KPGiFwKEA3As5g5c4BXwkqeeJZJV8Q9fugUMNUE4d")) + + // Space should be ignored + assertTrue(isValidRecoveryKey("EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d")) + + // All whitespace should be ignored + assertTrue(isValidRecoveryKey("EsTc LW2K PGiF wKEA 3As5 g5c4\r\nBXwk qeeJ ZJV8 Q9fu gUMN UE4d")) + } + + @Test + fun isValidRecoveryKey_null_false() { + assertFalse(isValidRecoveryKey(null)) + } + + @Test + fun isValidRecoveryKey_empty_false() { + assertFalse(isValidRecoveryKey("")) + } + + @Test + fun isValidRecoveryKey_wrong_size_false() { + assertFalse(isValidRecoveryKey("abc")) + } + + @Test + fun isValidRecoveryKey_bad_first_byte_false() { + assertFalse(isValidRecoveryKey("FsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d")) + } + + @Test + fun isValidRecoveryKey_bad_second_byte_false() { + assertFalse(isValidRecoveryKey("EqTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d")) + } + + @Test + fun isValidRecoveryKey_bad_parity_false() { + assertFalse(isValidRecoveryKey("EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4e")) + } + + @Test + fun computeRecoveryKey_ok() { + assertEquals("EsTcLW2KPGiFwKEA3As5g5c4BXwkqeeJZJV8Q9fugUMNUE4d", computeRecoveryKey(curve25519Key)) + } + + @Test + fun extractCurveKeyFromRecoveryKey_ok() { + assertArrayEquals(curve25519Key, extractCurveKeyFromRecoveryKey("EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d")) + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/store/db/HelperTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/store/db/HelperTest.kt new file mode 100644 index 0000000000..cac2d1cba9 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/store/db/HelperTest.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2018 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.store.db + +import org.matrix.android.sdk.MatrixTest +import org.matrix.android.sdk.internal.util.md5 +import org.junit.Assert.assertEquals +import org.junit.Test + +class HelperTest: MatrixTest { + + @Test + fun testHash_ok() { + assertEquals("e9ee13b1ba2afc0825f4e556114785dd", "alice_15428931567802abf5ba7-d685-4333-af47-d38417ab3724:localhost:8480".md5()) + } + + @Test + fun testHash_size_ok() { + // Any String will have a 32 char hash + for (i in 1..100) { + assertEquals(32, "a".repeat(i).md5().length) + } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/BinaryStringTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/BinaryStringTest.kt new file mode 100644 index 0000000000..0f8fe58b7f --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/BinaryStringTest.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.internal.crypto.verification.qrcode + +import org.matrix.android.sdk.MatrixTest +import org.amshove.kluent.shouldEqualTo +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runners.MethodSorters + +@FixMethodOrder(MethodSorters.JVM) +class BinaryStringTest: MatrixTest { + + /** + * I want to put bytes to a String, and vice versa + */ + @Test + fun testNominalCase() { + val byteArray = ByteArray(256) + for (i in byteArray.indices) { + byteArray[i] = i.toByte() // Random.nextInt(255).toByte() + } + + val str = byteArray.toString(Charsets.ISO_8859_1) + + str.length shouldEqualTo 256 + + // Ok convert back to bytearray + + val result = str.toByteArray(Charsets.ISO_8859_1) + + result.size shouldEqualTo 256 + + for (i in 0..255) { + result[i] shouldEqualTo i.toByte() + result[i] shouldEqualTo byteArray[i] + } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/task/CoroutineSequencersTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/task/CoroutineSequencersTest.kt new file mode 100644 index 0000000000..7bef439417 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/task/CoroutineSequencersTest.kt @@ -0,0 +1,130 @@ +/* + * Copyright 2019 New Vector Ltd + * + * 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. + */ + +package org.matrix.android.sdk.internal.task + +import org.matrix.android.sdk.MatrixTest +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.delay +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals +import org.junit.Test +import java.util.concurrent.Executors + +class CoroutineSequencersTest: MatrixTest { + + private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + + @Test + fun sequencer_should_run_sequential() { + val sequencer = SemaphoreCoroutineSequencer() + val results = ArrayList() + + val jobs = listOf( + GlobalScope.launch(dispatcher) { + sequencer.post { suspendingMethod("#1") }.also { + results.add(it) + } + }, + GlobalScope.launch(dispatcher) { + sequencer.post { suspendingMethod("#2") }.also { + results.add(it) + } + }, + GlobalScope.launch(dispatcher) { + sequencer.post { suspendingMethod("#3") }.also { + results.add(it) + } + } + ) + runBlocking { + jobs.joinAll() + } + assertEquals(3, results.size) + assertEquals(results[0], "#1") + assertEquals(results[1], "#2") + assertEquals(results[2], "#3") + } + + @Test + fun sequencer_should_run_parallel() { + val sequencer1 = SemaphoreCoroutineSequencer() + val sequencer2 = SemaphoreCoroutineSequencer() + val sequencer3 = SemaphoreCoroutineSequencer() + val results = ArrayList() + val jobs = listOf( + GlobalScope.launch(dispatcher) { + sequencer1.post { suspendingMethod("#1") }.also { + results.add(it) + } + }, + GlobalScope.launch(dispatcher) { + sequencer2.post { suspendingMethod("#2") }.also { + results.add(it) + } + }, + GlobalScope.launch(dispatcher) { + sequencer3.post { suspendingMethod("#3") }.also { + results.add(it) + } + } + ) + runBlocking { + jobs.joinAll() + } + assertEquals(3, results.size) + } + + @Test + fun sequencer_should_jump_to_next_when_current_job_canceled() { + val sequencer = SemaphoreCoroutineSequencer() + val results = ArrayList() + val jobs = listOf( + GlobalScope.launch(dispatcher) { + sequencer.post { suspendingMethod("#1") }.also { + results.add(it) + } + }, + GlobalScope.launch(dispatcher) { + val result = sequencer.post { suspendingMethod("#2") }.also { + results.add(it) + } + println("Result: $result") + }, + GlobalScope.launch(dispatcher) { + sequencer.post { suspendingMethod("#3") }.also { + results.add(it) + } + } + ) + // We are canceling the second job + jobs[1].cancel() + runBlocking { + jobs.joinAll() + } + assertEquals(2, results.size) + } + + private suspend fun suspendingMethod(name: String): String { + println("BLOCKING METHOD $name STARTS on ${Thread.currentThread().name}") + delay(1000) + println("BLOCKING METHOD $name ENDS on ${Thread.currentThread().name}") + return name + } +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000000..ade79d3acb --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include ':matrix-sdk-android' diff --git a/tools/import_from_element.sh b/tools/import_from_element.sh new file mode 100755 index 0000000000..bb61a14027 --- /dev/null +++ b/tools/import_from_element.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash + +### This script import SDK code from Element Android + +set -e + +elementAndroidPath="../element-android" + +if [ -d "$elementAndroidPath" ]; then + echo "Importing sdk module from Element Android located at ${elementAndroidPath}" +else + echo "Element Android not found at ${elementAndroidPath}. Can not continue." + exit 1 +fi + +# Check that Element Android is on master branch + +pushd $elementAndroidPath + +elementBranch=`git rev-parse --abbrev-ref HEAD` + +if [ "$elementBranch" != "master" ]; then + read -p "Warning, Element Android is not on master branch but on branch '${elementBranch}' . Continue (y/n)? " -n 1 CONT + echo + if [ "$CONT" != "y" ]; then + exit 0 + fi +fi + +popd + +# matrix SDK + +# Delete existing path +echo "Importing matrix-sdk-android..." +rm -rf ./matrix-sdk-android +cp -r ${elementAndroidPath}/matrix-sdk-android . + +# Add all changes to git +git add -A + +# Build the library +./gradlew clean assembleRelease + +# Success + +echo "Success" +echo +echo "Please check the version name before committing and update the changelog" \ No newline at end of file From 74059bda4864b4c8dc1a456db8962a15d34be73c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 14 Aug 2020 11:19:21 +0200 Subject: [PATCH 2/2] better format --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9e7074ee5a..7538303fd2 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,9 @@ Please open any issue in the Element Android project [Create an issue](https://g To integrate the SDK to your application, add the following gradle dependency to the build.gradle of your application module: -> implementation 'com.github.matrix-org:matrix-android-sdk2:v0.0.1' +```gradle +implementation 'com.github.matrix-org:matrix-android-sdk2:v0.0.1' +``` You need to add Jitpack as a repository in your main build.gradle file. Please follow instructions here: https://jitpack.io/ @@ -22,4 +24,4 @@ You need to add Jitpack as a repository in your main build.gradle file. Please f Sadly there is no official documentation on how to migrate from the old SDK to the new one. Because the new SDK API is totally new, we guess that there is no easy way to handle a migration. -We advice that new applications uses this new SDK. \ No newline at end of file +We advice that new applications uses this new SDK.