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 boolValue: Boolean? = null

}

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


View File

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

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

abstract class Condition(val kind: Kind) {

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

companion object {

fun fromString(value: String): Kind {
return when (value) {
"event_match" -> EVENT_MATCH
"contains_display_name" -> CONTAINS_DISPLAY_NAME
"room_member_count" -> ROOM_MEMBER_COUNT
"sender_notification_permission" -> SENDER_NOTIFICATION_PERMISSION
"event_match" -> event_match
"contains_display_name" -> contains_display_name
"room_member_count" -> room_member_count
"sender_notification_permission" -> sender_notification_permission
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 {
//TODO factory methods?
open fun technicalDescription(): String {
return "Kind: $kind"
}

}

View File

@ -15,11 +15,14 @@
*/
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 getOrderedPushrules(): List<PushRule>

fun onRulesUpdate(newRules: List<PushRule>)
fun resolveEventMatchCondition(eventMatchCondition: EventMatchCondition): Boolean
fun resolveRoomMemberCountCondition(roomMemberCountCondition: RoomMemberCountCondition): Boolean
fun resolveSenderNotificationPermissionCondition(senderNotificationPermissionCondition: SenderNotificationPermissionCondition): Boolean
fun resolveContainsDisplayNameCondition(containsDisplayNameCondition: ContainsDisplayNameCondition) : Boolean
}

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 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?
val rawJson = MoshiProvider.providesMoshi().adapter(Event::class.java).toJsonValue(event) as? Map<*, *>
?: return false

View File

@ -15,12 +15,19 @@
*/
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

interface PushRuleService {


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

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

//TODO update rule

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

fun removePushRuleListener(listener: PushRuleListener)

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

interface PushRuleListener {
fun onMatchRule(event: Event, actions: List<Action>)
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.JsonClass
import im.vector.matrix.android.api.pushrules.Condition
import im.vector.matrix.android.api.pushrules.EventMatchCondition
import im.vector.matrix.android.api.pushrules.*
import timber.log.Timber

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

fun asExecutableCondition(): Condition? {
return when (Condition.Kind.fromString(this.kind)) {
Condition.Kind.EVENT_MATCH -> {
Condition.Kind.event_match -> {
if (this.key != null && this.pattern != null) {
EventMatchCondition(key, pattern)
} else {
@ -45,10 +44,24 @@ data class PushCondition(
null
}
}
Condition.Kind.CONTAINS_DISPLAY_NAME -> TODO()
Condition.Kind.ROOM_MEMBER_COUNT -> TODO()
Condition.Kind.SENDER_NOTIFICATION_PERMISSION -> TODO()
Condition.Kind.UNRECOGNIZE -> null
Condition.Kind.contains_display_name -> {
ContainsDisplayNameCondition()
}
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.
val actions: List<Any>,
//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.
val enabled: Boolean,
//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.
*/
@JsonClass(generateAdapter = true)
data class PushruleResponse(
data class PushrulesResponse(
//Global rules, account level applying to all devices
val global: Ruleset,
//Device specific rules, apply only to current device

View File

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

fun getNumberOfJoinedMembers() : Int

/**
* 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 = "",
var scope: String = "",
var rulesetKey: String = "",
var pushRules: RealmList<PushRulesEntity> = RealmList()
var pushRules: RealmList<PushRuleEntity> = RealmList()
) : RealmObject() {
companion object
}

View File

@ -15,8 +15,10 @@
*/
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.PusherEntityFields
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.kotlin.where
@ -29,5 +31,11 @@ internal fun PusherEntity.Companion.where(realm: Realm, userId: String, 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.listeners.ProgressListener
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.cache.CacheService
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 {

companion object {
const val SCOPE: String = "session"
}
@ -497,4 +499,13 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi
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 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.session.cache.CacheService
import im.vector.matrix.android.api.session.group.GroupService
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.GroupSummaryUpdater
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.PushRulesManager
import im.vector.matrix.android.internal.session.notification.DefaultPushRuleService
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.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.PushersAPI
import im.vector.matrix.android.internal.session.room.*
@ -175,18 +174,25 @@ internal class SessionModule(private val sessionParams: SessionParams) {
}

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

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

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) {

View File

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

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.internal.database.RealmLiveEntityObserver
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.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,
private val credentials: Credentials,
private val taskExecutor: TaskExecutor,
private val pushRulesManager: PushRulesManager) :
private val defaultPushRuleService: DefaultPushRuleService) :
RealmLiveEntityObserver<EventEntity>(monarchy) {

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>) {
//TODO task
inserted.map { it.asDomain() }.let {
pushRulesManager.processEvents(it)
val rules = defaultPushRuleService.getPushrules("global")
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>) {
val params = InviteTask.Params(roomId, userId)
inviteTask.configureWith(params)

View File

@ -1,10 +1,23 @@
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.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.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.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

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

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 {
//TODO check if notifications are enabled for this device
//We need to use alarm in this mode
AlarmSyncBroadcastReceiver.scheduleAlarm(applicationContext,4_000L)
Timber.i("Alarm scheduled to restart service")
if (Matrix.getInstance().currentSession != null) {
AlarmSyncBroadcastReceiver.scheduleAlarm(applicationContext, 4_000L)
Timber.i("Alarm scheduled to restart service")
}

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


View File

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

// TODO Replace by real Bingrule class
// TODO Replace by real Bingrule class, then delete
class BingRule(rule: BingRule) {
fun shouldNotNotify() = 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.startSync()
get<PushRuleTriggerListener>().startWithSession(data)
data.fetchPushRules()
goToHome()
}


View File

@ -43,7 +43,7 @@ class NotifiableEventResolver(val context: 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
@ -55,7 +55,7 @@ class NotifiableEventResolver(val context: Context) {

when (event.getClearType()) {
EventType.MESSAGE -> {
return resolveMessageEvent(event, bingRule, session)
return resolveMessageEvent(event, session)
}
// EventType.ENCRYPTED -> {
// 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)
// 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...)
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(
eventId = event.eventId ?: "",
timestamp = event.originServerTs ?: 0,
noisy = noisy,
noisy = false,//will be updated
senderName = senderDisplayName,
senderId = event.senderId,
body = body,
@ -130,7 +128,7 @@ class NotifiableEventResolver(val context: Context) {
val notifiableEvent = NotifiableMessageEvent(
eventId = event.eventId!!,
timestamp = event.originServerTs ?: 0,
noisy = noisy,
noisy = false,//will be updated
senderName = senderDisplayName,
senderId = event.senderId,
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.
The notification corresponding to this event will not be displayed until

View File

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

override fun onMatchRule(event: Event, actions: List<Action>) {
Timber.v("Push rule match for event ${event.eventId}")
if (session == null) {
Timber.e("Called without active session")
return
}
resolver.resolveEvent(event,null,session!!)?.let {
drawerManager.onNotifiableEventReceived(it)
val notificationAction = NotificationAction.extractFrom(actions)
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.refreshNotificationDrawer()
}

}

View File

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

var index = 0

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

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

View File

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

import android.os.Bundle
import android.widget.LinearLayout
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@ -27,19 +26,21 @@ 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 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() {

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 epoxyController by lazy { PushGateWayController() }
private val epoxyController by lazy { PushGateWayController(StringProvider(requireContext().resources)) }

override fun 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?) {
@ -56,13 +57,26 @@ class PushGatewaysFragment : VectorBaseFragment() {
epoxyController.setData(it)
}

class PushGateWayController : TypedEpoxyController<PushGatewayViewState>() {
class PushGateWayController(private val stringProvider: StringProvider) : TypedEpoxyController<PushGatewayViewState>() {
override fun buildModels(data: PushGatewayViewState?) {
val pushers = data?.pushgateways?.invoke() ?: return
pushers.forEach {
pushGatewayItem {
id("${it.pushKey}_${it.appId}")
pusher(it)
data?.pushgateways?.invoke()?.let { pushers ->
if (pushers.isEmpty()) {
genericFooterItem {
id("footer")
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_security_and_privacy">Security &amp; Privacy</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>

View File

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

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

<SwitchPreference
android:dependency="SETTINGS_ENABLE_ALL_NOTIF_PREFERENCE_KEY"

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

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

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

<Preference
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" />

<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>
</PreferenceScreen>