diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/Matrix.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/Matrix.kt index 3844e27b..87f06213 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/Matrix.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/Matrix.kt @@ -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() } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/Action.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/Action.kt new file mode 100644 index 00000000..a7789aaa --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/Action.kt @@ -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? { + val actions = ArrayList() + 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 +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/Condition.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/Condition.kt new file mode 100644 index 00000000..d20f9692 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/Condition.kt @@ -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? + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/EventMatchCondition.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/EventMatchCondition.kt new file mode 100644 index 00000000..0c729799 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/EventMatchCondition.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/PushExt.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/PushExt.kt new file mode 100644 index 00000000..fd9a95be --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/PushExt.kt @@ -0,0 +1,2 @@ +package im.vector.matrix.android.api.pushrules + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/PushRuleService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/PushRuleService.kt new file mode 100644 index 00000000..47328d17 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/PushRuleService.kt @@ -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) + fun batchFinish() + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/PushRulesProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/PushRulesProvider.kt new file mode 100644 index 00000000..75520f9a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/PushRulesProvider.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.api.pushrules + +import im.vector.matrix.android.api.pushrules.rest.PushRule + +interface PushRulesProvider { + + fun getOrderedPushrules(): List + + fun onRulesUpdate(newRules: List) +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/RulesetKey.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/RulesetKey.kt new file mode 100644 index 00000000..834bdf2a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/RulesetKey.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.api.pushrules + + +enum class RulesetKey(val value: String) { + CONTENT("content"), + OVERRIDE("override"), + ROOM("room"), + SENDER("sender"), + UNDERRIDE("underride"), + UNKNOWN("") +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/rest/PushCondition.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/rest/PushCondition.kt new file mode 100644 index 00000000..72fc2b24 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/rest/PushCondition.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/rest/PushRule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/rest/PushRule.kt new file mode 100644 index 00000000..58d36cfc --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/rest/PushRule.kt @@ -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, + //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? = null, + //The glob-style pattern to match against. Only applicable to content rules. + val pattern: String? = null +) + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/rest/PushruleResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/rest/PushruleResponse.kt new file mode 100644 index 00000000..5642bf82 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/rest/PushruleResponse.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package 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 +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/rest/Ruleset.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/rest/Ruleset.kt new file mode 100644 index 00000000..5f4ca15a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/rest/Ruleset.kt @@ -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? = null, + val override: List? = null, + val room: List? = null, + val sender: List? = null, + val underride: List? = null +) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt index 6f8d745e..1ad26c2d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt @@ -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. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/pushers/Pusher.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/pushers/Pusher.kt new file mode 100644 index 00000000..f0f9453f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/pushers/Pusher.kt @@ -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 +) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/pushers/PushersService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/pushers/PushersService.kt new file mode 100644 index 00000000..c28f8313 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/pushers/PushersService.kt @@ -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 status = workManager.getWorkInfoByIdLiveData()) + */ + 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> +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/PushersMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/PushersMapper.kt new file mode 100644 index 00000000..c1df3b2e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/PushersMapper.kt @@ -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) +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/PushConditionEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/PushConditionEntity.kt new file mode 100644 index 00000000..e6c3d406 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/PushConditionEntity.kt @@ -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 + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/PushRuleEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/PushRuleEntity.kt new file mode 100644 index 00000000..30ef90a8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/PushRuleEntity.kt @@ -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? = RealmList(), + //The glob-style pattern to match against. Only applicable to content rules. + var pattern: String? = null +) : RealmObject() { + + companion object + +} + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/PushRulesEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/PushRulesEntity.kt new file mode 100644 index 00000000..fc8d66b5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/PushRulesEntity.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package 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 = RealmList() +) : RealmObject() { + companion object +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/PusherDataEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/PusherDataEntity.kt new file mode 100644 index 00000000..87694fb7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/PusherDataEntity.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package 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 +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/PusherEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/PusherEntity.kt new file mode 100644 index 00000000..117aea57 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/PusherEntity.kt @@ -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 +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt index 20ba8d44..b926da99 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt @@ -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 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt index 8fca0abd..6f4db6f4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt @@ -53,6 +53,14 @@ internal fun EventEntity.Companion.where(realm: Realm, } +internal fun EventEntity.Companion.types(realm: Realm, + typeList: List = emptyList()): RealmQuery { + val query = realm.where() + query.`in`(EventEntityFields.TYPE, typeList.toTypedArray()) + return query +} + + internal fun EventEntity.Companion.latestEvent(realm: Realm, roomId: String, includedTypes: List = emptyList(), diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/PushersQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/PushersQueries.kt new file mode 100644 index 00000000..f5e9a6d0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/PushersQueries.kt @@ -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 { + return realm.where() + .equalTo(PusherEntityFields.USER_ID, userId) + .apply { + if (pushKey != null) { + equalTo(PusherEntityFields.PUSH_KEY, pushKey) + } + } + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt index ecbdd156..20dc4cf7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt @@ -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() private val contentUrlResolver by inject() private val contentUploadProgressTracker by inject() + private val pushRuleService by inject() + private val pushersService by inject() private var isOpen = false + private val bingRuleWatcher by inject() + @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 { @@ -261,11 +300,11 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi override fun clearCache(callback: MatrixCallback) { assert(isOpen) - syncThread.pause() + // syncThread.pause() cacheService.clearCache(object : MatrixCallbackDelegate(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> { + return pushersService.livePushers() + } + } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index 4a5d3b4e..9d24ae54 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -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() 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(groupSummaryUpdater, userEntityUpdater, aggregationUpdater, eventsPruner) } + scope(DefaultSession.SCOPE) { + get().create(PushersAPI::class.java) + } + scope(DefaultSession.SCOPE) { + DefaultGetPusherTask(get()) as GetPushersTask + } + + scope(DefaultSession.SCOPE) { + DefaultPusherService(get(), get(), get(), get()) as PushersService + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/BingRuleWatcher.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/BingRuleWatcher.kt new file mode 100644 index 00000000..371c7f2c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/BingRuleWatcher.kt @@ -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(monarchy) { + + override val query = Monarchy.Query { + + EventEntity.types(it, listOf( + EventType.REDACTION, EventType.MESSAGE, EventType.REDACTION, EventType.ENCRYPTED) + ) + + } + + override fun processChanges(inserted: List, updated: List, deleted: List) { + //TODO task + inserted.map { it.asDomain() }.let { + pushRulesManager.processEvents(it) + } + } + + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/MockPushRuleProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/MockPushRuleProvider.kt new file mode 100644 index 00000000..e4329c65 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/MockPushRuleProvider.kt @@ -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 { + 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::class.java).fromJson(raw) + + return listOf( + pushRule!! + ) + } + + override fun onRulesUpdate(newRules: List) { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/PushRulesManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/PushRulesManager.kt new file mode 100644 index 00000000..5c5a7dab --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/PushRulesManager.kt @@ -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() + + + 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) { + 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 + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/AddHttpPusherWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/AddHttpPusherWorker.kt new file mode 100644 index 00000000..0eb6344f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/AddHttpPusherWorker.kt @@ -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() + private val monarchy by inject() + + override suspend fun doWork(): Result { + + val params = WorkerParamsFactory.fromData(inputData) + ?: return Result.failure() + + val pusher = params.pusher + + if (pusher.pushKey.isBlank()) { + return Result.failure() + } + + val result = executeRequest> { + 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() + + }) + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultPusherService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultPusherService.kt new file mode 100644 index 00000000..40b7fe42 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultPusherService.kt @@ -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 { + 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() + .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> { + return monarchy.findAllMappedWithChanges( + { realm -> PusherEntity.where(realm, sessionParam.credentials.userId) }, + { it.asDomain() } + ) + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/GetPushersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/GetPushersTask.kt new file mode 100644 index 00000000..5c730d14 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/GetPushersTask.kt @@ -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 + +internal class DefaultGetPusherTask(private val pushersAPI: PushersAPI) : GetPushersTask { + + override suspend fun execute(params: Unit): Try { + return executeRequest { + apiCall = pushersAPI.getPushers() + } + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/JsonPusher.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/JsonPusher.kt new file mode 100644 index 00000000..7616be49 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/JsonPusher.kt @@ -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. + * + * + * { + * "pushers": [ + * { + * "pushkey": "Xp/MzCt8/9DcSNE9cuiaoT5Ac55job3TdLSSmtmYl4A=", + * "kind": "http", + * "app_id": "face.mcapp.appy.prod", + * "app_display_name": "Appy McAppface", + * "device_display_name": "Alice's Phone", + * "profile_tag": "xyz", + * "lang": "en-US", + * "data": { + * "url": "https://example.com/_matrix/push/v1/notify" + * } + * }] + * } + * + */ + +@JsonClass(generateAdapter = true) +internal data class JsonPusher( + @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 +) + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/JsonPusherData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/JsonPusherData.kt new file mode 100644 index 00000000..0f0a55ee --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/JsonPusherData.kt @@ -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 +) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/PushersAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/PushersAPI.kt new file mode 100644 index 00000000..97251513 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/PushersAPI.kt @@ -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 + + /** + * 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> + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/PushersResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/PushersResponse.kt new file mode 100644 index 00000000..426b5c9e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/PushersResponse.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package 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? = null +) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt index 1f09f4c6..63bc3f43 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt @@ -29,7 +29,7 @@ import im.vector.matrix.android.internal.task.Task internal interface SyncTask : Task { - 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 { val requestParams = HashMap() - 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() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt new file mode 100644 index 00000000..300ae4c5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt @@ -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 and 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 { + 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() + + 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) { + + } + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncServiceOld.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncServiceOld.kt new file mode 100644 index 00000000..a76aa99d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncServiceOld.kt @@ -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 and 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 { + 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() + + 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) { + + } + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt index 7bad5e64..6d644ca5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt @@ -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 + } } - } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncWorker.kt new file mode 100644 index 00000000..441f7ef7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncWorker.kt @@ -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() + private val filterRepository by inject() + private val syncResponseHandler by inject() + private val sessionParamsStore by inject() + private val syncTokenStore by inject() + + val autoMode = false + + override suspend fun doWork(): Result { + Timber.i("Sync work starting") + val params = WorkerParamsFactory.fromData(inputData) + ?: Params() + + val requestParams = HashMap() + 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 { + 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() + .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) + + } + + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushRuleActionsTest.kt b/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushRuleActionsTest.kt new file mode 100644 index 00000000..eeef3f5c --- /dev/null +++ b/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushRuleActionsTest.kt @@ -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::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) + + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt b/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt new file mode 100644 index 00000000..2c7e29b2 --- /dev/null +++ b/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt @@ -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)) + } +} diff --git a/tools/tests/app_standby_off.sh b/tools/tests/app_standby_off.sh new file mode 100755 index 00000000..fc3de6a4 --- /dev/null +++ b/tools/tests/app_standby_off.sh @@ -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 diff --git a/tools/tests/app_standby_on.sh b/tools/tests/app_standby_on.sh new file mode 100755 index 00000000..cdd84333 --- /dev/null +++ b/tools/tests/app_standby_on.sh @@ -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 diff --git a/tools/tests/doze_mode_disable.sh b/tools/tests/doze_mode_disable.sh new file mode 100755 index 00000000..396a3db5 --- /dev/null +++ b/tools/tests/doze_mode_disable.sh @@ -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 diff --git a/tools/tests/doze_mode_enable.sh b/tools/tests/doze_mode_enable.sh new file mode 100755 index 00000000..ebe3be6c --- /dev/null +++ b/tools/tests/doze_mode_enable.sh @@ -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 diff --git a/vector/src/debug/res/layout/item_pushgateway.xml b/vector/src/debug/res/layout/item_pushgateway.xml new file mode 100644 index 00000000..e50a0015 --- /dev/null +++ b/vector/src/debug/res/layout/item_pushgateway.xml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/fdroid/java/im/vector/riotredesign/push/fcm/FcmHelper.java b/vector/src/fdroid/java/im/vector/riotredesign/push/fcm/FcmHelper.java index 82f2df9a..3efc4990 100755 --- a/vector/src/fdroid/java/im/vector/riotredesign/push/fcm/FcmHelper.java +++ b/vector/src/fdroid/java/im/vector/riotredesign/push/fcm/FcmHelper.java @@ -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 } } diff --git a/vector/src/gplay/google-services.json b/vector/src/gplay/google-services.json index 8ffc2cef..187c727a 100644 --- a/vector/src/gplay/google-services.json +++ b/vector/src/gplay/google-services.json @@ -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 + } + ] } } } diff --git a/vector/src/gplay/java/im/vector/riotredesign/push/fcm/FcmHelper.java b/vector/src/gplay/java/im/vector/riotredesign/push/fcm/FcmHelper.java index 00fff7dd..17bdc909 100755 --- a/vector/src/gplay/java/im/vector/riotredesign/push/fcm/FcmHelper.java +++ b/vector/src/gplay/java/im/vector/riotredesign/push/fcm/FcmHelper.java @@ -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() { - @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."); } - } +// } } /** diff --git a/vector/src/gplay/java/im/vector/riotredesign/push/fcm/VectorFirebaseMessagingService.kt b/vector/src/gplay/java/im/vector/riotredesign/push/fcm/VectorFirebaseMessagingService.kt index dfdfe6be..ba942124 100755 --- a/vector/src/gplay/java/im/vector/riotredesign/push/fcm/VectorFirebaseMessagingService.kt +++ b/vector/src/gplay/java/im/vector/riotredesign/push/fcm/VectorFirebaseMessagingService.kt @@ -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() + private val notificationDrawerManager by inject() + private val pusherManager by inject() 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 /*, pushManager: PushManager*/) { + private fun onMessageReceivedInternal(data: Map) { 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() + .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) ?: "" diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index fdbbd370..18e7eb77 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -3,7 +3,6 @@ xmlns:tools="http://schemas.android.com/tools" package="im.vector.riotredesign"> - + + + - @@ -58,9 +62,8 @@ android:label="@string/encryption_message_recovery" /> - @@ -69,6 +72,15 @@ + + + + + + + - \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/VectorApplication.kt b/vector/src/main/java/im/vector/riotredesign/VectorApplication.kt index 2f2de8ed..d8af4433 100644 --- a/vector/src/main/java/im/vector/riotredesign/VectorApplication.kt +++ b/vector/src/main/java/im/vector/riotredesign/VectorApplication.kt @@ -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() FontsContractCompat.requestFont(this, fontRequest, koin.koinContext.get(), 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(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() +// .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().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() +// } + + } + } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/di/AppModule.kt b/vector/src/main/java/im/vector/riotredesign/core/di/AppModule.kt index 5b875fd1..926a5bc7 100644 --- a/vector/src/main/java/im/vector/riotredesign/core/di/AppModule.kt +++ b/vector/src/main/java/im/vector/riotredesign/core/di/AppModule.kt @@ -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()) + } } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/platform/VectorPreferenceFragment.kt b/vector/src/main/java/im/vector/riotredesign/core/platform/VectorPreferenceFragment.kt index a915ea8a..ddd90809 100644 --- a/vector/src/main/java/im/vector/riotredesign/core/platform/VectorPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/core/platform/VectorPreferenceFragment.kt @@ -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}") } diff --git a/vector/src/main/java/im/vector/riotredesign/core/pushers/PushersManager.kt b/vector/src/main/java/im/vector/riotredesign/core/pushers/PushersManager.kt new file mode 100644 index 00000000..7bf5a4a2 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/pushers/PushersManager.kt @@ -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 + ) + + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/resources/AppNameProvider.kt b/vector/src/main/java/im/vector/riotredesign/core/resources/AppNameProvider.kt new file mode 100644 index 00000000..4adfde79 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/resources/AppNameProvider.kt @@ -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" + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/resources/LocaleProvider.kt b/vector/src/main/java/im/vector/riotredesign/core/resources/LocaleProvider.kt index 991177a7..8393bc09 100644 --- a/vector/src/main/java/im/vector/riotredesign/core/resources/LocaleProvider.kt +++ b/vector/src/main/java/im/vector/riotredesign/core/resources/LocaleProvider.kt @@ -25,6 +25,4 @@ class LocaleProvider(private val resources: Resources) { fun current(): Locale { return ConfigurationCompat.getLocales(resources.configuration)[0] } - - } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/services/HttpLongPoolingSyncService.kt b/vector/src/main/java/im/vector/riotredesign/core/services/HttpLongPoolingSyncService.kt new file mode 100644 index 00000000..501a2c77 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/services/HttpLongPoolingSyncService.kt @@ -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() +// 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) +// } +// } +// } +//} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/services/RestartBroadcastReceiver.kt b/vector/src/main/java/im/vector/riotredesign/core/services/RestartBroadcastReceiver.kt new file mode 100644 index 00000000..1834a994 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/services/RestartBroadcastReceiver.kt @@ -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 + } +} diff --git a/vector/src/main/java/im/vector/riotredesign/core/services/VectorSyncService.kt b/vector/src/main/java/im/vector/riotredesign/core/services/VectorSyncService.kt new file mode 100644 index 00000000..e56399b9 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/services/VectorSyncService.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package 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) + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/MainActivity.kt b/vector/src/main/java/im/vector/riotredesign/features/MainActivity.kt index 2cc31dff..f22655a3 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/MainActivity.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/MainActivity.kt @@ -59,17 +59,24 @@ class MainActivity : VectorBaseActivity() { } else { // Handle some wanted cleanup when { - clearCredentials -> session.signOut(object : MatrixCallback { - override fun onSuccess(data: Unit) { - Timber.w("SIGN_OUT: success, start app") - start() - } - }) - clearCache -> session.clearCache(object : MatrixCallback { - override fun onSuccess(data: Unit) { - start() - } - }) + clearCredentials -> { + session.signOut(object : MatrixCallback { + 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 { + override fun onSuccess(data: Unit) { + //TODO start sync service + start() + } + }) + } else -> start() } } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotredesign/features/home/HomeActivity.kt index ffe59919..ee63d806 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/HomeActivity.kt @@ -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() + private val pushManager by inject() // TODO Move this elsewhere private val incomingVerificationRequestHandler by inject() @@ -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) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewState.kt index 927bbba1..2cf41349 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewState.kt @@ -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, diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ActionsHandler.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ActionsHandler.kt index 84cfc40f..85b90129 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ActionsHandler.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ActionsHandler.kt @@ -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() { diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt index 95777b8d..f80a62f8 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt @@ -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() { diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt index 1d9c33f6..f523a777 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt @@ -37,7 +37,7 @@ data class SimpleAction(val uid: String, val titleRes: Int, val iconResId: Int?, data class MessageMenuState(val actions: List = 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(initialState) { diff --git a/vector/src/main/java/im/vector/riotredesign/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotredesign/features/login/LoginActivity.kt index ffadac11..eba8a85e 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/login/LoginActivity.kt @@ -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().startWithSession(data) goToHome() } diff --git a/vector/src/main/java/im/vector/riotredesign/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/riotredesign/features/notifications/NotifiableEventResolver.kt index eb83b12a..befff24a 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/notifications/NotifiableEventResolver.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/notifications/NotifiableEventResolver.kt @@ -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()?.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()?.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)*/) diff --git a/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationBroadcastReceiver.kt b/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationBroadcastReceiver.kt index 36692b7d..574d2679 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationBroadcastReceiver.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationBroadcastReceiver.kt @@ -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 { diff --git a/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationDrawerManager.kt index ce207397..15a3ef8b 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationDrawerManager.kt @@ -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) { diff --git a/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationUtils.kt index 9596fdad..aed05d25 100755 --- a/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationUtils.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationUtils.kt @@ -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 diff --git a/vector/src/main/java/im/vector/riotredesign/features/notifications/PushRuleTriggerListener.kt b/vector/src/main/java/im/vector/riotredesign/features/notifications/PushRuleTriggerListener.kt new file mode 100644 index 00000000..b94cb7f6 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/notifications/PushRuleTriggerListener.kt @@ -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) { + 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) + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/settings/PreferencesManager.java b/vector/src/main/java/im/vector/riotredesign/features/settings/PreferencesManager.java index 4a917592..2d0000c2 100755 --- a/vector/src/main/java/im/vector/riotredesign/features/settings/PreferencesManager.java +++ b/vector/src/main/java/im/vector/riotredesign/features/settings/PreferencesManager.java @@ -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"; diff --git a/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsActivity.kt b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsActivity.kt index 6208c75e..85f1e0be 100755 --- a/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsActivity.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsActivity.kt @@ -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 diff --git a/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsAdvancedNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsAdvancedNotificationPreferenceFragment.kt index eee04547..05109c28 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsAdvancedNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsAdvancedNotificationPreferenceFragment.kt @@ -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") } diff --git a/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsNotification.kt b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsNotification.kt new file mode 100644 index 00000000..d1ce1e77 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsNotification.kt @@ -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) + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsPreferencesFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsPreferencesFragment.kt index d821c7f7..d073d560 100755 --- a/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsPreferencesFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsPreferencesFragment.kt @@ -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() @@ -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) } diff --git a/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsPreferencesFragmentV2.kt b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsPreferencesFragmentV2.kt new file mode 100644 index 00000000..0ef9d93a --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsPreferencesFragmentV2.kt @@ -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) + } + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/settings/push/PushGatewayItem.kt b/vector/src/main/java/im/vector/riotredesign/features/settings/push/PushGatewayItem.kt new file mode 100644 index 00000000..f57d76b8 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/settings/push/PushGatewayItem.kt @@ -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() { + + @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(R.id.pushGatewayKind) + val pushKey by bind(R.id.pushGatewayKeyValue) + val deviceName by bind(R.id.pushGatewayDeviceNameValue) + val format by bind(R.id.pushGatewayFormatValue) + val url by bind(R.id.pushGatewayURLValue) + val appName by bind(R.id.pushGatewayAppNameValue) + val appId by bind(R.id.pushGatewayAppIdValue) + } +} + +// +//abstract class ReactionInfoSimpleItem : EpoxyModelWithHolder() { \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/settings/push/PushGatewaysFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/settings/push/PushGatewaysFragment.kt new file mode 100644 index 00000000..bdb29e83 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/settings/push/PushGatewaysFragment.kt @@ -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() { + override fun buildModels(data: PushGatewayViewState?) { + val pushers = data?.pushgateways?.invoke() ?: return + pushers.forEach { + pushGatewayItem { + id("${it.pushKey}_${it.appId}") + pusher(it) + } + } + } + + } +} diff --git a/vector/src/main/java/im/vector/riotredesign/features/settings/push/PushGatewaysViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/settings/push/PushGatewaysViewModel.kt new file mode 100644 index 00000000..69c0b9c4 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/settings/push/PushGatewaysViewModel.kt @@ -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> = Uninitialized) + : MvRxState + +class PushGatewaysViewModel(initialState: PushGatewayViewState) : VectorViewModel(initialState) { + + + companion object : MvRxViewModelFactory { + + override fun create(viewModelContext: ViewModelContext, state: PushGatewayViewState): PushGatewaysViewModel? { + val session = viewModelContext.activity.get() + 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 + } + + } + +} \ No newline at end of file diff --git a/vector/src/main/res/anim/fade_in.xml b/vector/src/main/res/anim/fade_in.xml new file mode 100644 index 00000000..fbbaaf17 --- /dev/null +++ b/vector/src/main/res/anim/fade_in.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/vector/src/main/res/anim/fade_out.xml b/vector/src/main/res/anim/fade_out.xml new file mode 100644 index 00000000..fe99ebf7 --- /dev/null +++ b/vector/src/main/res/anim/fade_out.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/vector/src/main/res/anim/right_in.xml b/vector/src/main/res/anim/right_in.xml new file mode 100644 index 00000000..35f261c9 --- /dev/null +++ b/vector/src/main/res/anim/right_in.xml @@ -0,0 +1,10 @@ + + + + diff --git a/vector/src/main/res/anim/right_out.xml b/vector/src/main/res/anim/right_out.xml new file mode 100644 index 00000000..504c39b8 --- /dev/null +++ b/vector/src/main/res/anim/right_out.xml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/vector/src/main/res/drawable-hdpi/ic_settings.png b/vector/src/main/res/drawable-hdpi/ic_settings.png deleted file mode 100755 index 7ef944db..00000000 Binary files a/vector/src/main/res/drawable-hdpi/ic_settings.png and /dev/null differ diff --git a/vector/src/main/res/drawable-hdpi/sync.png b/vector/src/main/res/drawable-hdpi/sync.png new file mode 100644 index 00000000..f191e2dd Binary files /dev/null and b/vector/src/main/res/drawable-hdpi/sync.png differ diff --git a/vector/src/main/res/drawable-mdpi/ic_settings.png b/vector/src/main/res/drawable-mdpi/ic_settings.png deleted file mode 100755 index 10f0d15d..00000000 Binary files a/vector/src/main/res/drawable-mdpi/ic_settings.png and /dev/null differ diff --git a/vector/src/main/res/drawable-mdpi/sync.png b/vector/src/main/res/drawable-mdpi/sync.png new file mode 100644 index 00000000..2542f326 Binary files /dev/null and b/vector/src/main/res/drawable-mdpi/sync.png differ diff --git a/vector/src/main/res/drawable-xhdpi/ic_settings.png b/vector/src/main/res/drawable-xhdpi/ic_settings.png deleted file mode 100755 index 56d5d9fc..00000000 Binary files a/vector/src/main/res/drawable-xhdpi/ic_settings.png and /dev/null differ diff --git a/vector/src/main/res/drawable-xhdpi/sync.png b/vector/src/main/res/drawable-xhdpi/sync.png new file mode 100644 index 00000000..64478c4e Binary files /dev/null and b/vector/src/main/res/drawable-xhdpi/sync.png differ diff --git a/vector/src/main/res/drawable-xxhdpi/ic_settings.png b/vector/src/main/res/drawable-xxhdpi/ic_settings.png deleted file mode 100755 index 879eb9a7..00000000 Binary files a/vector/src/main/res/drawable-xxhdpi/ic_settings.png and /dev/null differ diff --git a/vector/src/main/res/drawable-xxhdpi/sync.png b/vector/src/main/res/drawable-xxhdpi/sync.png new file mode 100644 index 00000000..b613b0d7 Binary files /dev/null and b/vector/src/main/res/drawable-xxhdpi/sync.png differ diff --git a/vector/src/main/res/drawable-xxxhdpi/ic_settings.png b/vector/src/main/res/drawable-xxxhdpi/ic_settings.png deleted file mode 100755 index 82d679bd..00000000 Binary files a/vector/src/main/res/drawable-xxxhdpi/ic_settings.png and /dev/null differ diff --git a/vector/src/main/res/drawable-xxxhdpi/sync.png b/vector/src/main/res/drawable-xxxhdpi/sync.png new file mode 100644 index 00000000..99adbef6 Binary files /dev/null and b/vector/src/main/res/drawable-xxxhdpi/sync.png differ diff --git a/vector/src/main/res/drawable/ic_bell.xml b/vector/src/main/res/drawable/ic_bell.xml new file mode 100644 index 00000000..cb648585 --- /dev/null +++ b/vector/src/main/res/drawable/ic_bell.xml @@ -0,0 +1,14 @@ + + + diff --git a/vector/src/main/res/drawable/ic_flair.xml b/vector/src/main/res/drawable/ic_flair.xml new file mode 100644 index 00000000..8d303344 --- /dev/null +++ b/vector/src/main/res/drawable/ic_flair.xml @@ -0,0 +1,22 @@ + + + + diff --git a/vector/src/main/res/drawable/ic_lock.xml b/vector/src/main/res/drawable/ic_lock.xml new file mode 100644 index 00000000..2929f412 --- /dev/null +++ b/vector/src/main/res/drawable/ic_lock.xml @@ -0,0 +1,22 @@ + + + + diff --git a/vector/src/main/res/drawable/ic_settings_general.xml b/vector/src/main/res/drawable/ic_settings_general.xml new file mode 100644 index 00000000..97b060f6 --- /dev/null +++ b/vector/src/main/res/drawable/ic_settings_general.xml @@ -0,0 +1,22 @@ + + + + diff --git a/vector/src/main/res/drawable/ic_settings_lab.xml b/vector/src/main/res/drawable/ic_settings_lab.xml new file mode 100644 index 00000000..d1f5aca7 --- /dev/null +++ b/vector/src/main/res/drawable/ic_settings_lab.xml @@ -0,0 +1,14 @@ + + + diff --git a/vector/src/main/res/drawable/ic_settings_x.xml b/vector/src/main/res/drawable/ic_settings_x.xml index 994da00c..08af7d65 100644 --- a/vector/src/main/res/drawable/ic_settings_x.xml +++ b/vector/src/main/res/drawable/ic_settings_x.xml @@ -9,7 +9,7 @@ android:strokeWidth="1.2" android:fillColor="#00000000" android:fillType="evenOdd" - android:strokeColor="#FFF" + android:strokeColor="#454545" android:strokeLineCap="round"/> diff --git a/vector/src/main/res/drawable/ic_sliders.xml b/vector/src/main/res/drawable/ic_sliders.xml new file mode 100644 index 00000000..55ccfcfb --- /dev/null +++ b/vector/src/main/res/drawable/ic_sliders.xml @@ -0,0 +1,14 @@ + + + diff --git a/vector/src/main/res/layout/fragment_home_drawer.xml b/vector/src/main/res/layout/fragment_home_drawer.xml index fd60368b..f12c5f85 100644 --- a/vector/src/main/res/layout/fragment_home_drawer.xml +++ b/vector/src/main/res/layout/fragment_home_drawer.xml @@ -74,6 +74,7 @@ android:background="?attr/selectableItemBackground" android:padding="16dp" android:src="@drawable/ic_settings_x" + android:tint="@android:color/white" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" /> diff --git a/vector/src/main/res/layout/fragment_settings_pushgateways.xml b/vector/src/main/res/layout/fragment_settings_pushgateways.xml new file mode 100644 index 00000000..7e5c0137 --- /dev/null +++ b/vector/src/main/res/layout/fragment_settings_pushgateways.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/config.xml b/vector/src/main/res/values/config.xml index 60789cd6..39c654e9 100755 --- a/vector/src/main/res/values/config.xml +++ b/vector/src/main/res/values/config.xml @@ -20,9 +20,12 @@ https://scalar.vector.im/api - - - + + https://matrix.org/_matrix/push/v1/notify + im.vector.app.android matrix.org diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index af56eb60..7583463f 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -11,4 +11,12 @@ You are already viewing this room! Quick Reactions + + + General + Preferences + Security & Privacy + Expert + Push Gateways + \ No newline at end of file diff --git a/vector/src/main/res/values/theme_dark.xml b/vector/src/main/res/values/theme_dark.xml index 986ed33d..76cd365e 100644 --- a/vector/src/main/res/values/theme_dark.xml +++ b/vector/src/main/res/values/theme_dark.xml @@ -28,11 +28,14 @@ @color/riotx_fab_label_bg_dark @color/riotx_fab_label_color_dark @color/riotx_touch_guard_bg_dark + @color/riotx_keys_backup_banner_accent_color_dark @drawable/highlighted_message_background_dark + @color/riotx_header_panel_border_mobile_dark + @color/riotx_accent @color/primary_color_dark_light diff --git a/vector/src/main/res/values/theme_light.xml b/vector/src/main/res/values/theme_light.xml index ad487d1d..f7f9a260 100644 --- a/vector/src/main/res/values/theme_light.xml +++ b/vector/src/main/res/values/theme_light.xml @@ -33,6 +33,8 @@ @drawable/highlighted_message_background_light + @color/riotx_header_panel_border_mobile_light + @color/riotx_accent diff --git a/vector/src/main/res/xml/vector_settings_notifications.xml b/vector/src/main/res/xml/vector_settings_notifications.xml new file mode 100644 index 00000000..0c890068 --- /dev/null +++ b/vector/src/main/res/xml/vector_settings_notifications.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/xml/vector_settings_preferences_root.xml b/vector/src/main/res/xml/vector_settings_preferences_root.xml new file mode 100644 index 00000000..cb3b5840 --- /dev/null +++ b/vector/src/main/res/xml/vector_settings_preferences_root.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file