mirror of
https://github.com/element-hq/element-android
synced 2024-11-24 10:25:35 +03:00
Merge branch 'develop' into feature/fga/rx_flow_migration
This commit is contained in:
commit
635ca8e276
74 changed files with 1999 additions and 728 deletions
|
@ -1,3 +1,12 @@
|
||||||
|
Changes in Element v1.3.6 (2021-10-26)
|
||||||
|
======================================
|
||||||
|
|
||||||
|
Bugfixes 🐛
|
||||||
|
----------
|
||||||
|
- Correctly handle url of type https://mobile.element.io/?hs_url=…&is_url=…
|
||||||
|
Skip the choose server screen when such URL are open when Element ([#2684](https://github.com/vector-im/element-android/issues/2684))
|
||||||
|
|
||||||
|
|
||||||
Changes in Element v1.3.5 (2021-10-25)
|
Changes in Element v1.3.5 (2021-10-25)
|
||||||
======================================
|
======================================
|
||||||
|
|
||||||
|
|
1
changelog.d/1491.bugfix
Normal file
1
changelog.d/1491.bugfix
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Stops showing a dedicated redacted event notification, the message notifications will update accordingly
|
1
changelog.d/3395.bugfix
Normal file
1
changelog.d/3395.bugfix
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Fixes marking individual notifications as read causing other notifications to be dismissed
|
1
changelog.d/4152.bugfix
Normal file
1
changelog.d/4152.bugfix
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Tentatively fixing the doubled notifications by updating the group summary at specific points in the notification rendering cycle
|
1
changelog.d/4255.bugfix
Normal file
1
changelog.d/4255.bugfix
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Fixes being unable to join rooms by name
|
1
changelog.d/4266.removal
Normal file
1
changelog.d/4266.removal
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Add API `LoginWizard.loginCustom(data: JsonDict): Session` to be able to login to a homeserver using arbitrary request content
|
1
changelog.d/4334.removal
Normal file
1
changelog.d/4334.removal
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Add optional deviceId to the login API
|
1
changelog.d/582.feature
Normal file
1
changelog.d/582.feature
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Adding the room name to the invitation notification (if the room summary is available)
|
2
fastlane/metadata/android/en-US/changelogs/40103060.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/40103060.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
Main changes in this version: Add Presence support, for Direct Message room (note: presence is disabled on matrix.org). Add again Android Auto support.
|
||||||
|
Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.6
|
|
@ -31,7 +31,7 @@ android {
|
||||||
// that the app's state is completely cleared between tests.
|
// that the app's state is completely cleared between tests.
|
||||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||||
|
|
||||||
buildConfigField "String", "SDK_VERSION", "\"1.3.6\""
|
buildConfigField "String", "SDK_VERSION", "\"1.3.7\""
|
||||||
|
|
||||||
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
|
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
|
||||||
resValue "string", "git_sdk_revision", "\"${gitRevision()}\""
|
resValue "string", "git_sdk_revision", "\"${gitRevision()}\""
|
||||||
|
@ -156,7 +156,7 @@ dependencies {
|
||||||
implementation libs.apache.commonsImaging
|
implementation libs.apache.commonsImaging
|
||||||
|
|
||||||
// Phone number https://github.com/google/libphonenumber
|
// Phone number https://github.com/google/libphonenumber
|
||||||
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.35'
|
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.36'
|
||||||
|
|
||||||
testImplementation libs.tests.junit
|
testImplementation libs.tests.junit
|
||||||
testImplementation 'org.robolectric:robolectric:4.6.1'
|
testImplementation 'org.robolectric:robolectric:4.6.1'
|
||||||
|
|
|
@ -105,9 +105,15 @@ interface AuthenticationService {
|
||||||
/**
|
/**
|
||||||
* Authenticate with a matrixId and a password
|
* Authenticate with a matrixId and a password
|
||||||
* Usually call this after a successful call to getWellKnownData()
|
* Usually call this after a successful call to getWellKnownData()
|
||||||
|
* @param homeServerConnectionConfig the information about the homeserver and other configuration
|
||||||
|
* @param matrixId the matrixId of the user
|
||||||
|
* @param password the password of the account
|
||||||
|
* @param initialDeviceName the initial device name
|
||||||
|
* @param deviceId the device id, optional. If not provided or null, the server will generate one.
|
||||||
*/
|
*/
|
||||||
suspend fun directAuthentication(homeServerConnectionConfig: HomeServerConnectionConfig,
|
suspend fun directAuthentication(homeServerConnectionConfig: HomeServerConnectionConfig,
|
||||||
matrixId: String,
|
matrixId: String,
|
||||||
password: String,
|
password: String,
|
||||||
initialDeviceName: String): Session
|
initialDeviceName: String,
|
||||||
|
deviceId: String? = null): Session
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
package org.matrix.android.sdk.api.auth.login
|
package org.matrix.android.sdk.api.auth.login
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
import org.matrix.android.sdk.api.util.JsonDict
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set of methods to be able to login to an existing account on a homeserver.
|
* Set of methods to be able to login to an existing account on a homeserver.
|
||||||
|
@ -34,12 +35,14 @@ interface LoginWizard {
|
||||||
*
|
*
|
||||||
* @param login the login field. Can be a user name, or a msisdn (email or phone number) associated to the account
|
* @param login the login field. Can be a user name, or a msisdn (email or phone number) associated to the account
|
||||||
* @param password the password of the account
|
* @param password the password of the account
|
||||||
* @param deviceName the initial device name
|
* @param initialDeviceName the initial device name
|
||||||
|
* @param deviceId the device id, optional. If not provided or null, the server will generate one.
|
||||||
* @return a [Session] if the login is successful
|
* @return a [Session] if the login is successful
|
||||||
*/
|
*/
|
||||||
suspend fun login(login: String,
|
suspend fun login(login: String,
|
||||||
password: String,
|
password: String,
|
||||||
deviceName: String): Session
|
initialDeviceName: String,
|
||||||
|
deviceId: String? = null): Session
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exchange a login token to an access token.
|
* Exchange a login token to an access token.
|
||||||
|
@ -49,6 +52,12 @@ interface LoginWizard {
|
||||||
*/
|
*/
|
||||||
suspend fun loginWithToken(loginToken: String): Session
|
suspend fun loginWithToken(loginToken: String): Session
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login to the homeserver by sending a custom JsonDict.
|
||||||
|
* The data should contain at least one entry "type" with a String value.
|
||||||
|
*/
|
||||||
|
suspend fun loginCustom(data: JsonDict): Session
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ask the homeserver to reset the user password. The password will not be reset until
|
* Ask the homeserver to reset the user password. The password will not be reset until
|
||||||
* [resetPasswordMailConfirmed] is successfully called.
|
* [resetPasswordMailConfirmed] is successfully called.
|
||||||
|
|
|
@ -22,6 +22,8 @@ import org.json.JSONObject
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
import org.matrix.android.sdk.api.failure.MatrixError
|
import org.matrix.android.sdk.api.failure.MatrixError
|
||||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||||
|
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.MessageContent
|
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.message.MessageType
|
||||||
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
||||||
|
@ -310,3 +312,6 @@ fun Event.isEdition(): Boolean {
|
||||||
fun Event.getPresenceContent(): PresenceContent? {
|
fun Event.getPresenceContent(): PresenceContent? {
|
||||||
return content.toModel<PresenceContent>()
|
return content.toModel<PresenceContent>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Event.isInvitation(): Boolean = type == EventType.STATE_ROOM_MEMBER &&
|
||||||
|
content?.toModel<RoomMemberContent>()?.membership == Membership.INVITE
|
||||||
|
|
|
@ -121,6 +121,10 @@ internal interface AuthAPI {
|
||||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login")
|
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login")
|
||||||
suspend fun login(@Body loginParams: TokenLoginParams): Credentials
|
suspend fun login(@Body loginParams: TokenLoginParams): Credentials
|
||||||
|
|
||||||
|
@Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000")
|
||||||
|
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login")
|
||||||
|
suspend fun login(@Body loginParams: JsonDict): Credentials
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ask the homeserver to reset the password associated with the provided email.
|
* Ask the homeserver to reset the password associated with the provided email.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -388,8 +388,15 @@ internal class DefaultAuthenticationService @Inject constructor(
|
||||||
override suspend fun directAuthentication(homeServerConnectionConfig: HomeServerConnectionConfig,
|
override suspend fun directAuthentication(homeServerConnectionConfig: HomeServerConnectionConfig,
|
||||||
matrixId: String,
|
matrixId: String,
|
||||||
password: String,
|
password: String,
|
||||||
initialDeviceName: String): Session {
|
initialDeviceName: String,
|
||||||
return directLoginTask.execute(DirectLoginTask.Params(homeServerConnectionConfig, matrixId, password, initialDeviceName))
|
deviceId: String?): Session {
|
||||||
|
return directLoginTask.execute(DirectLoginTask.Params(
|
||||||
|
homeServerConnectionConfig = homeServerConnectionConfig,
|
||||||
|
userId = matrixId,
|
||||||
|
password = password,
|
||||||
|
deviceName = initialDeviceName,
|
||||||
|
deviceId = deviceId
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildAuthAPI(homeServerConnectionConfig: HomeServerConnectionConfig): AuthAPI {
|
private fun buildAuthAPI(homeServerConnectionConfig: HomeServerConnectionConfig): AuthAPI {
|
||||||
|
|
|
@ -49,51 +49,54 @@ internal data class PasswordLoginParams(
|
||||||
|
|
||||||
fun userIdentifier(user: String,
|
fun userIdentifier(user: String,
|
||||||
password: String,
|
password: String,
|
||||||
deviceDisplayName: String? = null,
|
deviceDisplayName: String?,
|
||||||
deviceId: String? = null): PasswordLoginParams {
|
deviceId: String?): PasswordLoginParams {
|
||||||
return PasswordLoginParams(
|
return PasswordLoginParams(
|
||||||
mapOf(
|
identifier = mapOf(
|
||||||
IDENTIFIER_KEY_TYPE to IDENTIFIER_KEY_TYPE_USER,
|
IDENTIFIER_KEY_TYPE to IDENTIFIER_KEY_TYPE_USER,
|
||||||
IDENTIFIER_KEY_USER to user
|
IDENTIFIER_KEY_USER to user
|
||||||
),
|
),
|
||||||
password,
|
password = password,
|
||||||
LoginFlowTypes.PASSWORD,
|
type = LoginFlowTypes.PASSWORD,
|
||||||
deviceDisplayName,
|
deviceDisplayName = deviceDisplayName,
|
||||||
deviceId)
|
deviceId = deviceId
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun thirdPartyIdentifier(medium: String,
|
fun thirdPartyIdentifier(medium: String,
|
||||||
address: String,
|
address: String,
|
||||||
password: String,
|
password: String,
|
||||||
deviceDisplayName: String? = null,
|
deviceDisplayName: String?,
|
||||||
deviceId: String? = null): PasswordLoginParams {
|
deviceId: String?): PasswordLoginParams {
|
||||||
return PasswordLoginParams(
|
return PasswordLoginParams(
|
||||||
mapOf(
|
identifier = mapOf(
|
||||||
IDENTIFIER_KEY_TYPE to IDENTIFIER_KEY_TYPE_THIRD_PARTY,
|
IDENTIFIER_KEY_TYPE to IDENTIFIER_KEY_TYPE_THIRD_PARTY,
|
||||||
IDENTIFIER_KEY_MEDIUM to medium,
|
IDENTIFIER_KEY_MEDIUM to medium,
|
||||||
IDENTIFIER_KEY_ADDRESS to address
|
IDENTIFIER_KEY_ADDRESS to address
|
||||||
),
|
),
|
||||||
password,
|
password = password,
|
||||||
LoginFlowTypes.PASSWORD,
|
type = LoginFlowTypes.PASSWORD,
|
||||||
deviceDisplayName,
|
deviceDisplayName = deviceDisplayName,
|
||||||
deviceId)
|
deviceId = deviceId
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun phoneIdentifier(country: String,
|
fun phoneIdentifier(country: String,
|
||||||
phone: String,
|
phone: String,
|
||||||
password: String,
|
password: String,
|
||||||
deviceDisplayName: String? = null,
|
deviceDisplayName: String?,
|
||||||
deviceId: String? = null): PasswordLoginParams {
|
deviceId: String?): PasswordLoginParams {
|
||||||
return PasswordLoginParams(
|
return PasswordLoginParams(
|
||||||
mapOf(
|
identifier = mapOf(
|
||||||
IDENTIFIER_KEY_TYPE to IDENTIFIER_KEY_TYPE_PHONE,
|
IDENTIFIER_KEY_TYPE to IDENTIFIER_KEY_TYPE_PHONE,
|
||||||
IDENTIFIER_KEY_COUNTRY to country,
|
IDENTIFIER_KEY_COUNTRY to country,
|
||||||
IDENTIFIER_KEY_PHONE to phone
|
IDENTIFIER_KEY_PHONE to phone
|
||||||
),
|
),
|
||||||
password,
|
password = password,
|
||||||
LoginFlowTypes.PASSWORD,
|
type = LoginFlowTypes.PASSWORD,
|
||||||
deviceDisplayName,
|
deviceDisplayName = deviceDisplayName,
|
||||||
deviceId)
|
deviceId = deviceId
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import org.matrix.android.sdk.api.auth.login.LoginProfileInfo
|
||||||
import org.matrix.android.sdk.api.auth.login.LoginWizard
|
import org.matrix.android.sdk.api.auth.login.LoginWizard
|
||||||
import org.matrix.android.sdk.api.auth.registration.RegisterThreePid
|
import org.matrix.android.sdk.api.auth.registration.RegisterThreePid
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
import org.matrix.android.sdk.api.util.JsonDict
|
||||||
import org.matrix.android.sdk.internal.auth.AuthAPI
|
import org.matrix.android.sdk.internal.auth.AuthAPI
|
||||||
import org.matrix.android.sdk.internal.auth.PendingSessionStore
|
import org.matrix.android.sdk.internal.auth.PendingSessionStore
|
||||||
import org.matrix.android.sdk.internal.auth.SessionCreator
|
import org.matrix.android.sdk.internal.auth.SessionCreator
|
||||||
|
@ -52,11 +53,23 @@ internal class DefaultLoginWizard(
|
||||||
|
|
||||||
override suspend fun login(login: String,
|
override suspend fun login(login: String,
|
||||||
password: String,
|
password: String,
|
||||||
deviceName: String): Session {
|
initialDeviceName: String,
|
||||||
|
deviceId: String?): Session {
|
||||||
val loginParams = if (Patterns.EMAIL_ADDRESS.matcher(login).matches()) {
|
val loginParams = if (Patterns.EMAIL_ADDRESS.matcher(login).matches()) {
|
||||||
PasswordLoginParams.thirdPartyIdentifier(ThreePidMedium.EMAIL, login, password, deviceName)
|
PasswordLoginParams.thirdPartyIdentifier(
|
||||||
|
medium = ThreePidMedium.EMAIL,
|
||||||
|
address = login,
|
||||||
|
password = password,
|
||||||
|
deviceDisplayName = initialDeviceName,
|
||||||
|
deviceId = deviceId
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
PasswordLoginParams.userIdentifier(login, password, deviceName)
|
PasswordLoginParams.userIdentifier(
|
||||||
|
user = login,
|
||||||
|
password = password,
|
||||||
|
deviceDisplayName = initialDeviceName,
|
||||||
|
deviceId = deviceId
|
||||||
|
)
|
||||||
}
|
}
|
||||||
val credentials = executeRequest(null) {
|
val credentials = executeRequest(null) {
|
||||||
authAPI.login(loginParams)
|
authAPI.login(loginParams)
|
||||||
|
@ -79,6 +92,14 @@ internal class DefaultLoginWizard(
|
||||||
return sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig)
|
return sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun loginCustom(data: JsonDict): Session {
|
||||||
|
val credentials = executeRequest(null) {
|
||||||
|
authAPI.login(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun resetPassword(email: String, newPassword: String) {
|
override suspend fun resetPassword(email: String, newPassword: String) {
|
||||||
val param = RegisterAddThreePidTask.Params(
|
val param = RegisterAddThreePidTask.Params(
|
||||||
RegisterThreePid.Email(email),
|
RegisterThreePid.Email(email),
|
||||||
|
|
|
@ -37,7 +37,8 @@ internal interface DirectLoginTask : Task<DirectLoginTask.Params, Session> {
|
||||||
val homeServerConnectionConfig: HomeServerConnectionConfig,
|
val homeServerConnectionConfig: HomeServerConnectionConfig,
|
||||||
val userId: String,
|
val userId: String,
|
||||||
val password: String,
|
val password: String,
|
||||||
val deviceName: String
|
val deviceName: String,
|
||||||
|
val deviceId: String?
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,7 +56,12 @@ internal class DefaultDirectLoginTask @Inject constructor(
|
||||||
val authAPI = retrofitFactory.create(client, homeServerUrl)
|
val authAPI = retrofitFactory.create(client, homeServerUrl)
|
||||||
.create(AuthAPI::class.java)
|
.create(AuthAPI::class.java)
|
||||||
|
|
||||||
val loginParams = PasswordLoginParams.userIdentifier(params.userId, params.password, params.deviceName)
|
val loginParams = PasswordLoginParams.userIdentifier(
|
||||||
|
user = params.userId,
|
||||||
|
password = params.password,
|
||||||
|
deviceDisplayName = params.deviceName,
|
||||||
|
deviceId = params.deviceId
|
||||||
|
)
|
||||||
|
|
||||||
val credentials = try {
|
val credentials = try {
|
||||||
executeRequest(null) {
|
executeRequest(null) {
|
||||||
|
|
|
@ -34,7 +34,7 @@ internal interface SendEventTask : Task<SendEventTask.Params, String> {
|
||||||
|
|
||||||
internal class DefaultSendEventTask @Inject constructor(
|
internal class DefaultSendEventTask @Inject constructor(
|
||||||
private val localEchoRepository: LocalEchoRepository,
|
private val localEchoRepository: LocalEchoRepository,
|
||||||
private val encryptEventTask: DefaultEncryptEventTask,
|
private val encryptEventTask: EncryptEventTask,
|
||||||
private val loadRoomMembersTask: LoadRoomMembersTask,
|
private val loadRoomMembersTask: LoadRoomMembersTask,
|
||||||
private val roomAPI: RoomAPI,
|
private val roomAPI: RoomAPI,
|
||||||
private val globalErrorReceiver: GlobalErrorReceiver) : SendEventTask {
|
private val globalErrorReceiver: GlobalErrorReceiver) : SendEventTask {
|
||||||
|
|
|
@ -34,7 +34,7 @@ internal interface SendVerificationMessageTask : Task<SendVerificationMessageTas
|
||||||
|
|
||||||
internal class DefaultSendVerificationMessageTask @Inject constructor(
|
internal class DefaultSendVerificationMessageTask @Inject constructor(
|
||||||
private val localEchoRepository: LocalEchoRepository,
|
private val localEchoRepository: LocalEchoRepository,
|
||||||
private val encryptEventTask: DefaultEncryptEventTask,
|
private val encryptEventTask: EncryptEventTask,
|
||||||
private val roomAPI: RoomAPI,
|
private val roomAPI: RoomAPI,
|
||||||
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
|
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
|
||||||
private val globalErrorReceiver: GlobalErrorReceiver) : SendVerificationMessageTask {
|
private val globalErrorReceiver: GlobalErrorReceiver) : SendVerificationMessageTask {
|
||||||
|
|
|
@ -21,13 +21,13 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||||
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
|
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
|
||||||
import org.matrix.android.sdk.api.session.room.model.SpaceParentInfo
|
import org.matrix.android.sdk.api.session.room.model.SpaceParentInfo
|
||||||
import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
|
import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
|
||||||
|
import org.matrix.android.sdk.api.session.typing.TypingUsersTracker
|
||||||
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
|
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
|
||||||
import org.matrix.android.sdk.internal.database.model.presence.toUserPresence
|
import org.matrix.android.sdk.internal.database.model.presence.toUserPresence
|
||||||
import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class RoomSummaryMapper @Inject constructor(private val timelineEventMapper: TimelineEventMapper,
|
internal class RoomSummaryMapper @Inject constructor(private val timelineEventMapper: TimelineEventMapper,
|
||||||
private val typingUsersTracker: DefaultTypingUsersTracker) {
|
private val typingUsersTracker: TypingUsersTracker) {
|
||||||
|
|
||||||
fun map(roomSummaryEntity: RoomSummaryEntity): RoomSummary {
|
fun map(roomSummaryEntity: RoomSummaryEntity): RoomSummary {
|
||||||
val tags = roomSummaryEntity.tags().map {
|
val tags = roomSummaryEntity.tags().map {
|
||||||
|
|
|
@ -41,6 +41,7 @@ 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.file.FileService
|
||||||
import org.matrix.android.sdk.api.session.group.GroupService
|
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.homeserver.HomeServerCapabilitiesService
|
||||||
|
import org.matrix.android.sdk.api.session.identity.IdentityService
|
||||||
import org.matrix.android.sdk.api.session.initsync.SyncStatusService
|
import org.matrix.android.sdk.api.session.initsync.SyncStatusService
|
||||||
import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService
|
import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService
|
||||||
import org.matrix.android.sdk.api.session.media.MediaService
|
import org.matrix.android.sdk.api.session.media.MediaService
|
||||||
|
@ -72,7 +73,6 @@ import org.matrix.android.sdk.internal.di.SessionId
|
||||||
import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificate
|
import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificate
|
||||||
import org.matrix.android.sdk.internal.di.WorkManagerProvider
|
import org.matrix.android.sdk.internal.di.WorkManagerProvider
|
||||||
import org.matrix.android.sdk.internal.network.GlobalErrorHandler
|
import org.matrix.android.sdk.internal.network.GlobalErrorHandler
|
||||||
import org.matrix.android.sdk.internal.session.identity.DefaultIdentityService
|
|
||||||
import org.matrix.android.sdk.internal.session.sync.SyncTokenStore
|
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.SyncThread
|
||||||
import org.matrix.android.sdk.internal.session.sync.job.SyncWorker
|
import org.matrix.android.sdk.internal.session.sync.job.SyncWorker
|
||||||
|
@ -124,7 +124,7 @@ internal class DefaultSession @Inject constructor(
|
||||||
private val _sharedSecretStorageService: Lazy<SharedSecretStorageService>,
|
private val _sharedSecretStorageService: Lazy<SharedSecretStorageService>,
|
||||||
private val accountService: Lazy<AccountService>,
|
private val accountService: Lazy<AccountService>,
|
||||||
private val eventService: Lazy<EventService>,
|
private val eventService: Lazy<EventService>,
|
||||||
private val defaultIdentityService: DefaultIdentityService,
|
private val identityService: IdentityService,
|
||||||
private val integrationManagerService: IntegrationManagerService,
|
private val integrationManagerService: IntegrationManagerService,
|
||||||
private val thirdPartyService: Lazy<ThirdPartyService>,
|
private val thirdPartyService: Lazy<ThirdPartyService>,
|
||||||
private val callSignalingService: Lazy<CallSignalingService>,
|
private val callSignalingService: Lazy<CallSignalingService>,
|
||||||
|
@ -275,7 +275,7 @@ internal class DefaultSession @Inject constructor(
|
||||||
|
|
||||||
override fun cryptoService(): CryptoService = cryptoService.get()
|
override fun cryptoService(): CryptoService = cryptoService.get()
|
||||||
|
|
||||||
override fun identityService() = defaultIdentityService
|
override fun identityService() = identityService
|
||||||
|
|
||||||
override fun fileService(): FileService = defaultFileService.get()
|
override fun fileService(): FileService = defaultFileService.get()
|
||||||
|
|
||||||
|
|
|
@ -35,12 +35,12 @@ import org.matrix.android.sdk.api.failure.Failure
|
||||||
import org.matrix.android.sdk.api.failure.MatrixError
|
import org.matrix.android.sdk.api.failure.MatrixError
|
||||||
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
|
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
|
||||||
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
|
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
|
||||||
|
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
|
||||||
import org.matrix.android.sdk.internal.di.Authenticated
|
import org.matrix.android.sdk.internal.di.Authenticated
|
||||||
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
|
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
|
||||||
import org.matrix.android.sdk.internal.network.ProgressRequestBody
|
import org.matrix.android.sdk.internal.network.ProgressRequestBody
|
||||||
import org.matrix.android.sdk.internal.network.awaitResponse
|
import org.matrix.android.sdk.internal.network.awaitResponse
|
||||||
import org.matrix.android.sdk.internal.network.toFailure
|
import org.matrix.android.sdk.internal.network.toFailure
|
||||||
import org.matrix.android.sdk.internal.session.homeserver.DefaultHomeServerCapabilitiesService
|
|
||||||
import org.matrix.android.sdk.internal.util.TemporaryFileCreator
|
import org.matrix.android.sdk.internal.util.TemporaryFileCreator
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
|
@ -50,7 +50,7 @@ import javax.inject.Inject
|
||||||
internal class FileUploader @Inject constructor(
|
internal class FileUploader @Inject constructor(
|
||||||
@Authenticated private val okHttpClient: OkHttpClient,
|
@Authenticated private val okHttpClient: OkHttpClient,
|
||||||
private val globalErrorReceiver: GlobalErrorReceiver,
|
private val globalErrorReceiver: GlobalErrorReceiver,
|
||||||
private val homeServerCapabilitiesService: DefaultHomeServerCapabilitiesService,
|
private val homeServerCapabilitiesService: HomeServerCapabilitiesService,
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val temporaryFileCreator: TemporaryFileCreator,
|
private val temporaryFileCreator: TemporaryFileCreator,
|
||||||
contentUrlResolver: ContentUrlResolver,
|
contentUrlResolver: ContentUrlResolver,
|
||||||
|
|
|
@ -80,7 +80,7 @@ internal class DefaultIdentityService @Inject constructor(
|
||||||
private val identityApiProvider: IdentityApiProvider,
|
private val identityApiProvider: IdentityApiProvider,
|
||||||
private val accountDataDataSource: UserAccountDataDataSource,
|
private val accountDataDataSource: UserAccountDataDataSource,
|
||||||
private val homeServerCapabilitiesService: HomeServerCapabilitiesService,
|
private val homeServerCapabilitiesService: HomeServerCapabilitiesService,
|
||||||
private val sign3pidInvitationTask: DefaultSign3pidInvitationTask,
|
private val sign3pidInvitationTask: Sign3pidInvitationTask,
|
||||||
private val sessionParams: SessionParams
|
private val sessionParams: SessionParams
|
||||||
) : IdentityService, SessionLifecycleObserver {
|
) : IdentityService, SessionLifecycleObserver {
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import io.realm.RealmConfiguration
|
import io.realm.RealmConfiguration
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
import org.matrix.android.sdk.api.session.identity.IdentityService
|
||||||
import org.matrix.android.sdk.internal.database.RealmKeysUtils
|
import org.matrix.android.sdk.internal.database.RealmKeysUtils
|
||||||
import org.matrix.android.sdk.internal.di.AuthenticatedIdentity
|
import org.matrix.android.sdk.internal.di.AuthenticatedIdentity
|
||||||
import org.matrix.android.sdk.internal.di.IdentityDatabase
|
import org.matrix.android.sdk.internal.di.IdentityDatabase
|
||||||
|
@ -75,6 +76,9 @@ internal abstract class IdentityModule {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindIdentityService(service: DefaultIdentityService): IdentityService
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@AuthenticatedIdentity
|
@AuthenticatedIdentity
|
||||||
abstract fun bindAccessTokenProvider(provider: IdentityAccessTokenProvider): AccessTokenProvider
|
abstract fun bindAccessTokenProvider(provider: IdentityAccessTokenProvider): AccessTokenProvider
|
||||||
|
|
|
@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.notification
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.pushrules.rest.PushRule
|
import org.matrix.android.sdk.api.pushrules.rest.PushRule
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.isInvitation
|
||||||
import org.matrix.android.sdk.api.session.sync.model.RoomsSyncResponse
|
import org.matrix.android.sdk.api.session.sync.model.RoomsSyncResponse
|
||||||
import org.matrix.android.sdk.internal.di.UserId
|
import org.matrix.android.sdk.internal.di.UserId
|
||||||
import org.matrix.android.sdk.internal.task.Task
|
import org.matrix.android.sdk.internal.task.Task
|
||||||
|
@ -48,14 +49,18 @@ internal class DefaultProcessEventForPushTask @Inject constructor(
|
||||||
}
|
}
|
||||||
val newJoinEvents = params.syncResponse.join
|
val newJoinEvents = params.syncResponse.join
|
||||||
.mapNotNull { (key, value) ->
|
.mapNotNull { (key, value) ->
|
||||||
value.timeline?.events?.map { it.copy(roomId = key) }
|
value.timeline?.events?.mapNotNull {
|
||||||
|
it.takeIf { !it.isInvitation() }?.copy(roomId = key)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.flatten()
|
.flatten()
|
||||||
|
|
||||||
val inviteEvents = params.syncResponse.invite
|
val inviteEvents = params.syncResponse.invite
|
||||||
.mapNotNull { (key, value) ->
|
.mapNotNull { (key, value) ->
|
||||||
value.inviteState?.events?.map { it.copy(roomId = key) }
|
value.inviteState?.events?.map { it.copy(roomId = key) }
|
||||||
}
|
}
|
||||||
.flatten()
|
.flatten()
|
||||||
|
|
||||||
val allEvents = (newJoinEvents + inviteEvents).filter { event ->
|
val allEvents = (newJoinEvents + inviteEvents).filter { event ->
|
||||||
when (event.type) {
|
when (event.type) {
|
||||||
EventType.MESSAGE,
|
EventType.MESSAGE,
|
||||||
|
|
|
@ -42,11 +42,12 @@ internal class DefaultSignInAgainTask @Inject constructor(
|
||||||
signOutAPI.loginAgain(
|
signOutAPI.loginAgain(
|
||||||
PasswordLoginParams.userIdentifier(
|
PasswordLoginParams.userIdentifier(
|
||||||
// Reuse the same userId
|
// Reuse the same userId
|
||||||
sessionParams.userId,
|
user = sessionParams.userId,
|
||||||
params.password,
|
password = params.password,
|
||||||
// The spec says the initial device name will be ignored
|
// The spec says the initial device name will be ignored
|
||||||
// https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-login
|
// https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-login
|
||||||
// but https://github.com/matrix-org/synapse/issues/6525
|
// but https://github.com/matrix-org/synapse/issues/6525
|
||||||
|
deviceDisplayName = null,
|
||||||
// Reuse the same deviceId
|
// Reuse the same deviceId
|
||||||
deviceId = sessionParams.deviceId
|
deviceId = sessionParams.deviceId
|
||||||
)
|
)
|
||||||
|
|
|
@ -160,7 +160,7 @@ Formatter\.formatShortFileSize===1
|
||||||
# android\.text\.TextUtils
|
# android\.text\.TextUtils
|
||||||
|
|
||||||
### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt
|
### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt
|
||||||
enum class===106
|
enum class===107
|
||||||
|
|
||||||
### Do not import temporary legacy classes
|
### Do not import temporary legacy classes
|
||||||
import org.matrix.android.sdk.internal.legacy.riot===3
|
import org.matrix.android.sdk.internal.legacy.riot===3
|
||||||
|
|
|
@ -15,7 +15,7 @@ kapt {
|
||||||
// Note: 2 digits max for each value
|
// Note: 2 digits max for each value
|
||||||
ext.versionMajor = 1
|
ext.versionMajor = 1
|
||||||
ext.versionMinor = 3
|
ext.versionMinor = 3
|
||||||
ext.versionPatch = 6
|
ext.versionPatch = 7
|
||||||
|
|
||||||
static def getGitTimestamp() {
|
static def getGitTimestamp() {
|
||||||
def cmd = 'git show -s --format=%ct'
|
def cmd = 'git show -s --format=%ct'
|
||||||
|
@ -371,7 +371,7 @@ dependencies {
|
||||||
implementation 'com.facebook.stetho:stetho:1.6.0'
|
implementation 'com.facebook.stetho:stetho:1.6.0'
|
||||||
|
|
||||||
// Phone number https://github.com/google/libphonenumber
|
// Phone number https://github.com/google/libphonenumber
|
||||||
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.35'
|
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.36'
|
||||||
|
|
||||||
// FlowBinding
|
// FlowBinding
|
||||||
implementation libs.github.flowBinding
|
implementation libs.github.flowBinding
|
||||||
|
|
|
@ -22,6 +22,7 @@ import android.os.Build
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.platform.VectorBaseActivity
|
import im.vector.app.core.platform.VectorBaseActivity
|
||||||
import im.vector.app.core.utils.checkPermissions
|
import im.vector.app.core.utils.checkPermissions
|
||||||
|
@ -31,6 +32,7 @@ import im.vector.app.core.utils.registerForPermissionsResult
|
||||||
import im.vector.app.databinding.ActivityDebugPermissionBinding
|
import im.vector.app.databinding.ActivityDebugPermissionBinding
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
class DebugPermissionActivity : VectorBaseActivity<ActivityDebugPermissionBinding>() {
|
class DebugPermissionActivity : VectorBaseActivity<ActivityDebugPermissionBinding>() {
|
||||||
|
|
||||||
override fun getBinding() = ActivityDebugPermissionBinding.inflate(layoutInflater)
|
override fun getBinding() = ActivityDebugPermissionBinding.inflate(layoutInflater)
|
||||||
|
|
|
@ -29,16 +29,13 @@ import com.google.firebase.messaging.FirebaseMessagingService
|
||||||
import com.google.firebase.messaging.RemoteMessage
|
import com.google.firebase.messaging.RemoteMessage
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import im.vector.app.BuildConfig
|
import im.vector.app.BuildConfig
|
||||||
import im.vector.app.R
|
|
||||||
import im.vector.app.core.di.ActiveSessionHolder
|
import im.vector.app.core.di.ActiveSessionHolder
|
||||||
import im.vector.app.core.network.WifiDetector
|
import im.vector.app.core.network.WifiDetector
|
||||||
import im.vector.app.core.pushers.PushersManager
|
import im.vector.app.core.pushers.PushersManager
|
||||||
import im.vector.app.features.badge.BadgeProxy
|
import im.vector.app.features.badge.BadgeProxy
|
||||||
import im.vector.app.features.notifications.NotifiableEventResolver
|
import im.vector.app.features.notifications.NotifiableEventResolver
|
||||||
import im.vector.app.features.notifications.NotifiableMessageEvent
|
|
||||||
import im.vector.app.features.notifications.NotificationDrawerManager
|
import im.vector.app.features.notifications.NotificationDrawerManager
|
||||||
import im.vector.app.features.notifications.NotificationUtils
|
import im.vector.app.features.notifications.NotificationUtils
|
||||||
import im.vector.app.features.notifications.SimpleNotifiableEvent
|
|
||||||
import im.vector.app.features.settings.VectorDataStore
|
import im.vector.app.features.settings.VectorDataStore
|
||||||
import im.vector.app.features.settings.VectorPreferences
|
import im.vector.app.features.settings.VectorPreferences
|
||||||
import im.vector.app.push.fcm.FcmHelper
|
import im.vector.app.push.fcm.FcmHelper
|
||||||
|
@ -48,9 +45,7 @@ import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
import org.matrix.android.sdk.api.logger.LoggerTag
|
||||||
import org.matrix.android.sdk.api.pushrules.Action
|
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -201,12 +196,11 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
|
||||||
Timber.tag(loggerTag.value).d("Fast lane: start request")
|
Timber.tag(loggerTag.value).d("Fast lane: start request")
|
||||||
val event = tryOrNull { session.getEvent(roomId, eventId) } ?: return@launch
|
val event = tryOrNull { session.getEvent(roomId, eventId) } ?: return@launch
|
||||||
|
|
||||||
val resolvedEvent = notifiableEventResolver.resolveInMemoryEvent(session, event)
|
val resolvedEvent = notifiableEventResolver.resolveInMemoryEvent(session, event, canBeReplaced = true)
|
||||||
|
|
||||||
resolvedEvent
|
resolvedEvent
|
||||||
?.also { Timber.tag(loggerTag.value).d("Fast lane: notify drawer") }
|
?.also { Timber.tag(loggerTag.value).d("Fast lane: notify drawer") }
|
||||||
?.let {
|
?.let {
|
||||||
it.isPushGatewayEvent = true
|
|
||||||
notificationDrawerManager.onNotifiableEventReceived(it)
|
notificationDrawerManager.onNotifiableEventReceived(it)
|
||||||
notificationDrawerManager.refreshNotificationDrawer()
|
notificationDrawerManager.refreshNotificationDrawer()
|
||||||
}
|
}
|
||||||
|
@ -227,87 +221,4 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleNotificationWithoutSyncingMode(data: Map<String, String>, session: Session?) {
|
|
||||||
if (session == null) {
|
|
||||||
Timber.tag(loggerTag.value).e("## handleNotificationWithoutSyncingMode cannot find session")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// The Matrix event ID of the event being notified about.
|
|
||||||
// This is required if the notification is about a particular Matrix event.
|
|
||||||
// It may be omitted for notifications that only contain updated badge counts.
|
|
||||||
// This ID can and should be used to detect duplicate notification requests.
|
|
||||||
val eventId = data["event_id"] ?: return // Just ignore
|
|
||||||
|
|
||||||
val eventType = data["type"]
|
|
||||||
if (eventType == null) {
|
|
||||||
// Just add a generic unknown event
|
|
||||||
val simpleNotifiableEvent = SimpleNotifiableEvent(
|
|
||||||
session.myUserId,
|
|
||||||
eventId,
|
|
||||||
null,
|
|
||||||
true, // It's an issue in this case, all event will bing even if expected to be silent.
|
|
||||||
title = getString(R.string.notification_unknown_new_event),
|
|
||||||
description = "",
|
|
||||||
type = null,
|
|
||||||
timestamp = System.currentTimeMillis(),
|
|
||||||
soundName = Action.ACTION_OBJECT_VALUE_VALUE_DEFAULT,
|
|
||||||
isPushGatewayEvent = true
|
|
||||||
)
|
|
||||||
notificationDrawerManager.onNotifiableEventReceived(simpleNotifiableEvent)
|
|
||||||
notificationDrawerManager.refreshNotificationDrawer()
|
|
||||||
} else {
|
|
||||||
val event = parseEvent(data) ?: return
|
|
||||||
|
|
||||||
val notifiableEvent = notifiableEventResolver.resolveEvent(event, session)
|
|
||||||
|
|
||||||
if (notifiableEvent == null) {
|
|
||||||
Timber.tag(loggerTag.value).e("Unsupported notifiable event $eventId")
|
|
||||||
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
|
|
||||||
Timber.tag(loggerTag.value).e("--> $event")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (notifiableEvent is NotifiableMessageEvent) {
|
|
||||||
if (notifiableEvent.senderName.isNullOrEmpty()) {
|
|
||||||
notifiableEvent.senderName = data["sender_display_name"] ?: data["sender"] ?: ""
|
|
||||||
}
|
|
||||||
if (notifiableEvent.roomName.isNullOrEmpty()) {
|
|
||||||
notifiableEvent.roomName = findRoomNameBestEffort(data, session) ?: ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
notifiableEvent.isPushGatewayEvent = true
|
|
||||||
notifiableEvent.matrixID = session.myUserId
|
|
||||||
notificationDrawerManager.onNotifiableEventReceived(notifiableEvent)
|
|
||||||
notificationDrawerManager.refreshNotificationDrawer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun findRoomNameBestEffort(data: Map<String, String>, session: Session?): String? {
|
|
||||||
var roomName: String? = data["room_name"]
|
|
||||||
val roomId = data["room_id"]
|
|
||||||
if (null == roomName && null != roomId) {
|
|
||||||
// Try to get the room name from our store
|
|
||||||
roomName = session?.getRoom(roomId)?.roomSummary()?.displayName
|
|
||||||
}
|
|
||||||
return roomName
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Try to create an event from the FCM data
|
|
||||||
*
|
|
||||||
* @param data the FCM data
|
|
||||||
* @return the event or null if required data are missing
|
|
||||||
*/
|
|
||||||
private fun parseEvent(data: Map<String, String>?): Event? {
|
|
||||||
return Event(
|
|
||||||
eventId = data?.get("event_id") ?: return null,
|
|
||||||
senderId = data["sender"],
|
|
||||||
roomId = data["room_id"] ?: return null,
|
|
||||||
type = data["type"] ?: return null,
|
|
||||||
originServerTs = System.currentTimeMillis()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,8 @@ import org.matrix.android.sdk.api.auth.registration.RegisterThreePid
|
||||||
import org.matrix.android.sdk.internal.network.ssl.Fingerprint
|
import org.matrix.android.sdk.internal.network.ssl.Fingerprint
|
||||||
|
|
||||||
sealed class LoginAction : VectorViewModelAction {
|
sealed class LoginAction : VectorViewModelAction {
|
||||||
|
data class OnGetStarted(val resetLoginConfig: Boolean) : LoginAction()
|
||||||
|
|
||||||
data class UpdateServerType(val serverType: ServerType) : LoginAction()
|
data class UpdateServerType(val serverType: ServerType) : LoginAction()
|
||||||
data class UpdateHomeServer(val homeServerUrl: String) : LoginAction()
|
data class UpdateHomeServer(val homeServerUrl: String) : LoginAction()
|
||||||
data class UpdateSignMode(val signMode: SignMode) : LoginAction()
|
data class UpdateSignMode(val signMode: SignMode) : LoginAction()
|
||||||
|
|
|
@ -94,7 +94,6 @@ open class LoginActivity : VectorBaseActivity<ActivityLoginBinding>(), ToolbarCo
|
||||||
// Get config extra
|
// Get config extra
|
||||||
val loginConfig = intent.getParcelableExtra<LoginConfig?>(EXTRA_CONFIG)
|
val loginConfig = intent.getParcelableExtra<LoginConfig?>(EXTRA_CONFIG)
|
||||||
if (isFirstCreation()) {
|
if (isFirstCreation()) {
|
||||||
// TODO Check this
|
|
||||||
loginViewModel.handle(LoginAction.InitWith(loginConfig))
|
loginViewModel.handle(LoginAction.InitWith(loginConfig))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,10 +87,5 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment
|
||||||
|
|
||||||
override fun updateWithState(state: LoginViewState) {
|
override fun updateWithState(state: LoginViewState) {
|
||||||
updateSelectedChoice(state)
|
updateSelectedChoice(state)
|
||||||
|
|
||||||
if (state.loginMode != LoginMode.Unknown) {
|
|
||||||
// LoginFlow for matrix.org has been retrieved
|
|
||||||
loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -159,10 +159,5 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment<F
|
||||||
setupUi(state)
|
setupUi(state)
|
||||||
|
|
||||||
views.loginServerUrlFormClearHistory.isInvisible = state.knownCustomHomeServersUrls.isEmpty()
|
views.loginServerUrlFormClearHistory.isInvisible = state.knownCustomHomeServersUrls.isEmpty()
|
||||||
|
|
||||||
if (state.loginMode != LoginMode.Unknown) {
|
|
||||||
// The homeserver url is valid
|
|
||||||
loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,9 +22,13 @@ import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import im.vector.app.BuildConfig
|
import im.vector.app.BuildConfig
|
||||||
|
import im.vector.app.R
|
||||||
import im.vector.app.databinding.FragmentLoginSplashBinding
|
import im.vector.app.databinding.FragmentLoginSplashBinding
|
||||||
import im.vector.app.features.settings.VectorPreferences
|
import im.vector.app.features.settings.VectorPreferences
|
||||||
|
import org.matrix.android.sdk.api.failure.Failure
|
||||||
|
import java.net.UnknownHostException
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -45,7 +49,7 @@ class LoginSplashFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupViews() {
|
private fun setupViews() {
|
||||||
views.loginSplashSubmit.setOnClickListener { getStarted() }
|
views.loginSplashSubmit.debouncedClicks { getStarted() }
|
||||||
|
|
||||||
if (BuildConfig.DEBUG || vectorPreferences.developerMode()) {
|
if (BuildConfig.DEBUG || vectorPreferences.developerMode()) {
|
||||||
views.loginSplashVersion.isVisible = true
|
views.loginSplashVersion.isVisible = true
|
||||||
|
@ -57,10 +61,28 @@ class LoginSplashFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getStarted() {
|
private fun getStarted() {
|
||||||
loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OpenServerSelection))
|
loginViewModel.handle(LoginAction.OnGetStarted(resetLoginConfig = false))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun resetViewModel() {
|
override fun resetViewModel() {
|
||||||
// Nothing to do
|
// Nothing to do
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onError(throwable: Throwable) {
|
||||||
|
if (throwable is Failure.NetworkConnection &&
|
||||||
|
throwable.ioException is UnknownHostException) {
|
||||||
|
// Invalid homeserver from URL config
|
||||||
|
val url = loginViewModel.getInitialHomeServerUrl().orEmpty()
|
||||||
|
MaterialAlertDialogBuilder(requireActivity())
|
||||||
|
.setTitle(R.string.dialog_title_error)
|
||||||
|
.setMessage(getString(R.string.login_error_homeserver_from_url_not_found, url))
|
||||||
|
.setPositiveButton(R.string.login_error_homeserver_from_url_not_found_enter_manual) { _, _ ->
|
||||||
|
loginViewModel.handle(LoginAction.OnGetStarted(resetLoginConfig = true))
|
||||||
|
}
|
||||||
|
.setNegativeButton(R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
} else {
|
||||||
|
super.onError(throwable)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,6 @@ package im.vector.app.features.login
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import com.airbnb.mvrx.Fail
|
import com.airbnb.mvrx.Fail
|
||||||
import com.airbnb.mvrx.Loading
|
import com.airbnb.mvrx.Loading
|
||||||
import com.airbnb.mvrx.MavericksViewModelFactory
|
import com.airbnb.mvrx.MavericksViewModelFactory
|
||||||
|
@ -116,6 +115,7 @@ class LoginViewModel @AssistedInject constructor(
|
||||||
|
|
||||||
override fun handle(action: LoginAction) {
|
override fun handle(action: LoginAction) {
|
||||||
when (action) {
|
when (action) {
|
||||||
|
is LoginAction.OnGetStarted -> handleOnGetStarted(action)
|
||||||
is LoginAction.UpdateServerType -> handleUpdateServerType(action)
|
is LoginAction.UpdateServerType -> handleUpdateServerType(action)
|
||||||
is LoginAction.UpdateSignMode -> handleUpdateSignMode(action)
|
is LoginAction.UpdateSignMode -> handleUpdateSignMode(action)
|
||||||
is LoginAction.InitWith -> handleInitWith(action)
|
is LoginAction.InitWith -> handleInitWith(action)
|
||||||
|
@ -134,6 +134,27 @@ class LoginViewModel @AssistedInject constructor(
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleOnGetStarted(action: LoginAction.OnGetStarted) {
|
||||||
|
if (action.resetLoginConfig) {
|
||||||
|
loginConfig = null
|
||||||
|
}
|
||||||
|
|
||||||
|
val configUrl = loginConfig?.homeServerUrl?.takeIf { it.isNotEmpty() }
|
||||||
|
if (configUrl != null) {
|
||||||
|
// Use config from uri
|
||||||
|
val homeServerConnectionConfig = homeServerConnectionConfigFactory.create(configUrl)
|
||||||
|
if (homeServerConnectionConfig == null) {
|
||||||
|
// Url is invalid, in this case, just use the regular flow
|
||||||
|
Timber.w("Url from config url was invalid: $configUrl")
|
||||||
|
_viewEvents.post(LoginViewEvents.OpenServerSelection)
|
||||||
|
} else {
|
||||||
|
getLoginFlow(homeServerConnectionConfig, ServerType.Other)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_viewEvents.post(LoginViewEvents.OpenServerSelection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleUserAcceptCertificate(action: LoginAction.UserAcceptCertificate) {
|
private fun handleUserAcceptCertificate(action: LoginAction.UserAcceptCertificate) {
|
||||||
// It happens when we get the login flow, or during direct authentication.
|
// It happens when we get the login flow, or during direct authentication.
|
||||||
// So alter the homeserver config and retrieve again the login flow
|
// So alter the homeserver config and retrieve again the login flow
|
||||||
|
@ -732,7 +753,8 @@ class LoginViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig) {
|
private fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig,
|
||||||
|
serverTypeOverride: ServerType? = null) {
|
||||||
currentHomeServerConnectionConfig = homeServerConnectionConfig
|
currentHomeServerConnectionConfig = homeServerConnectionConfig
|
||||||
|
|
||||||
currentJob = viewModelScope.launch {
|
currentJob = viewModelScope.launch {
|
||||||
|
@ -743,7 +765,11 @@ class LoginViewModel @AssistedInject constructor(
|
||||||
asyncHomeServerLoginFlowRequest = Loading(),
|
asyncHomeServerLoginFlowRequest = Loading(),
|
||||||
// If user has entered https://matrix.org, ensure that server type is ServerType.MatrixOrg
|
// If user has entered https://matrix.org, ensure that server type is ServerType.MatrixOrg
|
||||||
// It is also useful to set the value again in the case of a certificate error on matrix.org
|
// It is also useful to set the value again in the case of a certificate error on matrix.org
|
||||||
serverType = if (homeServerConnectionConfig.homeServerUri.toString() == matrixOrgUrl) ServerType.MatrixOrg else serverType
|
serverType = if (homeServerConnectionConfig.homeServerUri.toString() == matrixOrgUrl) {
|
||||||
|
ServerType.MatrixOrg
|
||||||
|
} else {
|
||||||
|
serverTypeOverride ?: serverType
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -776,7 +802,6 @@ class LoginViewModel @AssistedInject constructor(
|
||||||
else -> LoginMode.Unsupported
|
else -> LoginMode.Unsupported
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME We should post a view event here normally?
|
|
||||||
setState {
|
setState {
|
||||||
copy(
|
copy(
|
||||||
asyncHomeServerLoginFlowRequest = Uninitialized,
|
asyncHomeServerLoginFlowRequest = Uninitialized,
|
||||||
|
@ -791,6 +816,7 @@ class LoginViewModel @AssistedInject constructor(
|
||||||
// Notify the UI
|
// Notify the UI
|
||||||
_viewEvents.post(LoginViewEvents.OutdatedHomeserver)
|
_viewEvents.post(LoginViewEvents.OutdatedHomeserver)
|
||||||
}
|
}
|
||||||
|
_viewEvents.post(LoginViewEvents.OnLoginFlowRetrieved)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -53,7 +53,7 @@ data class LoginViewState(
|
||||||
val loginMode: LoginMode = LoginMode.Unknown,
|
val loginMode: LoginMode = LoginMode.Unknown,
|
||||||
// Supported types for the login. We cannot use a sealed class for LoginType because it is not serializable
|
// Supported types for the login. We cannot use a sealed class for LoginType because it is not serializable
|
||||||
@PersistState
|
@PersistState
|
||||||
val loginModeSupportedTypes: List<String> = emptyList(),
|
val loginModeSupportedTypes: List<String> = emptyList(),
|
||||||
val knownCustomHomeServersUrls: List<String> = emptyList()
|
val knownCustomHomeServersUrls: List<String> = emptyList()
|
||||||
) : MavericksState {
|
) : MavericksState {
|
||||||
|
|
||||||
|
|
|
@ -547,7 +547,7 @@ class LoginViewModel2 @AssistedInject constructor(
|
||||||
safeLoginWizard.login(
|
safeLoginWizard.login(
|
||||||
login = login,
|
login = login,
|
||||||
password = password,
|
password = password,
|
||||||
deviceName = stringProvider.getString(R.string.login_default_session_public_name)
|
initialDeviceName = stringProvider.getString(R.string.login_default_session_public_name)
|
||||||
)
|
)
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
_viewEvents.post(LoginViewEvents2.Failure(failure))
|
_viewEvents.post(LoginViewEvents2.Failure(failure))
|
||||||
|
|
|
@ -15,22 +15,18 @@
|
||||||
*/
|
*/
|
||||||
package im.vector.app.features.notifications
|
package im.vector.app.features.notifications
|
||||||
|
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
|
|
||||||
data class InviteNotifiableEvent(
|
data class InviteNotifiableEvent(
|
||||||
override var matrixID: String?,
|
val matrixID: String?,
|
||||||
override val eventId: String,
|
override val eventId: String,
|
||||||
override val editedEventId: String?,
|
override val editedEventId: String?,
|
||||||
var roomId: String,
|
override val canBeReplaced: Boolean,
|
||||||
override var noisy: Boolean,
|
val roomId: String,
|
||||||
override val title: String,
|
val roomName: String?,
|
||||||
override val description: String,
|
val noisy: Boolean,
|
||||||
override val type: String?,
|
val title: String,
|
||||||
override val timestamp: Long,
|
val description: String,
|
||||||
override var soundName: String?,
|
val type: String?,
|
||||||
override var isPushGatewayEvent: Boolean = false) : NotifiableEvent {
|
val timestamp: Long,
|
||||||
|
val soundName: String?,
|
||||||
override var hasBeenDisplayed: Boolean = false
|
override val isRedacted: Boolean = false
|
||||||
override var isRedacted: Boolean = false
|
) : NotifiableEvent
|
||||||
override var lockScreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
|
|
||||||
}
|
|
||||||
|
|
|
@ -20,24 +20,11 @@ import java.io.Serializable
|
||||||
/**
|
/**
|
||||||
* Parent interface for all events which can be displayed as a Notification
|
* Parent interface for all events which can be displayed as a Notification
|
||||||
*/
|
*/
|
||||||
interface NotifiableEvent : Serializable {
|
sealed interface NotifiableEvent : Serializable {
|
||||||
var matrixID: String?
|
|
||||||
val eventId: String
|
val eventId: String
|
||||||
val editedEventId: String?
|
val editedEventId: String?
|
||||||
var noisy: Boolean
|
|
||||||
val title: String
|
|
||||||
val description: String?
|
|
||||||
val type: String?
|
|
||||||
val timestamp: Long
|
|
||||||
|
|
||||||
// NotificationCompat.VISIBILITY_PUBLIC , VISIBILITY_PRIVATE , VISIBILITY_SECRET
|
|
||||||
var lockScreenVisibility: Int
|
|
||||||
|
|
||||||
// Compat: Only for android <7, for newer version the sound is defined in the channel
|
|
||||||
var soundName: String?
|
|
||||||
var hasBeenDisplayed: Boolean
|
|
||||||
var isRedacted: Boolean
|
|
||||||
|
|
||||||
// Used to know if event should be replaced with the one coming from eventstream
|
// Used to know if event should be replaced with the one coming from eventstream
|
||||||
var isPushGatewayEvent: Boolean
|
val canBeReplaced: Boolean
|
||||||
|
val isRedacted: Boolean
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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 im.vector.app.features.notifications
|
||||||
|
|
||||||
|
import im.vector.app.features.invite.AutoAcceptInvites
|
||||||
|
import im.vector.app.features.notifications.ProcessedEvent.Type.KEEP
|
||||||
|
import im.vector.app.features.notifications.ProcessedEvent.Type.REMOVE
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
private typealias ProcessedEvents = List<ProcessedEvent<NotifiableEvent>>
|
||||||
|
|
||||||
|
class NotifiableEventProcessor @Inject constructor(
|
||||||
|
private val outdatedDetector: OutdatedEventDetector,
|
||||||
|
private val autoAcceptInvites: AutoAcceptInvites
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun process(queuedEvents: List<NotifiableEvent>, currentRoomId: String?, renderedEvents: ProcessedEvents): ProcessedEvents {
|
||||||
|
val processedEvents = queuedEvents.map {
|
||||||
|
val type = when (it) {
|
||||||
|
is InviteNotifiableEvent -> if (autoAcceptInvites.hideInvites) REMOVE else KEEP
|
||||||
|
is NotifiableMessageEvent -> if (shouldIgnoreMessageEventInRoom(currentRoomId, it.roomId) || outdatedDetector.isMessageOutdated(it)) {
|
||||||
|
REMOVE
|
||||||
|
} else KEEP
|
||||||
|
is SimpleNotifiableEvent -> when (it.type) {
|
||||||
|
EventType.REDACTION -> REMOVE
|
||||||
|
else -> KEEP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ProcessedEvent(type, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
val removedEventsDiff = renderedEvents.filter { renderedEvent ->
|
||||||
|
queuedEvents.none { it.eventId == renderedEvent.event.eventId }
|
||||||
|
}.map { ProcessedEvent(REMOVE, it.event) }
|
||||||
|
|
||||||
|
return removedEventsDiff + processedEvents
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun shouldIgnoreMessageEventInRoom(currentRoomId: String?, roomId: String?): Boolean {
|
||||||
|
return currentRoomId != null && roomId == currentRoomId
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,7 +15,6 @@
|
||||||
*/
|
*/
|
||||||
package im.vector.app.features.notifications
|
package im.vector.app.features.notifications
|
||||||
|
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import im.vector.app.BuildConfig
|
import im.vector.app.BuildConfig
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.resources.StringProvider
|
import im.vector.app.core.resources.StringProvider
|
||||||
|
@ -54,21 +53,19 @@ class NotifiableEventResolver @Inject constructor(
|
||||||
|
|
||||||
// private val eventDisplay = RiotEventDisplay(context)
|
// private val eventDisplay = RiotEventDisplay(context)
|
||||||
|
|
||||||
fun resolveEvent(event: Event/*, roomState: RoomState?, bingRule: PushRule?*/, session: Session): NotifiableEvent? {
|
fun resolveEvent(event: Event/*, roomState: RoomState?, bingRule: PushRule?*/, session: Session, isNoisy: Boolean): NotifiableEvent? {
|
||||||
val roomID = event.roomId ?: return null
|
val roomID = event.roomId ?: return null
|
||||||
val eventId = event.eventId ?: return null
|
val eventId = event.eventId ?: return null
|
||||||
if (event.getClearType() == EventType.STATE_ROOM_MEMBER) {
|
if (event.getClearType() == EventType.STATE_ROOM_MEMBER) {
|
||||||
return resolveStateRoomEvent(event, session)
|
return resolveStateRoomEvent(event, session, canBeReplaced = false, isNoisy = isNoisy)
|
||||||
}
|
}
|
||||||
val timelineEvent = session.getRoom(roomID)?.getTimeLineEvent(eventId) ?: return null
|
val timelineEvent = session.getRoom(roomID)?.getTimeLineEvent(eventId) ?: return null
|
||||||
when (event.getClearType()) {
|
when (event.getClearType()) {
|
||||||
EventType.MESSAGE -> {
|
EventType.MESSAGE -> {
|
||||||
return resolveMessageEvent(timelineEvent, session)
|
return resolveMessageEvent(timelineEvent, session, canBeReplaced = false, isNoisy = isNoisy)
|
||||||
}
|
}
|
||||||
EventType.ENCRYPTED -> {
|
EventType.ENCRYPTED -> {
|
||||||
val messageEvent = resolveMessageEvent(timelineEvent, session)
|
return resolveMessageEvent(timelineEvent, session, canBeReplaced = false, isNoisy = isNoisy)
|
||||||
messageEvent?.lockScreenVisibility = NotificationCompat.VISIBILITY_PRIVATE
|
|
||||||
return messageEvent
|
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
// If the event can be displayed, display it as is
|
// If the event can be displayed, display it as is
|
||||||
|
@ -85,12 +82,14 @@ class NotifiableEventResolver @Inject constructor(
|
||||||
description = bodyPreview,
|
description = bodyPreview,
|
||||||
title = stringProvider.getString(R.string.notification_unknown_new_event),
|
title = stringProvider.getString(R.string.notification_unknown_new_event),
|
||||||
soundName = null,
|
soundName = null,
|
||||||
type = event.type)
|
type = event.type,
|
||||||
|
canBeReplaced = false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resolveInMemoryEvent(session: Session, event: Event): NotifiableEvent? {
|
fun resolveInMemoryEvent(session: Session, event: Event, canBeReplaced: Boolean): NotifiableEvent? {
|
||||||
if (event.getClearType() != EventType.MESSAGE) return null
|
if (event.getClearType() != EventType.MESSAGE) return null
|
||||||
|
|
||||||
// Ignore message edition
|
// Ignore message edition
|
||||||
|
@ -114,24 +113,14 @@ class NotifiableEventResolver @Inject constructor(
|
||||||
avatarUrl = user.avatarUrl
|
avatarUrl = user.avatarUrl
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
resolveMessageEvent(timelineEvent, session, canBeReplaced = canBeReplaced, isNoisy = !notificationAction.soundName.isNullOrBlank())
|
||||||
val notifiableEvent = resolveMessageEvent(timelineEvent, session)
|
|
||||||
|
|
||||||
if (notifiableEvent == null) {
|
|
||||||
Timber.d("## Failed to resolve event")
|
|
||||||
// TODO
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
notifiableEvent.noisy = !notificationAction.soundName.isNullOrBlank()
|
|
||||||
notifiableEvent
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Timber.d("Matched push rule is set to not notify")
|
Timber.d("Matched push rule is set to not notify")
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resolveMessageEvent(event: TimelineEvent, session: Session): NotifiableEvent? {
|
private fun resolveMessageEvent(event: TimelineEvent, session: Session, canBeReplaced: Boolean, isNoisy: Boolean): NotifiableEvent {
|
||||||
// The event only contains an eventId, and roomId (type is m.room.*) , we need to get the displayable content (names, avatar, text, etc...)
|
// The event only contains an eventId, and roomId (type is m.room.*) , we need to get the displayable content (names, avatar, text, etc...)
|
||||||
val room = session.getRoom(event.root.roomId!! /*roomID cannot be null*/)
|
val room = session.getRoom(event.root.roomId!! /*roomID cannot be null*/)
|
||||||
|
|
||||||
|
@ -142,19 +131,19 @@ class NotifiableEventResolver @Inject constructor(
|
||||||
val roomName = stringProvider.getString(R.string.notification_unknown_room_name)
|
val roomName = stringProvider.getString(R.string.notification_unknown_room_name)
|
||||||
val senderDisplayName = event.senderInfo.disambiguatedDisplayName
|
val senderDisplayName = event.senderInfo.disambiguatedDisplayName
|
||||||
|
|
||||||
val notifiableEvent = NotifiableMessageEvent(
|
return NotifiableMessageEvent(
|
||||||
eventId = event.root.eventId!!,
|
eventId = event.root.eventId!!,
|
||||||
editedEventId = event.getEditedEventId(),
|
editedEventId = event.getEditedEventId(),
|
||||||
|
canBeReplaced = canBeReplaced,
|
||||||
timestamp = event.root.originServerTs ?: 0,
|
timestamp = event.root.originServerTs ?: 0,
|
||||||
noisy = false, // will be updated
|
noisy = isNoisy,
|
||||||
senderName = senderDisplayName,
|
senderName = senderDisplayName,
|
||||||
senderId = event.root.senderId,
|
senderId = event.root.senderId,
|
||||||
body = body.toString(),
|
body = body.toString(),
|
||||||
roomId = event.root.roomId!!,
|
roomId = event.root.roomId!!,
|
||||||
roomName = roomName)
|
roomName = roomName,
|
||||||
|
matrixID = session.myUserId
|
||||||
notifiableEvent.matrixID = session.myUserId
|
)
|
||||||
return notifiableEvent
|
|
||||||
} else {
|
} else {
|
||||||
if (event.root.isEncrypted() && event.root.mxDecryptionResult == null) {
|
if (event.root.isEncrypted() && event.root.mxDecryptionResult == null) {
|
||||||
// TODO use a global event decryptor? attache to session and that listen to new sessionId?
|
// TODO use a global event decryptor? attache to session and that listen to new sessionId?
|
||||||
|
@ -175,57 +164,56 @@ class NotifiableEventResolver @Inject constructor(
|
||||||
val roomName = room.roomSummary()?.displayName ?: ""
|
val roomName = room.roomSummary()?.displayName ?: ""
|
||||||
val senderDisplayName = event.senderInfo.disambiguatedDisplayName
|
val senderDisplayName = event.senderInfo.disambiguatedDisplayName
|
||||||
|
|
||||||
val notifiableEvent = NotifiableMessageEvent(
|
return NotifiableMessageEvent(
|
||||||
eventId = event.root.eventId!!,
|
eventId = event.root.eventId!!,
|
||||||
editedEventId = event.getEditedEventId(),
|
editedEventId = event.getEditedEventId(),
|
||||||
|
canBeReplaced = canBeReplaced,
|
||||||
timestamp = event.root.originServerTs ?: 0,
|
timestamp = event.root.originServerTs ?: 0,
|
||||||
noisy = false, // will be updated
|
noisy = isNoisy,
|
||||||
senderName = senderDisplayName,
|
senderName = senderDisplayName,
|
||||||
senderId = event.root.senderId,
|
senderId = event.root.senderId,
|
||||||
body = body,
|
body = body,
|
||||||
roomId = event.root.roomId!!,
|
roomId = event.root.roomId!!,
|
||||||
roomName = roomName,
|
roomName = roomName,
|
||||||
roomIsDirect = room.roomSummary()?.isDirect ?: false)
|
roomIsDirect = room.roomSummary()?.isDirect ?: false,
|
||||||
|
roomAvatarPath = session.contentUrlResolver()
|
||||||
notifiableEvent.matrixID = session.myUserId
|
.resolveThumbnail(room.roomSummary()?.avatarUrl,
|
||||||
notifiableEvent.soundName = null
|
250,
|
||||||
|
250,
|
||||||
// Get the avatars URL
|
ContentUrlResolver.ThumbnailMethod.SCALE),
|
||||||
notifiableEvent.roomAvatarPath = session.contentUrlResolver()
|
senderAvatarPath = session.contentUrlResolver()
|
||||||
.resolveThumbnail(room.roomSummary()?.avatarUrl,
|
.resolveThumbnail(event.senderInfo.avatarUrl,
|
||||||
250,
|
250,
|
||||||
250,
|
250,
|
||||||
ContentUrlResolver.ThumbnailMethod.SCALE)
|
ContentUrlResolver.ThumbnailMethod.SCALE),
|
||||||
|
matrixID = session.myUserId,
|
||||||
notifiableEvent.senderAvatarPath = session.contentUrlResolver()
|
soundName = null
|
||||||
.resolveThumbnail(event.senderInfo.avatarUrl,
|
)
|
||||||
250,
|
|
||||||
250,
|
|
||||||
ContentUrlResolver.ThumbnailMethod.SCALE)
|
|
||||||
|
|
||||||
return notifiableEvent
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resolveStateRoomEvent(event: Event, session: Session): NotifiableEvent? {
|
private fun resolveStateRoomEvent(event: Event, session: Session, canBeReplaced: Boolean, isNoisy: Boolean): NotifiableEvent? {
|
||||||
val content = event.content?.toModel<RoomMemberContent>() ?: return null
|
val content = event.content?.toModel<RoomMemberContent>() ?: return null
|
||||||
val roomId = event.roomId ?: return null
|
val roomId = event.roomId ?: return null
|
||||||
val dName = event.senderId?.let { session.getRoomMember(it, roomId)?.displayName }
|
val dName = event.senderId?.let { session.getRoomMember(it, roomId)?.displayName }
|
||||||
if (Membership.INVITE == content.membership) {
|
if (Membership.INVITE == content.membership) {
|
||||||
val body = noticeEventFormatter.format(event, dName, isDm = session.getRoomSummary(roomId)?.isDirect.orFalse())
|
val roomSummary = session.getRoomSummary(roomId)
|
||||||
|
val body = noticeEventFormatter.format(event, dName, isDm = roomSummary?.isDirect.orFalse())
|
||||||
?: stringProvider.getString(R.string.notification_new_invitation)
|
?: stringProvider.getString(R.string.notification_new_invitation)
|
||||||
return InviteNotifiableEvent(
|
return InviteNotifiableEvent(
|
||||||
session.myUserId,
|
session.myUserId,
|
||||||
eventId = event.eventId!!,
|
eventId = event.eventId!!,
|
||||||
editedEventId = null,
|
editedEventId = null,
|
||||||
|
canBeReplaced = canBeReplaced,
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
|
roomName = roomSummary?.displayName,
|
||||||
timestamp = event.originServerTs ?: 0,
|
timestamp = event.originServerTs ?: 0,
|
||||||
noisy = false, // will be set later
|
noisy = isNoisy,
|
||||||
title = stringProvider.getString(R.string.notification_new_invitation),
|
title = stringProvider.getString(R.string.notification_new_invitation),
|
||||||
description = body.toString(),
|
description = body.toString(),
|
||||||
soundName = null, // will be set later
|
soundName = null, // will be set later
|
||||||
type = event.getClearType(),
|
type = event.getClearType()
|
||||||
isPushGatewayEvent = false)
|
)
|
||||||
} else {
|
} else {
|
||||||
Timber.e("## unsupported notifiable event for eventId [${event.eventId}]")
|
Timber.e("## unsupported notifiable event for eventId [${event.eventId}]")
|
||||||
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
|
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
|
||||||
|
|
|
@ -15,43 +15,31 @@
|
||||||
*/
|
*/
|
||||||
package im.vector.app.features.notifications
|
package im.vector.app.features.notifications
|
||||||
|
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
|
|
||||||
data class NotifiableMessageEvent(
|
data class NotifiableMessageEvent(
|
||||||
override val eventId: String,
|
override val eventId: String,
|
||||||
override val editedEventId: String?,
|
override val editedEventId: String?,
|
||||||
override var noisy: Boolean,
|
override val canBeReplaced: Boolean,
|
||||||
override val timestamp: Long,
|
val noisy: Boolean,
|
||||||
var senderName: String?,
|
val timestamp: Long,
|
||||||
var senderId: String?,
|
val senderName: String?,
|
||||||
var body: String?,
|
val senderId: String?,
|
||||||
var roomId: String,
|
val body: String?,
|
||||||
var roomName: String?,
|
val roomId: String,
|
||||||
var roomIsDirect: Boolean = false
|
val roomName: String?,
|
||||||
|
val roomIsDirect: Boolean = false,
|
||||||
|
val roomAvatarPath: String? = null,
|
||||||
|
val senderAvatarPath: String? = null,
|
||||||
|
val matrixID: String? = null,
|
||||||
|
val soundName: String? = null,
|
||||||
|
// This is used for >N notification, as the result of a smart reply
|
||||||
|
val outGoingMessage: Boolean = false,
|
||||||
|
val outGoingMessageFailed: Boolean = false,
|
||||||
|
override val isRedacted: Boolean = false
|
||||||
) : NotifiableEvent {
|
) : NotifiableEvent {
|
||||||
|
|
||||||
override var matrixID: String? = null
|
val type: String = EventType.MESSAGE
|
||||||
override var soundName: String? = null
|
val description: String = body ?: ""
|
||||||
override var lockScreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
|
val title: String = senderName ?: ""
|
||||||
override var hasBeenDisplayed: Boolean = false
|
|
||||||
override var isRedacted: Boolean = false
|
|
||||||
|
|
||||||
var roomAvatarPath: String? = null
|
|
||||||
var senderAvatarPath: String? = null
|
|
||||||
|
|
||||||
override var isPushGatewayEvent: Boolean = false
|
|
||||||
|
|
||||||
override val type: String
|
|
||||||
get() = EventType.MESSAGE
|
|
||||||
|
|
||||||
override val description: String?
|
|
||||||
get() = body ?: ""
|
|
||||||
|
|
||||||
override val title: String
|
|
||||||
get() = senderName ?: ""
|
|
||||||
|
|
||||||
// This is used for >N notification, as the result of a smart reply
|
|
||||||
var outGoingMessage = false
|
|
||||||
var outGoingMessageFailed = false
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -130,19 +130,20 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
val notifiableMessageEvent = NotifiableMessageEvent(
|
val notifiableMessageEvent = NotifiableMessageEvent(
|
||||||
// Generate a Fake event id
|
// Generate a Fake event id
|
||||||
UUID.randomUUID().toString(),
|
eventId = UUID.randomUUID().toString(),
|
||||||
null,
|
editedEventId = null,
|
||||||
false,
|
noisy = false,
|
||||||
System.currentTimeMillis(),
|
timestamp = System.currentTimeMillis(),
|
||||||
session.getRoomMember(session.myUserId, room.roomId)?.displayName
|
senderName = session.getRoomMember(session.myUserId, room.roomId)?.displayName
|
||||||
?: context?.getString(R.string.notification_sender_me),
|
?: context?.getString(R.string.notification_sender_me),
|
||||||
session.myUserId,
|
senderId = session.myUserId,
|
||||||
message,
|
body = message,
|
||||||
room.roomId,
|
roomId = room.roomId,
|
||||||
room.roomSummary()?.displayName ?: room.roomId,
|
roomName = room.roomSummary()?.displayName ?: room.roomId,
|
||||||
room.roomSummary()?.isDirect == true
|
roomIsDirect = room.roomSummary()?.isDirect == true,
|
||||||
|
outGoingMessage = true,
|
||||||
|
canBeReplaced = false
|
||||||
)
|
)
|
||||||
notifiableMessageEvent.outGoingMessage = true
|
|
||||||
|
|
||||||
notificationDrawerManager.onNotifiableEventReceived(notifiableMessageEvent)
|
notificationDrawerManager.onNotifiableEventReceived(notifiableMessageEvent)
|
||||||
notificationDrawerManager.refreshNotificationDrawer()
|
notificationDrawerManager.refreshNotificationDrawer()
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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 im.vector.app.features.notifications
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class NotificationDisplayer @Inject constructor(context: Context) {
|
||||||
|
|
||||||
|
private val notificationManager = NotificationManagerCompat.from(context)
|
||||||
|
|
||||||
|
fun showNotificationMessage(tag: String?, id: Int, notification: Notification) {
|
||||||
|
notificationManager.notify(tag, id, notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancelNotificationMessage(tag: String?, id: Int) {
|
||||||
|
notificationManager.cancel(tag, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancelAllNotifications() {
|
||||||
|
// Keep this try catch (reported by GA)
|
||||||
|
try {
|
||||||
|
notificationManager.cancelAll()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "## cancelAllNotifications() failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,26 +16,15 @@
|
||||||
package im.vector.app.features.notifications
|
package im.vector.app.features.notifications
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.HandlerThread
|
import android.os.HandlerThread
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import androidx.core.app.Person
|
|
||||||
import androidx.core.content.pm.ShortcutInfoCompat
|
|
||||||
import androidx.core.content.pm.ShortcutManagerCompat
|
|
||||||
import androidx.core.graphics.drawable.IconCompat
|
|
||||||
import im.vector.app.ActiveSessionDataSource
|
import im.vector.app.ActiveSessionDataSource
|
||||||
import im.vector.app.BuildConfig
|
import im.vector.app.BuildConfig
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.resources.StringProvider
|
|
||||||
import im.vector.app.core.utils.FirstThrottler
|
import im.vector.app.core.utils.FirstThrottler
|
||||||
import im.vector.app.features.displayname.getBestName
|
import im.vector.app.features.displayname.getBestName
|
||||||
import im.vector.app.features.home.room.detail.RoomDetailActivity
|
|
||||||
import im.vector.app.features.invite.AutoAcceptInvites
|
|
||||||
import im.vector.app.features.settings.VectorPreferences
|
import im.vector.app.features.settings.VectorPreferences
|
||||||
import me.gujun.android.span.span
|
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
|
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
|
||||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||||
|
@ -52,14 +41,11 @@ import javax.inject.Singleton
|
||||||
*/
|
*/
|
||||||
@Singleton
|
@Singleton
|
||||||
class NotificationDrawerManager @Inject constructor(private val context: Context,
|
class NotificationDrawerManager @Inject constructor(private val context: Context,
|
||||||
private val notificationUtils: NotificationUtils,
|
private val notificationDisplayer: NotificationDisplayer,
|
||||||
private val vectorPreferences: VectorPreferences,
|
private val vectorPreferences: VectorPreferences,
|
||||||
private val stringProvider: StringProvider,
|
|
||||||
private val activeSessionDataSource: ActiveSessionDataSource,
|
private val activeSessionDataSource: ActiveSessionDataSource,
|
||||||
private val iconLoader: IconLoader,
|
private val notifiableEventProcessor: NotifiableEventProcessor,
|
||||||
private val bitmapLoader: BitmapLoader,
|
private val notificationRenderer: NotificationRenderer) {
|
||||||
private val outdatedDetector: OutdatedEventDetector?,
|
|
||||||
private val autoAcceptInvites: AutoAcceptInvites) {
|
|
||||||
|
|
||||||
private val handlerThread: HandlerThread = HandlerThread("NotificationDrawerManager", Thread.MIN_PRIORITY)
|
private val handlerThread: HandlerThread = HandlerThread("NotificationDrawerManager", Thread.MIN_PRIORITY)
|
||||||
private var backgroundHandler: Handler
|
private var backgroundHandler: Handler
|
||||||
|
@ -69,13 +55,23 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
|
||||||
backgroundHandler = Handler(handlerThread.looper)
|
backgroundHandler = Handler(handlerThread.looper)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The first time the notification drawer is refreshed, we force re-render of all notifications
|
/**
|
||||||
private var firstTime = true
|
* The notifiable events to render
|
||||||
|
* this is our source of truth for notifications, any changes to this list will be rendered as notifications
|
||||||
private val eventList = loadEventInfo()
|
* when events are removed the previously rendered notifications will be cancelled
|
||||||
|
* when adding or updating, the notifications will be notified
|
||||||
|
*
|
||||||
|
* Events are unique by their properties, we should be careful not to insert multiple events with the same event-id
|
||||||
|
*/
|
||||||
|
private val queuedEvents = loadEventInfo()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The last known rendered notifiable events
|
||||||
|
* we keep track of them in order to know which events have been removed from the eventList
|
||||||
|
* allowing us to cancel any notifications previous displayed by now removed events
|
||||||
|
*/
|
||||||
|
private var renderedEvents = emptyList<ProcessedEvent<NotifiableEvent>>()
|
||||||
private val avatarSize = context.resources.getDimensionPixelSize(R.dimen.profile_avatar_size)
|
private val avatarSize = context.resources.getDimensionPixelSize(R.dimen.profile_avatar_size)
|
||||||
|
|
||||||
private var currentRoomId: String? = null
|
private var currentRoomId: String? = null
|
||||||
|
|
||||||
// TODO Multi-session: this will have to be improved
|
// TODO Multi-session: this will have to be improved
|
||||||
|
@ -107,12 +103,12 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
|
||||||
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
|
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
|
||||||
Timber.d("onNotifiableEventReceived(): $notifiableEvent")
|
Timber.d("onNotifiableEventReceived(): $notifiableEvent")
|
||||||
} else {
|
} else {
|
||||||
Timber.d("onNotifiableEventReceived(): is push: ${notifiableEvent.isPushGatewayEvent}")
|
Timber.d("onNotifiableEventReceived(): is push: ${notifiableEvent.canBeReplaced}")
|
||||||
}
|
}
|
||||||
synchronized(eventList) {
|
synchronized(queuedEvents) {
|
||||||
val existing = eventList.firstOrNull { it.eventId == notifiableEvent.eventId }
|
val existing = queuedEvents.firstOrNull { it.eventId == notifiableEvent.eventId }
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
if (existing.isPushGatewayEvent) {
|
if (existing.canBeReplaced) {
|
||||||
// Use the event coming from the event stream as it may contains more info than
|
// Use the event coming from the event stream as it may contains more info than
|
||||||
// the fcm one (like type/content/clear text) (e.g when an encrypted message from
|
// the fcm one (like type/content/clear text) (e.g when an encrypted message from
|
||||||
// FCM should be update with clear text after a sync)
|
// FCM should be update with clear text after a sync)
|
||||||
|
@ -121,9 +117,8 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
|
||||||
// Use setOnlyAlertOnce to ensure update notification does not interfere with sound
|
// Use setOnlyAlertOnce to ensure update notification does not interfere with sound
|
||||||
// from first notify invocation as outlined in:
|
// from first notify invocation as outlined in:
|
||||||
// https://developer.android.com/training/notify-user/build-notification#Updating
|
// https://developer.android.com/training/notify-user/build-notification#Updating
|
||||||
notifiableEvent.hasBeenDisplayed = false
|
queuedEvents.remove(existing)
|
||||||
eventList.remove(existing)
|
queuedEvents.add(notifiableEvent)
|
||||||
eventList.add(notifiableEvent)
|
|
||||||
} else {
|
} else {
|
||||||
// keep the existing one, do not replace
|
// keep the existing one, do not replace
|
||||||
}
|
}
|
||||||
|
@ -131,7 +126,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
|
||||||
// Check if this is an edit
|
// Check if this is an edit
|
||||||
if (notifiableEvent.editedEventId != null) {
|
if (notifiableEvent.editedEventId != null) {
|
||||||
// This is an edition
|
// This is an edition
|
||||||
val eventBeforeEdition = eventList.firstOrNull {
|
val eventBeforeEdition = queuedEvents.firstOrNull {
|
||||||
// Edition of an event
|
// Edition of an event
|
||||||
it.eventId == notifiableEvent.editedEventId ||
|
it.eventId == notifiableEvent.editedEventId ||
|
||||||
// or edition of an edition
|
// or edition of an edition
|
||||||
|
@ -140,9 +135,9 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
|
||||||
|
|
||||||
if (eventBeforeEdition != null) {
|
if (eventBeforeEdition != null) {
|
||||||
// Replace the existing notification with the new content
|
// Replace the existing notification with the new content
|
||||||
eventList.remove(eventBeforeEdition)
|
queuedEvents.remove(eventBeforeEdition)
|
||||||
|
|
||||||
eventList.add(notifiableEvent)
|
queuedEvents.add(notifiableEvent)
|
||||||
} else {
|
} else {
|
||||||
// Ignore an edit of a not displayed event in the notification drawer
|
// Ignore an edit of a not displayed event in the notification drawer
|
||||||
}
|
}
|
||||||
|
@ -153,7 +148,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
|
||||||
Timber.d("onNotifiableEventReceived(): skipping event, already seen")
|
Timber.d("onNotifiableEventReceived(): skipping event, already seen")
|
||||||
} else {
|
} else {
|
||||||
seenEventIds.put(notifiableEvent.eventId)
|
seenEventIds.put(notifiableEvent.eventId)
|
||||||
eventList.add(notifiableEvent)
|
queuedEvents.add(notifiableEvent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -161,10 +156,13 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onEventRedacted(eventId: String) {
|
fun onEventRedacted(eventId: String) {
|
||||||
synchronized(eventList) {
|
synchronized(queuedEvents) {
|
||||||
eventList.find { it.eventId == eventId }?.apply {
|
queuedEvents.replace(eventId) {
|
||||||
isRedacted = true
|
when (it) {
|
||||||
hasBeenDisplayed = false
|
is InviteNotifiableEvent -> it.copy(isRedacted = true)
|
||||||
|
is NotifiableMessageEvent -> it.copy(isRedacted = true)
|
||||||
|
is SimpleNotifiableEvent -> it.copy(isRedacted = true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -173,8 +171,8 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
|
||||||
* Clear all known events and refresh the notification drawer
|
* Clear all known events and refresh the notification drawer
|
||||||
*/
|
*/
|
||||||
fun clearAllEvents() {
|
fun clearAllEvents() {
|
||||||
synchronized(eventList) {
|
synchronized(queuedEvents) {
|
||||||
eventList.clear()
|
queuedEvents.clear()
|
||||||
}
|
}
|
||||||
refreshNotificationDrawer()
|
refreshNotificationDrawer()
|
||||||
}
|
}
|
||||||
|
@ -183,14 +181,8 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
|
||||||
fun clearMessageEventOfRoom(roomId: String?) {
|
fun clearMessageEventOfRoom(roomId: String?) {
|
||||||
Timber.v("clearMessageEventOfRoom $roomId")
|
Timber.v("clearMessageEventOfRoom $roomId")
|
||||||
if (roomId != null) {
|
if (roomId != null) {
|
||||||
var shouldUpdate = false
|
val shouldUpdate = removeAll { it is NotifiableMessageEvent && it.roomId == roomId }
|
||||||
synchronized(eventList) {
|
|
||||||
shouldUpdate = eventList.removeAll { e ->
|
|
||||||
e is NotifiableMessageEvent && e.roomId == roomId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (shouldUpdate) {
|
if (shouldUpdate) {
|
||||||
notificationUtils.cancelNotificationMessage(roomId, ROOM_MESSAGES_NOTIFICATION_ID)
|
|
||||||
refreshNotificationDrawer()
|
refreshNotificationDrawer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -202,7 +194,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
|
||||||
*/
|
*/
|
||||||
fun setCurrentRoom(roomId: String?) {
|
fun setCurrentRoom(roomId: String?) {
|
||||||
var hasChanged: Boolean
|
var hasChanged: Boolean
|
||||||
synchronized(eventList) {
|
synchronized(queuedEvents) {
|
||||||
hasChanged = roomId != currentRoomId
|
hasChanged = roomId != currentRoomId
|
||||||
currentRoomId = roomId
|
currentRoomId = roomId
|
||||||
}
|
}
|
||||||
|
@ -212,12 +204,16 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearMemberShipNotificationForRoom(roomId: String) {
|
fun clearMemberShipNotificationForRoom(roomId: String) {
|
||||||
synchronized(eventList) {
|
val shouldUpdate = removeAll { it is InviteNotifiableEvent && it.roomId == roomId }
|
||||||
eventList.removeAll { e ->
|
if (shouldUpdate) {
|
||||||
e is InviteNotifiableEvent && e.roomId == roomId
|
refreshNotificationDrawerBg()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeAll(predicate: (NotifiableEvent) -> Boolean): Boolean {
|
||||||
|
return synchronized(queuedEvents) {
|
||||||
|
queuedEvents.removeAll(predicate)
|
||||||
}
|
}
|
||||||
notificationUtils.cancelNotificationMessage(roomId, ROOM_INVITATION_NOTIFICATION_ID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var firstThrottler = FirstThrottler(200)
|
private var firstThrottler = FirstThrottler(200)
|
||||||
|
@ -244,359 +240,36 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
|
||||||
private fun refreshNotificationDrawerBg() {
|
private fun refreshNotificationDrawerBg() {
|
||||||
Timber.v("refreshNotificationDrawerBg()")
|
Timber.v("refreshNotificationDrawerBg()")
|
||||||
|
|
||||||
val session = currentSession ?: return
|
val newSettings = vectorPreferences.useCompleteNotificationFormat()
|
||||||
|
if (newSettings != useCompleteNotificationFormat) {
|
||||||
val user = session.getUser(session.myUserId)
|
// Settings has changed, remove all current notifications
|
||||||
// myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash
|
notificationDisplayer.cancelAllNotifications()
|
||||||
val myUserDisplayName = user?.toMatrixItem()?.getBestName() ?: session.myUserId
|
useCompleteNotificationFormat = newSettings
|
||||||
val myUserAvatarUrl = session.contentUrlResolver().resolveThumbnail(user?.avatarUrl, avatarSize, avatarSize, ContentUrlResolver.ThumbnailMethod.SCALE)
|
|
||||||
synchronized(eventList) {
|
|
||||||
Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER ")
|
|
||||||
// TMP code
|
|
||||||
var hasNewEvent = false
|
|
||||||
var summaryIsNoisy = false
|
|
||||||
val summaryInboxStyle = NotificationCompat.InboxStyle()
|
|
||||||
|
|
||||||
// group events by room to create a single MessagingStyle notif
|
|
||||||
val roomIdToEventMap: MutableMap<String, MutableList<NotifiableMessageEvent>> = LinkedHashMap()
|
|
||||||
val simpleEvents: MutableList<SimpleNotifiableEvent> = ArrayList()
|
|
||||||
val invitationEvents: MutableList<InviteNotifiableEvent> = ArrayList()
|
|
||||||
|
|
||||||
val eventIterator = eventList.listIterator()
|
|
||||||
while (eventIterator.hasNext()) {
|
|
||||||
when (val event = eventIterator.next()) {
|
|
||||||
is NotifiableMessageEvent -> {
|
|
||||||
val roomId = event.roomId
|
|
||||||
val roomEvents = roomIdToEventMap.getOrPut(roomId) { ArrayList() }
|
|
||||||
|
|
||||||
if (shouldIgnoreMessageEventInRoom(roomId) || outdatedDetector?.isMessageOutdated(event) == true) {
|
|
||||||
// forget this event
|
|
||||||
eventIterator.remove()
|
|
||||||
} else {
|
|
||||||
roomEvents.add(event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is InviteNotifiableEvent -> {
|
|
||||||
if (autoAcceptInvites.hideInvites) {
|
|
||||||
// Forget this event
|
|
||||||
eventIterator.remove()
|
|
||||||
} else {
|
|
||||||
invitationEvents.add(event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is SimpleNotifiableEvent -> simpleEvents.add(event)
|
|
||||||
else -> Timber.w("Type not handled")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER ${roomIdToEventMap.size} room groups")
|
|
||||||
|
|
||||||
var globalLastMessageTimestamp = 0L
|
|
||||||
|
|
||||||
val newSettings = vectorPreferences.useCompleteNotificationFormat()
|
|
||||||
if (newSettings != useCompleteNotificationFormat) {
|
|
||||||
// Settings has changed, remove all current notifications
|
|
||||||
notificationUtils.cancelAllNotifications()
|
|
||||||
useCompleteNotificationFormat = newSettings
|
|
||||||
}
|
|
||||||
|
|
||||||
var simpleNotificationRoomCounter = 0
|
|
||||||
var simpleNotificationMessageCounter = 0
|
|
||||||
|
|
||||||
// events have been grouped by roomId
|
|
||||||
for ((roomId, events) in roomIdToEventMap) {
|
|
||||||
// Build the notification for the room
|
|
||||||
if (events.isEmpty() || events.all { it.isRedacted }) {
|
|
||||||
// Just clear this notification
|
|
||||||
Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER $roomId has no more events")
|
|
||||||
notificationUtils.cancelNotificationMessage(roomId, ROOM_MESSAGES_NOTIFICATION_ID)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
simpleNotificationRoomCounter++
|
|
||||||
val roomName = events[0].roomName ?: events[0].senderName ?: ""
|
|
||||||
|
|
||||||
val roomEventGroupInfo = RoomEventGroupInfo(
|
|
||||||
roomId = roomId,
|
|
||||||
isDirect = events[0].roomIsDirect,
|
|
||||||
roomDisplayName = roomName)
|
|
||||||
|
|
||||||
val style = NotificationCompat.MessagingStyle(Person.Builder()
|
|
||||||
.setName(myUserDisplayName)
|
|
||||||
.setIcon(iconLoader.getUserIcon(myUserAvatarUrl))
|
|
||||||
.setKey(events[0].matrixID)
|
|
||||||
.build())
|
|
||||||
|
|
||||||
style.isGroupConversation = !roomEventGroupInfo.isDirect
|
|
||||||
|
|
||||||
if (!roomEventGroupInfo.isDirect) {
|
|
||||||
style.conversationTitle = roomEventGroupInfo.roomDisplayName
|
|
||||||
}
|
|
||||||
|
|
||||||
val largeBitmap = getRoomBitmap(events)
|
|
||||||
|
|
||||||
for (event in events) {
|
|
||||||
// if all events in this room have already been displayed there is no need to update it
|
|
||||||
if (!event.hasBeenDisplayed && !event.isRedacted) {
|
|
||||||
roomEventGroupInfo.shouldBing = roomEventGroupInfo.shouldBing || event.noisy
|
|
||||||
roomEventGroupInfo.customSound = event.soundName
|
|
||||||
}
|
|
||||||
roomEventGroupInfo.hasNewEvent = roomEventGroupInfo.hasNewEvent || !event.hasBeenDisplayed
|
|
||||||
|
|
||||||
val senderPerson = if (event.outGoingMessage) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
Person.Builder()
|
|
||||||
.setName(event.senderName)
|
|
||||||
.setIcon(iconLoader.getUserIcon(event.senderAvatarPath))
|
|
||||||
.setKey(event.senderId)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
||||||
val openRoomIntent = RoomDetailActivity.shortcutIntent(context, roomId)
|
|
||||||
|
|
||||||
val shortcut = ShortcutInfoCompat.Builder(context, roomId)
|
|
||||||
.setLongLived(true)
|
|
||||||
.setIntent(openRoomIntent)
|
|
||||||
.setShortLabel(roomName)
|
|
||||||
.setIcon(largeBitmap?.let { IconCompat.createWithAdaptiveBitmap(it) } ?: iconLoader.getUserIcon(event.senderAvatarPath))
|
|
||||||
.build()
|
|
||||||
|
|
||||||
ShortcutManagerCompat.pushDynamicShortcut(context, shortcut)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.outGoingMessage && event.outGoingMessageFailed) {
|
|
||||||
style.addMessage(stringProvider.getString(R.string.notification_inline_reply_failed), event.timestamp, senderPerson)
|
|
||||||
roomEventGroupInfo.hasSmartReplyError = true
|
|
||||||
} else {
|
|
||||||
if (!event.isRedacted) {
|
|
||||||
simpleNotificationMessageCounter++
|
|
||||||
style.addMessage(event.body, event.timestamp, senderPerson)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
event.hasBeenDisplayed = true // we can consider it as displayed
|
|
||||||
|
|
||||||
// It is possible that this event was previously shown as an 'anonymous' simple notif.
|
|
||||||
// And now it will be merged in a single MessageStyle notif, so we can clean to be sure
|
|
||||||
notificationUtils.cancelNotificationMessage(event.eventId, ROOM_EVENT_NOTIFICATION_ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (events.size == 1) {
|
|
||||||
val event = events[0]
|
|
||||||
if (roomEventGroupInfo.isDirect) {
|
|
||||||
val line = span {
|
|
||||||
span {
|
|
||||||
textStyle = "bold"
|
|
||||||
+String.format("%s: ", event.senderName)
|
|
||||||
}
|
|
||||||
+(event.description ?: "")
|
|
||||||
}
|
|
||||||
summaryInboxStyle.addLine(line)
|
|
||||||
} else {
|
|
||||||
val line = span {
|
|
||||||
span {
|
|
||||||
textStyle = "bold"
|
|
||||||
+String.format("%s: %s ", roomName, event.senderName)
|
|
||||||
}
|
|
||||||
+(event.description ?: "")
|
|
||||||
}
|
|
||||||
summaryInboxStyle.addLine(line)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val summaryLine = stringProvider.getQuantityString(
|
|
||||||
R.plurals.notification_compat_summary_line_for_room, events.size, roomName, events.size)
|
|
||||||
summaryInboxStyle.addLine(summaryLine)
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
// String not found or bad format
|
|
||||||
Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER failed to resolve string")
|
|
||||||
summaryInboxStyle.addLine(roomName)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (firstTime || roomEventGroupInfo.hasNewEvent) {
|
|
||||||
// Should update displayed notification
|
|
||||||
Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER $roomId need refresh")
|
|
||||||
val lastMessageTimestamp = events.last().timestamp
|
|
||||||
|
|
||||||
if (globalLastMessageTimestamp < lastMessageTimestamp) {
|
|
||||||
globalLastMessageTimestamp = lastMessageTimestamp
|
|
||||||
}
|
|
||||||
|
|
||||||
val tickerText = if (roomEventGroupInfo.isDirect) {
|
|
||||||
stringProvider.getString(R.string.notification_ticker_text_dm, events.last().senderName, events.last().description)
|
|
||||||
} else {
|
|
||||||
stringProvider.getString(R.string.notification_ticker_text_group, roomName, events.last().senderName, events.last().description)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (useCompleteNotificationFormat) {
|
|
||||||
val notification = notificationUtils.buildMessagesListNotification(
|
|
||||||
style,
|
|
||||||
roomEventGroupInfo,
|
|
||||||
largeBitmap,
|
|
||||||
lastMessageTimestamp,
|
|
||||||
myUserDisplayName,
|
|
||||||
tickerText)
|
|
||||||
|
|
||||||
// is there an id for this room?
|
|
||||||
notificationUtils.showNotificationMessage(roomId, ROOM_MESSAGES_NOTIFICATION_ID, notification)
|
|
||||||
}
|
|
||||||
|
|
||||||
hasNewEvent = true
|
|
||||||
summaryIsNoisy = summaryIsNoisy || roomEventGroupInfo.shouldBing
|
|
||||||
} else {
|
|
||||||
Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER $roomId is up to date")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle invitation events
|
|
||||||
for (event in invitationEvents) {
|
|
||||||
// We build a invitation notification
|
|
||||||
if (firstTime || !event.hasBeenDisplayed) {
|
|
||||||
if (useCompleteNotificationFormat) {
|
|
||||||
val notification = notificationUtils.buildRoomInvitationNotification(event, session.myUserId)
|
|
||||||
notificationUtils.showNotificationMessage(event.roomId, ROOM_INVITATION_NOTIFICATION_ID, notification)
|
|
||||||
}
|
|
||||||
event.hasBeenDisplayed = true // we can consider it as displayed
|
|
||||||
hasNewEvent = true
|
|
||||||
summaryIsNoisy = summaryIsNoisy || event.noisy
|
|
||||||
summaryInboxStyle.addLine(event.description)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle simple events
|
|
||||||
for (event in simpleEvents) {
|
|
||||||
// We build a simple notification
|
|
||||||
if (firstTime || !event.hasBeenDisplayed) {
|
|
||||||
if (useCompleteNotificationFormat) {
|
|
||||||
val notification = notificationUtils.buildSimpleEventNotification(event, session.myUserId)
|
|
||||||
notificationUtils.showNotificationMessage(event.eventId, ROOM_EVENT_NOTIFICATION_ID, notification)
|
|
||||||
}
|
|
||||||
event.hasBeenDisplayed = true // we can consider it as displayed
|
|
||||||
hasNewEvent = true
|
|
||||||
summaryIsNoisy = summaryIsNoisy || event.noisy
|
|
||||||
summaryInboxStyle.addLine(event.description)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======== Build summary notification =========
|
|
||||||
// On Android 7.0 (API level 24) and higher, the system automatically builds a summary for
|
|
||||||
// your group using snippets of text from each notification. The user can expand this
|
|
||||||
// notification to see each separate notification.
|
|
||||||
// To support older versions, which cannot show a nested group of notifications,
|
|
||||||
// you must create an extra notification that acts as the summary.
|
|
||||||
// This appears as the only notification and the system hides all the others.
|
|
||||||
// So this summary should include a snippet from all the other notifications,
|
|
||||||
// which the user can tap to open your app.
|
|
||||||
// The behavior of the group summary may vary on some device types such as wearables.
|
|
||||||
// To ensure the best experience on all devices and versions, always include a group summary when you create a group
|
|
||||||
// https://developer.android.com/training/notify-user/group
|
|
||||||
|
|
||||||
if (eventList.isEmpty() || eventList.all { it.isRedacted }) {
|
|
||||||
notificationUtils.cancelNotificationMessage(null, SUMMARY_NOTIFICATION_ID)
|
|
||||||
} else if (hasNewEvent) {
|
|
||||||
// FIXME roomIdToEventMap.size is not correct, this is the number of rooms
|
|
||||||
val nbEvents = roomIdToEventMap.size + simpleEvents.size
|
|
||||||
val sumTitle = stringProvider.getQuantityString(R.plurals.notification_compat_summary_title, nbEvents, nbEvents)
|
|
||||||
summaryInboxStyle.setBigContentTitle(sumTitle)
|
|
||||||
// TODO get latest event?
|
|
||||||
.setSummaryText(stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages, nbEvents, nbEvents))
|
|
||||||
|
|
||||||
if (useCompleteNotificationFormat) {
|
|
||||||
val notification = notificationUtils.buildSummaryListNotification(
|
|
||||||
summaryInboxStyle,
|
|
||||||
sumTitle,
|
|
||||||
noisy = hasNewEvent && summaryIsNoisy,
|
|
||||||
lastMessageTimestamp = globalLastMessageTimestamp)
|
|
||||||
|
|
||||||
notificationUtils.showNotificationMessage(null, SUMMARY_NOTIFICATION_ID, notification)
|
|
||||||
} else {
|
|
||||||
// Add the simple events as message (?)
|
|
||||||
simpleNotificationMessageCounter += simpleEvents.size
|
|
||||||
val numberOfInvitations = invitationEvents.size
|
|
||||||
|
|
||||||
val privacyTitle = if (numberOfInvitations > 0) {
|
|
||||||
val invitationsStr = stringProvider.getQuantityString(R.plurals.notification_invitations, numberOfInvitations, numberOfInvitations)
|
|
||||||
if (simpleNotificationMessageCounter > 0) {
|
|
||||||
// Invitation and message
|
|
||||||
val messageStr = stringProvider.getQuantityString(R.plurals.room_new_messages_notification,
|
|
||||||
simpleNotificationMessageCounter, simpleNotificationMessageCounter)
|
|
||||||
if (simpleNotificationRoomCounter > 1) {
|
|
||||||
// In several rooms
|
|
||||||
val roomStr = stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages_in_room_rooms,
|
|
||||||
simpleNotificationRoomCounter, simpleNotificationRoomCounter)
|
|
||||||
stringProvider.getString(
|
|
||||||
R.string.notification_unread_notified_messages_in_room_and_invitation,
|
|
||||||
messageStr,
|
|
||||||
roomStr,
|
|
||||||
invitationsStr
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// In one room
|
|
||||||
stringProvider.getString(
|
|
||||||
R.string.notification_unread_notified_messages_and_invitation,
|
|
||||||
messageStr,
|
|
||||||
invitationsStr
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Only invitation
|
|
||||||
invitationsStr
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No invitation, only messages
|
|
||||||
val messageStr = stringProvider.getQuantityString(R.plurals.room_new_messages_notification,
|
|
||||||
simpleNotificationMessageCounter, simpleNotificationMessageCounter)
|
|
||||||
if (simpleNotificationRoomCounter > 1) {
|
|
||||||
// In several rooms
|
|
||||||
val roomStr = stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages_in_room_rooms,
|
|
||||||
simpleNotificationRoomCounter, simpleNotificationRoomCounter)
|
|
||||||
stringProvider.getString(R.string.notification_unread_notified_messages_in_room, messageStr, roomStr)
|
|
||||||
} else {
|
|
||||||
// In one room
|
|
||||||
messageStr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val notification = notificationUtils.buildSummaryListNotification(
|
|
||||||
style = null,
|
|
||||||
compatSummary = privacyTitle,
|
|
||||||
noisy = hasNewEvent && summaryIsNoisy,
|
|
||||||
lastMessageTimestamp = globalLastMessageTimestamp)
|
|
||||||
|
|
||||||
notificationUtils.showNotificationMessage(null, SUMMARY_NOTIFICATION_ID, notification)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasNewEvent && summaryIsNoisy) {
|
|
||||||
try {
|
|
||||||
// turn the screen on for 3 seconds
|
|
||||||
/*
|
|
||||||
TODO
|
|
||||||
if (Matrix.getInstance(VectorApp.getInstance())!!.pushManager.isScreenTurnedOn) {
|
|
||||||
val pm = VectorApp.getInstance().getSystemService<PowerManager>()!!
|
|
||||||
val wl = pm.newWakeLock(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or PowerManager.ACQUIRE_CAUSES_WAKEUP,
|
|
||||||
NotificationDrawerManager::class.java.name)
|
|
||||||
wl.acquire(3000)
|
|
||||||
wl.release()
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Timber.e(e, "## Failed to turn screen on")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// notice that we can get bit out of sync with actual display but not a big issue
|
|
||||||
firstTime = false
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun getRoomBitmap(events: List<NotifiableMessageEvent>): Bitmap? {
|
val eventsToRender = synchronized(queuedEvents) {
|
||||||
if (events.isEmpty()) return null
|
notifiableEventProcessor.process(queuedEvents, currentRoomId, renderedEvents).also {
|
||||||
|
queuedEvents.clear()
|
||||||
|
queuedEvents.addAll(it.onlyKeptEvents())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Use the last event (most recent?)
|
if (renderedEvents == eventsToRender) {
|
||||||
val roomAvatarPath = events.last().roomAvatarPath ?: events.last().senderAvatarPath
|
Timber.d("Skipping notification update due to event list not changing")
|
||||||
|
} else {
|
||||||
return bitmapLoader.getRoomBitmap(roomAvatarPath)
|
renderedEvents = eventsToRender
|
||||||
|
val session = currentSession ?: return
|
||||||
|
val user = session.getUser(session.myUserId)
|
||||||
|
// myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash
|
||||||
|
val myUserDisplayName = user?.toMatrixItem()?.getBestName() ?: session.myUserId
|
||||||
|
val myUserAvatarUrl = session.contentUrlResolver().resolveThumbnail(
|
||||||
|
contentUrl = user?.avatarUrl,
|
||||||
|
width = avatarSize,
|
||||||
|
height = avatarSize,
|
||||||
|
method = ContentUrlResolver.ThumbnailMethod.SCALE
|
||||||
|
)
|
||||||
|
notificationRenderer.render(session.myUserId, myUserDisplayName, myUserAvatarUrl, useCompleteNotificationFormat, eventsToRender)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun shouldIgnoreMessageEventInRoom(roomId: String?): Boolean {
|
fun shouldIgnoreMessageEventInRoom(roomId: String?): Boolean {
|
||||||
|
@ -604,8 +277,8 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
|
||||||
}
|
}
|
||||||
|
|
||||||
fun persistInfo() {
|
fun persistInfo() {
|
||||||
synchronized(eventList) {
|
synchronized(queuedEvents) {
|
||||||
if (eventList.isEmpty()) {
|
if (queuedEvents.isEmpty()) {
|
||||||
deleteCachedRoomNotifications()
|
deleteCachedRoomNotifications()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -613,7 +286,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
|
||||||
val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
|
val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
|
||||||
if (!file.exists()) file.createNewFile()
|
if (!file.exists()) file.createNewFile()
|
||||||
FileOutputStream(file).use {
|
FileOutputStream(file).use {
|
||||||
currentSession?.securelyStoreObject(eventList, KEY_ALIAS_SECRET_STORAGE, it)
|
currentSession?.securelyStoreObject(queuedEvents, KEY_ALIAS_SECRET_STORAGE, it)
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Timber.e(e, "## Failed to save cached notification info")
|
Timber.e(e, "## Failed to save cached notification info")
|
||||||
|
@ -645,15 +318,11 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun displayDiagnosticNotification() {
|
|
||||||
notificationUtils.displayDiagnosticNotification()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val SUMMARY_NOTIFICATION_ID = 0
|
const val SUMMARY_NOTIFICATION_ID = 0
|
||||||
private const val ROOM_MESSAGES_NOTIFICATION_ID = 1
|
const val ROOM_MESSAGES_NOTIFICATION_ID = 1
|
||||||
private const val ROOM_EVENT_NOTIFICATION_ID = 2
|
const val ROOM_EVENT_NOTIFICATION_ID = 2
|
||||||
private const val ROOM_INVITATION_NOTIFICATION_ID = 3
|
const val ROOM_INVITATION_NOTIFICATION_ID = 3
|
||||||
|
|
||||||
// TODO Mutliaccount
|
// TODO Mutliaccount
|
||||||
private const val ROOMS_NOTIFICATIONS_FILE_NAME = "im.vector.notifications.cache"
|
private const val ROOMS_NOTIFICATIONS_FILE_NAME = "im.vector.notifications.cache"
|
||||||
|
@ -661,3 +330,11 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
|
||||||
private const val KEY_ALIAS_SECRET_STORAGE = "notificationMgr"
|
private const val KEY_ALIAS_SECRET_STORAGE = "notificationMgr"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun MutableList<NotifiableEvent>.replace(eventId: String, block: (NotifiableEvent) -> NotifiableEvent) {
|
||||||
|
val indexToReplace = indexOfFirst { it.eventId == eventId }
|
||||||
|
if (indexToReplace == -1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
set(indexToReplace, block(get(indexToReplace)))
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,133 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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 im.vector.app.features.notifications
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import androidx.core.content.pm.ShortcutInfoCompat
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
private typealias ProcessedMessageEvents = List<ProcessedEvent<NotifiableMessageEvent>>
|
||||||
|
|
||||||
|
class NotificationFactory @Inject constructor(
|
||||||
|
private val notificationUtils: NotificationUtils,
|
||||||
|
private val roomGroupMessageCreator: RoomGroupMessageCreator,
|
||||||
|
private val summaryGroupMessageCreator: SummaryGroupMessageCreator
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun Map<String, ProcessedMessageEvents>.toNotifications(myUserDisplayName: String, myUserAvatarUrl: String?): List<RoomNotification> {
|
||||||
|
return map { (roomId, events) ->
|
||||||
|
when {
|
||||||
|
events.hasNoEventsToDisplay() -> RoomNotification.Removed(roomId)
|
||||||
|
else -> {
|
||||||
|
val messageEvents = events.onlyKeptEvents().filterNot { it.isRedacted }
|
||||||
|
roomGroupMessageCreator.createRoomMessage(messageEvents, roomId, myUserDisplayName, myUserAvatarUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ProcessedMessageEvents.hasNoEventsToDisplay() = isEmpty() || all {
|
||||||
|
it.type == ProcessedEvent.Type.REMOVE || it.event.canNotBeDisplayed()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun NotifiableMessageEvent.canNotBeDisplayed() = isRedacted
|
||||||
|
|
||||||
|
@JvmName("toNotificationsInviteNotifiableEvent")
|
||||||
|
fun List<ProcessedEvent<InviteNotifiableEvent>>.toNotifications(myUserId: String): List<OneShotNotification> {
|
||||||
|
return map { (processed, event) ->
|
||||||
|
when (processed) {
|
||||||
|
ProcessedEvent.Type.REMOVE -> OneShotNotification.Removed(key = event.roomId)
|
||||||
|
ProcessedEvent.Type.KEEP -> OneShotNotification.Append(
|
||||||
|
notificationUtils.buildRoomInvitationNotification(event, myUserId),
|
||||||
|
OneShotNotification.Append.Meta(
|
||||||
|
key = event.roomId,
|
||||||
|
summaryLine = event.description,
|
||||||
|
isNoisy = event.noisy,
|
||||||
|
timestamp = event.timestamp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmName("toNotificationsSimpleNotifiableEvent")
|
||||||
|
fun List<ProcessedEvent<SimpleNotifiableEvent>>.toNotifications(myUserId: String): List<OneShotNotification> {
|
||||||
|
return map { (processed, event) ->
|
||||||
|
when (processed) {
|
||||||
|
ProcessedEvent.Type.REMOVE -> OneShotNotification.Removed(key = event.eventId)
|
||||||
|
ProcessedEvent.Type.KEEP -> OneShotNotification.Append(
|
||||||
|
notificationUtils.buildSimpleEventNotification(event, myUserId),
|
||||||
|
OneShotNotification.Append.Meta(
|
||||||
|
key = event.eventId,
|
||||||
|
summaryLine = event.description,
|
||||||
|
isNoisy = event.noisy,
|
||||||
|
timestamp = event.timestamp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createSummaryNotification(roomNotifications: List<RoomNotification>,
|
||||||
|
invitationNotifications: List<OneShotNotification>,
|
||||||
|
simpleNotifications: List<OneShotNotification>,
|
||||||
|
useCompleteNotificationFormat: Boolean): SummaryNotification {
|
||||||
|
val roomMeta = roomNotifications.filterIsInstance<RoomNotification.Message>().map { it.meta }
|
||||||
|
val invitationMeta = invitationNotifications.filterIsInstance<OneShotNotification.Append>().map { it.meta }
|
||||||
|
val simpleMeta = simpleNotifications.filterIsInstance<OneShotNotification.Append>().map { it.meta }
|
||||||
|
return when {
|
||||||
|
roomMeta.isEmpty() && invitationMeta.isEmpty() && simpleMeta.isEmpty() -> SummaryNotification.Removed
|
||||||
|
else -> SummaryNotification.Update(
|
||||||
|
summaryGroupMessageCreator.createSummaryNotification(
|
||||||
|
roomNotifications = roomMeta,
|
||||||
|
invitationNotifications = invitationMeta,
|
||||||
|
simpleNotifications = simpleMeta,
|
||||||
|
useCompleteNotificationFormat = useCompleteNotificationFormat
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface RoomNotification {
|
||||||
|
data class Removed(val roomId: String) : RoomNotification
|
||||||
|
data class Message(val notification: Notification, val shortcutInfo: ShortcutInfoCompat?, val meta: Meta) : RoomNotification {
|
||||||
|
data class Meta(
|
||||||
|
val summaryLine: CharSequence,
|
||||||
|
val messageCount: Int,
|
||||||
|
val latestTimestamp: Long,
|
||||||
|
val roomId: String,
|
||||||
|
val shouldBing: Boolean
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface OneShotNotification {
|
||||||
|
data class Removed(val key: String) : OneShotNotification
|
||||||
|
data class Append(val notification: Notification, val meta: Meta) : OneShotNotification {
|
||||||
|
data class Meta(
|
||||||
|
val key: String,
|
||||||
|
val summaryLine: CharSequence,
|
||||||
|
val isNoisy: Boolean,
|
||||||
|
val timestamp: Long,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface SummaryNotification {
|
||||||
|
object Removed : SummaryNotification
|
||||||
|
data class Update(val notification: Notification) : SummaryNotification
|
||||||
|
}
|
|
@ -0,0 +1,135 @@
|
||||||
|
/*
|
||||||
|
* 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 im.vector.app.features.notifications
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
|
import androidx.core.content.pm.ShortcutManagerCompat
|
||||||
|
import im.vector.app.features.notifications.NotificationDrawerManager.Companion.ROOM_EVENT_NOTIFICATION_ID
|
||||||
|
import im.vector.app.features.notifications.NotificationDrawerManager.Companion.ROOM_INVITATION_NOTIFICATION_ID
|
||||||
|
import im.vector.app.features.notifications.NotificationDrawerManager.Companion.ROOM_MESSAGES_NOTIFICATION_ID
|
||||||
|
import im.vector.app.features.notifications.NotificationDrawerManager.Companion.SUMMARY_NOTIFICATION_ID
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class NotificationRenderer @Inject constructor(private val notificationDisplayer: NotificationDisplayer,
|
||||||
|
private val notificationFactory: NotificationFactory,
|
||||||
|
private val appContext: Context) {
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
fun render(myUserId: String,
|
||||||
|
myUserDisplayName: String,
|
||||||
|
myUserAvatarUrl: String?,
|
||||||
|
useCompleteNotificationFormat: Boolean,
|
||||||
|
eventsToProcess: List<ProcessedEvent<NotifiableEvent>>) {
|
||||||
|
val (roomEvents, simpleEvents, invitationEvents) = eventsToProcess.groupByType()
|
||||||
|
with(notificationFactory) {
|
||||||
|
val roomNotifications = roomEvents.toNotifications(myUserDisplayName, myUserAvatarUrl)
|
||||||
|
val invitationNotifications = invitationEvents.toNotifications(myUserId)
|
||||||
|
val simpleNotifications = simpleEvents.toNotifications(myUserId)
|
||||||
|
val summaryNotification = createSummaryNotification(
|
||||||
|
roomNotifications = roomNotifications,
|
||||||
|
invitationNotifications = invitationNotifications,
|
||||||
|
simpleNotifications = simpleNotifications,
|
||||||
|
useCompleteNotificationFormat = useCompleteNotificationFormat
|
||||||
|
)
|
||||||
|
|
||||||
|
// Remove summary first to avoid briefly displaying it after dismissing the last notification
|
||||||
|
when (summaryNotification) {
|
||||||
|
SummaryNotification.Removed -> {
|
||||||
|
Timber.d("Removing summary notification")
|
||||||
|
notificationDisplayer.cancelNotificationMessage(null, SUMMARY_NOTIFICATION_ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
roomNotifications.forEach { wrapper ->
|
||||||
|
when (wrapper) {
|
||||||
|
is RoomNotification.Removed -> {
|
||||||
|
Timber.d("Removing room messages notification ${wrapper.roomId}")
|
||||||
|
notificationDisplayer.cancelNotificationMessage(wrapper.roomId, ROOM_MESSAGES_NOTIFICATION_ID)
|
||||||
|
}
|
||||||
|
is RoomNotification.Message -> if (useCompleteNotificationFormat) {
|
||||||
|
Timber.d("Updating room messages notification ${wrapper.meta.roomId}")
|
||||||
|
wrapper.shortcutInfo?.let {
|
||||||
|
ShortcutManagerCompat.pushDynamicShortcut(appContext, it)
|
||||||
|
}
|
||||||
|
notificationDisplayer.showNotificationMessage(wrapper.meta.roomId, ROOM_MESSAGES_NOTIFICATION_ID, wrapper.notification)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
invitationNotifications.forEach { wrapper ->
|
||||||
|
when (wrapper) {
|
||||||
|
is OneShotNotification.Removed -> {
|
||||||
|
Timber.d("Removing invitation notification ${wrapper.key}")
|
||||||
|
notificationDisplayer.cancelNotificationMessage(wrapper.key, ROOM_INVITATION_NOTIFICATION_ID)
|
||||||
|
}
|
||||||
|
is OneShotNotification.Append -> if (useCompleteNotificationFormat) {
|
||||||
|
Timber.d("Updating invitation notification ${wrapper.meta.key}")
|
||||||
|
notificationDisplayer.showNotificationMessage(wrapper.meta.key, ROOM_INVITATION_NOTIFICATION_ID, wrapper.notification)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
simpleNotifications.forEach { wrapper ->
|
||||||
|
when (wrapper) {
|
||||||
|
is OneShotNotification.Removed -> {
|
||||||
|
Timber.d("Removing simple notification ${wrapper.key}")
|
||||||
|
notificationDisplayer.cancelNotificationMessage(wrapper.key, ROOM_EVENT_NOTIFICATION_ID)
|
||||||
|
}
|
||||||
|
is OneShotNotification.Append -> if (useCompleteNotificationFormat) {
|
||||||
|
Timber.d("Updating simple notification ${wrapper.meta.key}")
|
||||||
|
notificationDisplayer.showNotificationMessage(wrapper.meta.key, ROOM_EVENT_NOTIFICATION_ID, wrapper.notification)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update summary last to avoid briefly displaying it before other notifications
|
||||||
|
when (summaryNotification) {
|
||||||
|
is SummaryNotification.Update -> {
|
||||||
|
Timber.d("Updating summary notification")
|
||||||
|
notificationDisplayer.showNotificationMessage(null, SUMMARY_NOTIFICATION_ID, summaryNotification.notification)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<ProcessedEvent<NotifiableEvent>>.groupByType(): GroupedNotificationEvents {
|
||||||
|
val roomIdToEventMap: MutableMap<String, MutableList<ProcessedEvent<NotifiableMessageEvent>>> = LinkedHashMap()
|
||||||
|
val simpleEvents: MutableList<ProcessedEvent<SimpleNotifiableEvent>> = ArrayList()
|
||||||
|
val invitationEvents: MutableList<ProcessedEvent<InviteNotifiableEvent>> = ArrayList()
|
||||||
|
forEach {
|
||||||
|
when (val event = it.event) {
|
||||||
|
is InviteNotifiableEvent -> invitationEvents.add(it.castedToEventType())
|
||||||
|
is NotifiableMessageEvent -> {
|
||||||
|
val roomEvents = roomIdToEventMap.getOrPut(event.roomId) { ArrayList() }
|
||||||
|
roomEvents.add(it.castedToEventType())
|
||||||
|
}
|
||||||
|
is SimpleNotifiableEvent -> simpleEvents.add(it.castedToEventType())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return GroupedNotificationEvents(roomIdToEventMap, simpleEvents, invitationEvents)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
private fun <T : NotifiableEvent> ProcessedEvent<NotifiableEvent>.castedToEventType(): ProcessedEvent<T> = this as ProcessedEvent<T>
|
||||||
|
|
||||||
|
data class GroupedNotificationEvents(
|
||||||
|
val roomEvents: Map<String, List<ProcessedEvent<NotifiableMessageEvent>>>,
|
||||||
|
val simpleEvents: List<ProcessedEvent<SimpleNotifiableEvent>>,
|
||||||
|
val invitationEvents: List<ProcessedEvent<InviteNotifiableEvent>>
|
||||||
|
)
|
|
@ -642,7 +642,7 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
||||||
|
|
||||||
return NotificationCompat.Builder(context, channelID)
|
return NotificationCompat.Builder(context, channelID)
|
||||||
.setOnlyAlertOnce(true)
|
.setOnlyAlertOnce(true)
|
||||||
.setContentTitle(stringProvider.getString(R.string.app_name))
|
.setContentTitle(inviteNotifiableEvent.roomName ?: stringProvider.getString(R.string.app_name))
|
||||||
.setContentText(inviteNotifiableEvent.description)
|
.setContentText(inviteNotifiableEvent.description)
|
||||||
.setGroup(stringProvider.getString(R.string.app_name))
|
.setGroup(stringProvider.getString(R.string.app_name))
|
||||||
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL)
|
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL)
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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 im.vector.app.features.notifications
|
||||||
|
|
||||||
|
data class ProcessedEvent<T>(
|
||||||
|
val type: Type,
|
||||||
|
val event: T
|
||||||
|
) {
|
||||||
|
|
||||||
|
enum class Type {
|
||||||
|
KEEP,
|
||||||
|
REMOVE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> List<ProcessedEvent<T>>.onlyKeptEvents() = mapNotNull { processedEvent ->
|
||||||
|
processedEvent.event.takeIf { processedEvent.type == ProcessedEvent.Type.KEEP }
|
||||||
|
}
|
|
@ -40,12 +40,11 @@ class PushRuleTriggerListener @Inject constructor(
|
||||||
|
|
||||||
val notificationAction = actions.toNotificationAction()
|
val notificationAction = actions.toNotificationAction()
|
||||||
if (notificationAction.shouldNotify) {
|
if (notificationAction.shouldNotify) {
|
||||||
val notifiableEvent = resolver.resolveEvent(event, safeSession)
|
val notifiableEvent = resolver.resolveEvent(event, safeSession, isNoisy = !notificationAction.soundName.isNullOrBlank())
|
||||||
if (notifiableEvent == null) {
|
if (notifiableEvent == null) {
|
||||||
Timber.v("## Failed to resolve event")
|
Timber.v("## Failed to resolve event")
|
||||||
// TODO
|
// TODO
|
||||||
} else {
|
} else {
|
||||||
notifiableEvent.noisy = !notificationAction.soundName.isNullOrBlank()
|
|
||||||
Timber.v("New event to notify")
|
Timber.v("New event to notify")
|
||||||
notificationDrawerManager.onNotifiableEventReceived(notifiableEvent)
|
notificationDrawerManager.onNotifiableEventReceived(notifiableEvent)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,170 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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 im.vector.app.features.notifications
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.Person
|
||||||
|
import androidx.core.content.pm.ShortcutInfoCompat
|
||||||
|
import androidx.core.graphics.drawable.IconCompat
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.resources.StringProvider
|
||||||
|
import im.vector.app.features.home.room.detail.RoomDetailActivity
|
||||||
|
import me.gujun.android.span.Span
|
||||||
|
import me.gujun.android.span.span
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class RoomGroupMessageCreator @Inject constructor(
|
||||||
|
private val iconLoader: IconLoader,
|
||||||
|
private val bitmapLoader: BitmapLoader,
|
||||||
|
private val stringProvider: StringProvider,
|
||||||
|
private val notificationUtils: NotificationUtils,
|
||||||
|
private val appContext: Context
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun createRoomMessage(events: List<NotifiableMessageEvent>, roomId: String, userDisplayName: String, userAvatarUrl: String?): RoomNotification.Message {
|
||||||
|
val firstKnownRoomEvent = events[0]
|
||||||
|
val roomName = firstKnownRoomEvent.roomName ?: firstKnownRoomEvent.senderName ?: ""
|
||||||
|
val roomIsGroup = !firstKnownRoomEvent.roomIsDirect
|
||||||
|
val style = NotificationCompat.MessagingStyle(Person.Builder()
|
||||||
|
.setName(userDisplayName)
|
||||||
|
.setIcon(iconLoader.getUserIcon(userAvatarUrl))
|
||||||
|
.setKey(firstKnownRoomEvent.matrixID)
|
||||||
|
.build()
|
||||||
|
).also {
|
||||||
|
it.conversationTitle = roomName.takeIf { roomIsGroup }
|
||||||
|
it.isGroupConversation = roomIsGroup
|
||||||
|
it.addMessagesFromEvents(events)
|
||||||
|
}
|
||||||
|
|
||||||
|
val tickerText = if (roomIsGroup) {
|
||||||
|
stringProvider.getString(R.string.notification_ticker_text_group, roomName, events.last().senderName, events.last().description)
|
||||||
|
} else {
|
||||||
|
stringProvider.getString(R.string.notification_ticker_text_dm, events.last().senderName, events.last().description)
|
||||||
|
}
|
||||||
|
|
||||||
|
val largeBitmap = getRoomBitmap(events)
|
||||||
|
val shortcutInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
val openRoomIntent = RoomDetailActivity.shortcutIntent(appContext, roomId)
|
||||||
|
ShortcutInfoCompat.Builder(appContext, roomId)
|
||||||
|
.setLongLived(true)
|
||||||
|
.setIntent(openRoomIntent)
|
||||||
|
.setShortLabel(roomName)
|
||||||
|
.setIcon(largeBitmap?.let { IconCompat.createWithAdaptiveBitmap(it) } ?: iconLoader.getUserIcon(events.last().senderAvatarPath))
|
||||||
|
.build()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
val lastMessageTimestamp = events.last().timestamp
|
||||||
|
val smartReplyErrors = events.filter { it.isSmartReplyError() }
|
||||||
|
val messageCount = (events.size - smartReplyErrors.size)
|
||||||
|
val meta = RoomNotification.Message.Meta(
|
||||||
|
summaryLine = createRoomMessagesGroupSummaryLine(events, roomName, roomIsDirect = !roomIsGroup),
|
||||||
|
messageCount = messageCount,
|
||||||
|
latestTimestamp = lastMessageTimestamp,
|
||||||
|
roomId = roomId,
|
||||||
|
shouldBing = events.any { it.noisy }
|
||||||
|
)
|
||||||
|
return RoomNotification.Message(
|
||||||
|
notificationUtils.buildMessagesListNotification(
|
||||||
|
style,
|
||||||
|
RoomEventGroupInfo(roomId, roomName, isDirect = !roomIsGroup).also {
|
||||||
|
it.hasSmartReplyError = smartReplyErrors.isNotEmpty()
|
||||||
|
it.shouldBing = meta.shouldBing
|
||||||
|
it.customSound = events.last().soundName
|
||||||
|
},
|
||||||
|
largeIcon = largeBitmap,
|
||||||
|
lastMessageTimestamp,
|
||||||
|
userDisplayName,
|
||||||
|
tickerText
|
||||||
|
),
|
||||||
|
shortcutInfo,
|
||||||
|
meta
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun NotificationCompat.MessagingStyle.addMessagesFromEvents(events: List<NotifiableMessageEvent>) {
|
||||||
|
events.forEach { event ->
|
||||||
|
val senderPerson = if (event.outGoingMessage) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
Person.Builder()
|
||||||
|
.setName(event.senderName)
|
||||||
|
.setIcon(iconLoader.getUserIcon(event.senderAvatarPath))
|
||||||
|
.setKey(event.senderId)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
when {
|
||||||
|
event.isSmartReplyError() -> addMessage(stringProvider.getString(R.string.notification_inline_reply_failed), event.timestamp, senderPerson)
|
||||||
|
else -> addMessage(event.body, event.timestamp, senderPerson)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createRoomMessagesGroupSummaryLine(events: List<NotifiableMessageEvent>, roomName: String, roomIsDirect: Boolean): CharSequence {
|
||||||
|
return try {
|
||||||
|
when (events.size) {
|
||||||
|
1 -> createFirstMessageSummaryLine(events.first(), roomName, roomIsDirect)
|
||||||
|
else -> {
|
||||||
|
stringProvider.getQuantityString(
|
||||||
|
R.plurals.notification_compat_summary_line_for_room,
|
||||||
|
events.size,
|
||||||
|
roomName,
|
||||||
|
events.size
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
// String not found or bad format
|
||||||
|
Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER failed to resolve string")
|
||||||
|
roomName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createFirstMessageSummaryLine(event: NotifiableMessageEvent, roomName: String, roomIsDirect: Boolean): Span {
|
||||||
|
return if (roomIsDirect) {
|
||||||
|
span {
|
||||||
|
span {
|
||||||
|
textStyle = "bold"
|
||||||
|
+String.format("%s: ", event.senderName)
|
||||||
|
}
|
||||||
|
+(event.description)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
span {
|
||||||
|
span {
|
||||||
|
textStyle = "bold"
|
||||||
|
+String.format("%s: %s ", roomName, event.senderName)
|
||||||
|
}
|
||||||
|
+(event.description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getRoomBitmap(events: List<NotifiableMessageEvent>): Bitmap? {
|
||||||
|
// Use the last event (most recent?)
|
||||||
|
return events.lastOrNull()
|
||||||
|
?.roomAvatarPath
|
||||||
|
?.let { bitmapLoader.getRoomBitmap(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun NotifiableMessageEvent.isSmartReplyError() = outGoingMessage && outGoingMessageFailed
|
|
@ -15,21 +15,16 @@
|
||||||
*/
|
*/
|
||||||
package im.vector.app.features.notifications
|
package im.vector.app.features.notifications
|
||||||
|
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
|
|
||||||
data class SimpleNotifiableEvent(
|
data class SimpleNotifiableEvent(
|
||||||
override var matrixID: String?,
|
val matrixID: String?,
|
||||||
override val eventId: String,
|
override val eventId: String,
|
||||||
override val editedEventId: String?,
|
override val editedEventId: String?,
|
||||||
override var noisy: Boolean,
|
val noisy: Boolean,
|
||||||
override val title: String,
|
val title: String,
|
||||||
override val description: String,
|
val description: String,
|
||||||
override val type: String?,
|
val type: String?,
|
||||||
override val timestamp: Long,
|
val timestamp: Long,
|
||||||
override var soundName: String?,
|
val soundName: String?,
|
||||||
override var isPushGatewayEvent: Boolean = false) : NotifiableEvent {
|
override var canBeReplaced: Boolean,
|
||||||
|
override val isRedacted: Boolean = false
|
||||||
override var hasBeenDisplayed: Boolean = false
|
) : NotifiableEvent
|
||||||
override var isRedacted: Boolean = false
|
|
||||||
override var lockScreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,146 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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 im.vector.app.features.notifications
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.resources.StringProvider
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ======== Build summary notification =========
|
||||||
|
* On Android 7.0 (API level 24) and higher, the system automatically builds a summary for
|
||||||
|
* your group using snippets of text from each notification. The user can expand this
|
||||||
|
* notification to see each separate notification.
|
||||||
|
* To support older versions, which cannot show a nested group of notifications,
|
||||||
|
* you must create an extra notification that acts as the summary.
|
||||||
|
* This appears as the only notification and the system hides all the others.
|
||||||
|
* So this summary should include a snippet from all the other notifications,
|
||||||
|
* which the user can tap to open your app.
|
||||||
|
* The behavior of the group summary may vary on some device types such as wearables.
|
||||||
|
* To ensure the best experience on all devices and versions, always include a group summary when you create a group
|
||||||
|
* https://developer.android.com/training/notify-user/group
|
||||||
|
*/
|
||||||
|
class SummaryGroupMessageCreator @Inject constructor(
|
||||||
|
private val stringProvider: StringProvider,
|
||||||
|
private val notificationUtils: NotificationUtils
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun createSummaryNotification(roomNotifications: List<RoomNotification.Message.Meta>,
|
||||||
|
invitationNotifications: List<OneShotNotification.Append.Meta>,
|
||||||
|
simpleNotifications: List<OneShotNotification.Append.Meta>,
|
||||||
|
useCompleteNotificationFormat: Boolean): Notification {
|
||||||
|
val summaryInboxStyle = NotificationCompat.InboxStyle().also { style ->
|
||||||
|
roomNotifications.forEach { style.addLine(it.summaryLine) }
|
||||||
|
invitationNotifications.forEach { style.addLine(it.summaryLine) }
|
||||||
|
simpleNotifications.forEach { style.addLine(it.summaryLine) }
|
||||||
|
}
|
||||||
|
|
||||||
|
val summaryIsNoisy = roomNotifications.any { it.shouldBing } ||
|
||||||
|
invitationNotifications.any { it.isNoisy } ||
|
||||||
|
simpleNotifications.any { it.isNoisy }
|
||||||
|
|
||||||
|
val messageCount = roomNotifications.fold(initial = 0) { acc, current -> acc + current.messageCount }
|
||||||
|
|
||||||
|
val lastMessageTimestamp = roomNotifications.lastOrNull()?.latestTimestamp
|
||||||
|
?: invitationNotifications.lastOrNull()?.timestamp
|
||||||
|
?: simpleNotifications.last().timestamp
|
||||||
|
|
||||||
|
// FIXME roomIdToEventMap.size is not correct, this is the number of rooms
|
||||||
|
val nbEvents = roomNotifications.size + simpleNotifications.size
|
||||||
|
val sumTitle = stringProvider.getQuantityString(R.plurals.notification_compat_summary_title, nbEvents, nbEvents)
|
||||||
|
summaryInboxStyle.setBigContentTitle(sumTitle)
|
||||||
|
// TODO get latest event?
|
||||||
|
.setSummaryText(stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages, nbEvents, nbEvents))
|
||||||
|
return if (useCompleteNotificationFormat) {
|
||||||
|
notificationUtils.buildSummaryListNotification(
|
||||||
|
summaryInboxStyle,
|
||||||
|
sumTitle,
|
||||||
|
noisy = summaryIsNoisy,
|
||||||
|
lastMessageTimestamp = lastMessageTimestamp
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
processSimpleGroupSummary(
|
||||||
|
summaryIsNoisy,
|
||||||
|
messageCount,
|
||||||
|
simpleNotifications.size,
|
||||||
|
invitationNotifications.size,
|
||||||
|
roomNotifications.size,
|
||||||
|
lastMessageTimestamp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processSimpleGroupSummary(summaryIsNoisy: Boolean,
|
||||||
|
messageEventsCount: Int,
|
||||||
|
simpleEventsCount: Int,
|
||||||
|
invitationEventsCount: Int,
|
||||||
|
roomCount: Int,
|
||||||
|
lastMessageTimestamp: Long): Notification {
|
||||||
|
// Add the simple events as message (?)
|
||||||
|
val messageNotificationCount = messageEventsCount + simpleEventsCount
|
||||||
|
|
||||||
|
val privacyTitle = if (invitationEventsCount > 0) {
|
||||||
|
val invitationsStr = stringProvider.getQuantityString(R.plurals.notification_invitations, invitationEventsCount, invitationEventsCount)
|
||||||
|
if (messageNotificationCount > 0) {
|
||||||
|
// Invitation and message
|
||||||
|
val messageStr = stringProvider.getQuantityString(R.plurals.room_new_messages_notification,
|
||||||
|
messageNotificationCount, messageNotificationCount)
|
||||||
|
if (roomCount > 1) {
|
||||||
|
// In several rooms
|
||||||
|
val roomStr = stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages_in_room_rooms,
|
||||||
|
roomCount, roomCount)
|
||||||
|
stringProvider.getString(
|
||||||
|
R.string.notification_unread_notified_messages_in_room_and_invitation,
|
||||||
|
messageStr,
|
||||||
|
roomStr,
|
||||||
|
invitationsStr
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// In one room
|
||||||
|
stringProvider.getString(
|
||||||
|
R.string.notification_unread_notified_messages_and_invitation,
|
||||||
|
messageStr,
|
||||||
|
invitationsStr
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Only invitation
|
||||||
|
invitationsStr
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No invitation, only messages
|
||||||
|
val messageStr = stringProvider.getQuantityString(R.plurals.room_new_messages_notification,
|
||||||
|
messageNotificationCount, messageNotificationCount)
|
||||||
|
if (roomCount > 1) {
|
||||||
|
// In several rooms
|
||||||
|
val roomStr = stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages_in_room_rooms, roomCount, roomCount)
|
||||||
|
stringProvider.getString(R.string.notification_unread_notified_messages_in_room, messageStr, roomStr)
|
||||||
|
} else {
|
||||||
|
// In one room
|
||||||
|
messageStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return notificationUtils.buildSummaryListNotification(
|
||||||
|
style = null,
|
||||||
|
compatSummary = privacyTitle,
|
||||||
|
noisy = summaryIsNoisy,
|
||||||
|
lastMessageTimestamp = lastMessageTimestamp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -79,15 +79,14 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti
|
||||||
return when (permalinkData) {
|
return when (permalinkData) {
|
||||||
is PermalinkData.RoomLink -> {
|
is PermalinkData.RoomLink -> {
|
||||||
val roomId = permalinkData.getRoomId()
|
val roomId = permalinkData.getRoomId()
|
||||||
if (navigationInterceptor?.navToRoom(roomId, permalinkData.eventId, rawLink) != true) {
|
openRoom(
|
||||||
openRoom(
|
navigationInterceptor,
|
||||||
context = context,
|
context = context,
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
permalinkData = permalinkData,
|
permalinkData = permalinkData,
|
||||||
rawLink = rawLink,
|
rawLink = rawLink,
|
||||||
buildTask = buildTask
|
buildTask = buildTask
|
||||||
)
|
)
|
||||||
}
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
is PermalinkData.GroupLink -> {
|
is PermalinkData.GroupLink -> {
|
||||||
|
@ -146,6 +145,7 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti
|
||||||
* Open room either joined, or not
|
* Open room either joined, or not
|
||||||
*/
|
*/
|
||||||
private fun openRoom(
|
private fun openRoom(
|
||||||
|
navigationInterceptor: NavigationInterceptor?,
|
||||||
context: Context,
|
context: Context,
|
||||||
roomId: String?,
|
roomId: String?,
|
||||||
permalinkData: PermalinkData.RoomLink,
|
permalinkData: PermalinkData.RoomLink,
|
||||||
|
@ -167,7 +167,7 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti
|
||||||
membership?.isActive().orFalse() -> {
|
membership?.isActive().orFalse() -> {
|
||||||
if (!isSpace && membership == Membership.JOIN) {
|
if (!isSpace && membership == Membership.JOIN) {
|
||||||
// If it's a room you're in, let's just open it, you can tap back if needed
|
// If it's a room you're in, let's just open it, you can tap back if needed
|
||||||
navigator.openRoom(context, roomId, eventId, buildTask)
|
navigationInterceptor.openJoinedRoomScreen(buildTask, roomId, eventId, rawLink, context)
|
||||||
} else {
|
} else {
|
||||||
// maybe open space preview navigator.openSpacePreview(context, roomId)? if already joined?
|
// maybe open space preview navigator.openSpacePreview(context, roomId)? if already joined?
|
||||||
navigator.openMatrixToBottomSheet(context, rawLink.toString())
|
navigator.openMatrixToBottomSheet(context, rawLink.toString())
|
||||||
|
@ -180,6 +180,12 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun NavigationInterceptor?.openJoinedRoomScreen(buildTask: Boolean, roomId: String, eventId: String?, rawLink: Uri, context: Context) {
|
||||||
|
if (this?.navToRoom(roomId, eventId, rawLink) != true) {
|
||||||
|
navigator.openRoom(context, roomId, eventId, buildTask)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val MATRIX_TO_CUSTOM_SCHEME_URL_BASE = "element://"
|
const val MATRIX_TO_CUSTOM_SCHEME_URL_BASE = "element://"
|
||||||
const val ROOM_LINK_PREFIX = "${MATRIX_TO_CUSTOM_SCHEME_URL_BASE}room/"
|
const val ROOM_LINK_PREFIX = "${MATRIX_TO_CUSTOM_SCHEME_URL_BASE}room/"
|
||||||
|
|
|
@ -20,12 +20,14 @@ import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.airbnb.mvrx.Mavericks
|
import com.airbnb.mvrx.Mavericks
|
||||||
import com.google.android.material.appbar.MaterialToolbar
|
import com.google.android.material.appbar.MaterialToolbar
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.extensions.addFragment
|
import im.vector.app.core.extensions.addFragment
|
||||||
import im.vector.app.core.platform.ToolbarConfigurable
|
import im.vector.app.core.platform.ToolbarConfigurable
|
||||||
import im.vector.app.core.platform.VectorBaseActivity
|
import im.vector.app.core.platform.VectorBaseActivity
|
||||||
import im.vector.app.databinding.ActivitySimpleBinding
|
import im.vector.app.databinding.ActivitySimpleBinding
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
class PinActivity : VectorBaseActivity<ActivitySimpleBinding>(), ToolbarConfigurable, UnlockedActivity {
|
class PinActivity : VectorBaseActivity<ActivitySimpleBinding>(), ToolbarConfigurable, UnlockedActivity {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -29,6 +29,8 @@ import im.vector.app.core.extensions.addFragmentToBackstack
|
||||||
import im.vector.app.core.extensions.popBackstack
|
import im.vector.app.core.extensions.popBackstack
|
||||||
import im.vector.app.core.platform.VectorBaseActivity
|
import im.vector.app.core.platform.VectorBaseActivity
|
||||||
import im.vector.app.databinding.ActivitySimpleBinding
|
import im.vector.app.databinding.ActivitySimpleBinding
|
||||||
|
import im.vector.app.features.matrixto.MatrixToBottomSheet
|
||||||
|
import im.vector.app.features.navigation.Navigator
|
||||||
import im.vector.app.features.roomdirectory.createroom.CreateRoomArgs
|
import im.vector.app.features.roomdirectory.createroom.CreateRoomArgs
|
||||||
import im.vector.app.features.roomdirectory.createroom.CreateRoomFragment
|
import im.vector.app.features.roomdirectory.createroom.CreateRoomFragment
|
||||||
import im.vector.app.features.roomdirectory.picker.RoomDirectoryPickerFragment
|
import im.vector.app.features.roomdirectory.picker.RoomDirectoryPickerFragment
|
||||||
|
@ -37,7 +39,7 @@ import kotlinx.coroutines.flow.onEach
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class RoomDirectoryActivity : VectorBaseActivity<ActivitySimpleBinding>() {
|
class RoomDirectoryActivity : VectorBaseActivity<ActivitySimpleBinding>(), MatrixToBottomSheet.InteractionListener {
|
||||||
|
|
||||||
@Inject lateinit var roomDirectoryViewModelFactory: RoomDirectoryViewModel.Factory
|
@Inject lateinit var roomDirectoryViewModelFactory: RoomDirectoryViewModel.Factory
|
||||||
private val roomDirectoryViewModel: RoomDirectoryViewModel by viewModel()
|
private val roomDirectoryViewModel: RoomDirectoryViewModel by viewModel()
|
||||||
|
@ -84,6 +86,14 @@ class RoomDirectoryActivity : VectorBaseActivity<ActivitySimpleBinding>() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun mxToBottomSheetNavigateToRoom(roomId: String) {
|
||||||
|
navigator.openRoom(this, roomId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mxToBottomSheetSwitchToSpace(spaceId: String) {
|
||||||
|
navigator.switchToSpace(this, spaceId, Navigator.PostSwitchSpaceAction.None)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val INITIAL_FILTER = "INITIAL_FILTER"
|
private const val INITIAL_FILTER = "INITIAL_FILTER"
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.google.android.material.appbar.MaterialToolbar
|
import com.google.android.material.appbar.MaterialToolbar
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.extensions.addFragment
|
import im.vector.app.core.extensions.addFragment
|
||||||
import im.vector.app.core.platform.ToolbarConfigurable
|
import im.vector.app.core.platform.ToolbarConfigurable
|
||||||
|
@ -51,6 +52,7 @@ data class RoomPreviewData(
|
||||||
get() = MatrixItem.RoomItem(roomId, roomName ?: roomAlias, avatarUrl)
|
get() = MatrixItem.RoomItem(roomId, roomName ?: roomAlias, avatarUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
class RoomPreviewActivity : VectorBaseActivity<ActivitySimpleBinding>(), ToolbarConfigurable {
|
class RoomPreviewActivity : VectorBaseActivity<ActivitySimpleBinding>(), ToolbarConfigurable {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -19,6 +19,7 @@ package im.vector.app.features.signout.hard
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import im.vector.app.core.platform.VectorBaseActivity
|
import im.vector.app.core.platform.VectorBaseActivity
|
||||||
import im.vector.app.databinding.ActivitySignedOutBinding
|
import im.vector.app.databinding.ActivitySignedOutBinding
|
||||||
import im.vector.app.features.MainActivity
|
import im.vector.app.features.MainActivity
|
||||||
|
@ -29,6 +30,7 @@ import timber.log.Timber
|
||||||
/**
|
/**
|
||||||
* In this screen, the user is viewing a message informing that he has been logged out
|
* In this screen, the user is viewing a message informing that he has been logged out
|
||||||
*/
|
*/
|
||||||
|
@AndroidEntryPoint
|
||||||
class SignedOutActivity : VectorBaseActivity<ActivitySignedOutBinding>() {
|
class SignedOutActivity : VectorBaseActivity<ActivitySignedOutBinding>() {
|
||||||
|
|
||||||
override fun getBinding() = ActivitySignedOutBinding.inflate(layoutInflater)
|
override fun getBinding() = ActivitySignedOutBinding.inflate(layoutInflater)
|
||||||
|
|
|
@ -21,6 +21,7 @@ import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.airbnb.mvrx.Mavericks
|
import com.airbnb.mvrx.Mavericks
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.extensions.commitTransaction
|
import im.vector.app.core.extensions.commitTransaction
|
||||||
import im.vector.app.core.platform.VectorBaseActivity
|
import im.vector.app.core.platform.VectorBaseActivity
|
||||||
|
@ -30,6 +31,7 @@ import im.vector.app.features.spaces.preview.SpacePreviewFragment
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
class SpacePreviewActivity : VectorBaseActivity<ActivitySimpleBinding>() {
|
class SpacePreviewActivity : VectorBaseActivity<ActivitySimpleBinding>() {
|
||||||
|
|
||||||
lateinit var sharedActionViewModel: SpacePreviewSharedActionViewModel
|
lateinit var sharedActionViewModel: SpacePreviewSharedActionViewModel
|
||||||
|
|
|
@ -23,6 +23,7 @@ import androidx.core.view.isGone
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.airbnb.mvrx.Mavericks
|
import com.airbnb.mvrx.Mavericks
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.extensions.commitTransaction
|
import im.vector.app.core.extensions.commitTransaction
|
||||||
import im.vector.app.core.extensions.hideKeyboard
|
import im.vector.app.core.extensions.hideKeyboard
|
||||||
|
@ -33,6 +34,7 @@ import im.vector.app.features.spaces.share.ShareSpaceBottomSheet
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
class SpacePeopleActivity : VectorBaseActivity<ActivitySimpleLoadingBinding>() {
|
class SpacePeopleActivity : VectorBaseActivity<ActivitySimpleLoadingBinding>() {
|
||||||
|
|
||||||
override fun getBinding() = ActivitySimpleLoadingBinding.inflate(layoutInflater)
|
override fun getBinding() = ActivitySimpleLoadingBinding.inflate(layoutInflater)
|
||||||
|
|
|
@ -657,6 +657,8 @@
|
||||||
<string name="login_error_unknown_host">This URL is not reachable, please check it</string>
|
<string name="login_error_unknown_host">This URL is not reachable, please check it</string>
|
||||||
<string name="login_error_no_homeserver_found">This is not a valid Matrix server address</string>
|
<string name="login_error_no_homeserver_found">This is not a valid Matrix server address</string>
|
||||||
<string name="login_error_homeserver_not_found">Cannot reach a homeserver at this URL, please check it</string>
|
<string name="login_error_homeserver_not_found">Cannot reach a homeserver at this URL, please check it</string>
|
||||||
|
<string name="login_error_homeserver_from_url_not_found">Cannot reach a homeserver at the URL %s. Please check your link or choose a homeserver manually.</string>
|
||||||
|
<string name="login_error_homeserver_from_url_not_found_enter_manual">Choose homeserver</string>
|
||||||
<string name="login_error_ssl_peer_unverified">"SSL Error: the peer's identity has not been verified."</string>
|
<string name="login_error_ssl_peer_unverified">"SSL Error: the peer's identity has not been verified."</string>
|
||||||
<string name="login_error_ssl_other">"SSL Error."</string>
|
<string name="login_error_ssl_other">"SSL Error."</string>
|
||||||
<string name="login_error_ssl_handshake">Your device is using an outdated TLS security protocol, vulnerable to attack, for your security you will not be able to connect</string>
|
<string name="login_error_ssl_handshake">Your device is using an outdated TLS security protocol, vulnerable to attack, for your security you will not be able to connect</string>
|
||||||
|
|
|
@ -0,0 +1,192 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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 im.vector.app.features.notifications
|
||||||
|
|
||||||
|
import im.vector.app.features.notifications.ProcessedEvent.Type
|
||||||
|
import im.vector.app.test.fakes.FakeAutoAcceptInvites
|
||||||
|
import im.vector.app.test.fakes.FakeOutdatedEventDetector
|
||||||
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
|
import org.junit.Test
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
|
|
||||||
|
private val NOT_VIEWING_A_ROOM: String? = null
|
||||||
|
|
||||||
|
class NotifiableEventProcessorTest {
|
||||||
|
|
||||||
|
private val outdatedDetector = FakeOutdatedEventDetector()
|
||||||
|
private val autoAcceptInvites = FakeAutoAcceptInvites()
|
||||||
|
|
||||||
|
private val eventProcessor = NotifiableEventProcessor(outdatedDetector.instance, autoAcceptInvites)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given simple events when processing then keep simple events`() {
|
||||||
|
val events = listOf(
|
||||||
|
aSimpleNotifiableEvent(eventId = "event-1"),
|
||||||
|
aSimpleNotifiableEvent(eventId = "event-2")
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEvents = emptyList())
|
||||||
|
|
||||||
|
result shouldBeEqualTo listOfProcessedEvents(
|
||||||
|
Type.KEEP to events[0],
|
||||||
|
Type.KEEP to events[1]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given redacted simple event when processing then remove redaction event`() {
|
||||||
|
val events = listOf(aSimpleNotifiableEvent(eventId = "event-1", type = EventType.REDACTION))
|
||||||
|
|
||||||
|
val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEvents = emptyList())
|
||||||
|
|
||||||
|
result shouldBeEqualTo listOfProcessedEvents(
|
||||||
|
Type.REMOVE to events[0]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given invites are auto accepted when processing then remove invitations`() {
|
||||||
|
autoAcceptInvites._isEnabled = true
|
||||||
|
val events = listOf<NotifiableEvent>(
|
||||||
|
anInviteNotifiableEvent(roomId = "room-1"),
|
||||||
|
anInviteNotifiableEvent(roomId = "room-2")
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEvents = emptyList())
|
||||||
|
|
||||||
|
result shouldBeEqualTo listOfProcessedEvents(
|
||||||
|
Type.REMOVE to events[0],
|
||||||
|
Type.REMOVE to events[1]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given invites are not auto accepted when processing then keep invitation events`() {
|
||||||
|
autoAcceptInvites._isEnabled = false
|
||||||
|
val events = listOf(
|
||||||
|
anInviteNotifiableEvent(roomId = "room-1"),
|
||||||
|
anInviteNotifiableEvent(roomId = "room-2")
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEvents = emptyList())
|
||||||
|
|
||||||
|
result shouldBeEqualTo listOfProcessedEvents(
|
||||||
|
Type.KEEP to events[0],
|
||||||
|
Type.KEEP to events[1]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given out of date message event when processing then removes message event`() {
|
||||||
|
val events = listOf(aNotifiableMessageEvent(eventId = "event-1", roomId = "room-1"))
|
||||||
|
outdatedDetector.givenEventIsOutOfDate(events[0])
|
||||||
|
|
||||||
|
val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEvents = emptyList())
|
||||||
|
|
||||||
|
result shouldBeEqualTo listOfProcessedEvents(
|
||||||
|
Type.REMOVE to events[0],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given in date message event when processing then keep message event`() {
|
||||||
|
val events = listOf(aNotifiableMessageEvent(eventId = "event-1", roomId = "room-1"))
|
||||||
|
outdatedDetector.givenEventIsInDate(events[0])
|
||||||
|
|
||||||
|
val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEvents = emptyList())
|
||||||
|
|
||||||
|
result shouldBeEqualTo listOfProcessedEvents(
|
||||||
|
Type.KEEP to events[0],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given viewing the same room as message event when processing then removes message`() {
|
||||||
|
val events = listOf(aNotifiableMessageEvent(eventId = "event-1", roomId = "room-1"))
|
||||||
|
|
||||||
|
val result = eventProcessor.process(events, currentRoomId = "room-1", renderedEvents = emptyList())
|
||||||
|
|
||||||
|
result shouldBeEqualTo listOfProcessedEvents(
|
||||||
|
Type.REMOVE to events[0],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given events are different to rendered events when processing then removes difference`() {
|
||||||
|
val events = listOf(aSimpleNotifiableEvent(eventId = "event-1"))
|
||||||
|
val renderedEvents = listOf<ProcessedEvent<NotifiableEvent>>(
|
||||||
|
ProcessedEvent(Type.KEEP, events[0]),
|
||||||
|
ProcessedEvent(Type.KEEP, anInviteNotifiableEvent(roomId = "event-2"))
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEvents = renderedEvents)
|
||||||
|
|
||||||
|
result shouldBeEqualTo listOfProcessedEvents(
|
||||||
|
Type.REMOVE to renderedEvents[1].event,
|
||||||
|
Type.KEEP to renderedEvents[0].event
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun listOfProcessedEvents(vararg event: Pair<Type, NotifiableEvent>) = event.map {
|
||||||
|
ProcessedEvent(it.first, it.second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun aSimpleNotifiableEvent(eventId: String, type: String? = null) = SimpleNotifiableEvent(
|
||||||
|
matrixID = null,
|
||||||
|
eventId = eventId,
|
||||||
|
editedEventId = null,
|
||||||
|
noisy = false,
|
||||||
|
title = "title",
|
||||||
|
description = "description",
|
||||||
|
type = type,
|
||||||
|
timestamp = 0,
|
||||||
|
soundName = null,
|
||||||
|
canBeReplaced = false,
|
||||||
|
isRedacted = false
|
||||||
|
)
|
||||||
|
|
||||||
|
fun anInviteNotifiableEvent(roomId: String) = InviteNotifiableEvent(
|
||||||
|
matrixID = null,
|
||||||
|
eventId = "event-id",
|
||||||
|
roomId = roomId,
|
||||||
|
roomName = "a room name",
|
||||||
|
editedEventId = null,
|
||||||
|
noisy = false,
|
||||||
|
title = "title",
|
||||||
|
description = "description",
|
||||||
|
type = null,
|
||||||
|
timestamp = 0,
|
||||||
|
soundName = null,
|
||||||
|
canBeReplaced = false,
|
||||||
|
isRedacted = false
|
||||||
|
)
|
||||||
|
|
||||||
|
fun aNotifiableMessageEvent(eventId: String, roomId: String) = NotifiableMessageEvent(
|
||||||
|
eventId = eventId,
|
||||||
|
editedEventId = null,
|
||||||
|
noisy = false,
|
||||||
|
timestamp = 0,
|
||||||
|
senderName = "sender-name",
|
||||||
|
senderId = "sending-id",
|
||||||
|
body = "message-body",
|
||||||
|
roomId = roomId,
|
||||||
|
roomName = "room-name",
|
||||||
|
roomIsDirect = false,
|
||||||
|
canBeReplaced = false,
|
||||||
|
isRedacted = false
|
||||||
|
)
|
|
@ -0,0 +1,156 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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 im.vector.app.features.notifications
|
||||||
|
|
||||||
|
import im.vector.app.features.notifications.ProcessedEvent.Type
|
||||||
|
import im.vector.app.test.fakes.FakeNotificationUtils
|
||||||
|
import im.vector.app.test.fakes.FakeRoomGroupMessageCreator
|
||||||
|
import im.vector.app.test.fakes.FakeSummaryGroupMessageCreator
|
||||||
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
private const val MY_USER_ID = "user-id"
|
||||||
|
private const val A_ROOM_ID = "room-id"
|
||||||
|
private const val AN_EVENT_ID = "event-id"
|
||||||
|
|
||||||
|
private val MY_AVATAR_URL: String? = null
|
||||||
|
private val AN_INVITATION_EVENT = anInviteNotifiableEvent(roomId = A_ROOM_ID)
|
||||||
|
private val A_SIMPLE_EVENT = aSimpleNotifiableEvent(eventId = AN_EVENT_ID)
|
||||||
|
private val A_MESSAGE_EVENT = aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID)
|
||||||
|
|
||||||
|
class NotificationFactoryTest {
|
||||||
|
|
||||||
|
private val notificationUtils = FakeNotificationUtils()
|
||||||
|
private val roomGroupMessageCreator = FakeRoomGroupMessageCreator()
|
||||||
|
private val summaryGroupMessageCreator = FakeSummaryGroupMessageCreator()
|
||||||
|
|
||||||
|
private val notificationFactory = NotificationFactory(
|
||||||
|
notificationUtils.instance,
|
||||||
|
roomGroupMessageCreator.instance,
|
||||||
|
summaryGroupMessageCreator.instance
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a room invitation when mapping to notification then is Append`() = testWith(notificationFactory) {
|
||||||
|
val expectedNotification = notificationUtils.givenBuildRoomInvitationNotificationFor(AN_INVITATION_EVENT, MY_USER_ID)
|
||||||
|
val roomInvitation = listOf(ProcessedEvent(Type.KEEP, AN_INVITATION_EVENT))
|
||||||
|
|
||||||
|
val result = roomInvitation.toNotifications(MY_USER_ID)
|
||||||
|
|
||||||
|
result shouldBeEqualTo listOf(OneShotNotification.Append(
|
||||||
|
notification = expectedNotification,
|
||||||
|
meta = OneShotNotification.Append.Meta(
|
||||||
|
key = A_ROOM_ID,
|
||||||
|
summaryLine = AN_INVITATION_EVENT.description,
|
||||||
|
isNoisy = AN_INVITATION_EVENT.noisy,
|
||||||
|
timestamp = AN_INVITATION_EVENT.timestamp
|
||||||
|
))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a missing event in room invitation when mapping to notification then is Removed`() = testWith(notificationFactory) {
|
||||||
|
val missingEventRoomInvitation = listOf(ProcessedEvent(Type.REMOVE, AN_INVITATION_EVENT))
|
||||||
|
|
||||||
|
val result = missingEventRoomInvitation.toNotifications(MY_USER_ID)
|
||||||
|
|
||||||
|
result shouldBeEqualTo listOf(OneShotNotification.Removed(
|
||||||
|
key = A_ROOM_ID
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a simple event when mapping to notification then is Append`() = testWith(notificationFactory) {
|
||||||
|
val expectedNotification = notificationUtils.givenBuildSimpleInvitationNotificationFor(A_SIMPLE_EVENT, MY_USER_ID)
|
||||||
|
val roomInvitation = listOf(ProcessedEvent(Type.KEEP, A_SIMPLE_EVENT))
|
||||||
|
|
||||||
|
val result = roomInvitation.toNotifications(MY_USER_ID)
|
||||||
|
|
||||||
|
result shouldBeEqualTo listOf(OneShotNotification.Append(
|
||||||
|
notification = expectedNotification,
|
||||||
|
meta = OneShotNotification.Append.Meta(
|
||||||
|
key = AN_EVENT_ID,
|
||||||
|
summaryLine = A_SIMPLE_EVENT.description,
|
||||||
|
isNoisy = A_SIMPLE_EVENT.noisy,
|
||||||
|
timestamp = AN_INVITATION_EVENT.timestamp
|
||||||
|
))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a missing simple event when mapping to notification then is Removed`() = testWith(notificationFactory) {
|
||||||
|
val missingEventRoomInvitation = listOf(ProcessedEvent(Type.REMOVE, A_SIMPLE_EVENT))
|
||||||
|
|
||||||
|
val result = missingEventRoomInvitation.toNotifications(MY_USER_ID)
|
||||||
|
|
||||||
|
result shouldBeEqualTo listOf(OneShotNotification.Removed(
|
||||||
|
key = AN_EVENT_ID
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given room with message when mapping to notification then delegates to room group message creator`() = testWith(notificationFactory) {
|
||||||
|
val events = listOf(A_MESSAGE_EVENT)
|
||||||
|
val expectedNotification = roomGroupMessageCreator.givenCreatesRoomMessageFor(events, A_ROOM_ID, MY_USER_ID, MY_AVATAR_URL)
|
||||||
|
val roomWithMessage = mapOf(A_ROOM_ID to listOf(ProcessedEvent(Type.KEEP, A_MESSAGE_EVENT)))
|
||||||
|
|
||||||
|
val result = roomWithMessage.toNotifications(MY_USER_ID, MY_AVATAR_URL)
|
||||||
|
|
||||||
|
result shouldBeEqualTo listOf(expectedNotification)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a room with no events to display when mapping to notification then is Empty`() = testWith(notificationFactory) {
|
||||||
|
val events = listOf(ProcessedEvent(Type.REMOVE, A_MESSAGE_EVENT))
|
||||||
|
val emptyRoom = mapOf(A_ROOM_ID to events)
|
||||||
|
|
||||||
|
val result = emptyRoom.toNotifications(MY_USER_ID, MY_AVATAR_URL)
|
||||||
|
|
||||||
|
result shouldBeEqualTo listOf(RoomNotification.Removed(
|
||||||
|
roomId = A_ROOM_ID
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a room with only redacted events when mapping to notification then is Empty`() = testWith(notificationFactory) {
|
||||||
|
val redactedRoom = mapOf(A_ROOM_ID to listOf(ProcessedEvent(Type.KEEP, A_MESSAGE_EVENT.copy(isRedacted = true))))
|
||||||
|
|
||||||
|
val result = redactedRoom.toNotifications(MY_USER_ID, MY_AVATAR_URL)
|
||||||
|
|
||||||
|
result shouldBeEqualTo listOf(RoomNotification.Removed(
|
||||||
|
roomId = A_ROOM_ID
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a room with redacted and non redacted message events when mapping to notification then redacted events are removed`() = testWith(notificationFactory) {
|
||||||
|
val roomWithRedactedMessage = mapOf(A_ROOM_ID to listOf(
|
||||||
|
ProcessedEvent(Type.KEEP, A_MESSAGE_EVENT.copy(isRedacted = true)),
|
||||||
|
ProcessedEvent(Type.KEEP, A_MESSAGE_EVENT.copy(eventId = "not-redacted"))
|
||||||
|
))
|
||||||
|
val withRedactedRemoved = listOf(A_MESSAGE_EVENT.copy(eventId = "not-redacted"))
|
||||||
|
val expectedNotification = roomGroupMessageCreator.givenCreatesRoomMessageFor(withRedactedRemoved, A_ROOM_ID, MY_USER_ID, MY_AVATAR_URL)
|
||||||
|
|
||||||
|
val result = roomWithRedactedMessage.toNotifications(MY_USER_ID, MY_AVATAR_URL)
|
||||||
|
|
||||||
|
result shouldBeEqualTo listOf(expectedNotification)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> testWith(receiver: T, block: T.() -> Unit) {
|
||||||
|
receiver.block()
|
||||||
|
}
|
|
@ -0,0 +1,214 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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 im.vector.app.features.notifications
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import im.vector.app.test.fakes.FakeContext
|
||||||
|
import im.vector.app.test.fakes.FakeNotificationDisplayer
|
||||||
|
import im.vector.app.test.fakes.FakeNotificationFactory
|
||||||
|
import io.mockk.mockk
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
private const val MY_USER_ID = "my-user-id"
|
||||||
|
private const val MY_USER_DISPLAY_NAME = "display-name"
|
||||||
|
private const val MY_USER_AVATAR_URL = "avatar-url"
|
||||||
|
private const val AN_EVENT_ID = "event-id"
|
||||||
|
private const val A_ROOM_ID = "room-id"
|
||||||
|
private const val USE_COMPLETE_NOTIFICATION_FORMAT = true
|
||||||
|
|
||||||
|
private val AN_EVENT_LIST = listOf<ProcessedEvent<NotifiableEvent>>()
|
||||||
|
private val A_PROCESSED_EVENTS = GroupedNotificationEvents(emptyMap(), emptyList(), emptyList())
|
||||||
|
private val A_SUMMARY_NOTIFICATION = SummaryNotification.Update(mockk())
|
||||||
|
private val A_REMOVE_SUMMARY_NOTIFICATION = SummaryNotification.Removed
|
||||||
|
private val A_NOTIFICATION = mockk<Notification>()
|
||||||
|
private val MESSAGE_META = RoomNotification.Message.Meta(
|
||||||
|
summaryLine = "ignored", messageCount = 1, latestTimestamp = -1, roomId = A_ROOM_ID, shouldBing = false
|
||||||
|
)
|
||||||
|
private val ONE_SHOT_META = OneShotNotification.Append.Meta(key = "ignored", summaryLine = "ignored", isNoisy = false, timestamp = -1)
|
||||||
|
|
||||||
|
class NotificationRendererTest {
|
||||||
|
|
||||||
|
private val context = FakeContext()
|
||||||
|
private val notificationDisplayer = FakeNotificationDisplayer()
|
||||||
|
private val notificationFactory = FakeNotificationFactory()
|
||||||
|
|
||||||
|
private val notificationRenderer = NotificationRenderer(
|
||||||
|
notificationDisplayer = notificationDisplayer.instance,
|
||||||
|
notificationFactory = notificationFactory.instance,
|
||||||
|
appContext = context.instance
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given no notifications when rendering then cancels summary notification`() {
|
||||||
|
givenNoNotifications()
|
||||||
|
|
||||||
|
renderEventsAsNotifications()
|
||||||
|
|
||||||
|
notificationDisplayer.verifySummaryCancelled()
|
||||||
|
notificationDisplayer.verifyNoOtherInteractions()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given last room message group notification is removed when rendering then remove the summary and then remove message notification`() {
|
||||||
|
givenNotifications(roomNotifications = listOf(RoomNotification.Removed(A_ROOM_ID)), summaryNotification = A_REMOVE_SUMMARY_NOTIFICATION)
|
||||||
|
|
||||||
|
renderEventsAsNotifications()
|
||||||
|
|
||||||
|
notificationDisplayer.verifyInOrder {
|
||||||
|
cancelNotificationMessage(tag = null, NotificationDrawerManager.SUMMARY_NOTIFICATION_ID)
|
||||||
|
cancelNotificationMessage(tag = A_ROOM_ID, NotificationDrawerManager.ROOM_MESSAGES_NOTIFICATION_ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a room message group notification is removed when rendering then remove the message notification and update summary`() {
|
||||||
|
givenNotifications(roomNotifications = listOf(RoomNotification.Removed(A_ROOM_ID)))
|
||||||
|
|
||||||
|
renderEventsAsNotifications()
|
||||||
|
|
||||||
|
notificationDisplayer.verifyInOrder {
|
||||||
|
cancelNotificationMessage(tag = A_ROOM_ID, NotificationDrawerManager.ROOM_MESSAGES_NOTIFICATION_ID)
|
||||||
|
showNotificationMessage(tag = null, NotificationDrawerManager.SUMMARY_NOTIFICATION_ID, A_SUMMARY_NOTIFICATION.notification)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a room message group notification is added when rendering then show the message notification and update summary`() {
|
||||||
|
givenNotifications(roomNotifications = listOf(RoomNotification.Message(
|
||||||
|
A_NOTIFICATION,
|
||||||
|
shortcutInfo = null,
|
||||||
|
MESSAGE_META
|
||||||
|
)))
|
||||||
|
|
||||||
|
renderEventsAsNotifications()
|
||||||
|
|
||||||
|
notificationDisplayer.verifyInOrder {
|
||||||
|
showNotificationMessage(tag = A_ROOM_ID, NotificationDrawerManager.ROOM_MESSAGES_NOTIFICATION_ID, A_NOTIFICATION)
|
||||||
|
showNotificationMessage(tag = null, NotificationDrawerManager.SUMMARY_NOTIFICATION_ID, A_SUMMARY_NOTIFICATION.notification)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given last simple notification is removed when rendering then remove the summary and then remove simple notification`() {
|
||||||
|
givenNotifications(simpleNotifications = listOf(OneShotNotification.Removed(AN_EVENT_ID)), summaryNotification = A_REMOVE_SUMMARY_NOTIFICATION)
|
||||||
|
|
||||||
|
renderEventsAsNotifications()
|
||||||
|
|
||||||
|
notificationDisplayer.verifyInOrder {
|
||||||
|
cancelNotificationMessage(tag = null, NotificationDrawerManager.SUMMARY_NOTIFICATION_ID)
|
||||||
|
cancelNotificationMessage(tag = AN_EVENT_ID, NotificationDrawerManager.ROOM_EVENT_NOTIFICATION_ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a simple notification is removed when rendering then remove the simple notification and update summary`() {
|
||||||
|
givenNotifications(simpleNotifications = listOf(OneShotNotification.Removed(AN_EVENT_ID)))
|
||||||
|
|
||||||
|
renderEventsAsNotifications()
|
||||||
|
|
||||||
|
notificationDisplayer.verifyInOrder {
|
||||||
|
cancelNotificationMessage(tag = AN_EVENT_ID, NotificationDrawerManager.ROOM_EVENT_NOTIFICATION_ID)
|
||||||
|
showNotificationMessage(tag = null, NotificationDrawerManager.SUMMARY_NOTIFICATION_ID, A_SUMMARY_NOTIFICATION.notification)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a simple notification is added when rendering then show the simple notification and update summary`() {
|
||||||
|
givenNotifications(simpleNotifications = listOf(OneShotNotification.Append(
|
||||||
|
A_NOTIFICATION,
|
||||||
|
ONE_SHOT_META.copy(key = AN_EVENT_ID)
|
||||||
|
)))
|
||||||
|
|
||||||
|
renderEventsAsNotifications()
|
||||||
|
|
||||||
|
notificationDisplayer.verifyInOrder {
|
||||||
|
showNotificationMessage(tag = AN_EVENT_ID, NotificationDrawerManager.ROOM_EVENT_NOTIFICATION_ID, A_NOTIFICATION)
|
||||||
|
showNotificationMessage(tag = null, NotificationDrawerManager.SUMMARY_NOTIFICATION_ID, A_SUMMARY_NOTIFICATION.notification)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given last invitation notification is removed when rendering then remove the summary and then remove invitation notification`() {
|
||||||
|
givenNotifications(invitationNotifications = listOf(OneShotNotification.Removed(A_ROOM_ID)), summaryNotification = A_REMOVE_SUMMARY_NOTIFICATION)
|
||||||
|
|
||||||
|
renderEventsAsNotifications()
|
||||||
|
|
||||||
|
notificationDisplayer.verifyInOrder {
|
||||||
|
cancelNotificationMessage(tag = null, NotificationDrawerManager.SUMMARY_NOTIFICATION_ID)
|
||||||
|
cancelNotificationMessage(tag = A_ROOM_ID, NotificationDrawerManager.ROOM_INVITATION_NOTIFICATION_ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given an invitation notification is removed when rendering then remove the invitation notification and update summary`() {
|
||||||
|
givenNotifications(invitationNotifications = listOf(OneShotNotification.Removed(A_ROOM_ID)))
|
||||||
|
|
||||||
|
renderEventsAsNotifications()
|
||||||
|
|
||||||
|
notificationDisplayer.verifyInOrder {
|
||||||
|
cancelNotificationMessage(tag = A_ROOM_ID, NotificationDrawerManager.ROOM_INVITATION_NOTIFICATION_ID)
|
||||||
|
showNotificationMessage(tag = null, NotificationDrawerManager.SUMMARY_NOTIFICATION_ID, A_SUMMARY_NOTIFICATION.notification)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given an invitation notification is added when rendering then show the invitation notification and update summary`() {
|
||||||
|
givenNotifications(simpleNotifications = listOf(OneShotNotification.Append(
|
||||||
|
A_NOTIFICATION,
|
||||||
|
ONE_SHOT_META.copy(key = A_ROOM_ID)
|
||||||
|
)))
|
||||||
|
|
||||||
|
renderEventsAsNotifications()
|
||||||
|
|
||||||
|
notificationDisplayer.verifyInOrder {
|
||||||
|
showNotificationMessage(tag = A_ROOM_ID, NotificationDrawerManager.ROOM_EVENT_NOTIFICATION_ID, A_NOTIFICATION)
|
||||||
|
showNotificationMessage(tag = null, NotificationDrawerManager.SUMMARY_NOTIFICATION_ID, A_SUMMARY_NOTIFICATION.notification)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderEventsAsNotifications() {
|
||||||
|
notificationRenderer.render(
|
||||||
|
myUserId = MY_USER_ID,
|
||||||
|
myUserDisplayName = MY_USER_DISPLAY_NAME,
|
||||||
|
myUserAvatarUrl = MY_USER_AVATAR_URL,
|
||||||
|
useCompleteNotificationFormat = USE_COMPLETE_NOTIFICATION_FORMAT,
|
||||||
|
eventsToProcess = AN_EVENT_LIST
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun givenNoNotifications() {
|
||||||
|
givenNotifications(emptyList(), emptyList(), emptyList(), USE_COMPLETE_NOTIFICATION_FORMAT, A_REMOVE_SUMMARY_NOTIFICATION)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun givenNotifications(roomNotifications: List<RoomNotification> = emptyList(),
|
||||||
|
invitationNotifications: List<OneShotNotification> = emptyList(),
|
||||||
|
simpleNotifications: List<OneShotNotification> = emptyList(),
|
||||||
|
useCompleteNotificationFormat: Boolean = USE_COMPLETE_NOTIFICATION_FORMAT,
|
||||||
|
summaryNotification: SummaryNotification = A_SUMMARY_NOTIFICATION) {
|
||||||
|
notificationFactory.givenNotificationsFor(
|
||||||
|
groupedEvents = A_PROCESSED_EVENTS,
|
||||||
|
myUserId = MY_USER_ID,
|
||||||
|
myUserDisplayName = MY_USER_DISPLAY_NAME,
|
||||||
|
myUserAvatarUrl = MY_USER_AVATAR_URL,
|
||||||
|
useCompleteNotificationFormat = useCompleteNotificationFormat,
|
||||||
|
roomNotifications = roomNotifications,
|
||||||
|
invitationNotifications = invitationNotifications,
|
||||||
|
simpleNotifications = simpleNotifications,
|
||||||
|
summaryNotification = summaryNotification
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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 im.vector.app.test.fakes
|
||||||
|
|
||||||
|
import im.vector.app.features.invite.AutoAcceptInvites
|
||||||
|
|
||||||
|
class FakeAutoAcceptInvites : AutoAcceptInvites {
|
||||||
|
|
||||||
|
var _isEnabled: Boolean = false
|
||||||
|
|
||||||
|
override val isEnabled: Boolean
|
||||||
|
get() = _isEnabled
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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 im.vector.app.test.fakes
|
||||||
|
|
||||||
|
import im.vector.app.features.notifications.NotificationDisplayer
|
||||||
|
import im.vector.app.features.notifications.NotificationDrawerManager
|
||||||
|
import io.mockk.confirmVerified
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verify
|
||||||
|
import io.mockk.verifyOrder
|
||||||
|
|
||||||
|
class FakeNotificationDisplayer {
|
||||||
|
|
||||||
|
val instance = mockk<NotificationDisplayer>(relaxed = true)
|
||||||
|
|
||||||
|
fun verifySummaryCancelled() {
|
||||||
|
verify { instance.cancelNotificationMessage(tag = null, NotificationDrawerManager.SUMMARY_NOTIFICATION_ID) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun verifyNoOtherInteractions() {
|
||||||
|
confirmVerified(instance)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun verifyInOrder(verifyBlock: NotificationDisplayer.() -> Unit) {
|
||||||
|
verifyOrder { verifyBlock(instance) }
|
||||||
|
verifyNoOtherInteractions()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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 im.vector.app.test.fakes
|
||||||
|
|
||||||
|
import im.vector.app.features.notifications.GroupedNotificationEvents
|
||||||
|
import im.vector.app.features.notifications.NotificationFactory
|
||||||
|
import im.vector.app.features.notifications.OneShotNotification
|
||||||
|
import im.vector.app.features.notifications.RoomNotification
|
||||||
|
import im.vector.app.features.notifications.SummaryNotification
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
|
||||||
|
class FakeNotificationFactory {
|
||||||
|
|
||||||
|
val instance = mockk<NotificationFactory>()
|
||||||
|
|
||||||
|
fun givenNotificationsFor(groupedEvents: GroupedNotificationEvents,
|
||||||
|
myUserId: String,
|
||||||
|
myUserDisplayName: String,
|
||||||
|
myUserAvatarUrl: String?,
|
||||||
|
useCompleteNotificationFormat: Boolean,
|
||||||
|
roomNotifications: List<RoomNotification>,
|
||||||
|
invitationNotifications: List<OneShotNotification>,
|
||||||
|
simpleNotifications: List<OneShotNotification>,
|
||||||
|
summaryNotification: SummaryNotification) {
|
||||||
|
with(instance) {
|
||||||
|
every { groupedEvents.roomEvents.toNotifications(myUserDisplayName, myUserAvatarUrl) } returns roomNotifications
|
||||||
|
every { groupedEvents.invitationEvents.toNotifications(myUserId) } returns invitationNotifications
|
||||||
|
every { groupedEvents.simpleEvents.toNotifications(myUserId) } returns simpleNotifications
|
||||||
|
|
||||||
|
every {
|
||||||
|
createSummaryNotification(
|
||||||
|
roomNotifications,
|
||||||
|
invitationNotifications,
|
||||||
|
simpleNotifications,
|
||||||
|
useCompleteNotificationFormat
|
||||||
|
)
|
||||||
|
} returns summaryNotification
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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 im.vector.app.test.fakes
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import im.vector.app.features.notifications.InviteNotifiableEvent
|
||||||
|
import im.vector.app.features.notifications.NotificationUtils
|
||||||
|
import im.vector.app.features.notifications.SimpleNotifiableEvent
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
|
||||||
|
class FakeNotificationUtils {
|
||||||
|
|
||||||
|
val instance = mockk<NotificationUtils>()
|
||||||
|
|
||||||
|
fun givenBuildRoomInvitationNotificationFor(event: InviteNotifiableEvent, myUserId: String): Notification {
|
||||||
|
val mockNotification = mockk<Notification>()
|
||||||
|
every { instance.buildRoomInvitationNotification(event, myUserId) } returns mockNotification
|
||||||
|
return mockNotification
|
||||||
|
}
|
||||||
|
|
||||||
|
fun givenBuildSimpleInvitationNotificationFor(event: SimpleNotifiableEvent, myUserId: String): Notification {
|
||||||
|
val mockNotification = mockk<Notification>()
|
||||||
|
every { instance.buildSimpleEventNotification(event, myUserId) } returns mockNotification
|
||||||
|
return mockNotification
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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 im.vector.app.test.fakes
|
||||||
|
|
||||||
|
import im.vector.app.features.notifications.NotifiableEvent
|
||||||
|
import im.vector.app.features.notifications.OutdatedEventDetector
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
|
||||||
|
class FakeOutdatedEventDetector {
|
||||||
|
val instance = mockk<OutdatedEventDetector>()
|
||||||
|
|
||||||
|
fun givenEventIsOutOfDate(notifiableEvent: NotifiableEvent) {
|
||||||
|
every { instance.isMessageOutdated(notifiableEvent) } returns true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun givenEventIsInDate(notifiableEvent: NotifiableEvent) {
|
||||||
|
every { instance.isMessageOutdated(notifiableEvent) } returns false
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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 im.vector.app.test.fakes
|
||||||
|
|
||||||
|
import im.vector.app.features.notifications.NotifiableMessageEvent
|
||||||
|
import im.vector.app.features.notifications.RoomGroupMessageCreator
|
||||||
|
import im.vector.app.features.notifications.RoomNotification
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
|
||||||
|
class FakeRoomGroupMessageCreator {
|
||||||
|
|
||||||
|
val instance = mockk<RoomGroupMessageCreator>()
|
||||||
|
|
||||||
|
fun givenCreatesRoomMessageFor(events: List<NotifiableMessageEvent>,
|
||||||
|
roomId: String,
|
||||||
|
userDisplayName: String,
|
||||||
|
userAvatarUrl: String?): RoomNotification.Message {
|
||||||
|
val mockMessage = mockk<RoomNotification.Message>()
|
||||||
|
every { instance.createRoomMessage(events, roomId, userDisplayName, userAvatarUrl) } returns mockMessage
|
||||||
|
return mockMessage
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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 im.vector.app.test.fakes
|
||||||
|
|
||||||
|
import im.vector.app.features.notifications.SummaryGroupMessageCreator
|
||||||
|
import io.mockk.mockk
|
||||||
|
|
||||||
|
class FakeSummaryGroupMessageCreator {
|
||||||
|
|
||||||
|
val instance = mockk<SummaryGroupMessageCreator>()
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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 im.vector.app.test.fakes
|
||||||
|
|
||||||
|
import im.vector.app.features.settings.VectorPreferences
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
|
||||||
|
class FakeVectorPreferences {
|
||||||
|
|
||||||
|
val instance = mockk<VectorPreferences>()
|
||||||
|
|
||||||
|
fun givenUseCompleteNotificationFormat(value: Boolean) {
|
||||||
|
every { instance.useCompleteNotificationFormat() } returns value
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue