Get real push rules from server and evaluate them

This commit is contained in:
Valere 2019-06-21 18:26:35 +02:00 committed by Benoit Marty
parent 2e417a9143
commit 0584fc3666
49 changed files with 1535 additions and 274 deletions

View File

@ -43,51 +43,53 @@ class Action(val type: Type) {
var stringValue: String? = null var stringValue: String? = null
var boolValue: Boolean? = null var boolValue: Boolean? = null


} companion object {

fun mapFrom(pushRule: PushRule): List<Action>? {
fun PushRule.domainActions(): List<Action>? { val actions = ArrayList<Action>()
val actions = ArrayList<Action>() pushRule.actions.forEach { actionStrOrObj ->
this.actions.forEach { actionStrOrObj -> if (actionStrOrObj is String) {
if (actionStrOrObj is String) { when (actionStrOrObj) {
val action = when (actionStrOrObj) { Action.Type.NOTIFY.value -> Action(Action.Type.NOTIFY)
Action.Type.NOTIFY.value -> Action(Action.Type.NOTIFY) Action.Type.DONT_NOTIFY.value -> Action(Action.Type.DONT_NOTIFY)
Action.Type.DONT_NOTIFY.value -> Action(Action.Type.DONT_NOTIFY) else -> {
else -> { Timber.w("Unsupported action type ${actionStrOrObj}")
Timber.w("Unsupported action type ${actionStrOrObj}") null
null }
} }?.let {
}?.let { actions.add(it)
actions.add(it) }
} } else if (actionStrOrObj is Map<*, *>) {
} else if (actionStrOrObj is Map<*, *>) { val tweakAction = actionStrOrObj["set_tweak"] as? String
val tweakAction = actionStrOrObj["set_tweak"] as? String when (tweakAction) {
when (tweakAction) { "sound" -> {
"sound" -> { (actionStrOrObj["value"] as? String)?.let { stringValue ->
(actionStrOrObj["value"] as? String)?.let { stringValue -> Action(Action.Type.SET_TWEAK).also {
Action(Action.Type.SET_TWEAK).also { it.tweak_action = "sound"
it.tweak_action = "sound" it.stringValue = stringValue
it.stringValue = stringValue actions.add(it)
actions.add(it) }
}
}
"highlight" -> {
(actionStrOrObj["value"] as? Boolean)?.let { boolValue ->
Action(Action.Type.SET_TWEAK).also {
it.tweak_action = "highlight"
it.boolValue = boolValue
actions.add(it)
}
}
}
else -> {
Timber.w("Unsupported action type ${actionStrOrObj}")
} }
} }
} } else {
"highlight" -> {
(actionStrOrObj["value"] as? Boolean)?.let { boolValue ->
Action(Action.Type.SET_TWEAK).also {
it.tweak_action = "highlight"
it.boolValue = boolValue
actions.add(it)
}
}
}
else -> {
Timber.w("Unsupported action type ${actionStrOrObj}") Timber.w("Unsupported action type ${actionStrOrObj}")
return null
} }
} }
} else { return if (actions.isEmpty()) null else actions
Timber.w("Unsupported action type ${actionStrOrObj}")
return null
} }
} }
return if (actions.isEmpty()) null else actions
} }


View File

@ -15,25 +15,23 @@
*/ */
package im.vector.matrix.android.api.pushrules package im.vector.matrix.android.api.pushrules


import im.vector.matrix.android.api.session.events.model.Event

abstract class Condition(val kind: Kind) { abstract class Condition(val kind: Kind) {


enum class Kind(val value: String) { enum class Kind(val value: String) {
EVENT_MATCH("event_match"), event_match("event_match"),
CONTAINS_DISPLAY_NAME("contains_display_name"), contains_display_name("contains_display_name"),
ROOM_MEMBER_COUNT("room_member_count"), room_member_count("room_member_count"),
SENDER_NOTIFICATION_PERMISSION("sender_notification_permission"), sender_notification_permission("sender_notification_permission"),
UNRECOGNIZE(""); UNRECOGNIZE("");


companion object { companion object {


fun fromString(value: String): Kind { fun fromString(value: String): Kind {
return when (value) { return when (value) {
"event_match" -> EVENT_MATCH "event_match" -> event_match
"contains_display_name" -> CONTAINS_DISPLAY_NAME "contains_display_name" -> contains_display_name
"room_member_count" -> ROOM_MEMBER_COUNT "room_member_count" -> room_member_count
"sender_notification_permission" -> SENDER_NOTIFICATION_PERMISSION "sender_notification_permission" -> sender_notification_permission
else -> UNRECOGNIZE else -> UNRECOGNIZE
} }
} }
@ -42,10 +40,9 @@ abstract class Condition(val kind: Kind) {


} }


abstract fun isSatisfied(event: Event): Boolean abstract fun isSatisfied(conditionResolver: ConditionResolver): Boolean


companion object { open fun technicalDescription(): String {
//TODO factory methods? return "Kind: $kind"
} }

} }

View File

@ -15,11 +15,14 @@
*/ */
package im.vector.matrix.android.api.pushrules package im.vector.matrix.android.api.pushrules


import im.vector.matrix.android.api.pushrules.rest.PushRule /**
* Acts like a visitor on Conditions.
* This class as all required context needed to evaluate rules
*/
interface ConditionResolver {


interface PushRulesProvider { fun resolveEventMatchCondition(eventMatchCondition: EventMatchCondition): Boolean

fun resolveRoomMemberCountCondition(roomMemberCountCondition: RoomMemberCountCondition): Boolean
fun getOrderedPushrules(): List<PushRule> fun resolveSenderNotificationPermissionCondition(senderNotificationPermissionCondition: SenderNotificationPermissionCondition): Boolean

fun resolveContainsDisplayNameCondition(containsDisplayNameCondition: ContainsDisplayNameCondition) : Boolean
fun onRulesUpdate(newRules: List<PushRule>)
} }

View File

@ -0,0 +1,80 @@
/*
* 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.matrix.android.api.pushrules

import android.text.TextUtils
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import timber.log.Timber
import java.util.regex.Pattern

class ContainsDisplayNameCondition : Condition(Kind.contains_display_name) {

override fun isSatisfied(conditionResolver: ConditionResolver): Boolean {
return conditionResolver.resolveContainsDisplayNameCondition(this)
}

override fun technicalDescription(): String {
return "User is mentioned"
}

fun isSatisfied(event: Event, displayName: String): Boolean {
//TODO the spec says:
// Matches any message whose content is unencrypted and contains the user's current display name
var message = when (event.type) {
EventType.MESSAGE -> {
event.content.toModel<MessageContent>()
}
// EventType.ENCRYPTED -> {
// event.root.getClearContent()?.toModel<MessageContent>()
// }
else -> null
} ?: return false

return caseInsensitiveFind(displayName, message.body)
}


companion object {
/**
* Returns whether a string contains an occurrence of another, as a standalone word, regardless of case.
*
* @param subString the string to search for
* @param longString the string to search in
* @return whether a match was found
*/
fun caseInsensitiveFind(subString: String, longString: String): Boolean {
// add sanity checks
if (TextUtils.isEmpty(subString) || TextUtils.isEmpty(longString)) {
return false
}

var res = false

try {
val pattern = Pattern.compile("(\\W|^)" + Pattern.quote(subString) + "(\\W|$)", Pattern.CASE_INSENSITIVE)
res = pattern.matcher(longString).find()
} catch (e: Exception) {
Timber.e(e, "## caseInsensitiveFind() : failed")
}

return res
}
}
}

View File

@ -19,9 +19,18 @@ import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.di.MoshiProvider
import timber.log.Timber import timber.log.Timber


class EventMatchCondition(val key: String, val pattern: String) : Condition(Kind.EVENT_MATCH) { class EventMatchCondition(val key: String, val pattern: String) : Condition(Kind.event_match) {


override fun isSatisfied(event: Event): Boolean { override fun isSatisfied(conditionResolver: ConditionResolver) : Boolean {
return conditionResolver.resolveEventMatchCondition(this)
}

override fun technicalDescription(): String {
return "'$key' Matches '$pattern'"
}


fun isSatisfied(event: Event): Boolean {
//TODO encrypted events? //TODO encrypted events?
val rawJson = MoshiProvider.providesMoshi().adapter(Event::class.java).toJsonValue(event) as? Map<*, *> val rawJson = MoshiProvider.providesMoshi().adapter(Event::class.java).toJsonValue(event) as? Map<*, *>
?: return false ?: return false

View File

@ -15,12 +15,19 @@
*/ */
package im.vector.matrix.android.api.pushrules package im.vector.matrix.android.api.pushrules


import im.vector.matrix.android.api.pushrules.rest.PushRule
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event


interface PushRuleService { interface PushRuleService {




/**
* Fetch the push rules from the server
*/
fun fetchPushRules(scope: String = "global")

//TODO get push rule set //TODO get push rule set
fun getPushrules(scope: String = "global"): List<PushRule>


//TODO update rule //TODO update rule


@ -28,6 +35,8 @@ interface PushRuleService {


fun removePushRuleListener(listener: PushRuleListener) fun removePushRuleListener(listener: PushRuleListener)


// fun fulfilledBingRule(event: Event, rules: List<PushRule>): PushRule?

interface PushRuleListener { interface PushRuleListener {
fun onMatchRule(event: Event, actions: List<Action>) fun onMatchRule(event: Event, actions: List<Action>)
fun batchFinish() fun batchFinish()

View File

@ -0,0 +1,71 @@
/*
* 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.matrix.android.api.pushrules

import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.RoomService
import timber.log.Timber
import java.util.regex.Pattern

private val regex = Pattern.compile("^(==|<=|>=|<|>)?(\\d*)$")

class RoomMemberCountCondition(val `is`: String) : Condition(Kind.room_member_count) {

override fun isSatisfied(conditionResolver: ConditionResolver): Boolean {
return conditionResolver.resolveRoomMemberCountCondition(this)
}

override fun technicalDescription(): String {
return "Room member count is $`is`"
}

fun isSatisfied(event: Event, session: RoomService?): Boolean {
// sanity check^
val roomId = event.roomId ?: return false
val room = session?.getRoom(roomId) ?: return false

// Parse the is field into prefix and number the first time
val (prefix, count) = parseIsField() ?: return false

val numMembers = room.getNumberOfJoinedMembers()

return when (prefix) {
"<" -> numMembers < count
">" -> numMembers > count
"<=" -> numMembers <= count
">=" -> numMembers >= count
else -> numMembers == count
}
}

/**
* Parse the is field to extract meaningful information.
*/
private fun parseIsField(): Pair<String?, Int>? {
try {
val match = regex.matcher(`is`)
if (match.find()) {
val prefix = match.group(1)
val count = match.group(2).toInt()
return prefix to count
}
} catch (t: Throwable) {
Timber.d(t)
}
return null

}
}

View File

@ -0,0 +1,36 @@
package im.vector.matrix.android.api.pushrules

/*
* 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.
*/
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.PowerLevels


class SenderNotificationPermissionCondition(val key: String) : Condition(Kind.sender_notification_permission) {

override fun isSatisfied(conditionResolver: ConditionResolver): Boolean {
return conditionResolver.resolveSenderNotificationPermissionCondition(this)
}

override fun technicalDescription(): String {
return "User power level <$key>"
}


fun isSatisfied(event: Event, powerLevels: PowerLevels): Boolean {
return event.sender != null && powerLevels.getUserPowerLevel(event.sender) >= powerLevels.notificationLevel(key)
}
}

View File

@ -17,8 +17,7 @@ package im.vector.matrix.android.api.pushrules.rest


import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.pushrules.Condition import im.vector.matrix.android.api.pushrules.*
import im.vector.matrix.android.api.pushrules.EventMatchCondition
import timber.log.Timber import timber.log.Timber


@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
@ -37,7 +36,7 @@ data class PushCondition(


fun asExecutableCondition(): Condition? { fun asExecutableCondition(): Condition? {
return when (Condition.Kind.fromString(this.kind)) { return when (Condition.Kind.fromString(this.kind)) {
Condition.Kind.EVENT_MATCH -> { Condition.Kind.event_match -> {
if (this.key != null && this.pattern != null) { if (this.key != null && this.pattern != null) {
EventMatchCondition(key, pattern) EventMatchCondition(key, pattern)
} else { } else {
@ -45,10 +44,24 @@ data class PushCondition(
null null
} }
} }
Condition.Kind.CONTAINS_DISPLAY_NAME -> TODO() Condition.Kind.contains_display_name -> {
Condition.Kind.ROOM_MEMBER_COUNT -> TODO() ContainsDisplayNameCondition()
Condition.Kind.SENDER_NOTIFICATION_PERMISSION -> TODO() }
Condition.Kind.UNRECOGNIZE -> null Condition.Kind.room_member_count -> {
if (this.iz.isNullOrBlank()) {
Timber.e("Malformed ROOM_MEMBER_COUNT condition")
null
} else {
RoomMemberCountCondition(this.iz)
}
}
Condition.Kind.sender_notification_permission -> {
this.key?.let { SenderNotificationPermissionCondition(it) }
}
Condition.Kind.UNRECOGNIZE -> {
Timber.e("Unknwon kind $kind")
null
}
} }
} }
} }

View File

@ -25,7 +25,7 @@ data class PushRule(
//Required. The domainActions to perform when this rule is matched. //Required. The domainActions to perform when this rule is matched.
val actions: List<Any>, val actions: List<Any>,
//Required. Whether this is a default rule, or has been set explicitly. //Required. Whether this is a default rule, or has been set explicitly.
val default: Boolean, val default: Boolean? = false,
//Required. Whether the push rule is enabled or not. //Required. Whether the push rule is enabled or not.
val enabled: Boolean, val enabled: Boolean,
//Required. The ID of this rule. //Required. The ID of this rule.

View File

@ -22,7 +22,7 @@ import im.vector.matrix.android.api.pushrules.rest.Ruleset
* All push rulesets for a user. * All push rulesets for a user.
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class PushruleResponse( data class PushrulesResponse(
//Global rules, account level applying to all devices //Global rules, account level applying to all devices
val global: Ruleset, val global: Ruleset,
//Device specific rules, apply only to current device //Device specific rules, apply only to current device

View File

@ -49,6 +49,8 @@ interface MembershipService {
*/ */
fun getRoomMemberIdsLive(): LiveData<List<String>> fun getRoomMemberIdsLive(): LiveData<List<String>>


fun getNumberOfJoinedMembers() : Int

/** /**
* Invite a user in the room * Invite a user in the room
*/ */

View File

@ -0,0 +1,26 @@
package im.vector.matrix.android.internal.database.mapper

import im.vector.matrix.android.api.pushrules.rest.PushCondition
import im.vector.matrix.android.internal.database.model.PushConditionEntity


internal object PushConditionMapper {

fun map(entity: PushConditionEntity): PushCondition {
return PushCondition(
kind = entity.kind,
iz = entity.iz,
key = entity.key,
pattern = entity.pattern
)
}

fun map(domain: PushCondition): PushConditionEntity {
return PushConditionEntity(
kind = domain.kind,
iz = domain.iz,
key = domain.key,
pattern = domain.pattern
)
}
}

View File

@ -0,0 +1,105 @@
/*
* 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.matrix.android.internal.database.mapper

import com.squareup.moshi.Types
import im.vector.matrix.android.api.pushrules.Condition
import im.vector.matrix.android.api.pushrules.rest.PushCondition
import im.vector.matrix.android.api.pushrules.rest.PushRule
import im.vector.matrix.android.internal.database.model.PushRuleEntity
import im.vector.matrix.android.internal.di.MoshiProvider
import io.realm.RealmList
import timber.log.Timber


internal object PushRulesMapper {

private val moshiActionsAdapter = MoshiProvider.providesMoshi().adapter<List<Any>>(Types.newParameterizedType(List::class.java, Any::class.java))

// private val listOfAnyAdapter: JsonAdapter<List<Any>> =
// moshi.adapter<List<Any>>(Types.newParameterizedType(List::class.java, Any::class.java), kotlin.collections.emptySet(), "actions")

fun mapContentRule(pushrule: PushRuleEntity): PushRule {
return PushRule(
actions = fromActionStr(pushrule.actionsStr),
default = pushrule.default,
enabled = pushrule.enabled,
ruleId = pushrule.ruleId,
conditions = listOf(
PushCondition(Condition.Kind.event_match.name, "content.body", pushrule.pattern)
)
)
}

private fun fromActionStr(actionsStr: String?): List<Any> {
try {
return actionsStr?.let { moshiActionsAdapter.fromJson(it) } ?: emptyList()
} catch (e: Throwable) {
Timber.e(e, "## failed to map push rule actions <$actionsStr>")
return emptyList()
}
}


fun mapRoomRule(pushrule: PushRuleEntity): PushRule {
return PushRule(
actions = fromActionStr(pushrule.actionsStr),
default = pushrule.default,
enabled = pushrule.enabled,
ruleId = pushrule.ruleId,
conditions = listOf(
PushCondition(Condition.Kind.event_match.name, "room_id", pushrule.ruleId)
)
)
}

fun mapSenderRule(pushrule: PushRuleEntity): PushRule {
return PushRule(
actions = fromActionStr(pushrule.actionsStr),
default = pushrule.default,
enabled = pushrule.enabled,
ruleId = pushrule.ruleId,
conditions = listOf(
PushCondition(Condition.Kind.event_match.name, "user_id", pushrule.ruleId)
)
)
}


fun map(pushrule: PushRuleEntity): PushRule {
return PushRule(
actions = fromActionStr(pushrule.actionsStr),
default = pushrule.default,
enabled = pushrule.enabled,
ruleId = pushrule.ruleId,
conditions = pushrule.conditions?.map { PushConditionMapper.map(it) }
)
}

fun map(pushRule: PushRule): PushRuleEntity {
return PushRuleEntity(
actionsStr = moshiActionsAdapter.toJson(pushRule.actions),
default = pushRule.default ?: false,
enabled = pushRule.enabled,
ruleId = pushRule.ruleId,
pattern = pushRule.pattern,
conditions = pushRule.conditions?.let {
RealmList(*pushRule.conditions.map { PushConditionMapper.map(it) }.toTypedArray())
} ?: RealmList()
)
}

}

View File

@ -24,7 +24,7 @@ internal open class PushRulesEntity(
@Index var userId: String = "", @Index var userId: String = "",
var scope: String = "", var scope: String = "",
var rulesetKey: String = "", var rulesetKey: String = "",
var pushRules: RealmList<PushRulesEntity> = RealmList() var pushRules: RealmList<PushRuleEntity> = RealmList()
) : RealmObject() { ) : RealmObject() {
companion object companion object
} }

View File

@ -15,8 +15,10 @@
*/ */
package im.vector.matrix.android.internal.database.query package im.vector.matrix.android.internal.database.query


import im.vector.matrix.android.api.pushrules.rest.PushRule
import im.vector.matrix.android.internal.database.model.*
import im.vector.matrix.android.internal.database.model.PushRulesEntity
import im.vector.matrix.android.internal.database.model.PusherEntity import im.vector.matrix.android.internal.database.model.PusherEntity
import im.vector.matrix.android.internal.database.model.PusherEntityFields
import io.realm.Realm import io.realm.Realm
import io.realm.RealmQuery import io.realm.RealmQuery
import io.realm.kotlin.where import io.realm.kotlin.where
@ -29,5 +31,11 @@ internal fun PusherEntity.Companion.where(realm: Realm, userId: String, pushKey:
equalTo(PusherEntityFields.PUSH_KEY, pushKey) equalTo(PusherEntityFields.PUSH_KEY, pushKey)
} }
} }

}

internal fun PushRulesEntity.Companion.where(realm: Realm, userId: String, scope: String, rulesetKey: String) : RealmQuery<PushRulesEntity> {
return realm.where<PushRulesEntity>()
.equalTo(PushRulesEntityFields.USER_ID,userId)
.equalTo(PushRulesEntityFields.SCOPE,scope)
.equalTo(PushRulesEntityFields.RULESET_KEY,rulesetKey)
} }

View File

@ -25,6 +25,7 @@ import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.listeners.ProgressListener import im.vector.matrix.android.api.listeners.ProgressListener
import im.vector.matrix.android.api.pushrules.PushRuleService import im.vector.matrix.android.api.pushrules.PushRuleService
import im.vector.matrix.android.api.pushrules.rest.PushRule
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.cache.CacheService import im.vector.matrix.android.api.session.cache.CacheService
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
@ -82,6 +83,7 @@ import java.util.*




internal class DefaultSession(override val sessionParams: SessionParams) : Session, MatrixKoinComponent { internal class DefaultSession(override val sessionParams: SessionParams) : Session, MatrixKoinComponent {

companion object { companion object {
const val SCOPE: String = "session" const val SCOPE: String = "session"
} }
@ -497,4 +499,13 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi
return pushersService.livePushers() return pushersService.livePushers()
} }


override fun getPushrules(scope: String): List<PushRule> {
return pushRuleService.getPushrules(scope)
}

override fun fetchPushRules(scope: String) {
pushRuleService.fetchPushRules(scope)
}


} }

View File

@ -20,7 +20,6 @@ import android.content.Context
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.pushrules.PushRuleService import im.vector.matrix.android.api.pushrules.PushRuleService
import im.vector.matrix.android.api.pushrules.PushRulesProvider
import im.vector.matrix.android.api.session.cache.CacheService import im.vector.matrix.android.api.session.cache.CacheService
import im.vector.matrix.android.api.session.group.GroupService import im.vector.matrix.android.api.session.group.GroupService
import im.vector.matrix.android.api.session.pushers.PushersService import im.vector.matrix.android.api.session.pushers.PushersService
@ -38,11 +37,11 @@ import im.vector.matrix.android.internal.session.filter.*
import im.vector.matrix.android.internal.session.group.DefaultGroupService import im.vector.matrix.android.internal.session.group.DefaultGroupService
import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater
import im.vector.matrix.android.internal.session.notification.BingRuleWatcher import im.vector.matrix.android.internal.session.notification.BingRuleWatcher
import im.vector.matrix.android.internal.session.notification.MockPushRuleProvider import im.vector.matrix.android.internal.session.notification.DefaultPushRuleService
import im.vector.matrix.android.internal.session.notification.PushRulesManager
import im.vector.matrix.android.internal.session.pushers.* import im.vector.matrix.android.internal.session.pushers.*
import im.vector.matrix.android.internal.session.pushers.DefaultGetPusherTask import im.vector.matrix.android.internal.session.pushers.DefaultGetPusherTask
import im.vector.matrix.android.internal.session.pushers.DefaultPusherService import im.vector.matrix.android.internal.session.pushers.DefaultPusherService
import im.vector.matrix.android.internal.session.pushers.GetPushRulesTask
import im.vector.matrix.android.internal.session.pushers.GetPushersTask import im.vector.matrix.android.internal.session.pushers.GetPushersTask
import im.vector.matrix.android.internal.session.pushers.PushersAPI import im.vector.matrix.android.internal.session.pushers.PushersAPI
import im.vector.matrix.android.internal.session.room.* import im.vector.matrix.android.internal.session.room.*
@ -175,18 +174,25 @@ internal class SessionModule(private val sessionParams: SessionParams) {
} }


scope(DefaultSession.SCOPE) { scope(DefaultSession.SCOPE) {
MockPushRuleProvider() as PushRulesProvider val retrofit: Retrofit = get()
retrofit.create(PushrulesApi::class.java)
} }


scope(DefaultSession.SCOPE) { scope(DefaultSession.SCOPE) {
get<PushRulesManager>() as PushRuleService get<DefaultPushRuleService>() as PushRuleService
}
scope(DefaultSession.SCOPE) {
PushRulesManager(get(), get())
} }


scope(DefaultSession.SCOPE) { scope(DefaultSession.SCOPE) {
BingRuleWatcher(get(), get(), get(), get()) DefaultPushRuleService(get(), get(), get(), get())
}

scope(DefaultSession.SCOPE) {
DefaultGetPushrulesTask(get()) as GetPushRulesTask
}


scope(DefaultSession.SCOPE) {
BingRuleWatcher(get(), get())
} }


scope(DefaultSession.SCOPE) { scope(DefaultSession.SCOPE) {

View File

@ -16,19 +16,19 @@
package im.vector.matrix.android.internal.session.notification package im.vector.matrix.android.internal.session.notification


import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.pushrules.rest.PushRule
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.query.types import im.vector.matrix.android.internal.database.query.types
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.session.pushers.DefaultConditionResolver
import timber.log.Timber




internal class BingRuleWatcher(monarchy: Monarchy, internal class BingRuleWatcher(monarchy: Monarchy,
private val credentials: Credentials, private val defaultPushRuleService: DefaultPushRuleService) :
private val taskExecutor: TaskExecutor,
private val pushRulesManager: PushRulesManager) :
RealmLiveEntityObserver<EventEntity>(monarchy) { RealmLiveEntityObserver<EventEntity>(monarchy) {


override val query = Monarchy.Query<EventEntity> { override val query = Monarchy.Query<EventEntity> {
@ -41,9 +41,33 @@ internal class BingRuleWatcher(monarchy: Monarchy,


override fun processChanges(inserted: List<EventEntity>, updated: List<EventEntity>, deleted: List<EventEntity>) { override fun processChanges(inserted: List<EventEntity>, updated: List<EventEntity>, deleted: List<EventEntity>) {
//TODO task //TODO task
inserted.map { it.asDomain() }.let { val rules = defaultPushRuleService.getPushrules("global")
pushRulesManager.processEvents(it) inserted.map { it.asDomain() }.let { events ->
events.forEach { event ->
fulfilledBingRule(event, rules)?.let {
Timber.v("Rule $it match for event ${event.eventId}")
defaultPushRuleService.dispatchBing(event, it)
}
}
} }
defaultPushRuleService.dispatchFinish()
}

private fun fulfilledBingRule(event: Event, rules: List<PushRule>): PushRule? {
val conditionResolver = DefaultConditionResolver(event)
rules.filter { it.enabled }.forEach { rule ->
val isFullfilled = rule.conditions?.map {
it.asExecutableCondition()?.isSatisfied(conditionResolver) ?: false
}?.fold(true/*A rule with no conditions always matches*/, { acc, next ->
//All conditions must hold true for an event in order to apply the action for the event.
acc && next
}) ?: false

if (isFullfilled) {
return rule
}
}
return null
} }





View File

@ -0,0 +1,188 @@
/*
* 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.matrix.android.internal.session.notification

import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.pushrules.Action
import im.vector.matrix.android.api.pushrules.PushRuleService
import im.vector.matrix.android.api.pushrules.rest.PushRule
import im.vector.matrix.android.api.pushrules.rest.PushrulesResponse
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.internal.database.mapper.PushRulesMapper
import im.vector.matrix.android.internal.database.model.PushRulesEntity
import im.vector.matrix.android.internal.database.model.PusherEntityFields
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.pushers.GetPushRulesTask
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith


internal class DefaultPushRuleService(
private val sessionParams: SessionParams,
private val pushRulesTask: GetPushRulesTask,
private val taskExecutor: TaskExecutor,
private val monarchy: Monarchy
) : PushRuleService {


private var listeners = ArrayList<PushRuleService.PushRuleListener>()


override fun fetchPushRules(scope: String) {
pushRulesTask
.configureWith(Unit)
.dispatchTo(object : MatrixCallback<PushrulesResponse> {
override fun onSuccess(data: PushrulesResponse) {
monarchy.runTransactionSync { realm ->
//clear existings?
//TODO
realm.where(PushRulesEntity::class.java)
.equalTo(PusherEntityFields.USER_ID, sessionParams.credentials.userId)
.findAll().deleteAllFromRealm()

var content = PushRulesEntity(sessionParams.credentials.userId, scope, "content")
data.global.content?.forEach { rule ->
PushRulesMapper.map(rule).also {
content.pushRules.add(it)
}
}
realm.insertOrUpdate(content)

var override = PushRulesEntity(sessionParams.credentials.userId, scope, "override")
data.global.override?.forEach { rule ->
PushRulesMapper.map(rule).also {
override.pushRules.add(it)
}
}
realm.insertOrUpdate(override)

var rooms = PushRulesEntity(sessionParams.credentials.userId, scope, "room")
data.global.room?.forEach { rule ->
PushRulesMapper.map(rule).also {
rooms.pushRules.add(it)
}
}
realm.insertOrUpdate(rooms)

var senders = PushRulesEntity(sessionParams.credentials.userId, scope, "sender")
data.global.sender?.forEach { rule ->
PushRulesMapper.map(rule).also {
senders.pushRules.add(it)
}
}
realm.insertOrUpdate(senders)

var underrides = PushRulesEntity(sessionParams.credentials.userId, scope, "underride")
data.global.underride?.forEach { rule ->
PushRulesMapper.map(rule).also {
underrides.pushRules.add(it)
}
}
realm.insertOrUpdate(underrides)


}
}
})
.executeBy(taskExecutor)
}

override fun getPushrules(scope: String): List<PushRule> {

var contentRules: List<PushRule> = emptyList()
monarchy.doWithRealm { realm ->
PushRulesEntity.where(realm, sessionParams.credentials.userId, scope, "content").findFirst()?.let { re ->
contentRules = re.pushRules.map { PushRulesMapper.mapContentRule(it) }
}
}

var overrideRules: List<PushRule> = emptyList()
monarchy.doWithRealm { realm ->
PushRulesEntity.where(realm, sessionParams.credentials.userId, scope, "override").findFirst()?.let { re ->
overrideRules = re.pushRules.map { PushRulesMapper.map(it) }
}
}

var roomRules: List<PushRule> = emptyList()
monarchy.doWithRealm { realm ->
PushRulesEntity.where(realm, sessionParams.credentials.userId, scope, "room").findFirst()?.let { re ->
roomRules = re.pushRules.map { PushRulesMapper.mapRoomRule(it) }
}
}

var senderRules: List<PushRule> = emptyList()
monarchy.doWithRealm { realm ->
PushRulesEntity.where(realm, sessionParams.credentials.userId, scope, "sender").findFirst()?.let { re ->
senderRules = re.pushRules.map { PushRulesMapper.mapSenderRule(it) }
}
}

var underrideRules: List<PushRule> = emptyList()
monarchy.doWithRealm { realm ->
PushRulesEntity.where(realm, sessionParams.credentials.userId, scope, "underride").findFirst()?.let { re ->
underrideRules = re.pushRules.map { PushRulesMapper.map(it) }
}
}

return contentRules + overrideRules + roomRules + senderRules + underrideRules
}

override fun removePushRuleListener(listener: PushRuleService.PushRuleListener) {
listeners.remove(listener)
}


override fun addPushRuleListener(listener: PushRuleService.PushRuleListener) {
if (!listeners.contains(listener))
listeners.add(listener)
}

// fun processEvents(events: List<Event>) {
// var hasDoneSomething = false
// events.forEach { event ->
// fulfilledBingRule(event)?.let {
// hasDoneSomething = true
// dispatchBing(event, it)
// }
// }
// if (hasDoneSomething)
// dispatchFinish()
// }

fun dispatchBing(event: Event, rule: PushRule) {
try {
listeners.forEach {
it.onMatchRule(event, Action.mapFrom(rule) ?: emptyList())
}
} catch (e: Throwable) {

}
}

fun dispatchFinish() {
try {
listeners.forEach {
it.batchFinish()
}
} catch (e: Throwable) {

}
}


}

View File

@ -1,41 +0,0 @@
package im.vector.matrix.android.internal.session.notification

import im.vector.matrix.android.api.pushrules.PushRulesProvider
import im.vector.matrix.android.api.pushrules.rest.PushRule
import im.vector.matrix.android.internal.di.MoshiProvider


class MockPushRuleProvider : PushRulesProvider {
override fun getOrderedPushrules(): List<PushRule> {
val raw = """
{
"actions": [
"notify",
{
"set_tweak": "highlight",
"value": false
}
],
"conditions": [
{
"key": "type",
"kind": "event_match",
"pattern": "m.room.message"
}
],
"default": true,
"enabled": true,
"rule_id": ".m.rule.message"
}
""".trimIndent()
val pushRule = MoshiProvider.providesMoshi().adapter<PushRule>(PushRule::class.java).fromJson(raw)

return listOf<PushRule>(
pushRule!!
)
}

override fun onRulesUpdate(newRules: List<PushRule>) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}

View File

@ -1,85 +0,0 @@
/*
* 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.matrix.android.internal.session.notification

import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.pushrules.PushRuleService
import im.vector.matrix.android.api.pushrules.PushRulesProvider
import im.vector.matrix.android.api.pushrules.domainActions
import im.vector.matrix.android.api.pushrules.rest.PushRule
import im.vector.matrix.android.api.session.events.model.Event


internal class PushRulesManager(
private val sessionParams: SessionParams,
private val pushRulesProvider: PushRulesProvider) : PushRuleService {


private var listeners = ArrayList<PushRuleService.PushRuleListener>()


override fun removePushRuleListener(listener: PushRuleService.PushRuleListener) {
listeners.remove(listener)
}


override fun addPushRuleListener(listener: PushRuleService.PushRuleListener) {
if (!listeners.contains(listener))
listeners.add(listener)
}

fun processEvents(events: List<Event>) {
var hasDoneSomething = false
events.forEach { event ->
fulfilledBingRule(event)?.let {
hasDoneSomething = true
dispatchBing(event, it)
}
}
if (hasDoneSomething)
dispatchFinish()
}

fun dispatchBing(event: Event, rule: PushRule) {
try {
listeners.forEach {
it.onMatchRule(event, rule.domainActions() ?: emptyList())
}
} catch (e: Throwable) {

}
}

fun dispatchFinish() {
try {
listeners.forEach {
it.batchFinish()
}
} catch (e: Throwable) {

}
}

fun fulfilledBingRule(event: Event): PushRule? {
pushRulesProvider.getOrderedPushrules().forEach { rule ->
rule.conditions?.mapNotNull { it.asExecutableCondition() }?.forEach {
if (it.isSatisfied(event)) return rule
}
}
return null
}

}

View File

@ -0,0 +1,55 @@
/*
* 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.matrix.android.internal.session.pushers

import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.pushrules.*
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.RoomService
import im.vector.matrix.android.internal.di.MatrixKoinComponent
import org.koin.standalone.inject
import timber.log.Timber

internal class DefaultConditionResolver(val event: Event) : ConditionResolver, MatrixKoinComponent {

private val roomService by inject<RoomService>()

private val sessionParams by inject<SessionParams>()

override fun resolveEventMatchCondition(eventMatchCondition: EventMatchCondition): Boolean {
return eventMatchCondition.isSatisfied(event)
}

override fun resolveRoomMemberCountCondition(roomMemberCountCondition: RoomMemberCountCondition): Boolean {
return roomMemberCountCondition.isSatisfied(event, roomService)
}

override fun resolveSenderNotificationPermissionCondition(senderNotificationPermissionCondition: SenderNotificationPermissionCondition): Boolean {
// val roomId = event.roomId ?: return false
// val room = roomService.getRoom(roomId) ?: return false
//TODO RoomState not yet managed
Timber.e("POWER LEVELS STATE NOT YET MANAGED BY RIOTX")
return false //senderNotificationPermissionCondition.isSatisfied(event, )
}

override fun resolveContainsDisplayNameCondition(containsDisplayNameCondition: ContainsDisplayNameCondition): Boolean {
val roomId = event.roomId ?: return false
val room = roomService.getRoom(roomId) ?: return false
val myDisplayName = room.getRoomMember(sessionParams.credentials.userId)?.displayName
?: return false
return containsDisplayNameCondition.isSatisfied(event, myDisplayName)
}
}

View File

@ -0,0 +1,33 @@
/*
* 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.matrix.android.internal.session.pushers

import arrow.core.Try
import im.vector.matrix.android.api.pushrules.rest.PushrulesResponse
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task


internal interface GetPushRulesTask : Task<Unit, PushrulesResponse>

internal class DefaultGetPushrulesTask(private val pushrulesApi: PushrulesApi) : GetPushRulesTask {

override suspend fun execute(params: Unit): Try<PushrulesResponse> {
return executeRequest {
apiCall = pushrulesApi.getAllRules()
}
}
}

View File

@ -0,0 +1,72 @@
/*
* 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.matrix.android.internal.session.pushers

import im.vector.matrix.android.api.pushrules.rest.PushRule
import im.vector.matrix.android.api.pushrules.rest.PushrulesResponse
import im.vector.matrix.android.internal.network.NetworkConstants
import retrofit2.Call
import retrofit2.http.*


internal interface PushrulesApi {
/**
* Get all push rules
*/
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushrules/")
fun getAllRules(): Call<PushrulesResponse>

/**
* Update the ruleID enable status
*
* @param kind the notification kind (sender, room...)
* @param ruleId the ruleId
* @param enable the new enable status
*/
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushrules/global/{kind}/{ruleId}/enabled")
fun updateEnableRuleStatus(@Path("kind") kind: String, @Path("ruleId") ruleId: String, @Body enable: Boolean?): Call<Unit>


/**
* Update the ruleID enable status
*
* @param kind the notification kind (sender, room...)
* @param ruleId the ruleId
* @param actions the actions
*/
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushrules/global/{kind}/{ruleId}/actions")
fun updateRuleActions(@Path("kind") kind: String, @Path("ruleId") ruleId: String, @Body actions: Any): Call<Unit>


/**
* Update the ruleID enable status
*
* @param kind the notification kind (sender, room...)
* @param ruleId the ruleId
*/
@DELETE(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushrules/global/{kind}/{ruleId}")
fun deleteRule(@Path("kind") kind: String, @Path("ruleId") ruleId: String): Call<Unit>

/**
* Add the ruleID enable status
*
* @param kind the notification kind (sender, room...)
* @param ruleId the ruleId.
* @param rule the rule to add.
*/
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushrules/global/{kind}/{ruleId}")
fun addRule(@Path("kind") kind: String, @Path("ruleId") ruleId: String, @Body rule: PushRule): Call<Unit>
}

View File

@ -66,6 +66,14 @@ internal class DefaultMembershipService(private val roomId: String,
) )
} }


override fun getNumberOfJoinedMembers(): Int {
var result = 0
monarchy.runTransactionSync {
result = RoomMembers(it, roomId).getNumberOfJoinedMembers()
}
return result
}

override fun invite(userId: String, callback: MatrixCallback<Unit>) { override fun invite(userId: String, callback: MatrixCallback<Unit>) {
val params = InviteTask.Params(roomId, userId) val params = InviteTask.Params(roomId, userId)
inviteTask.configureWith(params) inviteTask.configureWith(params)

View File

@ -1,10 +1,23 @@
package im.vector.matrix.android.api.pushrules package im.vector.matrix.android.api.pushrules


import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.toContent import im.vector.matrix.android.api.session.events.model.toContent
import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.RoomService
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.util.Cancelable
import org.junit.Assert
import org.junit.Test import org.junit.Test


class PushrulesConditionTest { class PushrulesConditionTest {
@ -100,4 +113,179 @@ class PushrulesConditionTest {


assert(condition.isSatisfied(simpleTextEvent)) assert(condition.isSatisfied(simpleTextEvent))
} }


@Test
fun test_roommember_condition() {


val conditionEqual3 = RoomMemberCountCondition("3")
val conditionEqual3Bis = RoomMemberCountCondition("==3")
val conditionLessThan3 = RoomMemberCountCondition("<3")

val session = MockRoomService()

Event(
type = "m.room.message",
eventId = "mx0",
content = MessageTextContent("m.text", "A").toContent(),
originServerTs = 0,
roomId = "2joined").also {
Assert.assertFalse("This room does not have 3 members", conditionEqual3.isSatisfied(it, session))
Assert.assertFalse("This room does not have 3 members", conditionEqual3Bis.isSatisfied(it, session))
Assert.assertTrue("This room has less than 3 members", conditionLessThan3.isSatisfied(it, session))
}

Event(
type = "m.room.message",
eventId = "mx0",
content = MessageTextContent("m.text", "A").toContent(),
originServerTs = 0,
roomId = "3joined").also {
Assert.assertTrue("This room has 3 members",conditionEqual3.isSatisfied(it, session))
Assert.assertTrue("This room has 3 members",conditionEqual3Bis.isSatisfied(it, session))
Assert.assertFalse("This room has more than 3 members",conditionLessThan3.isSatisfied(it, session))
}
}


class MockRoomService() : RoomService {
override fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback<String>) {

}

override fun getRoom(roomId: String): Room? {
return when (roomId) {
"2joined" -> MockRoom(roomId, 2)
"3joined" -> MockRoom(roomId, 3)
else -> null
}
}

override fun liveRoomSummaries(): LiveData<List<RoomSummary>> {
return MutableLiveData()
}

}

class MockRoom(override val roomId: String, val _numberOfJoinedMembers: Int) : Room {


override fun getNumberOfJoinedMembers(): Int {
return _numberOfJoinedMembers
}

override val roomSummary: LiveData<RoomSummary>
get() = TODO("not implemented") //To change initializer of created properties use File | Settings | File Templates.

override fun createTimeline(eventId: String?, allowedTypes: List<String>?): Timeline {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}

override fun getTimeLineEvent(eventId: String): TimelineEvent? {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}

override fun sendTextMessage(text: String, msgType: String, autoMarkdown: Boolean): Cancelable {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}

override fun sendFormattedTextMessage(text: String, formattedText: String): Cancelable {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}

override fun sendMedia(attachment: ContentAttachmentData): Cancelable {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}

override fun sendMedias(attachments: List<ContentAttachmentData>): Cancelable {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}

override fun redactEvent(event: Event, reason: String?): Cancelable {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}

override fun markAllAsRead(callback: MatrixCallback<Unit>) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}

override fun setReadReceipt(eventId: String, callback: MatrixCallback<Unit>) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}

override fun setReadMarker(fullyReadEventId: String, callback: MatrixCallback<Unit>) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}

override fun isEventRead(eventId: String): Boolean {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}

override fun loadRoomMembersIfNeeded(): Cancelable {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}

override fun getRoomMember(userId: String): RoomMember? {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}

override fun getRoomMemberIdsLive(): LiveData<List<String>> {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}

override fun invite(userId: String, callback: MatrixCallback<Unit>) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}

override fun join(callback: MatrixCallback<Unit>) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}

override fun leave(callback: MatrixCallback<Unit>) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}

override fun updateTopic(topic: String, callback: MatrixCallback<Unit>) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}

override fun sendReaction(reaction: String, targetEventId: String): Cancelable {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}

override fun undoReaction(reaction: String, targetEventId: String, myUserId: String) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}

override fun updateQuickReaction(reaction: String, oppositeReaction: String, targetEventId: String, myUserId: String) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}

override fun editTextMessage(targetEventId: String, newBodyText: String, newBodyAutoMarkdown: Boolean, compatibilityBodyText: String): Cancelable {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}

override fun replyToMessage(eventReplied: Event, replyText: String): Cancelable? {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}

override fun getEventSummaryLive(eventId: String): LiveData<List<EventAnnotationsSummary>> {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}

override fun isEncrypted(): Boolean {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}

override fun encryptionAlgorithm(): String? {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}

override fun shouldEncryptForInvitedMembers(): Boolean {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}

}

} }

View File

@ -137,8 +137,10 @@ class VectorApplication : Application() {
} else { } else {
//TODO check if notifications are enabled for this device //TODO check if notifications are enabled for this device
//We need to use alarm in this mode //We need to use alarm in this mode
AlarmSyncBroadcastReceiver.scheduleAlarm(applicationContext,4_000L) if (Matrix.getInstance().currentSession != null) {
Timber.i("Alarm scheduled to restart service") AlarmSyncBroadcastReceiver.scheduleAlarm(applicationContext, 4_000L)
Timber.i("Alarm scheduled to restart service")
}


} }
} }
@ -150,6 +152,7 @@ class VectorApplication : Application() {
it.refreshPushers() it.refreshPushers()
//bind to the sync service //bind to the sync service
get<PushRuleTriggerListener>().startWithSession(it) get<PushRuleTriggerListener>().startWithSession(it)
it.fetchPushRules()
} }
} }



View File

@ -25,7 +25,7 @@ import android.widget.TextView
import androidx.preference.PreferenceViewHolder import androidx.preference.PreferenceViewHolder
import im.vector.riotredesign.R import im.vector.riotredesign.R


// TODO Replace by real Bingrule class // TODO Replace by real Bingrule class, then delete
class BingRule(rule: BingRule) { class BingRule(rule: BingRule) {
fun shouldNotNotify() = false fun shouldNotNotify() = false
fun shouldNotify() = false fun shouldNotify() = false

View File

@ -0,0 +1,62 @@
/*
* 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.riotredesign.core.ui.list

import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
import im.vector.riotredesign.core.extensions.setTextOrHide

/**
* A generic list item.
* Displays an item with a title, and optional description.
* Can display an accessory on the right, that can be an image or an indeterminate progress.
* If provided with an action, will display a button at the bottom of the list item.
*/
@EpoxyModelClass(layout = R.layout.item_generic_footer)
abstract class GenericFooterItem : VectorEpoxyModel<GenericFooterItem.Holder>() {


@EpoxyAttribute
var text: String? = null

@EpoxyAttribute
var style: GenericItem.STYLE = GenericItem.STYLE.NORMAL_TEXT

@EpoxyAttribute
var itemClickAction: GenericItem.Action? = null

override fun bind(holder: Holder) {

holder.text.setTextOrHide(text)
when (style) {
GenericItem.STYLE.BIG_TEXT -> holder.text.textSize = 18f
GenericItem.STYLE.NORMAL_TEXT -> holder.text.textSize = 14f
}


holder.view.setOnClickListener {
itemClickAction?.perform?.run()
}
}

class Holder : VectorEpoxyHolder() {
val text by bind<TextView>(R.id.itemGenericFooterText)
}
}

View File

@ -78,6 +78,7 @@ class LoginActivity : VectorBaseActivity() {
data.setFilter(FilterService.FilterPreset.RiotFilter) data.setFilter(FilterService.FilterPreset.RiotFilter)
data.startSync() data.startSync()
get<PushRuleTriggerListener>().startWithSession(data) get<PushRuleTriggerListener>().startWithSession(data)
data.fetchPushRules()
goToHome() goToHome()
} }



View File

@ -43,7 +43,7 @@ class NotifiableEventResolver(val context: Context) {


//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): NotifiableEvent? {




// val store = session.dataHandler.store // val store = session.dataHandler.store
@ -55,7 +55,7 @@ class NotifiableEventResolver(val context: Context) {


when (event.getClearType()) { when (event.getClearType()) {
EventType.MESSAGE -> { EventType.MESSAGE -> {
return resolveMessageEvent(event, bingRule, session) return resolveMessageEvent(event, session)
} }
// EventType.ENCRYPTED -> { // EventType.ENCRYPTED -> {
// val messageEvent = resolveMessageEvent(event, bingRule, session, store) // val messageEvent = resolveMessageEvent(event, bingRule, session, store)
@ -88,12 +88,10 @@ class NotifiableEventResolver(val context: Context) {
} }




private fun resolveMessageEvent(event: Event, pushRule: PushRule?, session: Session): NotifiableEvent? { private fun resolveMessageEvent(event: Event, session: Session): NotifiableEvent? {
//If we are here, that means that the event should be notified to the user, we check now how it should be presented (sound) //If we are here, that means that the event should be notified to the user, we check now how it should be presented (sound)
// val soundName = pushRule?.notificationSound // val soundName = pushRule?.notificationSound


val noisy = true//pushRule?.notificationSound != null

//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.roomId!! /*roomID cannot be null (see Matrix SDK code)*/) val room = session.getRoom(event.roomId!! /*roomID cannot be null (see Matrix SDK code)*/)


@ -109,7 +107,7 @@ class NotifiableEventResolver(val context: Context) {
val notifiableEvent = NotifiableMessageEvent( val notifiableEvent = NotifiableMessageEvent(
eventId = event.eventId ?: "", eventId = event.eventId ?: "",
timestamp = event.originServerTs ?: 0, timestamp = event.originServerTs ?: 0,
noisy = noisy, noisy = false,//will be updated
senderName = senderDisplayName, senderName = senderDisplayName,
senderId = event.senderId, senderId = event.senderId,
body = body, body = body,
@ -130,7 +128,7 @@ class NotifiableEventResolver(val context: Context) {
val notifiableEvent = NotifiableMessageEvent( val notifiableEvent = NotifiableMessageEvent(
eventId = event.eventId!!, eventId = event.eventId!!,
timestamp = event.originServerTs ?: 0, timestamp = event.originServerTs ?: 0,
noisy = noisy, noisy = false,//will be updated
senderName = senderDisplayName, senderName = senderDisplayName,
senderId = event.senderId, senderId = event.senderId,
body = body, body = body,

View File

@ -0,0 +1,41 @@
/*
* 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.riotredesign.features.notifications

import im.vector.matrix.android.api.pushrules.Action

data class NotificationAction(
val shouldNotify: Boolean,
val highlight: Boolean = false,
val soundName: String? = null
) {
companion object {
fun extractFrom(ruleActions: List<Action>): NotificationAction {
var shouldNotify = false
var highlight = false
var sound: String? = null
ruleActions.forEach {
if (it.type == Action.Type.NOTIFY) shouldNotify = true
if (it.type == Action.Type.DONT_NOTIFY) shouldNotify = false
if (it.type == Action.Type.SET_TWEAK) {
if (it.tweak_action == "highlight") highlight = it.boolValue ?: false
if (it.tweak_action == "sound") sound = it.stringValue
}
}
return NotificationAction(shouldNotify, highlight, sound)
}
}
}

View File

@ -57,28 +57,6 @@ class NotificationDrawerManager(val context: Context, private val outdatedDetect
} }
}) })


/**
* No multi session support for now
*/
private fun initWithSession(session: Session?) {
session?.let {
/*
myUserDisplayName = it.myUser?.displayname ?: it.myUserId

// User Avatar
it.myUser?.avatarUrl?.let { avatarUrl ->
val userAvatarUrlPath = it.mediaCache?.thumbnailCacheFile(avatarUrl, avatarSize)
if (userAvatarUrlPath != null) {
myUserAvatarUrl = userAvatarUrlPath.path
} else {
// prepare for the next time
session.mediaCache?.loadAvatarThumbnail(session.homeServerConfig, ImageView(context), avatarUrl, avatarSize)
}
}
*/
}
}

/** /**
Should be called as soon as a new event is ready to be displayed. Should be called as soon as a new event is ready to be displayed.
The notification corresponding to this event will not be displayed until The notification corresponding to this event will not be displayed until

View File

@ -16,12 +16,24 @@ class PushRuleTriggerListener(
var session: Session? = null var session: Session? = null


override fun onMatchRule(event: Event, actions: List<Action>) { override fun onMatchRule(event: Event, actions: List<Action>) {
Timber.v("Push rule match for event ${event.eventId}")
if (session == null) { if (session == null) {
Timber.e("Called without active session") Timber.e("Called without active session")
return return
} }
resolver.resolveEvent(event,null,session!!)?.let { val notificationAction = NotificationAction.extractFrom(actions)
drawerManager.onNotifiableEventReceived(it) if (notificationAction.shouldNotify) {
val resolveEvent = resolver.resolveEvent(event, session!!)
if (resolveEvent == null) {
Timber.v("## Failed to resolve event")
//TODO
} else {
resolveEvent.noisy = !notificationAction.soundName.isNullOrBlank()
Timber.v("New event to notify $resolveEvent tweaks:$notificationAction")
drawerManager.onNotifiableEventReceived(resolveEvent)
}
} else {
Timber.v("Matched push rule is set to not notify")
} }
} }


@ -43,4 +55,5 @@ class PushRuleTriggerListener(
drawerManager.clearAllEvents() drawerManager.clearAllEvents()
drawerManager.refreshNotificationDrawer() drawerManager.refreshNotificationDrawer()
} }

} }

View File

@ -1671,15 +1671,15 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref


var index = 0 var index = 0


for (pusher in mDisplayedPushers) { for (pushRule in mDisplayedPushers) {
if (null != pusher.lang) { if (null != pushRule.lang) {
val isThisDeviceTarget = TextUtils.equals(pushManager.currentRegistrationToken, pusher.pushkey) val isThisDeviceTarget = TextUtils.equals(pushManager.currentRegistrationToken, pushRule.pushkey)


val preference = VectorPreference(activity).apply { val preference = VectorPreference(activity).apply {
mTypeface = if (isThisDeviceTarget) Typeface.BOLD else Typeface.NORMAL mTypeface = if (isThisDeviceTarget) Typeface.BOLD else Typeface.NORMAL
} }
preference.title = pusher.deviceDisplayName preference.title = pushRule.deviceDisplayName
preference.summary = pusher.appDisplayName preference.summary = pushRule.appDisplayName
preference.key = PUSHER_PREFERENCE_KEY_BASE + index preference.key = PUSHER_PREFERENCE_KEY_BASE + index
index++ index++
mPushersSettingsCategory.addPreference(preference) mPushersSettingsCategory.addPreference(preference)
@ -1694,7 +1694,7 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
.setPositiveButton(R.string.remove) .setPositiveButton(R.string.remove)
{ _, _ -> { _, _ ->
displayLoadingView() displayLoadingView()
pushManager.unregister(session, pusher, object : MatrixCallback<Unit> { pushManager.unregister(session, pushRule, object : MatrixCallback<Unit> {
override fun onSuccess(info: Void?) { override fun onSuccess(info: Void?) {
refreshPushersList() refreshPushersList()
onCommonDone(null) onCommonDone(null)

View File

@ -17,7 +17,6 @@
package im.vector.riotredesign.features.settings.push package im.vector.riotredesign.features.settings.push


import android.os.Bundle import android.os.Bundle
import android.widget.LinearLayout
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -27,19 +26,21 @@ import com.airbnb.mvrx.withState
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.VectorBaseActivity import im.vector.riotredesign.core.platform.VectorBaseActivity
import im.vector.riotredesign.core.platform.VectorBaseFragment import im.vector.riotredesign.core.platform.VectorBaseFragment
import kotlinx.android.synthetic.main.fragment_settings_pushgateways.* import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.core.ui.list.genericFooterItem
import kotlinx.android.synthetic.main.fragment_generic_recycler_epoxy.*


class PushGatewaysFragment : VectorBaseFragment() { class PushGatewaysFragment : VectorBaseFragment() {


override fun getLayoutResId(): Int = R.layout.fragment_settings_pushgateways override fun getLayoutResId(): Int = R.layout.fragment_generic_recycler_epoxy


private val viewModel: PushGatewaysViewModel by fragmentViewModel(PushGatewaysViewModel::class) private val viewModel: PushGatewaysViewModel by fragmentViewModel(PushGatewaysViewModel::class)


private val epoxyController by lazy { PushGateWayController() } private val epoxyController by lazy { PushGateWayController(StringProvider(requireContext().resources)) }


override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
(activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_push_gateways) (activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_notifications_targets)
} }


override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
@ -56,13 +57,26 @@ class PushGatewaysFragment : VectorBaseFragment() {
epoxyController.setData(it) epoxyController.setData(it)
} }


class PushGateWayController : TypedEpoxyController<PushGatewayViewState>() { class PushGateWayController(private val stringProvider: StringProvider) : TypedEpoxyController<PushGatewayViewState>() {
override fun buildModels(data: PushGatewayViewState?) { override fun buildModels(data: PushGatewayViewState?) {
val pushers = data?.pushgateways?.invoke() ?: return data?.pushgateways?.invoke()?.let { pushers ->
pushers.forEach { if (pushers.isEmpty()) {
pushGatewayItem { genericFooterItem {
id("${it.pushKey}_${it.appId}") id("footer")
pusher(it) text(stringProvider.getString(R.string.settings_push_gateway_no_pushers))
}
} else {
pushers.forEach {
pushGatewayItem {
id("${it.pushKey}_${it.appId}")
pusher(it)
}
}
}
} ?: run {
genericFooterItem {
id("footer")
text(stringProvider.getString(R.string.loading))
} }
} }
} }

View File

@ -0,0 +1,71 @@
package im.vector.riotredesign.features.settings.push

import android.annotation.SuppressLint
import android.graphics.Color
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import com.airbnb.epoxy.EpoxyModelWithHolder
import im.vector.matrix.android.api.pushrules.Action
import im.vector.matrix.android.api.pushrules.rest.PushRule
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder
import im.vector.riotredesign.features.notifications.NotificationAction


@EpoxyModelClass(layout = R.layout.item_pushrule_raw)
abstract class PushRuleItem : EpoxyModelWithHolder<PushRuleItem.Holder>() {

@EpoxyAttribute
lateinit var pushRule: PushRule

@SuppressLint("SetTextI18n")
override fun bind(holder: Holder) {
val context = holder.view.context
if (pushRule.enabled) {
holder.view.setBackgroundColor(Color.TRANSPARENT)
holder.ruleId.text = pushRule.ruleId
} else {
holder.view.setBackgroundColor(ContextCompat.getColor(context, R.color.vector_silver_color))
holder.ruleId.text = "[Disabled] ${pushRule.ruleId}"
}
val actions = Action.mapFrom(pushRule)
if (actions.isNullOrEmpty()) {
holder.actionIcon.isInvisible = true
} else {
holder.actionIcon.isVisible = true
val notifAction = NotificationAction.extractFrom(actions)

if (notifAction.shouldNotify && !notifAction.soundName.isNullOrBlank()) {
holder.actionIcon.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_action_notify_noisy))
} else if (notifAction.shouldNotify) {
holder.actionIcon.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_action_notify_silent))
} else {
holder.actionIcon.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_action_dont_notify))
}

var description = StringBuffer()
pushRule.conditions?.forEachIndexed { i, condition ->
if (i > 0) description.append("\n")
description.append(condition.asExecutableCondition()?.technicalDescription()
?: "UNSUPPORTED")
}
if (description.isBlank()) {
holder.description.text = "No Conditions"
} else {
holder.description.text = description
}

}
}

class Holder : VectorEpoxyHolder() {
val ruleId by bind<TextView>(R.id.pushRuleId)
val description by bind<TextView>(R.id.pushRuleDescription)
val actionIcon by bind<ImageView>(R.id.pushRuleActionIcon)
}
}

View File

@ -0,0 +1,80 @@
/*
* 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.riotredesign.features.settings.push

import android.os.Bundle
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.VectorBaseActivity
import im.vector.riotredesign.core.platform.VectorBaseFragment
import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.core.ui.list.genericFooterItem
import kotlinx.android.synthetic.main.fragment_generic_recycler_epoxy.*


class PushRulesFragment : VectorBaseFragment() {

override fun getLayoutResId(): Int = R.layout.fragment_generic_recycler_epoxy

private val viewModel: PushRulesViewModel by fragmentViewModel(PushRulesViewModel::class)

private val epoxyController by lazy { PushRulesFragment.PushRulesController(StringProvider(requireContext().resources)) }


override fun onResume() {
super.onResume()
(activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_push_rules)
}

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
val lmgr = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
epoxyRecyclerView.layoutManager = lmgr
val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context,
lmgr.orientation)
epoxyRecyclerView.addItemDecoration(dividerItemDecoration)
epoxyRecyclerView.adapter = epoxyController.adapter
}

override fun invalidate() = withState(viewModel) {
epoxyController.setData(it)
}

class PushRulesController(private val stringProvider: StringProvider) : TypedEpoxyController<PushRulesViewState>() {

override fun buildModels(data: PushRulesViewState?) {
data?.let {
it.rules.forEach {
pushRuleItem {
id(it.ruleId)
pushRule(it)
}
}
} ?: run {
genericFooterItem {
id("footer")
text(stringProvider.getString(R.string.settings_push_rules_no_rules))
}
}
}

}
}

View File

@ -0,0 +1,43 @@
/*
* 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.riotredesign.features.settings.push

import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import im.vector.matrix.android.api.pushrules.rest.PushRule
import im.vector.matrix.android.api.session.Session
import im.vector.riotredesign.core.platform.VectorViewModel
import org.koin.android.ext.android.get

data class PushRulesViewState(
val rules: List<PushRule> = emptyList())
: MvRxState


class PushRulesViewModel(initialState: PushRulesViewState) : VectorViewModel<PushRulesViewState>(initialState) {

companion object : MvRxViewModelFactory<PushRulesViewModel, PushRulesViewState> {

override fun initialState(viewModelContext: ViewModelContext): PushRulesViewState? {
val session = viewModelContext.activity.get<Session>()
val rules = session.getPushrules()

return PushRulesViewState(rules)
}

}
}

View File

@ -0,0 +1,25 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="34dp"
android:height="34dp"
android:viewportWidth="34"
android:viewportHeight="34">
<path
android:pathData="M9.7169,15L9.7169,20.2316L7.5734,23.7393C7.5254,23.8178 7.5,23.908 7.5,24C7.5,24.2761 7.7239,24.5 8,24.5L26.4339,24.5C26.5259,24.5 26.6161,24.4746 26.6946,24.4266C26.9302,24.2826 27.0045,23.9749 26.8605,23.7393L24.7169,20.2316L24.7169,15C24.7169,11.558 22.3785,8.582 19.0923,7.7363L18.7169,7.6396L18.7169,6C18.7169,5.1716 18.0454,4.5 17.2169,4.5C16.3885,4.5 15.7169,5.1716 15.7169,6L15.7169,7.6396L15.3416,7.7363C12.0554,8.582 9.7169,11.558 9.7169,15ZM14.797,26.5C15.0741,27.3506 16.0402,28 17.2169,28C18.3937,28 19.3598,27.3506 19.6369,26.5L14.797,26.5Z"
android:strokeWidth="1"
android:fillColor="#00000000"
android:strokeColor="#224955"
android:fillType="evenOdd"/>
<path
android:pathData="M17,17m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0"
android:strokeWidth="1"
android:fillColor="#00000000"
android:strokeColor="#224955"
android:fillType="evenOdd"/>
<path
android:pathData="M4.5,27.25L28.2169,6"
android:strokeWidth="1"
android:fillColor="#00000000"
android:strokeColor="#224955"
android:fillType="evenOdd"
android:strokeLineCap="square"/>
</vector>

View File

@ -0,0 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="21dp"
android:height="25dp"
android:viewportWidth="21"
android:viewportHeight="25">
<path
android:pathData="M2.7169,11L2.7169,16.2316L0.5734,19.7393C0.5254,19.8178 0.5,19.908 0.5,20C0.5,20.2761 0.7239,20.5 1,20.5L19.4339,20.5C19.5259,20.5 19.6161,20.4746 19.6946,20.4266C19.9302,20.2826 20.0045,19.9749 19.8605,19.7393L17.7169,16.2316L17.7169,11C17.7169,7.558 15.3785,4.582 12.0923,3.7363L11.7169,3.6396L11.7169,2C11.7169,1.1716 11.0454,0.5 10.2169,0.5C9.3885,0.5 8.7169,1.1716 8.7169,2L8.7169,3.6396L8.3416,3.7363C5.0554,4.582 2.7169,7.558 2.7169,11ZM7.797,22.5C8.0741,23.3506 9.0402,24 10.2169,24C11.3937,24 12.3598,23.3506 12.6369,22.5L7.797,22.5Z"
android:strokeWidth="1"
android:fillColor="#00000000"
android:strokeColor="#224955"
android:fillType="evenOdd"/>
<path
android:pathData="M16,7m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0"
android:strokeWidth="1"
android:fillColor="#E8707B"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
</vector>

View File

@ -0,0 +1,24 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="23dp"
android:height="26dp"
android:viewportWidth="23"
android:viewportHeight="26">
<path
android:pathData="M2.7169,12L2.7169,17.2316L0.5734,20.7393C0.5254,20.8178 0.5,20.908 0.5,21C0.5,21.2761 0.7239,21.5 1,21.5L19.4339,21.5C19.5259,21.5 19.6161,21.4746 19.6946,21.4266C19.9302,21.2826 20.0045,20.9749 19.8605,20.7393L17.7169,17.2316L17.7169,12C17.7169,8.558 15.3785,5.582 12.0923,4.7363L11.7169,4.6396L11.7169,3C11.7169,2.1716 11.0454,1.5 10.2169,1.5C9.3885,1.5 8.7169,2.1716 8.7169,3L8.7169,4.6396L8.3416,4.7363C5.0554,5.582 2.7169,8.558 2.7169,12ZM7.797,23.5C8.0741,24.3506 9.0402,25 10.2169,25C11.3937,25 12.3598,24.3506 12.6369,23.5L7.797,23.5Z"
android:strokeWidth="1"
android:fillColor="#00000000"
android:strokeColor="#224955"
android:fillType="evenOdd"/>
<path
android:pathData="M17.5522,4.7158l1.1133,0l0,0.2842l-1.5366,0l0,-0.2227l1.0708,-1.6245l-1.062,0l0,-0.2856l1.4912,0l0,0.2139z"
android:strokeWidth="1"
android:fillColor="#4A4A4A"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
<path
android:pathData="M20.1045,4.4316l2.2266,0l0,0.5684l-3.0732,0l0,-0.4453l2.1416,-3.249l-2.124,0l0,-0.5713l2.9824,0l0,0.4277z"
android:strokeWidth="1"
android:fillColor="#4A4A4A"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
</vector>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">

<TextView
android:id="@+id/itemGenericFooterText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
tools:text="Empty list, nothing here" />

</LinearLayout>

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp">


<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:layout_weight="1"
android:orientation="vertical">

<TextView
android:id="@+id/pushRuleId"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
tools:text=".m.rule.contains_user_name"
android:textStyle="bold" />


<TextView
android:id="@+id/pushRuleDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:textStyle=""
tools:text="content matches valere" />
</LinearLayout>

<ImageView
android:id="@+id/pushRuleActionIcon"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_gravity="center_vertical"
tools:src="@drawable/ic_action_dont_notify" />

</LinearLayout>

View File

@ -17,6 +17,8 @@
<string name="settings_preferences">Preferences</string> <string name="settings_preferences">Preferences</string>
<string name="settings_security_and_privacy">Security &amp; Privacy</string> <string name="settings_security_and_privacy">Security &amp; Privacy</string>
<string name="settings_expert">Expert</string> <string name="settings_expert">Expert</string>
<string name="settings_push_gateways">Push Gateways</string> <string name="settings_push_rules">Push Rules</string>
<string name="settings_push_rules_no_rules">No push rules defined</string>
<string name="settings_push_gateway_no_pushers">No registered push gateways</string>


</resources> </resources>

View File

@ -2,14 +2,15 @@
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">


<SwitchPreference <!--<SwitchPreference-->
android:key="SETTINGS_ENABLE_ALL_NOTIF_PREFERENCE_KEY" <!--android:key="SETTINGS_ENABLE_ALL_NOTIF_PREFERENCE_KEY"-->
android:title="@string/settings_enable_all_notif" /> <!--android:title="@string/settings_enable_all_notif" />-->


<SwitchPreference <SwitchPreference
android:dependency="SETTINGS_ENABLE_ALL_NOTIF_PREFERENCE_KEY"
android:key="SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY" android:key="SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY"
android:title="@string/settings_enable_this_device" /> android:title="@string/settings_enable_this_device" />
<!--android:dependency="SETTINGS_ENABLE_ALL_NOTIF_PREFERENCE_KEY"-->


<!--<im.vector.riotredesign.core.preference.VectorSwitchPreference--> <!--<im.vector.riotredesign.core.preference.VectorSwitchPreference-->
<!--android:dependency="SETTINGS_ENABLE_ALL_NOTIF_PREFERENCE_KEY"--> <!--android:dependency="SETTINGS_ENABLE_ALL_NOTIF_PREFERENCE_KEY"-->
@ -27,6 +28,7 @@
android:persistent="false" android:persistent="false"
android:summary="@string/settings_notification_advanced_summary" android:summary="@string/settings_notification_advanced_summary"
android:title="@string/settings_notification_advanced" android:title="@string/settings_notification_advanced"
android:enabled="false"
app:fragment="im.vector.fragments.VectorSettingsNotificationsAdvancedFragment" /> app:fragment="im.vector.fragments.VectorSettingsNotificationsAdvancedFragment" />


<Preference <Preference
@ -38,8 +40,15 @@


<Preference <Preference
android:layout_width="match_parent" android:layout_width="match_parent"
android:title="@string/settings_push_gateways" android:title="@string/settings_notifications_targets"
android:persistent="false"
app:fragment="im.vector.riotredesign.features.settings.push.PushGatewaysFragment" /> app:fragment="im.vector.riotredesign.features.settings.push.PushGatewaysFragment" />


<Preference
android:layout_width="match_parent"
android:title="@string/settings_push_rules"
android:persistent="false"
app:fragment="im.vector.riotredesign.features.settings.push.PushRulesFragment" />

</PreferenceCategory> </PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>