This commit is contained in:
Valere 2019-06-19 10:46:59 +02:00 committed by Benoit Marty
parent 79735c6338
commit 0e46fc4c0a
112 changed files with 3957 additions and 249 deletions

View File

@ -21,7 +21,10 @@ import androidx.lifecycle.ProcessLifecycleOwner
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.api.auth.Authenticator
import im.vector.matrix.android.api.pushrules.Action
import im.vector.matrix.android.api.pushrules.PushRuleService
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.sync.FilterService
import im.vector.matrix.android.internal.auth.AuthModule
import im.vector.matrix.android.internal.di.MatrixKoinComponent
@ -30,7 +33,9 @@ import im.vector.matrix.android.internal.di.MatrixModule
import im.vector.matrix.android.internal.di.NetworkModule
import im.vector.matrix.android.internal.network.UserAgentHolder
import im.vector.matrix.android.internal.util.BackgroundDetectionObserver
import org.koin.standalone.get
import org.koin.standalone.inject
import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean

/**
@ -56,7 +61,9 @@ class Matrix private constructor(context: Context) : MatrixKoinComponent {
currentSession = it
it.open()
it.setFilter(FilterService.FilterPreset.RiotFilter)
it.startSync()
//TODO check if using push or not (should pause if we use push)
// it.shoudPauseOnBackground(false)
// it.startSync()
}
}


View File

@ -0,0 +1,93 @@
/*
* 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.pushrules.rest.PushRule
import timber.log.Timber


class Action(val type: Type) {

enum class Type(val value: String) {
NOTIFY("notify"),
DONT_NOTIFY("dont_notify"),
COALESCE("coalesce"),
SET_TWEAK("set_tweak");

companion object {

fun safeValueOf(value: String): Type? {
try {
return valueOf(value)
} catch (e: IllegalArgumentException) {
return null
}
}
}
}

var tweak_action: String? = null
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)
}
}
}
"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 {
Timber.w("Unsupported action type ${actionStrOrObj}")
return null
}
}
return if (actions.isEmpty()) null else actions
}

View File

@ -0,0 +1,51 @@
/*
* 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

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"),
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
else -> UNRECOGNIZE
}
}

}

}

abstract fun isSatisfied(event: Event): Boolean

companion object {
//TODO factory methods?
}

}

View File

@ -0,0 +1,88 @@
/*
* 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.internal.di.MoshiProvider
import timber.log.Timber

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

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

//Patterns with no special glob characters should be treated as having asterisks prepended
// and appended when testing the condition.
try {
val modPattern = if (hasSpecialGlobChar(pattern)) simpleGlobToRegExp(pattern) else simpleGlobToRegExp("*$pattern*")
val regex = Regex(modPattern, RegexOption.DOT_MATCHES_ALL)
return regex.containsMatchIn(value)
} catch (e: Throwable) {
//e.g PatternSyntaxException
Timber.e(e, "Failed to evaluate push condition")
return false
}

}


private fun extractField(jsonObject: Map<*, *>, fieldPath: String): String? {
val fieldParts = fieldPath.split(".")
if (fieldParts.isEmpty()) return null

var jsonElement: Map<*, *> = jsonObject
fieldParts.forEachIndexed { index, pathSegment ->
if (index == fieldParts.lastIndex) {
return jsonElement[pathSegment]?.toString()
} else {
val sub = jsonElement[pathSegment] ?: return null
if (sub is Map<*, *>) {
jsonElement = sub
} else {
return null
}
}
}
return null
}

companion object {

private fun hasSpecialGlobChar(glob: String): Boolean {
return glob.contains("*") || glob.contains("?")
}

//Very simple glob to regexp converter
private fun simpleGlobToRegExp(glob: String): String {
var out = ""//"^"
for (i in 0 until glob.length) {
val c = glob[i]
when (c) {
'*' -> out += ".*"
'?' -> out += '.'.toString()
'.' -> out += "\\."
'\\' -> out += "\\\\"
else -> out += c
}
}
out += ""//'$'.toString()
return out
}
}
}

View File

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

View File

@ -0,0 +1,35 @@
/*
* 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

interface PushRuleService {


//TODO get push rule set

//TODO update rule

fun addPushRuleListener(listener: PushRuleListener)

fun removePushRuleListener(listener: PushRuleListener)

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

View File

@ -0,0 +1,25 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.pushrules

import im.vector.matrix.android.api.pushrules.rest.PushRule

interface PushRulesProvider {

fun getOrderedPushrules(): List<PushRule>

fun onRulesUpdate(newRules: List<PushRule>)
}

View File

@ -0,0 +1,26 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.pushrules


enum class RulesetKey(val value: String) {
CONTENT("content"),
OVERRIDE("override"),
ROOM("room"),
SENDER("sender"),
UNDERRIDE("underride"),
UNKNOWN("")
}

View File

@ -0,0 +1,54 @@
/*
* 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.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 timber.log.Timber

@JsonClass(generateAdapter = true)
data class PushCondition(
//Required. The kind of condition to apply.
val kind: String,
//Required for event_match conditions. The dot- separated field of the event to match.
val key: String? = null,
//Required for event_match conditions.
val pattern: String? = null,
//Required for room_member_count conditions.
// A decimal integer optionally prefixed by one of, ==, <, >, >= or <=.
// A prefix of < matches rooms where the member count is strictly less than the given number and so forth. If no prefix is present, this parameter defaults to ==.
@Json(name = "is") val iz: String? = null
) {

fun asExecutableCondition(): Condition? {
return when (Condition.Kind.fromString(this.kind)) {
Condition.Kind.EVENT_MATCH -> {
if (this.key != null && this.pattern != null) {
EventMatchCondition(key, pattern)
} else {
Timber.e("Malformed Event match condition")
null
}
}
Condition.Kind.CONTAINS_DISPLAY_NAME -> TODO()
Condition.Kind.ROOM_MEMBER_COUNT -> TODO()
Condition.Kind.SENDER_NOTIFICATION_PERMISSION -> TODO()
Condition.Kind.UNRECOGNIZE -> null
}
}
}

View File

@ -0,0 +1,38 @@
/*
* 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.rest

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass


@JsonClass(generateAdapter = true)
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,
//Required. Whether the push rule is enabled or not.
val enabled: Boolean,
//Required. The ID of this rule.
@Json(name = "rule_id") val ruleId: String,
//The conditions that must hold true for an event in order for a rule to be applied to an event
val conditions: List<PushCondition>? = null,
//The glob-style pattern to match against. Only applicable to content rules.
val pattern: String? = null
)

View File

@ -0,0 +1,30 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.pushrules.rest

import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.pushrules.rest.Ruleset

/**
* All push rulesets for a user.
*/
@JsonClass(generateAdapter = true)
data class PushruleResponse(
//Global rules, account level applying to all devices
val global: Ruleset,
//Device specific rules, apply only to current device
val device: Ruleset? = null
)

View File

@ -0,0 +1,27 @@
/*
* 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.rest

import com.squareup.moshi.JsonClass

@JsonClass(generateAdapter = true)
data class Ruleset(
val content: List<PushRule>? = null,
val override: List<PushRule>? = null,
val room: List<PushRule>? = null,
val sender: List<PushRule>? = null,
val underride: List<PushRule>? = null
)

View File

@ -19,11 +19,13 @@ package im.vector.matrix.android.api.session
import androidx.annotation.MainThread
import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.pushrules.PushRuleService
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.ContentUrlResolver
import im.vector.matrix.android.api.session.crypto.CryptoService
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.room.RoomDirectoryService
import im.vector.matrix.android.api.session.room.RoomService
import im.vector.matrix.android.api.session.signout.SignOutService
@ -43,7 +45,9 @@ interface Session :
CryptoService,
CacheService,
SignOutService,
FilterService {
FilterService,
PushRuleService,
PushersService {

/**
* The params associated to the session
@ -56,17 +60,35 @@ interface Session :
@MainThread
fun open()

/**
* This method start the sync thread.
*/
@MainThread
fun startSync()
// /**
// * This method start the sync thread.
// */
// @MainThread
// fun startSync()
//
//
// fun isSyncThreadAlice() : Boolean
// fun syncThreadState() : String
//
//// fun pauseSync()
//// fun resumeSync()
//
// fun shoudPauseOnBackground(shouldPause: Boolean)

/**
* This method stop the sync thread.
* Configures the sync long pooling options
* @param timoutMS The maximum time to wait, in milliseconds, before returning the sync request.
* If no events (or other data) become available before this time elapses, the server will return a response with empty fields.
* If set to 0 the server will return immediately even if the response is empty.
* @param delayMs When the server responds to a sync request, the client waits for `longPoolDelay` before calling a new sync.
*/
@MainThread
fun stopSync()
// fun configureSyncLongPooling(timoutMS : Long, delayMs : Long )

// /**
// * This method stop the sync thread.
// */
// @MainThread
// fun stopSync()

/**
* This method allows to listen the sync state.

View File

@ -0,0 +1,44 @@
/*
* 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.session.pushers

data class Pusher(

val userId: String,

val pushKey: String,
val kind: String,
val appId: String,
val appDisplayName: String,
val deviceDisplayName: String,
val profileTag: String? = null,
val lang: String,
val data: PusherData,

val state: PusherState
)

enum class PusherState {
UNREGISTRED,
REGISTERING,
REGISTERED,
FAILED_TO_REGISTER
}

data class PusherData(
val url: String? = null,
val format: String? = null
)

View File

@ -0,0 +1,60 @@
/*
* 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.session.pushers

import androidx.lifecycle.LiveData
import java.util.*


interface PushersService {

/**
* Refresh pushers from server state
*/
fun refreshPushers()

/**
* Add a new HTTP pusher.
*
* @param pushkey the pushkey
* @param appId the application id
* @param profileTag the profile tag
* @param lang the language
* @param appDisplayName a human-readable application name
* @param deviceDisplayName a human-readable device name
* @param url the URL that should be used to send notifications
* @param append append the pusher
* @param withEventIdOnly true to limit the push content
*
* @return A work request uuid. Can be used to listen to the status
* (LiveData<WorkInfo> status = workManager.getWorkInfoByIdLiveData(<UUID>))
*/
fun addHttpPusher(pushkey: String,
appId: String,
profileTag: String,
lang: String,
appDisplayName: String,
deviceDisplayName: String,
url: String,
append: Boolean,
withEventIdOnly: Boolean): UUID

companion object {
const val EVENT_ID_ONLY = "event_id_only"
}

fun livePushers(): LiveData<List<Pusher>>
}

View File

@ -0,0 +1,63 @@
/*
* 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 im.vector.matrix.android.api.session.pushers.Pusher
import im.vector.matrix.android.api.session.pushers.PusherData
import im.vector.matrix.android.internal.database.model.PusherDataEntity
import im.vector.matrix.android.internal.database.model.PusherEntity
import im.vector.matrix.android.internal.session.pushers.JsonPusher

internal object PushersMapper {

fun map(pushEntity: PusherEntity): Pusher {

return Pusher(
userId = pushEntity.userId,
pushKey = pushEntity.pushKey,
kind = pushEntity.kind,
appId = pushEntity.appId,
appDisplayName = pushEntity.appDisplayName,
deviceDisplayName = pushEntity.deviceDisplayName,
profileTag = pushEntity.profileTag,
lang = pushEntity.lang,
data = PusherData(pushEntity.data?.url, pushEntity.data?.format),
state = pushEntity.state
)
}

fun map(pusher: JsonPusher, userId: String): PusherEntity {
return PusherEntity(
userId = userId,
pushKey = pusher.pushKey,
kind = pusher.kind,
appId = pusher.appId,
appDisplayName = pusher.appDisplayName,
deviceDisplayName = pusher.deviceDisplayName,
profileTag = pusher.profileTag,
lang = pusher.lang,
data = PusherDataEntity(pusher.data.url, pusher.data.format)
)
}
}

internal fun PusherEntity.asDomain(): Pusher {
return PushersMapper.map(this)
}

internal fun JsonPusher.toEntity(userId: String): PusherEntity {
return PushersMapper.map(this, userId)
}

View File

@ -0,0 +1,29 @@
/*
* 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.model

import io.realm.RealmObject

internal open class PushConditionEntity(
var kind: String = "",
var key: String? = null,
var pattern: String? = null,
var iz: String? = null
) : RealmObject() {

companion object

}

View File

@ -0,0 +1,39 @@
/*
* 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.model

import io.realm.RealmList
import io.realm.RealmObject

internal open class PushRuleEntity(
//Required. The domainActions to perform when this rule is matched.
var actionsStr: String? = null,
//Required. Whether this is a default rule, or has been set explicitly.
var default: Boolean = false,
//Required. Whether the push rule is enabled or not.
var enabled: Boolean = true,
//Required. The ID of this rule.
var ruleId: String = "",
//The conditions that must hold true for an event in order for a rule to be applied to an event
var conditions: RealmList<PushConditionEntity>? = RealmList(),
//The glob-style pattern to match against. Only applicable to content rules.
var pattern: String? = null
) : RealmObject() {

companion object

}

View File

@ -0,0 +1,30 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.database.model

import io.realm.RealmList
import io.realm.RealmObject
import io.realm.annotations.Index


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

View File

@ -0,0 +1,25 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.database.model

import io.realm.RealmObject

internal open class PusherDataEntity(
var url: String? = null,
var format: String? = null
) : RealmObject() {
companion object
}

View File

@ -0,0 +1,58 @@
/*
* 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.model

import im.vector.matrix.android.api.session.pushers.PusherState
import io.realm.RealmObject
import io.realm.annotations.Index

//TODO
// at java.lang.Thread.run(Thread.java:764)
// Caused by: java.lang.IllegalArgumentException: 'value' is not a valid managed object.
// at io.realm.ProxyState.checkValidObject(ProxyState.java:213)
// at io.realm.im_vector_matrix_android_internal_database_model_PusherEntityRealmProxy.realmSet$data(im_vector_matrix_android_internal_database_model_PusherEntityRealmProxy.java:413)
// at im.vector.matrix.android.internal.database.model.PusherEntity.setData(PusherEntity.kt:16)
// at im.vector.matrix.android.internal.session.pushers.AddHttpPusherWorker$doWork$$inlined$fold$lambda$2.execute(AddHttpPusherWorker.kt:70)
// at io.realm.Realm.executeTransaction(Realm.java:1493)
internal open class PusherEntity(
@Index var userId: String = "",
var pushKey: String = "",
var kind: String = "",
var appId: String = "",
var appDisplayName: String = "",
var deviceDisplayName: String = "",
var profileTag: String? = null,
var lang: String = "",
var data: PusherDataEntity? = null
) : RealmObject() {
private var stateStr: String = PusherState.UNREGISTRED.name

var state: PusherState
get() {
try {
return PusherState.valueOf(stateStr)
} catch (e: Exception) {
//can this happen?
return PusherState.UNREGISTRED
}

}
set(value) {
stateStr = value.name
}

companion object
}

View File

@ -36,6 +36,11 @@ import io.realm.annotations.RealmModule
UserEntity::class,
EventAnnotationsSummaryEntity::class,
ReactionAggregatedSummaryEntity::class,
EditAggregatedSummaryEntity::class
EditAggregatedSummaryEntity::class,
PushRulesEntity::class,
PushRuleEntity::class,
PushConditionEntity::class,
PusherEntity::class,
PusherDataEntity::class
])
internal class SessionRealmModule

View File

@ -53,6 +53,14 @@ internal fun EventEntity.Companion.where(realm: Realm,
}


internal fun EventEntity.Companion.types(realm: Realm,
typeList: List<String> = emptyList()): RealmQuery<EventEntity> {
val query = realm.where<EventEntity>()
query.`in`(EventEntityFields.TYPE, typeList.toTypedArray())
return query
}


internal fun EventEntity.Companion.latestEvent(realm: Realm,
roomId: String,
includedTypes: List<String> = emptyList(),

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.database.query

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

internal fun PusherEntity.Companion.where(realm: Realm, userId: String, pushKey: String? = null): RealmQuery<PusherEntity> {
return realm.where<PusherEntity>()
.equalTo(PusherEntityFields.USER_ID, userId)
.apply {
if (pushKey != null) {
equalTo(PusherEntityFields.PUSH_KEY, pushKey)
}
}

}

View File

@ -24,6 +24,7 @@ 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.listeners.ProgressListener
import im.vector.matrix.android.api.pushrules.PushRuleService
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
@ -36,6 +37,8 @@ import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.group.Group
import im.vector.matrix.android.api.session.group.GroupService
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.pushers.Pusher
import im.vector.matrix.android.api.session.pushers.PushersService
import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.RoomDirectoryService
import im.vector.matrix.android.api.session.room.RoomService
@ -65,6 +68,7 @@ import im.vector.matrix.android.internal.di.MatrixKoinComponent
import im.vector.matrix.android.internal.di.MatrixKoinHolder
import im.vector.matrix.android.internal.session.content.ContentModule
import im.vector.matrix.android.internal.session.group.GroupModule
import im.vector.matrix.android.internal.session.notification.BingRuleWatcher
import im.vector.matrix.android.internal.session.room.RoomModule
import im.vector.matrix.android.internal.session.signout.SignOutModule
import im.vector.matrix.android.internal.session.sync.SyncModule
@ -73,6 +77,7 @@ import im.vector.matrix.android.internal.session.user.UserModule
import org.koin.core.scope.Scope
import org.koin.standalone.inject
import timber.log.Timber
import java.util.*


internal class DefaultSession(override val sessionParams: SessionParams) : Session, MatrixKoinComponent {
@ -96,8 +101,12 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi
private val syncThread by inject<SyncThread>()
private val contentUrlResolver by inject<ContentUrlResolver>()
private val contentUploadProgressTracker by inject<ContentUploadStateTracker>()
private val pushRuleService by inject<PushRuleService>()
private val pushersService by inject<PushersService>()
private var isOpen = false

private val bingRuleWatcher by inject<BingRuleWatcher>()

@MainThread
override fun open() {
assertMainThread()
@ -124,19 +133,48 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi
monarchy.openManually()
}
liveEntityUpdaters.forEach { it.start() }
bingRuleWatcher.start()
}

@MainThread
override fun startSync() {
assert(isOpen)
syncThread.start()
}
// @MainThread
// override fun startSync() {
// assert(isOpen)
// if (!syncThread.isAlive) {
// syncThread.start()
// } else {
// syncThread.restart()
// Timber.w("Attempt to start an already started thread")
// }
// }
//
// override fun isSyncThreadAlice(): Boolean = syncThread.isAlive
//
// override fun syncThreadState(): String = syncThread.getSyncState()
//
// override fun shoudPauseOnBackground(shouldPause: Boolean) {
// //TODO check if using push or not (should pause if we use push)
// syncThread.shouldPauseOnBackground = shouldPause
// }

@MainThread
override fun stopSync() {
assert(isOpen)
syncThread.kill()
}
// override fun resumeSync() {
// assert(isOpen)
// syncThread.restart()
// }
//
// override fun pauseSync() {
// assert(isOpen)
// syncThread.pause()
// }

// override fun configureSyncLongPooling(timoutMS: Long, delayMs: Long) {
// syncThread.configureLongPoolingSettings(timoutMS, delayMs)
// }
//
// @MainThread
// override fun stopSync() {
// assert(isOpen)
// syncThread.kill()
// }

@MainThread
override fun close() {
@ -144,6 +182,7 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi
assert(isOpen)
liveEntityUpdaters.forEach { it.dispose() }
cryptoService.close()
bingRuleWatcher.dispose()
if (monarchy.isMonarchyThreadOpen) {
monarchy.closeManually()
}
@ -160,8 +199,8 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi
Timber.w("SIGN_OUT: start")

assert(isOpen)
Timber.w("SIGN_OUT: kill sync thread")
syncThread.kill()
//Timber.w("SIGN_OUT: kill sync thread")
//syncThread.kill()

Timber.w("SIGN_OUT: call webservice")
return signOutService.signOut(object : MatrixCallback<Unit> {
@ -261,11 +300,11 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi

override fun clearCache(callback: MatrixCallback<Unit>) {
assert(isOpen)
syncThread.pause()
// syncThread.pause()
cacheService.clearCache(object : MatrixCallbackDelegate<Unit>(callback) {
override fun onSuccess(data: Unit) {
// Restart the sync
syncThread.restart()
// syncThread.restart()

super.onSuccess(data)
}
@ -426,6 +465,16 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi
cryptoService.clearCryptoCache(callback)
}

// PUSH RULE SERVICE

override fun addPushRuleListener(listener: PushRuleService.PushRuleListener) {
pushRuleService.addPushRuleListener(listener)
}

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

// Private methods *****************************************************************************

private fun assertMainThread() {
@ -434,4 +483,28 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi
}
}

override fun refreshPushers() {
pushersService.refreshPushers()
}

override fun addHttpPusher(
pushkey: String,
appId: String,
profileTag: String,
lang: String,
appDisplayName: String,
deviceDisplayName: String,
url: String,
append: Boolean,
withEventIdOnly: Boolean): UUID {
return pushersService
.addHttpPusher(
pushkey, appId, profileTag, lang, appDisplayName, deviceDisplayName, url, append, withEventIdOnly
)
}

override fun livePushers(): LiveData<List<Pusher>> {
return pushersService.livePushers()
}

}

View File

@ -19,8 +19,11 @@ package im.vector.matrix.android.internal.session
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
import im.vector.matrix.android.api.session.room.RoomDirectoryService
import im.vector.matrix.android.api.session.room.RoomService
import im.vector.matrix.android.api.session.signout.SignOutService
@ -34,6 +37,14 @@ import im.vector.matrix.android.internal.session.cache.RealmClearCacheTask
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.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.GetPushersTask
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.directory.DefaultGetPublicRoomTask
import im.vector.matrix.android.internal.session.room.directory.DefaultGetThirdPartyProtocolsTask
@ -163,6 +174,21 @@ internal class SessionModule(private val sessionParams: SessionParams) {
retrofit.create(FilterApi::class.java)
}

scope(DefaultSession.SCOPE) {
MockPushRuleProvider() as PushRulesProvider
}

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

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

scope(DefaultSession.SCOPE) {
val groupSummaryUpdater = GroupSummaryUpdater(get())
val userEntityUpdater = UserEntityUpdater(get(), get(), get())
@ -172,6 +198,16 @@ internal class SessionModule(private val sessionParams: SessionParams) {
listOf<LiveEntityObserver>(groupSummaryUpdater, userEntityUpdater, aggregationUpdater, eventsPruner)
}

scope(DefaultSession.SCOPE) {
get<Retrofit>().create(PushersAPI::class.java)
}
scope(DefaultSession.SCOPE) {
DefaultGetPusherTask(get()) as GetPushersTask
}

scope(DefaultSession.SCOPE) {
DefaultPusherService(get(), get(), get(), get()) as PushersService
}

}


View File

@ -0,0 +1,50 @@
/*
* 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.auth.data.Credentials
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


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

override val query = Monarchy.Query<EventEntity> {

EventEntity.types(it, listOf(
EventType.REDACTION, EventType.MESSAGE, EventType.REDACTION, EventType.ENCRYPTED)
)

}

override fun processChanges(inserted: List<EventEntity>, updated: List<EventEntity>, deleted: List<EventEntity>) {
//TODO task
inserted.map { it.asDomain() }.let {
pushRulesManager.processEvents(it)
}
}


}

View File

@ -0,0 +1,41 @@
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

@ -0,0 +1,85 @@
/*
* 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,98 @@
/*
* 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 android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.pushers.PusherState
import im.vector.matrix.android.internal.database.mapper.toEntity
import im.vector.matrix.android.internal.database.model.PusherDataEntity
import im.vector.matrix.android.internal.database.model.PusherEntity
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.MatrixKoinComponent
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.util.WorkerParamsFactory
import org.koin.standalone.inject

class AddHttpPusherWorker(context: Context, params: WorkerParameters)
: CoroutineWorker(context, params), MatrixKoinComponent {


@JsonClass(generateAdapter = true)
internal data class Params(
val pusher: JsonPusher,
val userId: String
)

private val pushersAPI by inject<PushersAPI>()
private val monarchy by inject<Monarchy>()

override suspend fun doWork(): Result {

val params = WorkerParamsFactory.fromData<Params>(inputData)
?: return Result.failure()

val pusher = params.pusher

if (pusher.pushKey.isBlank()) {
return Result.failure()
}

val result = executeRequest<Map<String, Any>> {
apiCall = pushersAPI.setPusher(pusher)
}
return result.fold({
when (it) {
is Failure.NetworkConnection -> Result.retry()
else -> {
monarchy.runTransactionSync { realm ->
PusherEntity.where(realm, params.userId, pusher.pushKey).findFirst()?.let {
//update it
it.state = PusherState.FAILED_TO_REGISTER
}
}
//always return success, or the chain will be stuck for ever!
Result.failure()
}
}
}, {
monarchy.runTransactionSync { realm ->
val echo = PusherEntity.where(realm, params.userId, pusher.pushKey).findFirst()
if (echo != null) {
//update it
echo.appDisplayName = pusher.appDisplayName
echo.appId = pusher.appId
echo.kind = pusher.kind
echo.lang = pusher.lang
echo.profileTag = pusher.profileTag
echo.data = PusherDataEntity(pusher.data.url, pusher.data.format)
echo.state = PusherState.REGISTERED
} else {
pusher.toEntity(params.userId).also {
it.state = PusherState.REGISTERED
realm.insertOrUpdate(it)
}
}
}
Result.success()

})
}
}

View File

@ -0,0 +1,107 @@
/*
* 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 androidx.lifecycle.LiveData
import androidx.work.*
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.session.pushers.Pusher
import im.vector.matrix.android.api.session.pushers.PusherState
import im.vector.matrix.android.api.session.pushers.PushersService
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.mapper.toEntity
import im.vector.matrix.android.internal.database.model.PusherEntity
import im.vector.matrix.android.internal.database.model.PusherEntityFields
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.WorkerParamsFactory
import java.util.*
import java.util.concurrent.TimeUnit


internal class DefaultPusherService(
private val monarchy: Monarchy,
private val sessionParam: SessionParams,
private val getPusherTask: GetPushersTask,
private val taskExecutor: TaskExecutor
) : PushersService {


override fun refreshPushers() {
getPusherTask
.configureWith(Unit)
.dispatchTo(object : MatrixCallback<PushersResponse> {
override fun onSuccess(data: PushersResponse) {
monarchy.runTransactionSync { realm ->
//clear existings?
realm.where(PusherEntity::class.java)
.equalTo(PusherEntityFields.USER_ID, sessionParam.credentials.userId)
.findAll().deleteAllFromRealm()
data.pushers?.forEach { jsonPusher ->
jsonPusher.toEntity(sessionParam.credentials.userId).also {
it.state = PusherState.REGISTERED
realm.insertOrUpdate(it)
}
}
}
}
})
.executeBy(taskExecutor)
}

/**
*
*/
override fun addHttpPusher(pushkey: String, appId: String, profileTag: String,
lang: String, appDisplayName: String, deviceDisplayName: String,
url: String, append: Boolean, withEventIdOnly: Boolean)
: UUID {

val pusher = JsonPusher(
pushKey = pushkey,
kind = "http",
appId = appId,
appDisplayName = appDisplayName,
deviceDisplayName = deviceDisplayName,
profileTag = profileTag,
lang = lang,
data = JsonPusherData(url, if (withEventIdOnly) PushersService.EVENT_ID_ONLY else null),
append = append)


val params = AddHttpPusherWorker.Params(pusher, sessionParam.credentials.userId)

val constraints = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()

val request = OneTimeWorkRequestBuilder<AddHttpPusherWorker>()
.setConstraints(constraints)
.setInputData(WorkerParamsFactory.toData(params))
.setBackoffCriteria(BackoffPolicy.LINEAR, 10_000L, TimeUnit.MILLISECONDS)
.build()
WorkManager.getInstance().enqueue(request)
return request.id
}

override fun livePushers(): LiveData<List<Pusher>> {
return monarchy.findAllMappedWithChanges(
{ realm -> PusherEntity.where(realm, sessionParam.credentials.userId) },
{ it.asDomain() }
)
}
}

View File

@ -0,0 +1,31 @@
/*
* 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.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task

internal interface GetPushersTask : Task<Unit, PushersResponse>

internal class DefaultGetPusherTask(private val pushersAPI: PushersAPI) : GetPushersTask {

override suspend fun execute(params: Unit): Try<PushersResponse> {
return executeRequest {
apiCall = pushersAPI.getPushers()
}
}
}

View File

@ -0,0 +1,68 @@
/*
* 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 com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

/**
* pushkey string Required. This is a unique identifier for this pusher. See /set for more detail. Max length, 512 bytes.
* kind string Required. The kind of pusher. "http" is a pusher that sends HTTP pokes.
* app_id string Required. This is a reverse-DNS style identifier for the application. Max length, 64 chars.
* app_display_name string Required. A string that will allow the user to identify what application owns this pusher.
* device_display_name string Required. A string that will allow the user to identify what device owns this pusher.
* profile_tag string This string determines which set of device specific rules this pusher executes.
* lang string Required. The preferred language for receiving notifications (e.g. 'en' or 'en-US')
* data PusherData Required. A dictionary of information for the pusher implementation itself.
*
* <code>
* {
* "pushers": [
* {
* "pushkey": "Xp/MzCt8/9DcSNE9cuiaoT5Ac55job3TdLSSmtmYl4A=",
* "kind": "http",
* "app_id": "face.mcapp.appy.prod",
* "app_display_name": "Appy McAppface",
* "device_display_name": "Alice's Phone",
* "profile_tag": "xyz",
* "lang": "en-US",
* "data": {
* "url": "https://example.com/_matrix/push/v1/notify"
* }
* }]
* }
* </code>
*/

@JsonClass(generateAdapter = true)
internal data class JsonPusher(
@Json(name = "pushkey") val pushKey: String,
@Json(name = "kind") val kind: String,
@Json(name = "app_id") val appId: String,
@Json(name = "app_display_name") val appDisplayName: String,
@Json(name = "device_display_name") val deviceDisplayName: String,
@Json(name = "profile_tag") val profileTag: String? = null,
@Json(name = "lang") val lang: String,
@Json(name = "data") val data: JsonPusherData,

// Only used to update add Pusher (body of api request)
// Used If true, the homeserver should add another pusher with the given pushkey and App ID in addition
// to any others with different user IDs.
// Otherwise, the homeserver must remove any other pushers with the same App ID and pushkey for different users.
// The default is false.
@Json(name = "append") val append: Boolean? = false
)

View File

@ -0,0 +1,27 @@
/*
* 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 com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

@JsonClass(generateAdapter = true)
internal data class JsonPusherData(
//Required if kind is http. The URL to use to send notifications to.
@Json(name = "url") val url: String? = null,
//The format to use when sending notifications to the Push Gateway.
@Json(name = "format") val format: String? = null
)

View File

@ -0,0 +1,42 @@
/*
* 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.internal.network.NetworkConstants
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST


internal interface PushersAPI {

/**
* Get the pushers for this user.
*
* Ref: https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-thirdparty-protocols
*/
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushers")
fun getPushers(): Call<PushersResponse>

/**
* This endpoint allows the creation, modification and deletion of pushers for this user ID.
* The behaviour of this endpoint varies depending on the values in the JSON body.
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushers/set")
fun setPusher(@Body jsonPusher: JsonPusher): Call<Map<String, Any>>

}

View File

@ -0,0 +1,25 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.pushers

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass


@JsonClass(generateAdapter = true)
internal class PushersResponse(
@Json(name = "pushers") val pushers: List<JsonPusher>? = null
)

View File

@ -29,7 +29,7 @@ import im.vector.matrix.android.internal.task.Task

internal interface SyncTask : Task<SyncTask.Params, SyncResponse> {

data class Params(val token: String?)
data class Params(val token: String?, var timeout: Long = 30_1000L)

}

@ -42,10 +42,10 @@ internal class DefaultSyncTask(private val syncAPI: SyncAPI,

override suspend fun execute(params: SyncTask.Params): Try<SyncResponse> {
val requestParams = HashMap<String, String>()
var timeout = 0
var timeout = 0L
if (params.token != null) {
requestParams["since"] = params.token
timeout = 30000
timeout = params.timeout
}
requestParams["timeout"] = timeout.toString()
requestParams["filter"] = filterRepository.getFilter()

View File

@ -0,0 +1,238 @@
/*
* 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.sync.job

import android.app.Service
import android.content.Intent
import android.os.Binder
import android.os.IBinder
import com.squareup.moshi.JsonEncodingException
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.di.MatrixKoinComponent
import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
import im.vector.matrix.android.internal.session.sync.SyncTask
import im.vector.matrix.android.internal.session.sync.SyncTokenStore
import im.vector.matrix.android.internal.session.sync.model.SyncResponse
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.TaskThread
import im.vector.matrix.android.internal.task.configureWith
import org.koin.standalone.inject
import timber.log.Timber
import java.net.SocketTimeoutException
import java.util.*
import kotlin.collections.ArrayList

private const val DEFAULT_LONG_POOL_TIMEOUT = 10_000L
private const val BACKGROUND_LONG_POOL_TIMEOUT = 0L

/**
* Can execute periodic sync task.
* An IntentService is used in conjunction with the AlarmManager and a Broadcast Receiver
* in order to be able to perform a sync even if the app is not running.
* The <receiver> and <service> must be declared in the Manifest or the app using the SDK
*/
open class SyncService : Service(), MatrixKoinComponent {

private var mIsSelfDestroyed: Boolean = false
private var cancelableTask: Cancelable? = null
private val syncTokenStore: SyncTokenStore by inject()

private val syncTask: SyncTask by inject()
private val networkConnectivityChecker: NetworkConnectivityChecker by inject()
private val taskExecutor: TaskExecutor by inject()

private var localBinder = LocalBinder()

var timer = Timer()

var nextBatchDelay = 0L
var timeout = 10_000L

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Timber.i("onStartCommand ${intent}")
nextBatchDelay = 60_000L
timeout = 0
intent?.let {
if (cancelableTask == null) {
timer.cancel()
timer = Timer()
doSync()
} else {
//Already syncing ignore
Timber.i("Received a start while was already syncking... ignore")
}
}
//No intent just start the service, an alarm will should call with intent
return START_STICKY
}

override fun onDestroy() {
Timber.i("## onDestroy() : $this")

if (!mIsSelfDestroyed) {
Timber.w("## Destroy by the system : $this")
}

cancelableTask?.cancel()
super.onDestroy()
}

fun stopMe() {
timer.cancel()
timer = Timer()
cancelableTask?.cancel()
mIsSelfDestroyed = true
stopSelf()
}

fun doSync() {
var nextBatch = syncTokenStore.getLastToken()
if (!networkConnectivityChecker.isConnected()) {
Timber.v("Sync is Paused. Waiting...")
//TODO Retry in ?
timer.schedule(object : TimerTask() {
override fun run() {
doSync()
}
}, 10_000L)
} else {
Timber.v("Execute sync request with token $nextBatch and timeout $timeout")
val params = SyncTask.Params(nextBatch, timeout)
cancelableTask = syncTask.configureWith(params)
.callbackOn(TaskThread.CALLER)
.executeOn(TaskThread.CALLER)
.dispatchTo(object : MatrixCallback<SyncResponse> {
override fun onSuccess(data: SyncResponse) {
cancelableTask = null
nextBatch = data.nextBatch
syncTokenStore.saveToken(nextBatch)
localBinder.notifySyncFinish()
timer.schedule(object : TimerTask() {
override fun run() {
doSync()
}
}, nextBatchDelay)
}

override fun onFailure(failure: Throwable) {
Timber.e(failure)
cancelableTask = null
localBinder.notifyFailure(failure)
if (failure is Failure.NetworkConnection
&& failure.cause is SocketTimeoutException) {
// Timeout are not critical
timer.schedule(object : TimerTask() {
override fun run() {
doSync()
}
}, 10_000L)
}

if (failure !is Failure.NetworkConnection
|| failure.cause is JsonEncodingException) {
// Wait 10s before retrying
timer.schedule(object : TimerTask() {
override fun run() {
doSync()
}
}, 10_000L)
}

if (failure is Failure.ServerError
&& (failure.error.code == MatrixError.UNKNOWN_TOKEN || failure.error.code == MatrixError.MISSING_TOKEN)) {
// No token or invalid token, stop the thread
stopSelf()
}

}

})
.executeBy(taskExecutor)

}
}

override fun onBind(intent: Intent?): IBinder {
return localBinder
}

inner class LocalBinder : Binder() {

private var listeners = ArrayList<SyncListener>()

fun addListener(listener: SyncListener) {
if (!listeners.contains(listener)) {
listeners.add(listener)
}
}

fun removeListener(listener: SyncListener) {
listeners.remove(listener)
}

internal fun notifySyncFinish() {

listeners.forEach {
try {
it.onSyncFinsh()
} catch (t: Throwable) {
Timber.e("Failed to notify listener $it")
}
}
}

internal fun notifyNetworkNotAvailable() {
listeners.forEach {
try {
it.networkNotAvailable()
} catch (t: Throwable) {
Timber.e("Failed to notify listener $it")
}
}
}

internal fun notifyFailure(throwable: Throwable) {

listeners.forEach {
try {
it.onFailed(throwable)
} catch (t: Throwable) {
Timber.e("Failed to notify listener $it")
}
}

}

fun getService(): SyncService = this@SyncService

}

interface SyncListener {
fun onSyncFinsh()
fun networkNotAvailable()
fun onFailed(throwable: Throwable)
}

companion object {

fun startLongPool(delay: Long) {

}
}
}

View File

@ -0,0 +1,201 @@
package im.vector.matrix.android.internal.session.sync.job

import android.app.Service
import android.content.Intent
import android.os.Binder
import android.os.IBinder
import com.squareup.moshi.JsonEncodingException
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.di.MatrixKoinComponent
import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
import im.vector.matrix.android.internal.session.sync.SyncTask
import im.vector.matrix.android.internal.session.sync.SyncTokenStore
import im.vector.matrix.android.internal.session.sync.model.SyncResponse
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.TaskThread
import im.vector.matrix.android.internal.task.configureWith
import org.koin.standalone.inject
import timber.log.Timber
import java.net.SocketTimeoutException
import java.util.*
import kotlin.collections.ArrayList

private const val DEFAULT_LONG_POOL_TIMEOUT = 10_000L
private const val BACKGROUND_LONG_POOL_TIMEOUT = 0L

/**
* Can execute periodic sync task.
* An IntentService is used in conjunction with the AlarmManager and a Broadcast Receiver
* in order to be able to perform a sync even if the app is not running.
* The <receiver> and <service> must be declared in the Manifest or the app using the SDK
*/
open class SyncServiceOld : Service(), MatrixKoinComponent {

private var mIsSelfDestroyed: Boolean = false
private var cancelableTask: Cancelable? = null
private val syncTokenStore: SyncTokenStore by inject()

private val syncTask: SyncTask by inject()
private val networkConnectivityChecker: NetworkConnectivityChecker by inject()
private val taskExecutor: TaskExecutor by inject()

private var localBinder = LocalBinder()

val timer = Timer()

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Timber.i("onStartCommand ${intent}")
intent?.let {
if (cancelableTask == null) {
doSync(BACKGROUND_LONG_POOL_TIMEOUT)
} else {
//Already syncing ignore
Timber.i("Received a start while was already syncking... ignore")
}
}
//No intent just start the service, an alarm will should call with intent
return START_STICKY
}

override fun onDestroy() {
Timber.i("## onDestroy() : $this")

if (!mIsSelfDestroyed) {
Timber.w("## Destroy by the system : $this")
}

cancelableTask?.cancel()
super.onDestroy()
}

fun stopMe() {
cancelableTask?.cancel()
mIsSelfDestroyed = true
stopSelf()
}

fun doSync(currentLongPoolTimeoutMs: Long = DEFAULT_LONG_POOL_TIMEOUT) {
var nextBatch = syncTokenStore.getLastToken()
if (!networkConnectivityChecker.isConnected()) {
Timber.v("Sync is Paused. Waiting...")
//TODO Retry in ?
} else {
Timber.v("Execute sync request with token $nextBatch and timeout $currentLongPoolTimeoutMs")
val params = SyncTask.Params(nextBatch, currentLongPoolTimeoutMs)
cancelableTask = syncTask.configureWith(params)
.callbackOn(TaskThread.CALLER)
.executeOn(TaskThread.CALLER)
.dispatchTo(object : MatrixCallback<SyncResponse> {
override fun onSuccess(data: SyncResponse) {
cancelableTask = null
nextBatch = data.nextBatch
syncTokenStore.saveToken(nextBatch)
localBinder.notifySyncFinish()
}

override fun onFailure(failure: Throwable) {
Timber.e(failure)
cancelableTask = null
localBinder.notifyFailure(failure)
// if (failure is Failure.NetworkConnection
// && failure.cause is SocketTimeoutException) {
// // Timeout are not critical
// localBinder.notifyFailure()
// }
//
// if (failure !is Failure.NetworkConnection
// || failure.cause is JsonEncodingException) {
// // Wait 10s before retrying
//// Thread.sleep(RETRY_WAIT_TIME_MS)
// //TODO Retry in 10S?
// }
//
// if (failure is Failure.ServerError
// && (failure.error.code == MatrixError.UNKNOWN_TOKEN || failure.error.code == MatrixError.MISSING_TOKEN)) {
// // No token or invalid token, stop the thread
// stopSelf()
// }

}

})
.executeBy(taskExecutor)

//TODO return and schedule a new one?
// Timber.v("Waiting for $currentLongPoolDelayMs delay before new pool...")
// if (currentLongPoolDelayMs > 0) Thread.sleep(currentLongPoolDelayMs)
// Timber.v("...Continue")
}
}

override fun onBind(intent: Intent?): IBinder {
return localBinder
}

inner class LocalBinder : Binder() {

private var listeners = ArrayList<SyncListener>()

fun addListener(listener: SyncListener) {
if (!listeners.contains(listener)) {
listeners.add(listener)
}
}

fun removeListener(listener: SyncListener) {
listeners.remove(listener)
}

internal fun notifySyncFinish() {

listeners.forEach {
try {
it.onSyncFinsh()
} catch (t: Throwable) {
Timber.e("Failed to notify listener $it")
}
}
}

internal fun notifyNetworkNotAvailable() {
listeners.forEach {
try {
it.networkNotAvailable()
} catch (t: Throwable) {
Timber.e("Failed to notify listener $it")
}
}
}

internal fun notifyFailure(throwable: Throwable) {

listeners.forEach {
try {
it.onFailed(throwable)
} catch (t: Throwable) {
Timber.e("Failed to notify listener $it")
}
}

}

fun getService(): SyncServiceOld = this@SyncServiceOld

}

interface SyncListener {
fun onSyncFinsh()
fun networkNotAvailable()
fun onFailed(throwable: Throwable)
}

companion object {

fun startLongPool(delay: Long) {

}
}
}

View File

@ -37,6 +37,13 @@ import java.net.SocketTimeoutException
import java.util.concurrent.CountDownLatch

private const val RETRY_WAIT_TIME_MS = 10_000L
private const val DEFAULT_LONG_POOL_TIMEOUT = 10_000L
private const val DEFAULT_LONG_POOL_DELAY = 0L


private const val DEFAULT_BACKGROUND_LONG_POOL_TIMEOUT = 0L
private const val DEFAULT_BACKGROUND_LONG_POOL_DELAY = 30_000L


internal class SyncThread(private val syncTask: SyncTask,
private val networkConnectivityChecker: NetworkConnectivityChecker,
@ -55,6 +62,27 @@ internal class SyncThread(private val syncTask: SyncTask,
updateStateTo(SyncState.IDLE)
}

/**
* The maximum time to wait, in milliseconds, before returning this request.
* If no events (or other data) become available before this time elapses, the server will return a response with empty fields.
* If set to 0 the server will return immediately even if the response is empty.
*/
private var longPoolTimeoutMs = DEFAULT_LONG_POOL_TIMEOUT
/**
* When the server responds to a sync request, the client waits for `longPoolDelay` before calling a new sync.
*/
private var longPoolDelayMs = DEFAULT_LONG_POOL_DELAY


var shouldPauseOnBackground: Boolean = true
private var backgroundedLongPoolTimeoutMs = DEFAULT_BACKGROUND_LONG_POOL_TIMEOUT
private var backgroundedLongPoolDelayMs = DEFAULT_BACKGROUND_LONG_POOL_DELAY


private var currentLongPoolTimeoutMs = longPoolTimeoutMs
private var currentLongPoolDelayMs = longPoolDelayMs


fun restart() = synchronized(lock) {
if (state is SyncState.PAUSED) {
Timber.v("Resume sync...")
@ -65,6 +93,30 @@ internal class SyncThread(private val syncTask: SyncTask,
}
}

/**
* Configures the long pooling settings
*/
fun configureLongPoolingSettings(timoutMS: Long, delayMs: Long) {
longPoolTimeoutMs = Math.max(0, timoutMS)
longPoolDelayMs = Math.max(0, delayMs)
}

/**
* Configures the long pooling settings in background mode (used only if should not pause on BG)
*/
fun configureBackgroundeLongPoolingSettings(timoutMS: Long, delayMs: Long) {
backgroundedLongPoolTimeoutMs = Math.max(0, timoutMS)
backgroundedLongPoolDelayMs = Math.max(0, delayMs)
}


fun resetLongPoolingSettings() {
longPoolTimeoutMs = DEFAULT_LONG_POOL_TIMEOUT
longPoolDelayMs = DEFAULT_LONG_POOL_DELAY
backgroundedLongPoolTimeoutMs = DEFAULT_BACKGROUND_LONG_POOL_TIMEOUT
backgroundedLongPoolDelayMs = DEFAULT_BACKGROUND_LONG_POOL_DELAY
}

fun pause() = synchronized(lock) {
if (state is SyncState.RUNNING) {
Timber.v("Pause sync...")
@ -91,14 +143,14 @@ internal class SyncThread(private val syncTask: SyncTask,

while (state != SyncState.KILLING) {
if (!networkConnectivityChecker.isConnected() || state == SyncState.PAUSED) {
Timber.v("Waiting...")
Timber.v("Sync is Paused. Waiting...")
synchronized(lock) {
lock.wait()
}
} else {
Timber.v("Execute sync request with token $nextBatch")
Timber.v("Execute sync request with token $nextBatch and timeout $currentLongPoolTimeoutMs")
val latch = CountDownLatch(1)
val params = SyncTask.Params(nextBatch)
val params = SyncTask.Params(nextBatch, currentLongPoolTimeoutMs)
cancelableTask = syncTask.configureWith(params)
.callbackOn(TaskThread.CALLER)
.executeOn(TaskThread.CALLER)
@ -135,10 +187,15 @@ internal class SyncThread(private val syncTask: SyncTask,

})
.executeBy(taskExecutor)
latch.await()

latch.await()
if (state is SyncState.RUNNING) {
updateStateTo(SyncState.RUNNING(catchingUp = false))
}

Timber.v("Waiting for $currentLongPoolDelayMs delay before new pool...")
if (currentLongPoolDelayMs > 0) sleep(currentLongPoolDelayMs)
Timber.v("...Continue")
}
}
Timber.v("Sync killed")
@ -159,14 +216,22 @@ internal class SyncThread(private val syncTask: SyncTask,
}

override fun onMoveToForeground() {
currentLongPoolTimeoutMs = longPoolTimeoutMs
currentLongPoolDelayMs = longPoolDelayMs
restart()
}

override fun onMoveToBackground() {
pause()
if (shouldPauseOnBackground) {
pause()
} else {
Timber.v("Slower sync in background mode")
//we continue but with a slower pace
currentLongPoolTimeoutMs = backgroundedLongPoolTimeoutMs
currentLongPoolDelayMs = backgroundedLongPoolDelayMs
}
}


}



View File

@ -0,0 +1,119 @@
/*
* 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.sync.job

import android.content.Context
import androidx.work.*
import arrow.core.failure
import arrow.core.recoverWith
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.di.MatrixKoinComponent
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.filter.FilterRepository
import im.vector.matrix.android.internal.session.sync.SyncAPI
import im.vector.matrix.android.internal.session.sync.SyncResponseHandler
import im.vector.matrix.android.internal.session.sync.SyncTokenStore
import im.vector.matrix.android.internal.session.sync.model.SyncResponse
import im.vector.matrix.android.internal.util.WorkerParamsFactory
import org.koin.standalone.inject
import timber.log.Timber
import java.net.SocketTimeoutException
import java.util.concurrent.TimeUnit


private const val DEFAULT_LONG_POOL_TIMEOUT = 0L

class SyncWorker(context: Context,
workerParameters: WorkerParameters
) : CoroutineWorker(context, workerParameters), MatrixKoinComponent {

@JsonClass(generateAdapter = true)
internal data class Params(
val timeout: Long = DEFAULT_LONG_POOL_TIMEOUT
)

private val syncAPI by inject<SyncAPI>()
private val filterRepository by inject<FilterRepository>()
private val syncResponseHandler by inject<SyncResponseHandler>()
private val sessionParamsStore by inject<SessionParamsStore>()
private val syncTokenStore by inject<SyncTokenStore>()

val autoMode = false

override suspend fun doWork(): Result {
Timber.i("Sync work starting")
val params = WorkerParamsFactory.fromData<Params>(inputData)
?: Params()

val requestParams = HashMap<String, String>()
requestParams["timeout"] = params.timeout.toString()
requestParams["filter"] = filterRepository.getFilter()
val token = syncTokenStore.getLastToken()?.also { requestParams["since"] = it }
Timber.i("Sync work last token $token")

return executeRequest<SyncResponse> {
apiCall = syncAPI.sync(requestParams)
}.recoverWith { throwable ->
// Intercept 401
if (throwable is Failure.ServerError
&& throwable.error.code == MatrixError.UNKNOWN_TOKEN) {
sessionParamsStore.delete()
}
Timber.i("Sync work failed $throwable")
// Transmit the throwable
throwable.failure()
}.fold(
{
Timber.i("Sync work failed $it")
again()
if (it is Failure.NetworkConnection && it.cause is SocketTimeoutException) {
// Timeout are not critical
Result.Success()
} else {
Result.Success()
}
},
{
Timber.i("Sync work success next batch ${it.nextBatch}")
syncResponseHandler.handleResponse(it, token, false)
syncTokenStore.saveToken(it.nextBatch)
again()
Result.success()
}
)

}

fun again() {
if (autoMode) {
Timber.i("Sync work Again!!")
val workRequest = OneTimeWorkRequestBuilder<SyncWorker>()
.setInitialDelay(30_000, TimeUnit.MILLISECONDS)
.setConstraints(Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build())
.setBackoffCriteria(BackoffPolicy.LINEAR, 10_000, TimeUnit.MILLISECONDS)
.build()
WorkManager.getInstance().enqueueUniqueWork("BG_SYNCP", ExistingWorkPolicy.APPEND, workRequest)

}

}

}

View File

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

import im.vector.matrix.android.api.pushrules.rest.PushRule
import im.vector.matrix.android.internal.di.MoshiProvider
import org.junit.Assert
import org.junit.Test


class PushRuleActionsTest {

@Test
fun test_action_parsing() {
val rawPushRule = """
{
"rule_id": ".m.rule.invite_for_me",
"default": true,
"enabled": true,
"conditions": [
{
"key": "type",
"kind": "event_match",
"pattern": "m.room.member"
},
{
"key": "content.membership",
"kind": "event_match",
"pattern": "invite"
},
{
"key": "state_key",
"kind": "event_match",
"pattern": "[the user's Matrix ID]"
}
],
"domainActions": [
"notify",
{
"set_tweak": "sound",
"value": "default"
},
{
"set_tweak": "highlight",
"value": false
}
]
}
""".trimIndent()


val pushRule = MoshiProvider.providesMoshi().adapter<PushRule>(PushRule::class.java).fromJson(rawPushRule)

Assert.assertNotNull("Should have parsed the rule", pushRule)
Assert.assertNotNull("Failed to parse domainActions", pushRule?.domainActions())
Assert.assertEquals(3, pushRule!!.domainActions()!!.size)


Assert.assertEquals("First action should be notify", Action.Type.NOTIFY, pushRule!!.domainActions()!![0].type)


Assert.assertEquals("Second action should be tweak", Action.Type.SET_TWEAK, pushRule!!.domainActions()!![1].type)
Assert.assertEquals("Second action tweak key should be sound", "sound", pushRule!!.domainActions()!![1].tweak_action)
Assert.assertEquals("Second action should have default as stringValue", "default", pushRule!!.domainActions()!![1].stringValue)
Assert.assertNull("Second action boolValue should be null", pushRule!!.domainActions()!![1].boolValue)


Assert.assertEquals("Third action should be tweak", Action.Type.SET_TWEAK, pushRule!!.domainActions()!![2].type)
Assert.assertEquals("Third action tweak key should be highlight", "highlight", pushRule!!.domainActions()!![2].tweak_action)
Assert.assertEquals("Third action tweak param should be false", false, pushRule!!.domainActions()!![2].boolValue)
Assert.assertNull("Third action stringValue should be null", pushRule!!.domainActions()!![2].stringValue)

}
}

View File

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

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.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
import org.junit.Test

class PushrulesConditionTest {


@Test
fun test_eventmatch_type_condition() {
val condition = EventMatchCondition("type", "m.room.message")

val simpleTextEvent = Event(
type = "m.room.message",
eventId = "mx0",
content = MessageTextContent("m.text", "Yo wtf?").toContent(),
originServerTs = 0)

val rm = RoomMember(
Membership.INVITE,
displayName = "Foo",
avatarUrl = "mxc://matrix.org/EqMZYbREvHXvYFyfxOlkf"
)
val simpleRoomMemberEvent = Event(
type = "m.room.member",
eventId = "mx0",
stateKey = "@foo:matrix.org",
content = rm.toContent(),
originServerTs = 0)

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

@Test
fun test_eventmatch_path_condition() {
val condition = EventMatchCondition("content.msgtype", "m.text")

val simpleTextEvent = Event(
type = "m.room.message",
eventId = "mx0",
content = MessageTextContent("m.text", "Yo wtf?").toContent(),
originServerTs = 0)

assert(condition.isSatisfied(simpleTextEvent))

Event(
type = "m.room.member",
eventId = "mx0",
stateKey = "@foo:matrix.org",
content = RoomMember(
Membership.INVITE,
displayName = "Foo",
avatarUrl = "mxc://matrix.org/EqMZYbREvHXvYFyfxOlkf"
).toContent(),
originServerTs = 0
).apply {
assert(EventMatchCondition("content.membership", "invite").isSatisfied(this))
}

}

@Test
fun test_eventmatch_cake_condition() {
val condition = EventMatchCondition("content.body", "cake")

Event(
type = "m.room.message",
eventId = "mx0",
content = MessageTextContent("m.text", "How was the cake?").toContent(),
originServerTs = 0
).apply {
assert(condition.isSatisfied(this))
}

Event(
type = "m.room.message",
eventId = "mx0",
content = MessageTextContent("m.text", "Howwasthecake?").toContent(),
originServerTs = 0
).apply {
assert(condition.isSatisfied(this))
}

}

@Test
fun test_eventmatch_cakelie_condition() {
val condition = EventMatchCondition("content.body", "cake*lie")

val simpleTextEvent = Event(
type = "m.room.message",
eventId = "mx0",
content = MessageTextContent("m.text", "How was the cakeisalie?").toContent(),
originServerTs = 0)

assert(condition.isSatisfied(simpleTextEvent))
}
}

10
tools/tests/app_standby_off.sh Executable file
View File

@ -0,0 +1,10 @@
#!/usr/bin/env bash

# Ref: https://developer.android.com/training/monitoring-device-state/doze-standby#testing_your_app_with_app_standby

echo "Standby OFF"
echo "adb shell dumpsys battery reset"
adb shell dumpsys battery reset

echo "adb shell am set-inactive im.vector.riotredesign false"
adb shell am set-inactive im.vector.riotredesign false

10
tools/tests/app_standby_on.sh Executable file
View File

@ -0,0 +1,10 @@
#!/usr/bin/env bash

# Ref: https://developer.android.com/training/monitoring-device-state/doze-standby#testing_your_app_with_app_standby

echo "Standby ON"
echo "adb shell dumpsys battery unplug"
adb shell dumpsys battery unplug

echo "adb shell am set-inactive im.vector.riotredesign true"
adb shell am set-inactive im.vector.riotredesign true

View File

@ -0,0 +1,11 @@
#!/usr/bin/env bash

# Ref: https://developer.android.com/training/monitoring-device-state/doze-standby#testing_doze

echo "Exit doze mode"
echo "shell dumpsys deviceidle unforce"
adb shell dumpsys deviceidle unforce

echo "Reactivate device"
echo "shell dumpsys battery reset"
adb shell dumpsys battery reset

View File

@ -0,0 +1,7 @@
#!/usr/bin/env bash

# Ref: https://developer.android.com/training/monitoring-device-state/doze-standby#testing_doze

echo "Enable doze mode"
echo "adb shell dumpsys deviceidle force-idle"
adb shell dumpsys deviceidle force-idle

View File

@ -0,0 +1,122 @@
<?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/pushGatewayKind"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:textStyle="bold"
android:textSize="20sp"
android:textAllCaps="true"
tools:text="Http Pusher" />

<TextView
android:id="@+id/pushGatewayAppId"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:text="app_id"
android:textStyle="bold" />


<TextView
android:id="@+id/pushGatewayAppIdValue"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:textStyle=""
tools:text="im.vector.app.android" />

<TextView
android:id="@+id/pushGatewayKey"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:text="push_key:"
android:textStyle="bold" />


<TextView
android:id="@+id/pushGatewayKeyValue"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:textStyle=""
tools:text="fBbCDxVa-n8:APA91bE0wGY4ijpj-LQkkmjJYhNp2vA_9Xvabh02xaTKua9WA9wpNZwxfHdsbIDWthVXKPFTNcCl75ek1kqMGOggnUwnSCj-8ReF4G69pZVUhz-" />

<TextView
android:id="@+id/pushGatewayAppName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:text="app_display_name"
android:textStyle="bold" />


<TextView
android:id="@+id/pushGatewayAppNameValue"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:textStyle=""
tools:text="EBMDOLFJD" />

<TextView
android:id="@+id/pushGatewayDeviceName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:text="device_name:"
android:textStyle="bold" />


<TextView
android:id="@+id/pushGatewayDeviceNameValue"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:textStyle=""
tools:text="EBMDOLFJD" />


<TextView
android:id="@+id/pushGatewayURL"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:text="Url:"
android:textStyle="bold" />

<TextView
android:id="@+id/pushGatewayURLValue"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:textStyle=""
tools:text="EBMDOLFJD" />

<TextView
android:id="@+id/pushGatewayFormat"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:text="Format:"
android:textStyle="bold" />


<TextView
android:id="@+id/pushGatewayFormatValue"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:textStyle=""
tools:text="event_id_only" />


</LinearLayout>

View File

@ -50,7 +50,7 @@ public class FcmHelper {
*
* @param activity the first launch Activity
*/
public static void ensureFcmTokenIsRetrieved(final Activity activity) {
public static void ensureFcmTokenIsRetrieved(final Activity activity, PushersManager pushersManager) {
// No op
}
}

View File

@ -10,14 +10,10 @@
"client_info": {
"mobilesdk_app_id": "1:912726360885:android:448c9b63161abc9c",
"android_client_info": {
"package_name": "im.vector.riotredesign"
"package_name": "im.vector.alpha"
}
},
"oauth_client": [
{
"client_id": "912726360885-rsae0i66rgqt6ivnudu1pv4tksg9i8b2.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com",
"client_type": 3
@ -29,15 +25,100 @@
}
],
"services": {
"analytics_service": {
"status": 1
},
"appinvite_service": {
"status": 1,
"other_platform_oauth_client": []
},
"ads_service": {
"status": 2
"other_platform_oauth_client": [
{
"client_id": "912726360885-rsae0i66rgqt6ivnudu1pv4tksg9i8b2.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:912726360885:android:3120c24f6ef22f2b",
"android_client_info": {
"package_name": "im.vector.app"
}
},
"oauth_client": [
{
"client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyAFZX8IhIfgzdOZvxDP_ISO5WYoU7jmQ5c"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "912726360885-rsae0i66rgqt6ivnudu1pv4tksg9i8b2.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:912726360885:android:25ef253beaff462e",
"android_client_info": {
"package_name": "im.vector.riotredesign"
}
},
"oauth_client": [
{
"client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyAFZX8IhIfgzdOZvxDP_ISO5WYoU7jmQ5c"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "912726360885-rsae0i66rgqt6ivnudu1pv4tksg9i8b2.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:912726360885:android:bb204b7a7b08a10b",
"android_client_info": {
"package_name": "im.veon"
}
},
"oauth_client": [
{
"client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyAFZX8IhIfgzdOZvxDP_ISO5WYoU7jmQ5c"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "912726360885-rsae0i66rgqt6ivnudu1pv4tksg9i8b2.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
}

View File

@ -23,16 +23,15 @@ import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.widget.Toast;

import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
import com.google.android.gms.tasks.OnFailureListener;
import com.google.android.gms.tasks.OnSuccessListener;
import com.google.firebase.iid.FirebaseInstanceId;
import com.google.firebase.iid.InstanceIdResult;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
import com.google.firebase.iid.FirebaseInstanceId;

import im.vector.riotredesign.R;
import im.vector.riotredesign.core.pushers.PushersManager;
import timber.log.Timber;

/**
@ -66,6 +65,7 @@ public class FcmHelper {
.edit()
.putString(PREFS_KEY_FCM_TOKEN, token)
.apply();

}

/**
@ -73,8 +73,8 @@ public class FcmHelper {
*
* @param activity the first launch Activity
*/
public static void ensureFcmTokenIsRetrieved(final Activity activity) {
if (TextUtils.isEmpty(getFcmToken(activity))) {
public static void ensureFcmTokenIsRetrieved(final Activity activity, PushersManager pushersManager) {
// if (TextUtils.isEmpty(getFcmToken(activity))) {


//vfe: according to firebase doc
@ -82,18 +82,11 @@ public class FcmHelper {
if (checkPlayServices(activity)) {
try {
FirebaseInstanceId.getInstance().getInstanceId()
.addOnSuccessListener(activity, new OnSuccessListener<InstanceIdResult>() {
@Override
public void onSuccess(InstanceIdResult instanceIdResult) {
storeFcmToken(activity, instanceIdResult.getToken());
}
.addOnSuccessListener(activity, instanceIdResult -> {
storeFcmToken(activity, instanceIdResult.getToken());
pushersManager.registerPusherWithFcmKey(instanceIdResult.getToken());
})
.addOnFailureListener(activity, new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed " + e.getMessage());
}
});
.addOnFailureListener(activity, e -> Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed " + e.getMessage()));
} catch (Throwable e) {
Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed " + e.getMessage());
}
@ -101,7 +94,7 @@ public class FcmHelper {
Toast.makeText(activity, R.string.no_valid_google_play_services_apk, Toast.LENGTH_SHORT).show();
Timber.e("No valid Google Play Services found. Cannot use FCM.");
}
}
// }
}

/**

View File

@ -22,13 +22,19 @@ package im.vector.riotredesign.push.fcm
import android.os.Handler
import android.os.Looper
import android.text.TextUtils
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.work.*
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.internal.session.sync.job.SyncWorker
import im.vector.riotredesign.BuildConfig
import im.vector.riotredesign.R
import im.vector.riotredesign.core.preference.BingRule
import im.vector.riotredesign.core.pushers.PushersManager
import im.vector.riotredesign.features.badge.BadgeProxy
import im.vector.riotredesign.features.notifications.NotifiableEventResolver
import im.vector.riotredesign.features.notifications.NotifiableMessageEvent
@ -36,13 +42,15 @@ import im.vector.riotredesign.features.notifications.NotificationDrawerManager
import im.vector.riotredesign.features.notifications.SimpleNotifiableEvent
import org.koin.android.ext.android.inject
import timber.log.Timber
import java.util.concurrent.TimeUnit

/**
* Class extending FirebaseMessagingService.
*/
class VectorFirebaseMessagingService : FirebaseMessagingService() {

val notificationDrawerManager by inject<NotificationDrawerManager>()
private val notificationDrawerManager by inject<NotificationDrawerManager>()
private val pusherManager by inject<PushersManager>()

private val notifiableEventResolver by lazy {
NotifiableEventResolver(this)
@ -67,18 +75,14 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
Timber.i("## onMessageReceived()" + message.data.toString())
Timber.i("## onMessageReceived() from FCM with priority " + message.priority)
}

//safe guard
/* TODO
val pushManager = Matrix.getInstance(applicationContext).pushManager
if (!pushManager.areDeviceNotificationsAllowed()) {
Timber.i("## onMessageReceived() : the notifications are disabled")
return
mUIHandler.post {
if (ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
//we are in foreground, let the sync do the things?
Timber.v("PUSH received in a foreground state, ignore")
} else {
onMessageReceivedInternal(message.data)
}
}
*/

//TODO if the app is in foreground, we could just ignore this. The sync loop is already going?
// TODO mUIHandler.post { onMessageReceivedInternal(message.data, pushManager) }
}

/**
@ -90,9 +94,22 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
override fun onNewToken(refreshedToken: String?) {
Timber.i("onNewToken: FCM Token has been updated")
FcmHelper.storeFcmToken(this, refreshedToken)
// TODO Matrix.getInstance(this)?.pushManager?.resetFCMRegistration(refreshedToken)
if (refreshedToken == null) {
Timber.w("onNewToken:received null token")
} else {
pusherManager.registerPusherWithFcmKey(refreshedToken)
}
}

/**
* Called when the FCM server deletes pending messages. This may be due to:
* - Too many messages stored on the FCM server.
* This can occur when an app's servers send a bunch of non-collapsible messages to FCM servers while the device is offline.
* - The device hasn't connected in a long time and the app server has recently (within the last 4 weeks)
* sent a message to the app on that device.
*
* It is recommended that the app do a full sync with the app server after receiving this call.
*/
override fun onDeletedMessages() {
Timber.v("## onDeletedMessages()")
}
@ -103,53 +120,63 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
* @param data Data map containing message data as key/value pairs.
* For Set of keys use data.keySet().
*/
private fun onMessageReceivedInternal(data: Map<String, String> /*, pushManager: PushManager*/) {
private fun onMessageReceivedInternal(data: Map<String, String>) {
try {
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
Timber.i("## onMessageReceivedInternal() : $data")
}
val eventId = data["event_id"]
val roomId = data["room_id"]
if (eventId == null || roomId == null) {
Timber.e("## onMessageReceivedInternal() missing eventId and/or roomId")
return
}
// update the badge counter
val unreadCount = data.get("unread")?.let { Integer.parseInt(it) } ?: 0
BadgeProxy.updateBadgeCount(applicationContext, unreadCount)

/* TODO
val session = Matrix.getInstance(applicationContext)?.defaultSession
val session = safeGetCurrentSession()

if (VectorApp.isAppInBackground() && !pushManager.isBackgroundSyncAllowed) {
//Notification contains metadata and maybe data information
handleNotificationWithoutSyncingMode(data, session)
if (session == null) {
Timber.w("## Can't sync from push, no current session")
} else {
// Safe guard... (race?)
if (isEventAlreadyKnown(data["event_id"], data["room_id"])) return
//Catch up!!
EventStreamServiceX.onPushReceived(this)
if (isEventAlreadyKnown(eventId, roomId)) {
Timber.i("Ignoring push, event already knwown")
} else {
Timber.v("Requesting background sync")
val workRequest = OneTimeWorkRequestBuilder<SyncWorker>()
.setInputData(Data.Builder().put("timeout", 0L).build())
.setConstraints(Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build())
.setBackoffCriteria(BackoffPolicy.LINEAR, 10_000, TimeUnit.MILLISECONDS)
.build()
WorkManager.getInstance().enqueueUniqueWork("BG_SYNCP", ExistingWorkPolicy.REPLACE, workRequest)
}
}
*/

} catch (e: Exception) {
Timber.e(e, "## onMessageReceivedInternal() failed : " + e.message)
}
}

fun safeGetCurrentSession(): Session? {
try {
return Matrix.getInstance().currentSession
} catch (e: Throwable) {
Timber.e(e, "## Failed to get current session")
return null
}
}

// check if the event was not yet received
// a previous catchup might have already retrieved the notified event
private fun isEventAlreadyKnown(eventId: String?, roomId: String?): Boolean {
if (null != eventId && null != roomId) {
try {
/* TODO
val sessions = Matrix.getInstance(applicationContext).sessions

if (null != sessions && !sessions.isEmpty()) {
for (session in sessions) {
if (session.dataHandler?.store?.isReady == true) {
session.dataHandler.store?.getEvent(eventId, roomId)?.let {
Timber.e("## isEventAlreadyKnown() : ignore the event " + eventId
+ " in room " + roomId + " because it is already known")
return true
}
}
}
}
*/
val session = safeGetCurrentSession() ?: return false
val room = session.getRoom(roomId) ?: return false
return room.getTimeLineEvent(eventId) != null
} catch (e: Exception) {
Timber.e(e, "## isEventAlreadyKnown() : failed to check if the event was already defined " + e.message)
}
@ -199,7 +226,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
return
} else {

var notifiableEvent = notifiableEventResolver.resolveEvent(event, null, null /* TODO session.fulfillRule(event) */, session)
var notifiableEvent = notifiableEventResolver.resolveEvent(event, null /* TODO session.fulfillRule(event) */, session)

if (notifiableEvent == null) {
Timber.e("Unsupported notifiable event ${eventId}")
@ -211,7 +238,8 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {

if (notifiableEvent is NotifiableMessageEvent) {
if (TextUtils.isEmpty(notifiableEvent.senderName)) {
notifiableEvent.senderName = data["sender_display_name"] ?: data["sender"] ?: ""
notifiableEvent.senderName = data["sender_display_name"]
?: data["sender"] ?: ""
}
if (TextUtils.isEmpty(notifiableEvent.roomName)) {
notifiableEvent.roomName = findRoomNameBestEffort(data, session) ?: ""

View File

@ -3,7 +3,6 @@
xmlns:tools="http://schemas.android.com/tools"
package="im.vector.riotredesign">


<uses-permission android:name="android.permission.INTERNET" />

<application
@ -16,6 +15,12 @@
android:supportsRtl="true"
android:theme="@style/AppTheme.Light"
tools:replace="android:allowBackup">
<receiver
android:name=".core.services.RestartBroadcastReceiver"
android:enabled="true"
android:exported="false">

</receiver>

<activity
android:name=".features.MainActivity"
@ -32,7 +37,6 @@
</intent-filter>
</activity-alias>


<activity android:name=".features.home.HomeActivity" />
<activity android:name=".features.login.LoginActivity" />
<activity android:name=".features.media.ImageMediaViewerActivity" />
@ -58,9 +62,8 @@
android:label="@string/encryption_message_recovery" />

<activity
android:name="im.vector.riotredesign.features.reactions.EmojiReactionPickerActivity"
android:name=".features.reactions.EmojiReactionPickerActivity"
android:label="@string/title_activity_emoji_reaction_picker" />

<activity android:name=".features.roomdirectory.RoomDirectoryActivity" />
<activity android:name=".features.roomdirectory.roompreview.RoomPreviewActivity" />
<activity android:name=".features.home.room.detail.RoomDetailActivity" />
@ -69,6 +72,15 @@
<service
android:name=".core.services.CallService"
android:exported="false" />
<!--<service-->
<!--android:name="im.vector.matrix.android.internal.session.sync.job.SyncService"-->
<!--android:exported="false" />-->

<service
android:name=".core.services.VectorSyncService"
android:exported="false">

</service>

<provider
android:name="androidx.core.content.FileProvider"
@ -79,7 +91,6 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/riotx_provider_paths" />
</provider>

</application>

</manifest>

View File

@ -16,14 +16,25 @@

package im.vector.riotredesign

import android.app.AlarmManager
import android.app.Application
import android.app.PendingIntent
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.content.res.Configuration
import android.os.Handler
import android.os.HandlerThread
import android.os.IBinder
import androidx.core.provider.FontRequest
import androidx.core.provider.FontsContractCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.multidex.MultiDex
import androidx.work.*
import com.airbnb.epoxy.EpoxyAsyncUtil
import com.airbnb.epoxy.EpoxyController
import com.facebook.stetho.Stetho
@ -31,24 +42,32 @@ import com.github.piasy.biv.BigImageViewer
import com.github.piasy.biv.loader.glide.GlideImageLoader
import com.jakewharton.threetenabp.AndroidThreeTen
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.internal.session.sync.job.SyncService
import im.vector.matrix.android.internal.session.sync.job.SyncWorker
import im.vector.riotredesign.core.di.AppModule
import im.vector.riotredesign.core.services.RestartBroadcastReceiver
import im.vector.riotredesign.core.services.VectorSyncService
import im.vector.riotredesign.features.configuration.VectorConfiguration
import im.vector.riotredesign.features.crypto.keysbackup.KeysBackupModule
import im.vector.riotredesign.features.home.HomeModule
import im.vector.riotredesign.features.lifecycle.VectorActivityLifecycleCallbacks
import im.vector.riotredesign.features.notifications.NotificationUtils
import im.vector.riotredesign.features.notifications.PushRuleTriggerListener
import im.vector.riotredesign.features.rageshake.VectorFileLogger
import im.vector.riotredesign.features.rageshake.VectorUncaughtExceptionHandler
import im.vector.riotredesign.features.roomdirectory.RoomDirectoryModule
import im.vector.riotredesign.features.version.getVersion
import org.koin.android.ext.android.get
import org.koin.android.ext.android.inject
import org.koin.log.EmptyLogger
import org.koin.standalone.StandAloneContext.startKoin
import timber.log.Timber
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.TimeUnit

class VectorApplication : Application(), SyncService.SyncListener {

class VectorApplication : Application() {

lateinit var appContext: Context
//font thread handler
@ -56,6 +75,29 @@ class VectorApplication : Application() {

val vectorConfiguration: VectorConfiguration by inject()

private var mBinder: SyncService.LocalBinder? = null

private val connection = object : ServiceConnection {
override fun onServiceDisconnected(name: ComponentName?) {
Timber.v("Service unbounded")
mBinder?.removeListener(this@VectorApplication)
mBinder = null
}

override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
Timber.v("Service bounded")
mBinder = service as SyncService.LocalBinder
mBinder?.addListener(this@VectorApplication)
mBinder?.getService()?.nextBatchDelay = 0
mBinder?.getService()?.timeout = 10_000L
mBinder?.getService()?.doSync()
}

}

// var slowMode = false


override fun onCreate() {
super.onCreate()
appContext = this
@ -91,10 +133,111 @@ class VectorApplication : Application() {
R.array.com_google_android_gms_fonts_certs
)

// val efp = koin.koinContext.get<EmojiCompatFontProvider>()
FontsContractCompat.requestFont(this, fontRequest, koin.koinContext.get<EmojiCompatFontProvider>(), getFontThreadHandler())

vectorConfiguration.initConfiguration()

NotificationUtils.createNotificationChannels(applicationContext)

ProcessLifecycleOwner.get().lifecycle.addObserver(object : LifecycleObserver {

fun cancelAlarm() {
val intent = Intent(applicationContext, RestartBroadcastReceiver::class.java)
val pIntent = PendingIntent.getBroadcast(applicationContext, RestartBroadcastReceiver.REQUEST_CODE,
intent, PendingIntent.FLAG_UPDATE_CURRENT)
val alarm = getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarm.cancel(pIntent)
}

@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun entersForeground() {
// HttpLongPoolingSyncService.startService(applicationContext)
// cancelAlarm()
if (Matrix.getInstance().currentSession == null) return
WorkManager.getInstance().cancelAllWorkByTag("BG_SYNC")
Intent(applicationContext, VectorSyncService::class.java).also { intent ->
// intent.action = "NORMAL"
// try {
// startService(intent)
// } catch (e: Throwable) {
// Timber.e("Failed to launch sync service")
// }
bindService(intent, connection, Context.BIND_AUTO_CREATE)

}
}

var isPushAvailable = true
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
fun entersBackground() {
Timber.i("App entered background")
//we have here 3 modes

if (isPushAvailable) {
// PUSH IS AVAILABLE:
// Just stop the service, we will sync when a notification is received
try {
unbindService(connection)
mBinder?.getService()?.stopMe()
mBinder = null
} catch (t: Throwable) {
Timber.e(t)
}
} else {

// NO PUSH, and don't care about battery
// unbindService(connection)
// mBinder?.getService()?.stopMe()// kill also
// mBinder = null
//In this case we will keep a permanent

//TODO if no push schedule reccuring alarm

// val workRequest = PeriodicWorkRequestBuilder<SyncWorker>(1, TimeUnit.MINUTES)
// .setConstraints(Constraints.Builder()
// .setRequiredNetworkType(NetworkType.CONNECTED)
// .build())
// .setBackoffCriteria(BackoffPolicy.LINEAR, 10_000, TimeUnit.MILLISECONDS)
// .build()
// WorkManager.getInstance().enqueueUniquePeriodicWork(
// "BG_SYNC",
// ExistingPeriodicWorkPolicy.KEEP,
// workRequest)
val workRequest = OneTimeWorkRequestBuilder<SyncWorker>()
// .setInitialDelay(30_000, TimeUnit.MILLISECONDS)
.setInputData(Data.Builder().put("timeout", 0L).build())
.setConstraints(Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build())
.setBackoffCriteria(BackoffPolicy.LINEAR, 10_000, TimeUnit.MILLISECONDS)
.build()
WorkManager.getInstance().enqueueUniqueWork("BG_SYNCP", ExistingWorkPolicy.REPLACE, workRequest)

// val intent = Intent(applicationContext, RestartBroadcastReceiver::class.java)
// // Create a PendingIntent to be triggered when the alarm goes off
// val pIntent = PendingIntent.getBroadcast(applicationContext, RestartBroadcastReceiver.REQUEST_CODE,
// intent, PendingIntent.FLAG_UPDATE_CURRENT);
// // Setup periodic alarm every every half hour from this point onwards
// val firstMillis = System.currentTimeMillis(); // alarm is set right away
// val alarmMgr = getSystemService(Context.ALARM_SERVICE) as AlarmManager
// // First parameter is the type: ELAPSED_REALTIME, ELAPSED_REALTIME_WAKEUP, RTC_WAKEUP
// // Interval can be INTERVAL_FIFTEEN_MINUTES, INTERVAL_HALF_HOUR, INTERVAL_HOUR, INTERVAL_DAY
//// alarm.setInexactRepeating(AlarmManager.RTC_WAKEUP, firstMillis,
//// 30_000L, pIntent)
// alarmMgr.set(AlarmManager.RTC_WAKEUP, firstMillis, pIntent);

Timber.i("Alarm scheduled to restart service")
}
}

})


Matrix.getInstance().currentSession?.let {
it.refreshPushers()
//bind to the sync service
get<PushRuleTriggerListener>().startWithSession(it)
}
}

private fun logInfo() {
@ -130,4 +273,36 @@ class VectorApplication : Application() {
return mFontThreadHandler!!
}

override fun onSyncFinsh() {
//in foreground sync right now!!
Timber.v("Sync just finished")
// mBinder?.getService()?.doSync()
}

override fun networkNotAvailable() {
//we then want to retry in 10s?
}

override fun onFailed(failure: Throwable) {
//stop it also?
// if (failure is Failure.NetworkConnection
// && failure.cause is SocketTimeoutException) {
// // Timeout are not critical just retry?
// //TODO
// }
//
// if (failure !is Failure.NetworkConnection
// || failure.cause is JsonEncodingException) {
// //TODO Retry in 10S?
// }
//
// if (failure is Failure.ServerError
// && (failure.error.code == MatrixError.UNKNOWN_TOKEN || failure.error.code == MatrixError.MISSING_TOKEN)) {
// // No token or invalid token, stop the thread
// mBinder?.getService()?.unbindService(connection)
// mBinder?.getService()?.stopMe()
// }

}

}

View File

@ -21,6 +21,8 @@ import android.content.Context.MODE_PRIVATE
import im.vector.matrix.android.api.Matrix
import im.vector.riotredesign.EmojiCompatFontProvider
import im.vector.riotredesign.core.error.ErrorFormatter
import im.vector.riotredesign.core.pushers.PushersManager
import im.vector.riotredesign.core.resources.AppNameProvider
import im.vector.riotredesign.core.resources.LocaleProvider
import im.vector.riotredesign.core.resources.StringArrayProvider
import im.vector.riotredesign.core.resources.StringProvider
@ -33,7 +35,9 @@ import im.vector.riotredesign.features.home.room.list.AlphabeticalRoomComparator
import im.vector.riotredesign.features.home.room.list.ChronologicalRoomComparator
import im.vector.riotredesign.features.navigation.DefaultNavigator
import im.vector.riotredesign.features.navigation.Navigator
import im.vector.riotredesign.features.notifications.NotifiableEventResolver
import im.vector.riotredesign.features.notifications.NotificationDrawerManager
import im.vector.riotredesign.features.notifications.PushRuleTriggerListener
import org.koin.dsl.module.module

class AppModule(private val context: Context) {
@ -80,10 +84,18 @@ class AppModule(private val context: Context) {
ErrorFormatter(get())
}

single {
PushRuleTriggerListener(get(),get())
}

single {
NotificationDrawerManager(context)
}

single {
NotifiableEventResolver(context)
}

factory {
Matrix.getInstance().currentSession!!
}
@ -103,5 +115,13 @@ class AppModule(private val context: Context) {
single {
EmojiCompatFontProvider()
}

single {
AppNameProvider(context)
}

single {
PushersManager(get(), get(), get(), get())
}
}
}

View File

@ -28,6 +28,8 @@ abstract class VectorPreferenceFragment : PreferenceFragmentCompat() {
activity as VectorBaseActivity
}

abstract var titleRes: Int

/* ==========================================================================================
* Life cycle
* ========================================================================================== */
@ -36,6 +38,7 @@ abstract class VectorPreferenceFragment : PreferenceFragmentCompat() {
override fun onResume() {
super.onResume()

(activity as? VectorBaseActivity)?.supportActionBar?.setTitle(titleRes)
Timber.v("onResume Fragment ${this.javaClass.simpleName}")
}


View File

@ -0,0 +1,34 @@
package im.vector.riotredesign.core.pushers

import im.vector.matrix.android.api.session.Session
import im.vector.riotredesign.R
import im.vector.riotredesign.core.resources.AppNameProvider
import im.vector.riotredesign.core.resources.LocaleProvider
import im.vector.riotredesign.core.resources.StringProvider

private const val DEFAULT_PUSHER_FILE_TAG = "mobile"

class PushersManager(
private val currentSession: Session,
private val localeProvider: LocaleProvider,
private val stringProvider: StringProvider,
private val appNameProvider: AppNameProvider
) {

fun registerPusherWithFcmKey(pushKey: String) {
var profileTag = DEFAULT_PUSHER_FILE_TAG + "_" + Math.abs(currentSession.sessionParams.credentials.userId.hashCode())

currentSession.addHttpPusher(
pushKey,
stringProvider.getString(R.string.pusher_app_id),
profileTag,
localeProvider.current().language,
appNameProvider.getAppName(),
currentSession.sessionParams.credentials.deviceId ?: "MOBILE",
stringProvider.getString(R.string.pusher_http_url),
false,
true
)

}
}

View File

@ -0,0 +1,26 @@
package im.vector.riotredesign.core.resources

import android.content.Context
import timber.log.Timber


class AppNameProvider(private val context: Context) {

fun getAppName(): String {
try {
val appPackageName = context.applicationContext.packageName
val pm = context.packageManager
val appInfo = pm.getApplicationInfo(appPackageName, 0)
var appName = pm.getApplicationLabel(appInfo).toString()

// Use appPackageName instead of appName if appName contains any non-ASCII character
if (!appName.matches("\\A\\p{ASCII}*\\z".toRegex())) {
appName = appPackageName
}
return appName
} catch (e: Exception) {
Timber.e(e, "## AppNameProvider() : failed " + e.message)
return "RiotXAndroid"
}
}
}

View File

@ -25,6 +25,4 @@ class LocaleProvider(private val resources: Resources) {
fun current(): Locale {
return ConfigurationCompat.getLocales(resources.configuration)[0]
}


}

View File

@ -0,0 +1,102 @@
//package im.vector.riotredesign.core.services
//
//import android.app.NotificationManager
//import android.content.Context
//import android.content.Intent
//import android.os.Build.VERSION.SDK_INT
//import android.os.Build.VERSION_CODES
//import android.os.Handler
//import android.os.HandlerThread
//import android.os.Looper
//import androidx.core.content.ContextCompat.startForegroundService
//import im.vector.matrix.android.api.Matrix
//import im.vector.matrix.android.api.session.Session
//import im.vector.riotredesign.R
//import im.vector.riotredesign.features.notifications.NotificationUtils
//import timber.log.Timber
//import java.net.HttpURLConnection
//import java.net.URL
//
//
///**
// *
// * This is used to display message notifications to the user when Push is not enabled (or not configured)
// *
// * This service is used to implement a long pooling mechanism in order to get messages from
// * the home server when the user is not interacting with the app.
// *
// * It is intended to be started when the app enters background, and stopped when app is in foreground.
// *
// * When in foreground, the app uses another mechanism to get messages (doing sync wia a thread).
// *
// */
//class HttpLongPoolingSyncService : VectorService() {
//
// private var mServiceLooper: Looper? = null
// private var mHandler: Handler? = null
// private val currentSessions = ArrayList<Session>()
// private var mCount = 0
// private var lastTimeMs = System.currentTimeMillis()
//
// lateinit var myRun: () -> Unit
// override fun onCreate() {
// //Add the permanent listening notification
// super.onCreate()
//
// if (SDK_INT >= VERSION_CODES.O) {
// val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
// val notification = NotificationUtils.buildForegroundServiceNotification(applicationContext, R.string.notification_listening_for_events, false)
// startForeground(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE, notification)
// }
// val thread = HandlerThread("My service Handler")
// thread.start()
//
// mServiceLooper = thread.looper
// mHandler = Handler(mServiceLooper)
// myRun = {
// val diff = System.currentTimeMillis() - lastTimeMs
// lastTimeMs = System.currentTimeMillis()
// val isAlive = Matrix.getInstance().currentSession?.isSyncThreadAlice()
// val state = Matrix.getInstance().currentSession?.syncThreadState()
// Timber.w(" timeDiff[${diff/1000}] Yo me here $mCount, sync thread is Alive? $isAlive, state:$state")
// mCount++
// mHandler?.postDelayed(Runnable { myRun() }, 10_000L)
// }
// }
//
// override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// //START_STICKY mode makes sense for things that will be explicitly started
// //and stopped to run for arbitrary periods of time
//
// mHandler?.post {
// myRun()
// }
// return START_STICKY
// }
//
//
// override fun onDestroy() {
// //TODO test if this service should be relaunched (preference)
// Timber.i("Service is destroyed, relaunch asap")
// Intent(applicationContext, RestartBroadcastReceiver::class.java).also { sendBroadcast(it) }
// super.onDestroy()
// }
//
// companion object {
//
// fun startService(context: Context) {
// Timber.i("Start sync service")
// val intent = Intent(context, HttpLongPoolingSyncService::class.java)
// try {
// if (SDK_INT >= VERSION_CODES.O) {
// startForegroundService(context, intent)
// } else {
// context.startService(intent)
// }
// } catch (ex: Throwable) {
// //TODO
// Timber.e(ex)
// }
// }
// }
//}

View File

@ -0,0 +1,37 @@
package im.vector.riotredesign.core.services

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Build.VERSION.SDK_INT
import androidx.core.content.ContextCompat
import androidx.legacy.content.WakefulBroadcastReceiver
import im.vector.matrix.android.internal.session.sync.job.SyncService
import timber.log.Timber

class RestartBroadcastReceiver : BroadcastReceiver() {

override fun onReceive(context: Context, intent: Intent) {
// This method is called when the BroadcastReceiver is receiving an Intent broadcast.
Timber.d("RestartBroadcastReceiver received intent")
Intent(context,VectorSyncService::class.java).also {
it.action = "SLOW"
context.startService(it)
try {
if (SDK_INT >= Build.VERSION_CODES.O) {
ContextCompat.startForegroundService(context, intent)
} else {
context.startService(intent)
}
} catch (ex: Throwable) {
//TODO
Timber.e(ex)
}
}
}

companion object {
const val REQUEST_CODE = 0
}
}

View File

@ -0,0 +1,74 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.core.services

import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Build.VERSION.SDK_INT
import android.os.IBinder
import im.vector.matrix.android.internal.session.sync.job.SyncService
import im.vector.riotredesign.R
import im.vector.riotredesign.features.notifications.NotificationUtils
import timber.log.Timber
import java.util.*

class VectorSyncService : SyncService() {

override fun onCreate() {
Timber.v("VectorSyncService - onCreate ")
super.onCreate()
}

override fun onDestroy() {
Timber.v("VectorSyncService - onDestroy ")
removeForegroundNotif()
super.onDestroy()
}

private fun removeForegroundNotif() {
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.cancel(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE)
}

/**
* Service is started only in fdroid mode when no FCM is available
* Otherwise it is bounded
*/
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Timber.v("VectorSyncService - onStartCommand ")
if (SDK_INT >= Build.VERSION_CODES.O) {
val notification = NotificationUtils.buildForegroundServiceNotification(applicationContext, R.string.notification_listening_for_events, false)
startForeground(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE, notification)
}
return super.onStartCommand(intent, flags, startId)
}

/**
* If the service is bounded and the service was previously started we can remove foreground notif
*/
override fun onBind(intent: Intent?): IBinder {
Timber.v("VectorSyncService - onBind ")
stopForeground(true)
return super.onBind(intent)
}

override fun onUnbind(intent: Intent?): Boolean {
Timber.v("VectorSyncService - onUnbind ")
return super.onUnbind(intent)
}
}

View File

@ -59,17 +59,24 @@ class MainActivity : VectorBaseActivity() {
} else {
// Handle some wanted cleanup
when {
clearCredentials -> session.signOut(object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
Timber.w("SIGN_OUT: success, start app")
start()
}
})
clearCache -> session.clearCache(object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
start()
}
})
clearCredentials -> {
session.signOut(object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
Timber.w("SIGN_OUT: success, start app")
//TODO stop sync service
start()
}
})
}
clearCache -> {
//TODO stop sync service
session.clearCache(object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
//TODO start sync service
start()
}
})
}
else -> start()
}
}

View File

@ -39,9 +39,11 @@ import im.vector.riotredesign.core.platform.ToolbarConfigurable
import im.vector.riotredesign.core.platform.VectorBaseActivity
import im.vector.riotredesign.features.crypto.keysrequest.KeyRequestHandler
import im.vector.riotredesign.features.crypto.verification.IncomingVerificationRequestHandler
import im.vector.riotredesign.core.pushers.PushersManager
import im.vector.riotredesign.features.rageshake.BugReporter
import im.vector.riotredesign.features.rageshake.VectorUncaughtExceptionHandler
import im.vector.riotredesign.features.workers.signout.SignOutUiWorker
import im.vector.riotredesign.push.fcm.FcmHelper
import kotlinx.android.synthetic.main.activity_home.*
import org.koin.android.ext.android.inject
import org.koin.android.scope.ext.android.bindScope
@ -52,7 +54,7 @@ import im.vector.riotredesign.features.workers.signout.SignOutViewModel

class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {

// Supported navigation actions for this Activity
// Supported navigation domainActions for this Activity
sealed class Navigation {
object OpenDrawer : Navigation()
}
@ -60,6 +62,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
private val homeActivityViewModel: HomeActivityViewModel by viewModel()
private lateinit var navigationViewModel: HomeNavigationViewModel
private val homeNavigator by inject<HomeNavigator>()
private val pushManager by inject<PushersManager>()

// TODO Move this elsewhere
private val incomingVerificationRequestHandler by inject<IncomingVerificationRequestHandler>()
@ -80,6 +83,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
super.onCreate(savedInstanceState)
bindScope(getOrCreateScope(HomeModule.HOME_SCOPE))
homeNavigator.activity = this
FcmHelper.ensureFcmTokenIsRetrieved(this, pushManager)

navigationViewModel = ViewModelProviders.of(this).get(HomeNavigationViewModel::class.java)


View File

@ -30,7 +30,7 @@ import im.vector.matrix.android.api.session.user.model.User
* QUOTE: User is currently quoting a message
* EDIT: User is currently editing an existing message
*
* Depending on the state the bottom toolbar will change (icons/preview/actions...)
* Depending on the state the bottom toolbar will change (icons/preview/domainActions...)
*/
enum class SendMode {
REGULAR,

View File

@ -20,7 +20,7 @@ import androidx.lifecycle.ViewModel
import im.vector.riotredesign.core.utils.LiveEvent

/**
* Activity shared view model to handle message actions
* Activity shared view model to handle message domainActions
*/
class ActionsHandler : ViewModel() {


View File

@ -38,8 +38,8 @@ import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInf
import kotlinx.android.synthetic.main.bottom_sheet_message_actions.*

/**
* Bottom sheet fragment that shows a message preview with list of contextual actions
* (Includes fragments for quick reactions and list of actions)
* Bottom sheet fragment that shows a message preview with list of contextual domainActions
* (Includes fragments for quick reactions and list of domainActions)
*/
class MessageActionsBottomSheet : BaseMvRxBottomSheetDialog() {


View File

@ -37,7 +37,7 @@ data class SimpleAction(val uid: String, val titleRes: Int, val iconResId: Int?,
data class MessageMenuState(val actions: List<SimpleAction> = emptyList()) : MvRxState

/**
* Manages list actions for a given message (copy / paste / forward...)
* Manages list domainActions for a given message (copy / paste / forward...)
*/
class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<MessageMenuState>(initialState) {


View File

@ -32,10 +32,12 @@ import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.showPassword
import im.vector.riotredesign.core.platform.VectorBaseActivity
import im.vector.riotredesign.features.home.HomeActivity
import im.vector.riotredesign.features.notifications.PushRuleTriggerListener
import io.reactivex.Observable
import io.reactivex.functions.Function3
import io.reactivex.rxkotlin.subscribeBy
import kotlinx.android.synthetic.main.activity_login.*
import org.koin.android.ext.android.get

private const val DEFAULT_HOME_SERVER_URI = "https://matrix.org"
private const val DEFAULT_IDENTITY_SERVER_URI = "https://vector.im"
@ -74,8 +76,10 @@ class LoginActivity : VectorBaseActivity() {
Matrix.getInstance().currentSession = data
data.open()
data.setFilter(FilterService.FilterPreset.RiotFilter)
data.startSync()

//TODO sync
// data.shoudPauseOnBackground(false)
// data.startSync()
get<PushRuleTriggerListener>().startWithSession(data)
goToHome()
}


View File

@ -16,9 +16,16 @@
package im.vector.riotredesign.features.notifications

import android.content.Context
import im.vector.matrix.android.api.pushrules.rest.PushRule
import im.vector.matrix.android.api.session.Session
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.toContent
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.riotredesign.R
import im.vector.riotredesign.core.preference.BingRule
import timber.log.Timber

// TODO Remove
class RoomState {
@ -36,128 +43,128 @@ class NotifiableEventResolver(val context: Context) {

//private val eventDisplay = RiotEventDisplay(context)

fun resolveEvent(event: Event, roomState: RoomState?, bingRule: BingRule?, session: Session): NotifiableEvent? {
// TODO
return null
/*
val store = session.dataHandler.store
if (store == null) {
Log.e("## NotifiableEventResolver, unable to get store")
//TODO notify somehow that something did fail?
return null
}
fun resolveEvent(event: Event/*, roomState: RoomState?*/, bingRule: PushRule?, session: Session): NotifiableEvent? {


// val store = session.dataHandler.store
// if (store == null) {
// Log.e("## NotifiableEventResolver, unable to get store")
// //TODO notify somehow that something did fail?
// return null
// }

when (event.getClearType()) {
EventType.MESSAGE -> {
return resolveMessageEvent(event, bingRule, session, store)
}
EventType.ENCRYPTED -> {
val messageEvent = resolveMessageEvent(event, bingRule, session, store)
messageEvent?.lockScreenVisibility = NotificationCompat.VISIBILITY_PRIVATE
return messageEvent
}
EventType.STATE_ROOM_MEMBER -> {
return resolveStateRoomEvent(event, bingRule, session, store)
EventType.MESSAGE -> {
return resolveMessageEvent(event, bingRule, session)
}
// EventType.ENCRYPTED -> {
// val messageEvent = resolveMessageEvent(event, bingRule, session, store)
// messageEvent?.lockScreenVisibility = NotificationCompat.VISIBILITY_PRIVATE
// return messageEvent
// }
// EventType.STATE_ROOM_MEMBER -> {
// return resolveStateRoomEvent(event, bingRule, session, store)
// }
else -> {

//If the event can be displayed, display it as is
eventDisplay.getTextualDisplay(event, roomState)?.toString()?.let { body ->
return SimpleNotifiableEvent(
session.myUserId,
eventId = event.eventId,
noisy = bingRule?.notificationSound != null,
timestamp = event.originServerTs,
description = body,
soundName = bingRule?.notificationSound,
title = context.getString(R.string.notification_unknown_new_event),
type = event.getClearType())
}
// eventDisplay.getTextualDisplay(event, roomState)?.toString()?.let { body ->
// return SimpleNotifiableEvent(
// session.myUserId,
// eventId = event.eventId,
// noisy = bingRule?.notificationSound != null,
// timestamp = event.originServerTs,
// description = body,
// soundName = bingRule?.notificationSound,
// title = context.getString(R.string.notification_unknown_new_event),
// type = event.type)
// }

//Unsupported event
Timber.w("NotifiableEventResolver Received an unsupported event matching a bing rule")
return null
}
}
*/
}

/*
private fun resolveMessageEvent(event: Event, bingRule: BingRule?, session: MXSession, store: IMXStore): NotifiableEvent? {

private fun resolveMessageEvent(event: Event, pushRule: PushRule?, 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 = bingRule?.notificationSound
val noisy = bingRule?.notificationSound != null
// 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 = store.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)*/)


if (room == null) {
Timber.e("## Unable to resolve room for eventId [${event.eventId}] and roomID [${event.roomId}]")
// Ok room is not known in store, but we can still display something
val body = eventDisplay.getTextualDisplay(event, null)?.toString()
val body = event.content?.toModel<MessageContent>()?.body
?: context.getString(R.string.notification_unknown_new_event)
val roomName = context.getString(R.string.notification_unknown_room_name)
val senderDisplayName = event.sender ?: ""
val senderDisplayName = event.senderId ?: ""

val notifiableEvent = NotifiableMessageEvent(
eventId = event.eventId,
timestamp = event.originServerTs,
eventId = event.eventId ?: "",
timestamp = event.originServerTs ?: 0,
noisy = noisy,
senderName = senderDisplayName,
senderId = event.sender,
senderId = event.senderId,
body = body,
roomId = event.roomId,
roomId = event.roomId ?: "",
roomName = roomName)

notifiableEvent.matrixID = session.myUserId
notifiableEvent.soundName = soundName
notifiableEvent.matrixID = session.sessionParams.credentials.userId
// notifiableEvent.soundName = soundName

return notifiableEvent
} else {

val body = eventDisplay.getTextualDisplay(event, room.state)?.toString()
val tEvent = room.getTimeLineEvent(event.eventId!!)
val body = event.content?.toModel<MessageContent>()?.body
?: context.getString(R.string.notification_unknown_new_event)
val roomName = room.getRoomDisplayName(context)
val senderDisplayName = room.state.getMemberName(event.sender) ?: event.sender ?: ""
val roomName = event.roomId ?: "room"
val senderDisplayName = tEvent?.senderName ?: "?"

val notifiableEvent = NotifiableMessageEvent(
eventId = event.eventId,
timestamp = event.originServerTs,
eventId = event.eventId!!,
timestamp = event.originServerTs ?: 0,
noisy = noisy,
senderName = senderDisplayName,
senderId = event.sender,
senderId = event.senderId,
body = body,
roomId = event.roomId,
roomId = event.roomId ?: "00",
roomName = roomName,
roomIsDirect = room.isDirect)
roomIsDirect = true)

notifiableEvent.matrixID = session.myUserId
notifiableEvent.soundName = soundName
notifiableEvent.matrixID = session.sessionParams.credentials.userId
notifiableEvent.soundName = null


val roomAvatarPath = session.mediaCache?.thumbnailCacheFile(room.avatarUrl, 50)
if (roomAvatarPath != null) {
notifiableEvent.roomAvatarPath = roomAvatarPath.path
} else {
// prepare for the next time
session.mediaCache?.loadAvatarThumbnail(session.homeServerConfig, ImageView(context), room.avatarUrl, 50)
}

room.state.getMember(event.sender)?.avatarUrl?.let {
val size = context.resources.getDimensionPixelSize(R.dimen.profile_avatar_size)
val userAvatarUrlPath = session.mediaCache?.thumbnailCacheFile(it, size)
if (userAvatarUrlPath != null) {
notifiableEvent.senderAvatarPath = userAvatarUrlPath.path
} else {
// prepare for the next time
session.mediaCache?.loadAvatarThumbnail(session.homeServerConfig, ImageView(context), it, size)
}
}
// val roomAvatarPath = session.mediaCache?.thumbnailCacheFile(room.avatarUrl, 50)
// if (roomAvatarPath != null) {
// notifiableEvent.roomAvatarPath = roomAvatarPath.path
// } else {
// // prepare for the next time
// session.mediaCache?.loadAvatarThumbnail(session.homeServerConfig, ImageView(context), room.avatarUrl, 50)
// }
//
// room.state.getMember(event.sender)?.avatarUrl?.let {
// val size = context.resources.getDimensionPixelSize(R.dimen.profile_avatar_size)
// val userAvatarUrlPath = session.mediaCache?.thumbnailCacheFile(it, size)
// if (userAvatarUrlPath != null) {
// notifiableEvent.senderAvatarPath = userAvatarUrlPath.path
// } else {
// // prepare for the next time
// session.mediaCache?.loadAvatarThumbnail(session.homeServerConfig, ImageView(context), it, size)
// }
// }

return notifiableEvent
}
}

/*
private fun resolveStateRoomEvent(event: Event, bingRule: BingRule?, session: MXSession, store: IMXStore): NotifiableEvent? {
if (RoomMember.MEMBERSHIP_INVITE == event.contentAsJsonObject?.getAsJsonPrimitive("membership")?.asString) {
val room = store.getRoom(event.roomId /*roomID cannot be null (see Matrix SDK code)*/)

View File

@ -27,7 +27,7 @@ import org.koin.standalone.inject
import timber.log.Timber

/**
* Receives actions broadcast by notification (on click, on dismiss, inline replies, etc.)
* Receives domainActions broadcast by notification (on click, on dismiss, inline replies, etc.)
*/
class NotificationBroadcastReceiver : BroadcastReceiver(), KoinComponent {


View File

@ -42,7 +42,7 @@ class NotificationDrawerManager(val context: Context) {
private var firstTime = true

private var eventList = loadEventInfo()
private var myUserDisplayName: String = ""
private var myUserDisplayName: String = "Todo"
private var myUserAvatarUrl: String = ""

private val avatarSize = context.resources.getDimensionPixelSize(R.dimen.profile_avatar_size)
@ -185,7 +185,8 @@ class NotificationDrawerManager(val context: Context) {

if (myUserDisplayName.isBlank()) {
// Should not happen, but myUserDisplayName cannot be blank if used to create a Person
return
//TODO
// return
}

synchronized(eventList) {

View File

@ -64,7 +64,7 @@ object NotificationUtils {
const val NOTIFICATION_ID_FOREGROUND_SERVICE = 61

/* ==========================================================================================
* IDs for actions
* IDs for domainActions
* ========================================================================================== */

private const val JOIN_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.JOIN_ACTION"
@ -180,7 +180,7 @@ object NotificationUtils {
* @return the polling thread listener notification
*/
@SuppressLint("NewApi")
fun buildForegroundServiceNotification(context: Context, @StringRes subTitleResId: Int): Notification {
fun buildForegroundServiceNotification(context: Context, @StringRes subTitleResId: Int, withProgress: Boolean = true): Notification {
// build the pending intent go to the home screen if this is clicked.
val i = Intent(context, HomeActivity::class.java)
i.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
@ -190,16 +190,21 @@ object NotificationUtils {

val builder = NotificationCompat.Builder(context, LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID)
.setContentTitle(context.getString(subTitleResId))
.setCategory(NotificationCompat.CATEGORY_PROGRESS)
.setSmallIcon(R.drawable.logo_transparent)
.setProgress(0, 0, true)
.setSmallIcon(R.drawable.sync)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.setColor(accentColor)
.setContentIntent(pi)
.apply {
if (withProgress) {
setProgress(0, 0, true)
}
}

// hide the notification from the status bar
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
builder.priority = NotificationCompat.PRIORITY_MIN
}
// PRIORITY_MIN should not be used with Service#startForeground(int, Notification)
builder.priority = NotificationCompat.PRIORITY_LOW
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
// builder.priority = NotificationCompat.PRIORITY_MIN
// }

val notification = builder.build()

@ -220,7 +225,7 @@ object NotificationUtils {
PendingIntent::class.java)
deprecatedMethod.invoke(notification, context, context.getString(R.string.app_name), context.getString(subTitleResId), pi)
} catch (ex: Exception) {
Timber.e(ex, "## buildNotification(): Exception - setLatestEventInfo() Msg=" + ex.message)
Timber.e(ex, "## buildNotification(): Exception - setLatestEventInfo() Msg=")
}

}
@ -421,7 +426,7 @@ object NotificationUtils {
priority = NotificationCompat.PRIORITY_LOW
}

//Add actions and notification intents
//Add domainActions and notification intents
// Mark room as read
val markRoomReadIntent = Intent(context, NotificationBroadcastReceiver::class.java)
markRoomReadIntent.action = MARK_ROOM_READ_ACTION

View File

@ -0,0 +1,46 @@
package im.vector.riotredesign.features.notifications

import im.vector.matrix.android.api.pushrules.Action
import im.vector.matrix.android.api.pushrules.PushRuleService
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.Event
import timber.log.Timber


class PushRuleTriggerListener(
private val resolver: NotifiableEventResolver,
private val drawerManager: NotificationDrawerManager
) : PushRuleService.PushRuleListener {


var session: Session? = null

override fun onMatchRule(event: Event, actions: List<Action>) {
if (session == null) {
Timber.e("Called without active session")
return
}
resolver.resolveEvent(event,null,session!!)?.let {
drawerManager.onNotifiableEventReceived(it)
}
}

override fun batchFinish() {
drawerManager.refreshNotificationDrawer(null)
}

fun startWithSession(session: Session) {
if (this.session != null) {
stop()
}
this.session = session
session.addPushRuleListener(this)
}

fun stop() {
session?.removePushRuleListener(this)
session = null
drawerManager.clearAllEvents()
drawerManager.refreshNotificationDrawer(null)
}
}

View File

@ -50,6 +50,8 @@ public class PreferencesManager {
public static final String SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY = "SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY";
public static final String SETTINGS_APP_TERM_CONDITIONS_PREFERENCE_KEY = "SETTINGS_APP_TERM_CONDITIONS_PREFERENCE_KEY";
public static final String SETTINGS_PRIVACY_POLICY_PREFERENCE_KEY = "SETTINGS_PRIVACY_POLICY_PREFERENCE_KEY";

//TODO delete
public static final String SETTINGS_NOTIFICATION_PRIVACY_PREFERENCE_KEY = "SETTINGS_NOTIFICATION_PRIVACY_PREFERENCE_KEY";
public static final String SETTINGS_NOTIFICATION_TROUBLESHOOT_PREFERENCE_KEY = "SETTINGS_NOTIFICATION_TROUBLESHOOT_PREFERENCE_KEY";
public static final String SETTINGS_NOTIFICATION_ADVANCED_PREFERENCE_KEY = "SETTINGS_NOTIFICATION_ADVANCED_PREFERENCE_KEY";

View File

@ -26,6 +26,7 @@ import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.VectorBaseActivity
import kotlinx.android.synthetic.main.activity_vector_settings.*
import org.koin.android.ext.android.inject
import timber.log.Timber

/**
* Displays the client settings.
@ -35,7 +36,6 @@ class VectorSettingsActivity : VectorBaseActivity(),
FragmentManager.OnBackStackChangedListener,
VectorSettingsFragmentInteractionListener {

private lateinit var vectorSettingsPreferencesFragment: VectorSettingsPreferencesFragment

override fun getLayoutRes() = R.layout.activity_vector_settings

@ -48,14 +48,15 @@ class VectorSettingsActivity : VectorBaseActivity(),
override fun initUiAndData() {
configureToolbar(settingsToolbar)

var vectorSettingsPreferencesFragment: Fragment? = null
if (isFirstCreation()) {
vectorSettingsPreferencesFragment = VectorSettingsPreferencesFragment.newInstance(session.sessionParams.credentials.userId)
vectorSettingsPreferencesFragment = VectorSettingsPreferencesFragmentV2.newInstance()
// display the fragment
supportFragmentManager.beginTransaction()
.replace(R.id.vector_settings_page, vectorSettingsPreferencesFragment, FRAGMENT_TAG)
.commit()
} else {
vectorSettingsPreferencesFragment = supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as VectorSettingsPreferencesFragment
vectorSettingsPreferencesFragment = supportFragmentManager.findFragmentByTag(FRAGMENT_TAG)
}


@ -77,19 +78,33 @@ class VectorSettingsActivity : VectorBaseActivity(),
override fun onPreferenceStartFragment(caller: PreferenceFragmentCompat?, pref: Preference?): Boolean {
var oFragment: Fragment? = null

if (PreferencesManager.SETTINGS_NOTIFICATION_TROUBLESHOOT_PREFERENCE_KEY == pref?.key) {
if ("Legacy" == pref?.title) {
oFragment = VectorSettingsPreferencesFragment.newInstance(session.sessionParams.credentials.userId)
} else if (PreferencesManager.SETTINGS_NOTIFICATION_TROUBLESHOOT_PREFERENCE_KEY == pref?.key) {
oFragment = VectorSettingsNotificationsTroubleshootFragment.newInstance(session.sessionParams.credentials.userId)
} else if (PreferencesManager.SETTINGS_NOTIFICATION_ADVANCED_PREFERENCE_KEY == pref?.key) {
oFragment = VectorSettingsAdvancedNotificationPreferenceFragment.newInstance(session.sessionParams.credentials.userId)
} else {
try {
pref?.fragment?.let {
oFragment = supportFragmentManager.fragmentFactory
.instantiate(
classLoader,
it, pref.extras)
}
} catch (e: Throwable) {
showSnackbar(getString(R.string.not_implemented))
Timber.e(e)
}
}

if (oFragment != null) {
oFragment.setTargetFragment(caller, 0)
oFragment!!.setTargetFragment(caller, 0)
// Replace the existing Fragment with the new Fragment
supportFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.anim_slide_in_bottom, R.anim.anim_slide_out_bottom,
R.anim.anim_slide_in_bottom, R.anim.anim_slide_out_bottom)
.replace(R.id.vector_settings_page, oFragment, pref?.title.toString())
.setCustomAnimations(R.anim.right_in, R.anim.fade_out,
R.anim.fade_in, R.anim.right_out)
.replace(R.id.vector_settings_page, oFragment!!, pref?.title.toString())
.addToBackStack(null)
.commit()
return true

View File

@ -51,6 +51,8 @@ class VectorSettingsAdvancedNotificationPreferenceFragment : VectorPreferenceFra
}
} */

override var titleRes: Int = R.string.settings_notification_advanced

override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
// define the layout
addPreferencesFromResource(R.xml.vector_settings_notification_advanced_preferences)
@ -177,7 +179,6 @@ class VectorSettingsAdvancedNotificationPreferenceFragment : VectorPreferenceFra

override fun onResume() {
super.onResume()
(activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_notification_advanced)
// find the view from parent activity
mLoadingView = activity!!.findViewById(R.id.vector_settings_spinner_views)

@ -222,14 +223,14 @@ class VectorSettingsAdvancedNotificationPreferenceFragment : VectorPreferenceFra
if (TextUtils.equals(ruleId, BingRule.RULE_ID_DISABLE_ALL) || TextUtils.equals(ruleId, BingRule.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS)) {
isEnabled = !isEnabled
} else if (isEnabled) {
val actions = rule!!.actions
val domainActions = rule!!.domainActions

// no action -> noting will be done
if (null == actions || actions.isEmpty()) {
if (null == domainActions || domainActions.isEmpty()) {
isEnabled = false
} else if (1 == actions.size) {
} else if (1 == domainActions.size) {
try {
isEnabled = !TextUtils.equals(actions[0] as String, BingRule.ACTION_DONT_NOTIFY)
isEnabled = !TextUtils.equals(domainActions[0] as String, BingRule.ACTION_DONT_NOTIFY)
} catch (e: Exception) {
Timber.e(e, "## refreshPreferences failed")
}

View File

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

import android.os.Bundle
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.VectorBaseActivity
import im.vector.riotredesign.core.platform.VectorPreferenceFragment


class VectorSettingsNotificationPreferenceFragment : VectorPreferenceFragment() {

override var titleRes: Int = R.string.settings_notifications

override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.vector_settings_notifications)
}

}

View File

@ -84,6 +84,7 @@ import java.util.*

class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPreferences.OnSharedPreferenceChangeListener {

override var titleRes: Int = R.string.title_activity_settings
// members
private val mSession by inject<Session>()

@ -1493,14 +1494,14 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
if (TextUtils.equals(ruleId, BingRule.RULE_ID_DISABLE_ALL) || TextUtils.equals(ruleId, BingRule.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS)) {
isEnabled = !isEnabled
} else if (isEnabled) {
val actions = rule?.actions
val domainActions = rule?.domainActions

// no action -> noting will be done
if (null == actions || actions.isEmpty()) {
if (null == domainActions || domainActions.isEmpty()) {
isEnabled = false
} else if (1 == actions.size) {
} else if (1 == domainActions.size) {
try {
isEnabled = !TextUtils.equals(actions[0] as String, BingRule.ACTION_DONT_NOTIFY)
isEnabled = !TextUtils.equals(domainActions[0] as String, BingRule.ACTION_DONT_NOTIFY)
} catch (e: Exception) {
Timber.e(e, "## refreshPreferences failed " + e.message)
}

View File

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

import android.os.Bundle
import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.withArgs
import im.vector.riotredesign.core.platform.VectorPreferenceFragment

class VectorSettingsPreferencesFragmentV2 : VectorPreferenceFragment() {

override var titleRes: Int = R.string.title_activity_settings

override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.vector_settings_preferences_root)
}


companion object {
fun newInstance() = VectorSettingsPreferencesFragmentV2()
.withArgs {
//putString(ARG_MATRIX_ID, matrixId)
}
}

}

View File

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

import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import com.airbnb.epoxy.EpoxyModelWithHolder
import im.vector.matrix.android.api.session.pushers.Pusher
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder


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

@EpoxyAttribute
lateinit var pusher: Pusher

override fun bind(holder: Holder) {
holder.kind.text = when (pusher.kind) {
"http" -> "Http Pusher"
"mail" -> "Email Pusher"
else -> pusher.kind
}

holder.appId.text = pusher.appId
holder.pushKey.text = pusher.pushKey
holder.appName.text = pusher.appDisplayName
holder.url.text = pusher.data.url
holder.format.text = pusher.data.format
holder.deviceName.text = pusher.deviceDisplayName
}


class Holder : VectorEpoxyHolder() {
val kind by bind<TextView>(R.id.pushGatewayKind)
val pushKey by bind<TextView>(R.id.pushGatewayKeyValue)
val deviceName by bind<TextView>(R.id.pushGatewayDeviceNameValue)
val format by bind<TextView>(R.id.pushGatewayFormatValue)
val url by bind<TextView>(R.id.pushGatewayURLValue)
val appName by bind<TextView>(R.id.pushGatewayAppNameValue)
val appId by bind<TextView>(R.id.pushGatewayAppIdValue)
}
}

//
//abstract class ReactionInfoSimpleItem : EpoxyModelWithHolder<ReactionInfoSimpleItem.Holder>() {

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.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
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 kotlinx.android.synthetic.main.fragment_settings_pushgateways.*

class PushGatewaysFragment : VectorBaseFragment() {

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

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

private val epoxyController by lazy { PushGateWayController() }

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

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 PushGateWayController : TypedEpoxyController<PushGatewayViewState>() {
override fun buildModels(data: PushGatewayViewState?) {
val pushers = data?.pushgateways?.invoke() ?: return
pushers.forEach {
pushGatewayItem {
id("${it.pushKey}_${it.appId}")
pusher(it)
}
}
}

}
}

View File

@ -0,0 +1,54 @@
/*
* 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 androidx.lifecycle.Observer
import com.airbnb.mvrx.*
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.pushers.Pusher
import im.vector.riotredesign.core.platform.VectorViewModel
import org.koin.android.ext.android.get


data class PushGatewayViewState(
val pushgateways: Async<List<Pusher>> = Uninitialized)
: MvRxState

class PushGatewaysViewModel(initialState: PushGatewayViewState) : VectorViewModel<PushGatewayViewState>(initialState) {


companion object : MvRxViewModelFactory<PushGatewaysViewModel, PushGatewayViewState> {

override fun create(viewModelContext: ViewModelContext, state: PushGatewayViewState): PushGatewaysViewModel? {
val session = viewModelContext.activity.get<Session>()
val fragment = (viewModelContext as FragmentViewModelContext).fragment

val livePushers = session.livePushers()

val viewModel = PushGatewaysViewModel(state)

livePushers.observe(fragment, Observer {
viewModel.setState {
PushGatewayViewState(Success(it))
}
})
return viewModel
}

}

}

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">

<alpha
android:duration="300"
android:fromAlpha="0.0"
android:toAlpha="1.0" />

</set>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">

<alpha
android:duration="300"
android:fromAlpha="1.0"
android:toAlpha="0.0" />

</set>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<translate
android:duration="300"
android:fromXDelta="100%"
android:interpolator="@android:anim/decelerate_interpolator"
android:toXDelta="0%" />
</set>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<translate
android:duration="300"
android:fromXDelta="0%"
android:interpolator="@android:anim/decelerate_interpolator"
android:toXDelta="100%" />

<alpha
android:duration="300"
android:fromAlpha="1.0"
android:toAlpha="0.0" />

</set>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 726 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 466 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 538 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1018 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 882 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="12dp"
android:height="14dp"
android:viewportWidth="12"
android:viewportHeight="14">
<path
android:strokeWidth="1"
android:pathData="M12,10.002L0,10.002a1.8,1.8 0,0 0,1.8 -1.8L1.8,5.2a4.2,4.2 0,1 1,8.4 0v3a1.8,1.8 0,0 0,1.8 1.8zM7.038,12.402a1.2,1.2 0,0 1,-2.076 0h2.076z"
android:strokeLineJoin="round"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#454545"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,22 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="10dp"
android:height="14dp"
android:viewportWidth="10"
android:viewportHeight="14">
<path
android:strokeWidth="1"
android:pathData="M1,4.818a4,3.818 0,1 0,8 0a4,3.818 0,1 0,-8 0z"
android:strokeLineJoin="round"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#454545"
android:strokeLineCap="round"/>
<path
android:strokeWidth="1"
android:pathData="M2.834,8.03L2.143,13 5,11.364 7.857,13l-0.691,-4.975"
android:strokeLineJoin="round"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#454545"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,22 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="13dp"
android:height="14dp"
android:viewportWidth="13"
android:viewportHeight="14">
<path
android:strokeWidth="1"
android:pathData="M2.2,6.4L10.8,6.4A1.2,1.2 0,0 1,12 7.6L12,11.8A1.2,1.2 0,0 1,10.8 13L2.2,13A1.2,1.2 0,0 1,1 11.8L1,7.6A1.2,1.2 0,0 1,2.2 6.4z"
android:strokeLineJoin="round"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#454545"
android:strokeLineCap="round"/>
<path
android:strokeWidth="1"
android:pathData="M3.444,6.4L3.444,4c0,-1.657 1.368,-3 3.056,-3s3.056,1.343 3.056,3v2.4"
android:strokeLineJoin="round"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#454545"
android:strokeLineCap="round"/>
</vector>

Some files were not shown because too many files have changed in this diff Show More