Merge pull request #213 from vector-im/feature/notification_alpha

Riot X alpha - Notification phase 1
This commit is contained in:
Benoit Marty 2019-06-26 13:30:22 +02:00 committed by GitHub
commit bec5ca1420
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
164 changed files with 6220 additions and 1226 deletions

281
docs/notifications.md Normal file
View file

@ -0,0 +1,281 @@
This document aims to describe how Riot X android displays notifications to the end user. It also clarifies notifications and background settings in the app.
# Table of Contents
1. [Prerequisites Knowledge](#prerequisites-knowledge)
* [How does a matrix client gets a message from a Home Server?](#how-does-a-matrix-client-gets-a-message-from-a-home-server)
* [How does a mobile app receives push notification?](#how-does-a-mobile-app-receives-push-notification)
* [Push VS Notification](#push-vs-notification)
* [Push in the matrix federated world](#push-in-the-matrix-federated-world)
* [How does the Home Server knows when to notify a client?](#how-does-the-home-server-knows-when-to-notify-a-client)
* [Push vs privacy, and mitigation](#push-vs-privacy-and-mitigation)
* [Background processing limitations](#background-processing-limitations)
2. [RiotX Notification implementations](#riotx-notification-implementations)
* [Requirements](#requirements)
* [Foreground sync mode (Gplay & Fdroid)](#foreground-sync-mode-gplay-fdroid)
* [Push (FCM) received in background](#push-fcm-received-in-background)
* [FCM Fallback mode](#fcm-fallback-mode)
* [f-droid background Mode](#f-droid-background-mode)
3. [Application Settings](#application-settings)
First let's start with some prerequisite knowledge
# Prerequisites Knowledge
## How does a matrix client gets a message from a Home Server?
In order to get messages from a home server, a matrix client need to perform a ``sync`` operation.
`To read events, the intended flow of operation is for clients to first call the /sync API without a since parameter. This returns the most recent message events for each room, as well as the state of the room at the start of the returned timeline. `
The client need to call the `sync`API periodically in order to get incremental updates of the server state (new messages).
This mechanism is known as **HTTP long pooling**.
Using the **HTTP Long pooling** mechanism a client polls a server requesting new information.
The server *holds the request open until new data is available*.
Once available, the server responds and sends the new information.
When the client receives the new information, it immediately sends another request, and the operation is repeated.
This effectively emulates a server push feature.
The HTTP long pooling can be fine tuned in the **SDK** using two parameters:
* timout (Sync request timeout)
* delay (Delay between each sync)
**timeout** is a server paramter, defined by:
```
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.
By default, this is 0, so the server will return immediately even if the response is empty.
```
**delay** is a client preference. When the server responds to a sync request, the client waits for `delay`before calling a new sync.
When the Riot X Android app is open (i.e in foreground state), the default timeout is 30 seconds, and delay is 0.
## How does a mobile app receives push notification
Push notification is used as a way to wake up a mobile application when some important information is available and should be processed.
Typically in order to get push notification, an application relies on a **Push Notification Service** or **Push Provider**.
For example iOS uses APNS (Apple Push Notification Service).
Most of android devices relies on Google's Firebase Cloud Messaging (FCM).
> FCM has replaced Google Cloud Messaging (GCM - deprecated April 10 2018)
FCM will only work on android devices that have Google plays services installed
(In simple terms, Google Play Services is a background service that runs on Android, which in turn helps in integrating Googles advanced functionalities to other applications)
De-Googlified devices need to rely on something else in order to stay up to date with a server.
There some cases when devices with google services cannot use FCM (network infrastructure limitations -firewalls- ,
privacy and or independency requirement, source code licence)
## Push VS Notification
This need some disambiguation, because it is the source of common confusion:
*The fact that you see a notification on your screen does not mean that you have successfully configured your PUSH plateform.*
Technically there is a difference between a push and a notification. A notification is what you see on screen and/or in the notification Menu/Drawer (in the top bar of the phone).
Notifications are not always triggered by a push (One can display a notification locally triggered by an alarm)
## Push in the matrix federated world
In order to send a push to a mobile, App developers need to have a server that will use the FCM APIs, and these APIs requires authentication!
This server is called a **Push Gateway** in the matrix world
That means that Riot X Android, a matrix client created by New Vector, is using a **Push Gateway** with the needed credentials (FCM API secret Key) in order to send push to the New Vector client.
If you create your own matrix client, you will also need to deploy an instance of a **Push Gateway** with the credentials needed to use FCM for your app.
On registration, a matrix client must tell to it's Home Server what Push Gateway to use.
See [Sygnal](https://github.com/matrix-org/sygnal/) for a reference implementation.
```
+--------------------+ +-------------------+
Matrix HTTP | | | |
Notification Protocol | App Developer | | Device Vendor |
| | | |
+-------------------+ | +----------------+ | | +---------------+ |
| | | | | | | | | |
| Matrix homeserver +-----> Push Gateway +------> Push Provider | |
| | | | | | | | | |
+-^-----------------+ | +----------------+ | | +----+----------+ |
| | | | | |
Matrix | | | | | |
Client/Server API + | | | | |
| | +--------------------+ +-------------------+
| +--+-+ |
| | <-------------------------------------------+
+---+ |
| | Provider Push Protocol
+----+
Mobile Device or Client
```
Recommended reading:
* https://thomask.sdf.org/blog/2016/12/11/riots-magical-push-notifications-in-ios.html
* https://matrix.org/docs/spec/client_server/r0.4.0.html#id128
## How does the Home Server knows when to notify a client?
This is defined by [**push rules**](https://matrix.org/docs/spec/client_server/r0.4.0.html#push-rules-).
`A push rule is a single rule that states under what conditions an event should be passed onto a push gateway and how the notification should be presented (sound / importance).`
A Home Server can be configured with default rules (for Direct messages, group messages, mentions, etc.. ).
There are different kind of push rules, it can be per room (each new message on this room should be notified), it can also define a pattern that a message should match (when you are mentioned, or key word based).
Notifications have 2 'levels' (`highlighted = true/false sound = default/custom`). In RiotX these notifications level are reflected as Noisy/Silent.
**What about encrypted messages?**
Of course, content patterns matching cannot be used for encrypted messages server side (as the content is encrypted).
That is why clients are able to **process the push rules client side** to decide what kind of notification should be presented for a given event.
## Push vs privacy, and mitigation
As seen previously, App developers don't directly send a push to the end user's device, they use a Push Provider as intermediary. So technically this intermediary is able to read the content of what is sent.
App developers usually mitigate this by sending a `silent notification`, that is a notification with no identifiable data, or with an encrypted payload. When the push is received the app can then synchronise to it's server in order to generate a local notification.
## Background processing limitations
A mobile applications process live in a managed word, meaning that its process can be limited (e.g no network access), stopped or killed at almost anytime by the Operating System.
In order to improve the battery life of their devices some constructors started to implement mechanism to drastically limit background execution of applications (e.g MIUI/Xiaomi restrictions, Sony stamina mode).
Then starting android M, android has also put more focus on improving device performances, introducing several IDLE modes, App-Standby, Light Doze, Doze.
In a nutshell, apps can't do much in background now.
If the devices is not plugged and stays IDLE for a certain amount of time, radio (mobile connectivity) and CPU can/will be turned off.
For an application like riot X, where users can receive important information at anytime, the best option is to rely on a push system (Google's Firebase Message a.k.a FCM). FCM high priority push can wake up the device and whitelist an application to perform background task (for a limited but unspecified amount of time).
Notice that this is still evolving, and in future versions application that has been 'background restricted' by users won't be able to wake up even when a high priority push is received. Also high priority notifications could be rate limited (not defined anywhere)
It's getting a lot more complicated when you cannot rely on FCM (because: closed sources, network/firewall restrictions, privacy concerns).
The documentation on this subject is vague, and as per our experiments not always exact, also device's behaviour is fragmented.
It is getting more and more complex to have reliable notifications when FCM is not used.
# RiotX Notification implementations
## Requirements
RiotX Android must work with and without FCM.
* The riotX android app published on fdroid do not rely on FCM (all related dependencies are not present)
* The RiotX android app published on google play rely on FCM, with a fallback mode when FCM registration has failed (e.g outdated or missing Google Play Services)
## Foreground sync mode (Gplay & Fdroid)
When in foreground, riotX performs sync continuously with a timeout value set to 10 seconds (see HttpPooling).
As this mode does not need to live beyond the scope of the application, and as per Google recommendation, riotX uses the internal app resources (Thread and Timers) to perform the syncs.
This mode is turned on when the app enters foreground, and off when enters background.
In background, and depending on wether push is available or not, riotX will use different methods to perform the syncs (Workers / Alarms / Service)
## Push (FCM) received in background
In order to enable Push, riotX must first get a push token from the firebase SDK, then register a pusher with this token on the HomeServer.
When a message should be notified to a user, the user's homeserver notifies the registered `push gateway` for riotX, that is [sygnal](https://github.com/matrix-org/sygnal) _- The reference implementation for push gateways -_ hosted by matrix.org.
This sygnal instance is configured with the required FCM API authentication token, and will then use the FCM API in order to notify the user's device running riotX.
```
Homeserver ----> Sygnal (configured for riotX) ----> FCM ----> RiotX
```
The push gateway is configured to only send `(eventId,roomId)` in the push payload (for better [privacy](#push-vs-privacy-and-mitigation)).
RiotX needs then to synchronise with the user's HomeServer, in order to resolve the event and create a notification.
As per [Google recommendation](https://android-developers.googleblog.com/2018/09/notifying-your-users-with-fcm.html), riotX will then use the WorkManager API in order to trigger a background sync.
**Google recommendations:**
> We recommend using FCM messages in combination with the WorkManager 1 or JobScheduler API
> Avoid background services. One common pitfall is using a background service to fetch data in the FCM message handler, since background service will be stopped by the system per recent changes to Google Play Policy
```
Homeserver ----> Sygnal ----> FCM ----> RiotX
(Sync) ----> Homeserver
<----
Display notification
```
**Possible outcomes**
Upon reception of the FCM push, RiotX will perform a sync call to the Home Server, during this process it is possible that:
* Happy path, the sync is performed, the message resolved and displayed in the notification drawer
* The notified message is not in the sync. Can happen if a lot of things did happen since the push (`gappy sync`)
* The sync generates additional notifications (e.g an encrypted message where the user is mentioned detected locally)
* The sync takes too long and the process is killed before completion, or network is not reliable and the sync fails.
Riot X implements several strategies in these cases (TODO document)
## FCM Fallback mode
It is possible that riotX is not able to get a FCM push token.
Common errors (amoung several others) that can cause that:
* Google Play Services is outdated
* Google Play Service fails in someways with FCM servers (infamous `SERVICE_NOT_AVAILABLE`)
If riotX is able to detect one of this cases, it will notifies it to the users and when possible help him fix it via a dedicated troubleshoot screen.
Meanwhile, in order to offer a minimal service, and as per Google's recommendation for background activities, riotX will launch periodic background sync in order to stays in sync with servers.
The fallback mode is impacted by all the battery life saving mechanism implemented by android. Meaning that if the app is not used for a certain amount of time (`App-Standby`), or the device stays still and unplugged (`Light Doze`) , the sync will become less frequent.
And if the device stays unplugged and still for too long (`Doze Mode`), no background sync will be perform at all (the system's `Ignore Battery Optimization option` has no effect on that).
Also the time interval between sync is elastic, controlled by the system to group other apps background sync request and start radio/cpu only once for all.
Usually in this mode, what happen is when you take back your phone in your hand, you suddenly receive notifications.
The fallback mode is supposed to be a temporary state waiting for the user to fix issues for FCM, or for App Developers that has done a fork to correctly configure their FCM settings.
## f-droid background Mode
The f-droid riotX flavor has no dependencies to FCM, therefore cannot relies on Push.
Also Google's recommended background processing method cannot be applied. This is because all of these methods are affected by IDLE modes, and will result on the user not being notified at all when the app is in a Doze mode (only in maintenance windows that could happens only after hours).
Only solution left is to use `AlarmManager`, that offers new API to allow launching some process even if the App is in IDLE modes.
Notice that these alarms, due to their potential impact on battery life, can still be restricted by the system. Documentation says that they will not be triggered more than every minutes under normal system operation, and when in low power mode about every 15 mn.
These restrictions can be relaxed by requirering the app to be white listed from battery optimization.
F-droid version will schedule alarms that will then trigger a Broadcast Receiver, that in turn will launch a Service (in the classic android way), and the reschedule an alarm for next time.
Depending on the system status (or device make), it is still possible that the app is not given enough time to launch the service, or that the radio is still turned off thus preventing the sync to success (that's why Alarms are not recommended for network related tasks).
That is why on riotX Fdroid, the broadcast receiver will acquire a temporary WAKE_LOCK for several seconds (thus securing cpu/network), and launch the service in foreground. The service performs the sync.
Note that foreground services require to put a notification informing the user that the app is doing something even if not launched).
# Application Settings
**Notifications > Enable notifications for this account**
Configure Sygnal to send or not notifications to all user devices.
**Notifications > Enable notifications for this device**
Disable notifications locally. The push server will continue to send notifications to the device but this one will ignore them.

View file

@ -23,7 +23,7 @@ import io.reactivex.Observable
class RxRoom(private val room: Room) {
fun liveRoomSummary(): Observable<RoomSummary> {
return room.roomSummary.asObservable()
return room.liveRoomSummary.asObservable()
}
fun liveRoomMemberIds(): Observable<List<String>> {

View file

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

View file

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

View file

@ -0,0 +1,48 @@
/*
* 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
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(conditionResolver: ConditionResolver): Boolean
open fun technicalDescription(): String {
return "Kind: $kind"
}
}

View file

@ -0,0 +1,28 @@
/*
* 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
/**
* Acts like a visitor on Conditions.
* This class as all required context needed to evaluate rules
*/
interface ConditionResolver {
fun resolveEventMatchCondition(eventMatchCondition: EventMatchCondition): Boolean
fun resolveRoomMemberCountCondition(roomMemberCountCondition: RoomMemberCountCondition): Boolean
fun resolveSenderNotificationPermissionCondition(senderNotificationPermissionCondition: SenderNotificationPermissionCondition): Boolean
fun resolveContainsDisplayNameCondition(containsDisplayNameCondition: ContainsDisplayNameCondition) : Boolean
}

View file

@ -0,0 +1,80 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.pushrules
import android.text.TextUtils
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import timber.log.Timber
import java.util.regex.Pattern
class ContainsDisplayNameCondition : Condition(Kind.contains_display_name) {
override fun isSatisfied(conditionResolver: ConditionResolver): Boolean {
return conditionResolver.resolveContainsDisplayNameCondition(this)
}
override fun technicalDescription(): String {
return "User is mentioned"
}
fun isSatisfied(event: Event, displayName: String): Boolean {
//TODO the spec says:
// Matches any message whose content is unencrypted and contains the user's current display name
var message = when (event.type) {
EventType.MESSAGE -> {
event.content.toModel<MessageContent>()
}
// EventType.ENCRYPTED -> {
// event.root.getClearContent()?.toModel<MessageContent>()
// }
else -> null
} ?: return false
return caseInsensitiveFind(displayName, message.body)
}
companion object {
/**
* Returns whether a string contains an occurrence of another, as a standalone word, regardless of case.
*
* @param subString the string to search for
* @param longString the string to search in
* @return whether a match was found
*/
fun caseInsensitiveFind(subString: String, longString: String): Boolean {
// add sanity checks
if (TextUtils.isEmpty(subString) || TextUtils.isEmpty(longString)) {
return false
}
var res = false
try {
val pattern = Pattern.compile("(\\W|^)" + Pattern.quote(subString) + "(\\W|$)", Pattern.CASE_INSENSITIVE)
res = pattern.matcher(longString).find()
} catch (e: Exception) {
Timber.e(e, "## caseInsensitiveFind() : failed")
}
return res
}
}
}

View file

@ -0,0 +1,97 @@
/*
* 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(conditionResolver: ConditionResolver) : Boolean {
return conditionResolver.resolveEventMatchCondition(this)
}
override fun technicalDescription(): String {
return "'$key' Matches '$pattern'"
}
fun isSatisfied(event: Event): Boolean {
//TODO encrypted events?
val rawJson = MoshiProvider.providesMoshi().adapter(Event::class.java).toJsonValue(event) as? Map<*, *>
?: return false
val value = extractField(rawJson, key) ?: return false
//Patterns with no special glob characters should be treated as having asterisks prepended
// and appended when testing the condition.
try {
val modPattern = if (hasSpecialGlobChar(pattern)) simpleGlobToRegExp(pattern) else simpleGlobToRegExp("*$pattern*")
val regex = Regex(modPattern, RegexOption.DOT_MATCHES_ALL)
return regex.containsMatchIn(value)
} catch (e: Throwable) {
//e.g PatternSyntaxException
Timber.e(e, "Failed to evaluate push condition")
return false
}
}
private fun extractField(jsonObject: Map<*, *>, fieldPath: String): String? {
val fieldParts = fieldPath.split(".")
if (fieldParts.isEmpty()) return null
var jsonElement: Map<*, *> = jsonObject
fieldParts.forEachIndexed { index, pathSegment ->
if (index == fieldParts.lastIndex) {
return jsonElement[pathSegment]?.toString()
} else {
val sub = jsonElement[pathSegment] ?: return null
if (sub is Map<*, *>) {
jsonElement = sub
} else {
return null
}
}
}
return null
}
companion object {
private fun hasSpecialGlobChar(glob: String): Boolean {
return glob.contains("*") || glob.contains("?")
}
//Very simple glob to regexp converter
private fun simpleGlobToRegExp(glob: String): String {
var out = ""//"^"
for (i in 0 until glob.length) {
val c = glob[i]
when (c) {
'*' -> out += ".*"
'?' -> out += '.'.toString()
'.' -> out += "\\."
'\\' -> out += "\\\\"
else -> out += c
}
}
out += ""//'$'.toString()
return out
}
}
}

View file

@ -0,0 +1,46 @@
/*
* 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.MatrixCallback
import im.vector.matrix.android.api.pushrules.rest.PushRule
import im.vector.matrix.android.api.session.events.model.Event
interface PushRuleService {
/**
* Fetch the push rules from the server
*/
fun fetchPushRules(scope: String = "global")
//TODO get push rule set
fun getPushRules(scope: String = "global"): List<PushRule>
//TODO update rule
fun updatePushRuleEnableStatus(kind: String, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback<Unit>)
fun addPushRuleListener(listener: PushRuleListener)
fun removePushRuleListener(listener: PushRuleListener)
// fun fulfilledBingRule(event: Event, rules: List<PushRule>): PushRule?
interface PushRuleListener {
fun onMatchRule(event: Event, actions: List<Action>)
fun batchFinish()
}
}

View file

@ -0,0 +1,71 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.pushrules
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.RoomService
import timber.log.Timber
import java.util.regex.Pattern
private val regex = Pattern.compile("^(==|<=|>=|<|>)?(\\d*)$")
class RoomMemberCountCondition(val `is`: String) : Condition(Kind.room_member_count) {
override fun isSatisfied(conditionResolver: ConditionResolver): Boolean {
return conditionResolver.resolveRoomMemberCountCondition(this)
}
override fun technicalDescription(): String {
return "Room member count is $`is`"
}
fun isSatisfied(event: Event, session: RoomService?): Boolean {
// sanity check^
val roomId = event.roomId ?: return false
val room = session?.getRoom(roomId) ?: return false
// Parse the is field into prefix and number the first time
val (prefix, count) = parseIsField() ?: return false
val numMembers = room.getNumberOfJoinedMembers()
return when (prefix) {
"<" -> numMembers < count
">" -> numMembers > count
"<=" -> numMembers <= count
">=" -> numMembers >= count
else -> numMembers == count
}
}
/**
* Parse the is field to extract meaningful information.
*/
private fun parseIsField(): Pair<String?, Int>? {
try {
val match = regex.matcher(`is`)
if (match.find()) {
val prefix = match.group(1)
val count = match.group(2).toInt()
return prefix to count
}
} catch (t: Throwable) {
Timber.d(t)
}
return null
}
}

View file

@ -0,0 +1,47 @@
/*
* 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
/**
* Known rule ids
*
* Ref: https://matrix.org/docs/spec/client_server/latest#predefined-rules
*/
object RuleIds {
// Default Override Rules
const val RULE_ID_DISABLE_ALL = ".m.rule.master"
const val RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS = ".m.rule.suppress_notices"
const val RULE_ID_INVITE_ME = ".m.rule.invite_for_me"
const val RULE_ID_PEOPLE_JOIN_LEAVE = ".m.rule.member_event"
const val RULE_ID_CONTAIN_DISPLAY_NAME = ".m.rule.contains_display_name"
const val RULE_ID_TOMBSTONE = ".m.rule.tombstone"
const val RULE_ID_ROOM_NOTIF = ".m.rule.roomnotif"
// Default Content Rules
const val RULE_ID_CONTAIN_USER_NAME = ".m.rule.contains_user_name"
// Default Underride Rules
const val RULE_ID_CALL = ".m.rule.call"
const val RULE_ID_one_to_one_encrypted_room = ".m.rule.encrypted_room_one_to_one"
const val RULE_ID_ONE_TO_ONE_ROOM = ".m.rule.room_one_to_one"
const val RULE_ID_ALL_OTHER_MESSAGES_ROOMS = ".m.rule.message"
const val RULE_ID_ENCRYPTED = ".m.rule.encrypted"
// Not documented
const val RULE_ID_FALLBACK = ".m.rule.fallback"
}

View file

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

View file

@ -0,0 +1,36 @@
package im.vector.matrix.android.api.pushrules
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.PowerLevels
class SenderNotificationPermissionCondition(val key: String) : Condition(Kind.sender_notification_permission) {
override fun isSatisfied(conditionResolver: ConditionResolver): Boolean {
return conditionResolver.resolveSenderNotificationPermissionCondition(this)
}
override fun technicalDescription(): String {
return "User power level <$key>"
}
fun isSatisfied(event: Event, powerLevels: PowerLevels): Boolean {
return event.senderId != null && powerLevels.getUserPowerLevel(event.senderId) >= powerLevels.notificationLevel(key)
}
}

View file

@ -0,0 +1,37 @@
/*
* 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
/**
* All push rulesets for a user.
*/
@JsonClass(generateAdapter = true)
data class GetPushRulesResponse(
/**
* Global rules, account level applying to all devices
*/
@Json(name = "global")
val global: Ruleset,
/**
* Device specific rules, apply only to current device
*/
@Json(name = "device")
val device: Ruleset? = null
)

View file

@ -0,0 +1,78 @@
/*
* 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.*
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 -> {
ContainsDisplayNameCondition()
}
Condition.Kind.room_member_count -> {
if (this.iz.isNullOrBlank()) {
Timber.e("Malformed ROOM_MEMBER_COUNT condition")
null
} else {
RoomMemberCountCondition(this.iz)
}
}
Condition.Kind.sender_notification_permission -> {
this.key?.let { SenderNotificationPermissionCondition(it) }
}
Condition.Kind.UNRECOGNIZE -> {
Timber.e("Unknwon kind $kind")
null
}
}
}
}

View file

@ -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.api.pushrules.rest
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class PushRule(
/**
* Required. The actions to perform when this rule is matched.
*/
val actions: List<Any>,
/**
* Required. Whether this is a default rule, or has been set explicitly.
*/
val default: Boolean? = false,
/**
* Required. Whether the push rule is enabled or not.
*/
val enabled: Boolean,
/**
* Required. The ID of this rule.
*/
@Json(name = "rule_id") val ruleId: String,
/**
* The conditions that must hold true for an event in order for a rule to be applied to an event
*/
val conditions: List<PushCondition>? = null,
/**
* The glob-style pattern to match against. Only applicable to content rules.
*/
val pattern: String? = null
)

View file

@ -0,0 +1,27 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.pushrules.rest
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class Ruleset(
val content: List<PushRule>? = null,
val override: List<PushRule>? = null,
val room: List<PushRule>? = null,
val sender: List<PushRule>? = null,
val underride: List<PushRule>? = null
)

View file

@ -19,11 +19,13 @@ package im.vector.matrix.android.api.session
import androidx.annotation.MainThread
import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.pushrules.PushRuleService
import im.vector.matrix.android.api.session.cache.CacheService
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.group.GroupService
import im.vector.matrix.android.api.session.pushers.PushersService
import im.vector.matrix.android.api.session.room.RoomDirectoryService
import im.vector.matrix.android.api.session.room.RoomService
import im.vector.matrix.android.api.session.signout.SignOutService
@ -43,7 +45,9 @@ interface Session :
CryptoService,
CacheService,
SignOutService,
FilterService {
FilterService,
PushRuleService,
PushersService {
/**
* The params associated to the session
@ -56,6 +60,20 @@ interface Session :
@MainThread
fun open()
/**
* Requires a one time background sync
*/
fun requireBackgroundSync()
/**
* Launches infinite periodic background syncs
* THis does not work in doze mode :/
* If battery optimization is on it can work in app standby but that's all :/
*/
fun startAutomaticBackgroundSync(repeatDelay: Long = 30_000L)
fun stopAnyBackgroundSync()
/**
* This method start the sync thread.
*/

View file

@ -0,0 +1,45 @@
/*
* 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 {
UNREGISTERED,
REGISTERING,
UNREGISTERING,
REGISTERED,
FAILED_TO_REGISTER
}
data class PusherData(
val url: String? = null,
val format: String? = null
)

View file

@ -0,0 +1,64 @@
/*
* 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 im.vector.matrix.android.api.MatrixCallback
import java.util.*
interface PushersService {
/**
* Refresh pushers from server state
*/
fun refreshPushers()
/**
* Add a new HTTP pusher.
*
* @param pushkey the pushkey
* @param appId the application id
* @param profileTag the profile tag
* @param lang the language
* @param appDisplayName a human-readable application name
* @param deviceDisplayName a human-readable device name
* @param url the URL that should be used to send notifications
* @param append append the pusher
* @param withEventIdOnly true to limit the push content
*
* @return A work request uuid. Can be used to listen to the status
* (LiveData<WorkInfo> status = workManager.getWorkInfoByIdLiveData(<UUID>))
*/
fun addHttpPusher(pushkey: String,
appId: String,
profileTag: String,
lang: String,
appDisplayName: String,
deviceDisplayName: String,
url: String,
append: Boolean,
withEventIdOnly: Boolean): UUID
fun removeHttpPusher(pushkey: String, appId: String, callback: MatrixCallback<Unit>)
companion object {
const val EVENT_ID_ONLY = "event_id_only"
}
fun livePushers(): LiveData<List<Pusher>>
}

View file

@ -47,6 +47,8 @@ interface Room :
* A live [RoomSummary] associated with the room
* You can observe this summary to get dynamic data from this room.
*/
val roomSummary: LiveData<RoomSummary>
val liveRoomSummary: LiveData<RoomSummary>
val roomSummary: RoomSummary?
}

View file

@ -49,6 +49,8 @@ interface MembershipService {
*/
fun getRoomMemberIdsLive(): LiveData<List<String>>
fun getNumberOfJoinedMembers() : Int
/**
* Invite a user in the room
*/

View file

@ -39,4 +39,6 @@ data class CallInviteContent(
}
}
fun isVideo(): Boolean = offer.sdp.contains(Offer.SDP_VIDEO)
}

View file

@ -38,4 +38,5 @@ interface ReadService {
*/
fun setReadMarker(fullyReadEventId: String, callback: MatrixCallback<Unit>)
fun isEventRead(eventId: String): Boolean
}

View file

@ -0,0 +1,26 @@
package im.vector.matrix.android.internal.database.mapper
import im.vector.matrix.android.api.pushrules.rest.PushCondition
import im.vector.matrix.android.internal.database.model.PushConditionEntity
internal object PushConditionMapper {
fun map(entity: PushConditionEntity): PushCondition {
return PushCondition(
kind = entity.kind,
iz = entity.iz,
key = entity.key,
pattern = entity.pattern
)
}
fun map(domain: PushCondition): PushConditionEntity {
return PushConditionEntity(
kind = domain.kind,
iz = domain.iz,
key = domain.key,
pattern = domain.pattern
)
}
}

View file

@ -0,0 +1,105 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.database.mapper
import com.squareup.moshi.Types
import im.vector.matrix.android.api.pushrules.Condition
import im.vector.matrix.android.api.pushrules.rest.PushCondition
import im.vector.matrix.android.api.pushrules.rest.PushRule
import im.vector.matrix.android.internal.database.model.PushRuleEntity
import im.vector.matrix.android.internal.di.MoshiProvider
import io.realm.RealmList
import timber.log.Timber
internal object PushRulesMapper {
private val moshiActionsAdapter = MoshiProvider.providesMoshi().adapter<List<Any>>(Types.newParameterizedType(List::class.java, Any::class.java))
// private val listOfAnyAdapter: JsonAdapter<List<Any>> =
// moshi.adapter<List<Any>>(Types.newParameterizedType(List::class.java, Any::class.java), kotlin.collections.emptySet(), "actions")
fun mapContentRule(pushrule: PushRuleEntity): PushRule {
return PushRule(
actions = fromActionStr(pushrule.actionsStr),
default = pushrule.default,
enabled = pushrule.enabled,
ruleId = pushrule.ruleId,
conditions = listOf(
PushCondition(Condition.Kind.event_match.name, "content.body", pushrule.pattern)
)
)
}
private fun fromActionStr(actionsStr: String?): List<Any> {
try {
return actionsStr?.let { moshiActionsAdapter.fromJson(it) } ?: emptyList()
} catch (e: Throwable) {
Timber.e(e, "## failed to map push rule actions <$actionsStr>")
return emptyList()
}
}
fun mapRoomRule(pushrule: PushRuleEntity): PushRule {
return PushRule(
actions = fromActionStr(pushrule.actionsStr),
default = pushrule.default,
enabled = pushrule.enabled,
ruleId = pushrule.ruleId,
conditions = listOf(
PushCondition(Condition.Kind.event_match.name, "room_id", pushrule.ruleId)
)
)
}
fun mapSenderRule(pushrule: PushRuleEntity): PushRule {
return PushRule(
actions = fromActionStr(pushrule.actionsStr),
default = pushrule.default,
enabled = pushrule.enabled,
ruleId = pushrule.ruleId,
conditions = listOf(
PushCondition(Condition.Kind.event_match.name, "user_id", pushrule.ruleId)
)
)
}
fun map(pushrule: PushRuleEntity): PushRule {
return PushRule(
actions = fromActionStr(pushrule.actionsStr),
default = pushrule.default,
enabled = pushrule.enabled,
ruleId = pushrule.ruleId,
conditions = pushrule.conditions?.map { PushConditionMapper.map(it) }
)
}
fun map(pushRule: PushRule): PushRuleEntity {
return PushRuleEntity(
actionsStr = moshiActionsAdapter.toJson(pushRule.actions),
default = pushRule.default ?: false,
enabled = pushRule.enabled,
ruleId = pushRule.ruleId,
pattern = pushRule.pattern,
conditions = pushRule.conditions?.let {
RealmList(*pushRule.conditions.map { PushConditionMapper.map(it) }.toTypedArray())
} ?: RealmList()
)
}
}

View file

@ -0,0 +1,63 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.database.mapper
import im.vector.matrix.android.api.session.pushers.Pusher
import im.vector.matrix.android.api.session.pushers.PusherData
import im.vector.matrix.android.internal.database.model.PusherDataEntity
import im.vector.matrix.android.internal.database.model.PusherEntity
import im.vector.matrix.android.internal.session.pushers.JsonPusher
internal object PushersMapper {
fun map(pushEntity: PusherEntity): Pusher {
return Pusher(
userId = pushEntity.userId,
pushKey = pushEntity.pushKey,
kind = pushEntity.kind ?: "",
appId = pushEntity.appId,
appDisplayName = pushEntity.appDisplayName,
deviceDisplayName = pushEntity.deviceDisplayName,
profileTag = pushEntity.profileTag,
lang = pushEntity.lang,
data = PusherData(pushEntity.data?.url, pushEntity.data?.format),
state = pushEntity.state
)
}
fun map(pusher: JsonPusher, userId: String): PusherEntity {
return PusherEntity(
userId = userId,
pushKey = pusher.pushKey,
kind = pusher.kind,
appId = pusher.appId,
appDisplayName = pusher.appDisplayName,
deviceDisplayName = pusher.deviceDisplayName,
profileTag = pusher.profileTag,
lang = pusher.lang,
data = PusherDataEntity(pusher.data?.url, pusher.data?.format)
)
}
}
internal fun PusherEntity.asDomain(): Pusher {
return PushersMapper.map(this)
}
internal fun JsonPusher.toEntity(userId: String): PusherEntity {
return PushersMapper.map(this, userId)
}

View file

@ -0,0 +1,29 @@
/*
* copyright 2019 new vector ltd
*
* licensed under the apache license, version 2.0 (the "license");
* you may not use this file except in compliance with the license.
* you may obtain a copy of the license at
*
* http://www.apache.org/licenses/license-2.0
*
* unless required by applicable law or agreed to in writing, software
* distributed under the license is distributed on an "as is" basis,
* without warranties or conditions of any kind, either express or implied.
* see the license for the specific language governing permissions and
* limitations under the license.
*/
package im.vector.matrix.android.internal.database.model
import io.realm.RealmObject
internal open class PushConditionEntity(
var kind: String = "",
var key: String? = null,
var pattern: String? = null,
var iz: String? = null
) : RealmObject() {
companion object
}

View file

@ -0,0 +1,39 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.database.model
import io.realm.RealmList
import io.realm.RealmObject
internal open class PushRuleEntity(
//Required. The actions to perform when this rule is matched.
var actionsStr: String? = null,
//Required. Whether this is a default rule, or has been set explicitly.
var default: Boolean = false,
//Required. Whether the push rule is enabled or not.
var enabled: Boolean = true,
//Required. The ID of this rule.
var ruleId: String = "",
//The conditions that must hold true for an event in order for a rule to be applied to an event
var conditions: RealmList<PushConditionEntity>? = RealmList(),
//The glob-style pattern to match against. Only applicable to content rules.
var pattern: String? = null
) : RealmObject() {
companion object
}

View file

@ -0,0 +1,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.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 = "",
// "content", etc.
var rulesetKey: String = "",
var pushRules: RealmList<PushRuleEntity> = RealmList()
) : RealmObject() {
companion object
}

View file

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

View file

@ -0,0 +1,58 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.database.model
import im.vector.matrix.android.api.session.pushers.PusherState
import io.realm.RealmObject
import io.realm.annotations.Index
//TODO
// at java.lang.Thread.run(Thread.java:764)
// Caused by: java.lang.IllegalArgumentException: 'value' is not a valid managed object.
// at io.realm.ProxyState.checkValidObject(ProxyState.java:213)
// at io.realm.im_vector_matrix_android_internal_database_model_PusherEntityRealmProxy.realmSet$data(im_vector_matrix_android_internal_database_model_PusherEntityRealmProxy.java:413)
// at im.vector.matrix.android.internal.database.model.PusherEntity.setData(PusherEntity.kt:16)
// at im.vector.matrix.android.internal.session.pushers.AddHttpPusherWorker$doWork$$inlined$fold$lambda$2.execute(AddHttpPusherWorker.kt:70)
// at io.realm.Realm.executeTransaction(Realm.java:1493)
internal open class PusherEntity(
@Index var userId: String = "",
var pushKey: String = "",
var kind: String? = null,
var appId: String = "",
var appDisplayName: String? = null,
var deviceDisplayName: String? = null,
var profileTag: String? = null,
var lang: String? = null,
var data: PusherDataEntity? = null
) : RealmObject() {
private var stateStr: String = PusherState.UNREGISTERED.name
var state: PusherState
get() {
try {
return PusherState.valueOf(stateStr)
} catch (e: Exception) {
//can this happen?
return PusherState.UNREGISTERED
}
}
set(value) {
stateStr = value.name
}
companion object
}

View file

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

View file

@ -53,6 +53,14 @@ internal fun EventEntity.Companion.where(realm: Realm,
}
internal fun EventEntity.Companion.types(realm: Realm,
typeList: List<String> = emptyList()): RealmQuery<EventEntity> {
val query = realm.where<EventEntity>()
query.`in`(EventEntityFields.TYPE, typeList.toTypedArray())
return query
}
internal fun EventEntity.Companion.latestEvent(realm: Realm,
roomId: String,
includedTypes: List<String> = emptyList(),

View file

@ -0,0 +1,46 @@
/*
* 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.PushRulesEntity
import im.vector.matrix.android.internal.database.model.PushRulesEntityFields
import im.vector.matrix.android.internal.database.model.PusherEntity
import im.vector.matrix.android.internal.database.model.PusherEntityFields
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.kotlin.where
internal fun PusherEntity.Companion.where(realm: Realm,
userId: String,
pushKey: String? = null): RealmQuery<PusherEntity> {
return realm.where<PusherEntity>()
.equalTo(PusherEntityFields.USER_ID, userId)
.apply {
if (pushKey != null) {
equalTo(PusherEntityFields.PUSH_KEY, pushKey)
}
}
}
internal fun PushRulesEntity.Companion.where(realm: Realm,
userId: String,
scope: String,
ruleSetKey: String): RealmQuery<PushRulesEntity> {
return realm.where<PushRulesEntity>()
.equalTo(PushRulesEntityFields.USER_ID, userId)
.equalTo(PushRulesEntityFields.SCOPE, scope)
.equalTo(PushRulesEntityFields.RULESET_KEY, ruleSetKey)
}

View file

@ -25,6 +25,7 @@ import im.vector.matrix.android.internal.session.sync.model.UserAccountDataDirec
import im.vector.matrix.android.internal.session.sync.model.UserAccountDataFallback
import im.vector.matrix.android.internal.util.JsonCanonicalizer
object MoshiProvider {
private val moshi: Moshi = Moshi.Builder()
@ -42,6 +43,7 @@ object MoshiProvider {
.registerSubtype(MessageLocationContent::class.java, MessageType.MSGTYPE_LOCATION)
.registerSubtype(MessageFileContent::class.java, MessageType.MSGTYPE_FILE)
)
.add(SerializeNulls.JSON_ADAPTER_FACTORY)
.build()
fun providesMoshi(): Moshi {
@ -63,3 +65,4 @@ object MoshiProvider {
}
}

View file

@ -0,0 +1,23 @@
package im.vector.matrix.android.internal.di
import androidx.annotation.Nullable
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonQualifier
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import java.lang.reflect.Type
@Retention(AnnotationRetention.RUNTIME)
@JsonQualifier
annotation class SerializeNulls {
companion object {
val JSON_ADAPTER_FACTORY: JsonAdapter.Factory = object : JsonAdapter.Factory {
@Nullable
override fun create(type: Type, annotations: Set<Annotation>, moshi: Moshi): JsonAdapter<*>? {
val nextAnnotations = Types.nextAnnotations(annotations, SerializeNulls::class.java)
?: return null
return moshi.nextAdapter<Any>(this, type, nextAnnotations).serializeNulls()
}
}
}
}

View file

@ -24,6 +24,8 @@ 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.pushrules.rest.PushRule
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.cache.CacheService
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
@ -36,6 +38,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,17 +69,21 @@ 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
import im.vector.matrix.android.internal.session.sync.job.SyncThread
import im.vector.matrix.android.internal.session.sync.job.SyncWorker
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 {
companion object {
const val SCOPE: String = "session"
}
@ -96,8 +104,12 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi
private val syncThread by inject<SyncThread>()
private val contentUrlResolver by inject<ContentUrlResolver>()
private val contentUploadProgressTracker by inject<ContentUploadStateTracker>()
private val pushRuleService by inject<PushRuleService>()
private val pushersService by inject<PushersService>()
private var isOpen = false
private val bingRuleWatcher by inject<BingRuleWatcher>()
@MainThread
override fun open() {
assertMainThread()
@ -124,12 +136,30 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi
monarchy.openManually()
}
liveEntityUpdaters.forEach { it.start() }
bingRuleWatcher.start()
}
override fun requireBackgroundSync() {
SyncWorker.requireBackgroundSync()
}
override fun startAutomaticBackgroundSync(repeatDelay: Long) {
SyncWorker.automaticallyBackgroundSync(0, repeatDelay)
}
override fun stopAnyBackgroundSync() {
SyncWorker.stopAnyBackgroundSync()
}
@MainThread
override fun startSync() {
assert(isOpen)
syncThread.start()
if (!syncThread.isAlive) {
syncThread.start()
} else {
syncThread.restart()
Timber.w("Attempt to start an already started thread")
}
}
@MainThread
@ -144,6 +174,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 +191,8 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi
Timber.w("SIGN_OUT: start")
assert(isOpen)
Timber.w("SIGN_OUT: kill sync thread")
syncThread.kill()
//Timber.w("SIGN_OUT: kill sync thread")
//syncThread.kill()
Timber.w("SIGN_OUT: call webservice")
return signOutService.signOut(object : MatrixCallback<Unit> {
@ -261,11 +292,11 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi
override fun clearCache(callback: MatrixCallback<Unit>) {
assert(isOpen)
syncThread.pause()
// syncThread.pause()
cacheService.clearCache(object : MatrixCallbackDelegate<Unit>(callback) {
override fun onSuccess(data: Unit) {
// Restart the sync
syncThread.restart()
// syncThread.restart()
super.onSuccess(data)
}
@ -426,6 +457,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 +475,45 @@ 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 removeHttpPusher(pushkey: String, appId: String, callback: MatrixCallback<Unit>) {
pushersService.removeHttpPusher(pushkey, appId, callback)
}
override fun livePushers(): LiveData<List<Pusher>> {
return pushersService.livePushers()
}
override fun getPushRules(scope: String): List<PushRule> {
return pushRuleService.getPushRules(scope)
}
override fun updatePushRuleEnableStatus(kind: String, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback<Unit>) {
pushRuleService.updatePushRuleEnableStatus(kind, pushRule, enabled, callback)
}
override fun fetchPushRules(scope: String) {
pushRuleService.fetchPushRules(scope)
}
}

View file

@ -19,8 +19,10 @@ 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.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 +36,11 @@ 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.DefaultProcessEventForPushTask
import im.vector.matrix.android.internal.session.notification.DefaultPushRuleService
import im.vector.matrix.android.internal.session.notification.ProcessEventForPushTask
import im.vector.matrix.android.internal.session.pushers.*
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 +170,35 @@ internal class SessionModule(private val sessionParams: SessionParams) {
retrofit.create(FilterApi::class.java)
}
scope(DefaultSession.SCOPE) {
val retrofit: Retrofit = get()
retrofit.create(PushRulesApi::class.java)
}
scope(DefaultSession.SCOPE) {
get<DefaultPushRuleService>() as PushRuleService
}
scope(DefaultSession.SCOPE) {
DefaultPushRuleService(get(), get(), get(), get(), get())
}
scope(DefaultSession.SCOPE) {
DefaultGetPushRulesTask(get()) as GetPushRulesTask
}
scope(DefaultSession.SCOPE) {
DefaultUpdatePushRuleEnableStatusTask(get()) as UpdatePushRuleEnableStatusTask
}
scope(DefaultSession.SCOPE) {
DefaultProcessEventForPushTask(get()) as ProcessEventForPushTask
}
scope(DefaultSession.SCOPE) {
BingRuleWatcher(get(), get(), get(), get(), get())
}
scope(DefaultSession.SCOPE) {
val groupSummaryUpdater = GroupSummaryUpdater(get())
val userEntityUpdater = UserEntityUpdater(get(), get(), get())
@ -172,6 +208,20 @@ internal class SessionModule(private val sessionParams: SessionParams) {
listOf<LiveEntityObserver>(groupSummaryUpdater, userEntityUpdater, aggregationUpdater, eventsPruner)
}
scope(DefaultSession.SCOPE) {
get<Retrofit>().create(PushersAPI::class.java)
}
scope(DefaultSession.SCOPE) {
DefaultGetPusherTask(get()) as GetPushersTask
}
scope(DefaultSession.SCOPE) {
DefaultRemovePusherTask(get(), get()) as RemovePusherTask
}
scope(DefaultSession.SCOPE) {
DefaultPusherService(get(), get(), get(), get(), get()) as PushersService
}
}

View file

@ -0,0 +1,55 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.notification
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.query.types
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
internal class BingRuleWatcher(monarchy: Monarchy,
private val task: ProcessEventForPushTask,
private val defaultPushRuleService: DefaultPushRuleService,
private val sessionParams: SessionParams,
private val taskExecutor: TaskExecutor) :
RealmLiveEntityObserver<EventEntity>(monarchy) {
override val query = Monarchy.Query<EventEntity> {
EventEntity.types(it, listOf(
EventType.REDACTION, EventType.MESSAGE, EventType.REDACTION, EventType.ENCRYPTED)
)
}
override fun processChanges(inserted: List<EventEntity>, updated: List<EventEntity>, deleted: List<EventEntity>) {
val rules = defaultPushRuleService.getPushRules("global")
inserted.map { it.asDomain() }
.filter { it.senderId != sessionParams.credentials.userId }
.let { events ->
task.configureWith(ProcessEventForPushTask.Params(events, rules))
.executeBy(taskExecutor)
}
}
}

View file

@ -0,0 +1,185 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.notification
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.pushrules.Action
import im.vector.matrix.android.api.pushrules.PushRuleService
import im.vector.matrix.android.api.pushrules.rest.GetPushRulesResponse
import im.vector.matrix.android.api.pushrules.rest.PushRule
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.internal.database.mapper.PushRulesMapper
import im.vector.matrix.android.internal.database.model.PushRulesEntity
import im.vector.matrix.android.internal.database.model.PusherEntityFields
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.pushers.GetPushRulesTask
import im.vector.matrix.android.internal.session.pushers.UpdatePushRuleEnableStatusTask
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import timber.log.Timber
internal class DefaultPushRuleService(
private val sessionParams: SessionParams,
private val pushRulesTask: GetPushRulesTask,
private val updatePushRuleEnableStatusTask: UpdatePushRuleEnableStatusTask,
private val taskExecutor: TaskExecutor,
private val monarchy: Monarchy
) : PushRuleService {
private var listeners = ArrayList<PushRuleService.PushRuleListener>()
override fun fetchPushRules(scope: String) {
pushRulesTask
.configureWith(Unit)
.dispatchTo(object : MatrixCallback<GetPushRulesResponse> {
override fun onSuccess(data: GetPushRulesResponse) {
monarchy.runTransactionSync { realm ->
//clear existings?
//TODO
realm.where(PushRulesEntity::class.java)
.equalTo(PusherEntityFields.USER_ID, sessionParams.credentials.userId)
.findAll().deleteAllFromRealm()
val content = PushRulesEntity(sessionParams.credentials.userId, scope, "content")
data.global.content?.forEach { rule ->
PushRulesMapper.map(rule).also {
content.pushRules.add(it)
}
}
realm.insertOrUpdate(content)
val override = PushRulesEntity(sessionParams.credentials.userId, scope, "override")
data.global.override?.forEach { rule ->
PushRulesMapper.map(rule).also {
override.pushRules.add(it)
}
}
realm.insertOrUpdate(override)
val rooms = PushRulesEntity(sessionParams.credentials.userId, scope, "room")
data.global.room?.forEach { rule ->
PushRulesMapper.map(rule).also {
rooms.pushRules.add(it)
}
}
realm.insertOrUpdate(rooms)
val senders = PushRulesEntity(sessionParams.credentials.userId, scope, "sender")
data.global.sender?.forEach { rule ->
PushRulesMapper.map(rule).also {
senders.pushRules.add(it)
}
}
realm.insertOrUpdate(senders)
val underrides = PushRulesEntity(sessionParams.credentials.userId, scope, "underride")
data.global.underride?.forEach { rule ->
PushRulesMapper.map(rule).also {
underrides.pushRules.add(it)
}
}
realm.insertOrUpdate(underrides)
}
}
})
.executeBy(taskExecutor)
}
override fun getPushRules(scope: String): List<PushRule> {
var contentRules: List<PushRule> = emptyList()
var overrideRules: List<PushRule> = emptyList()
var roomRules: List<PushRule> = emptyList()
var senderRules: List<PushRule> = emptyList()
var underrideRules: List<PushRule> = emptyList()
// TODO Create const for ruleSetKey
monarchy.doWithRealm { realm ->
PushRulesEntity.where(realm, sessionParams.credentials.userId, scope, "content").findFirst()?.let { re ->
contentRules = re.pushRules.map { PushRulesMapper.mapContentRule(it) }
}
PushRulesEntity.where(realm, sessionParams.credentials.userId, scope, "override").findFirst()?.let { re ->
overrideRules = re.pushRules.map { PushRulesMapper.map(it) }
}
PushRulesEntity.where(realm, sessionParams.credentials.userId, scope, "room").findFirst()?.let { re ->
roomRules = re.pushRules.map { PushRulesMapper.mapRoomRule(it) }
}
PushRulesEntity.where(realm, sessionParams.credentials.userId, scope, "sender").findFirst()?.let { re ->
senderRules = re.pushRules.map { PushRulesMapper.mapSenderRule(it) }
}
PushRulesEntity.where(realm, sessionParams.credentials.userId, scope, "underride").findFirst()?.let { re ->
underrideRules = re.pushRules.map { PushRulesMapper.map(it) }
}
}
return contentRules + overrideRules + roomRules + senderRules + underrideRules
}
override fun updatePushRuleEnableStatus(kind: String, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback<Unit>) {
updatePushRuleEnableStatusTask
.configureWith(UpdatePushRuleEnableStatusTask.Params(kind, pushRule, enabled))
// TODO Fetch the rules
.dispatchTo(callback)
.executeBy(taskExecutor)
}
override fun removePushRuleListener(listener: PushRuleService.PushRuleListener) {
listeners.remove(listener)
}
override fun addPushRuleListener(listener: PushRuleService.PushRuleListener) {
if (!listeners.contains(listener))
listeners.add(listener)
}
// fun processEvents(events: List<Event>) {
// var hasDoneSomething = false
// events.forEach { event ->
// fulfilledBingRule(event)?.let {
// hasDoneSomething = true
// dispatchBing(event, it)
// }
// }
// if (hasDoneSomething)
// dispatchFinish()
// }
fun dispatchBing(event: Event, rule: PushRule) {
try {
listeners.forEach {
it.onMatchRule(event, Action.mapFrom(rule) ?: emptyList())
}
} catch (e: Throwable) {
Timber.e(e, "Error while dispatching bing")
}
}
fun dispatchFinish() {
try {
listeners.forEach {
it.batchFinish()
}
} catch (e: Throwable) {
}
}
}

View file

@ -0,0 +1,51 @@
package im.vector.matrix.android.internal.session.notification
import arrow.core.Try
import im.vector.matrix.android.api.pushrules.rest.PushRule
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.internal.session.pushers.DefaultConditionResolver
import im.vector.matrix.android.internal.task.Task
import timber.log.Timber
internal interface ProcessEventForPushTask : Task<ProcessEventForPushTask.Params, Unit> {
data class Params(
val events: List<Event>,
val rules: List<PushRule>
)
}
internal class DefaultProcessEventForPushTask(
private val defaultPushRuleService: DefaultPushRuleService
) : ProcessEventForPushTask {
override suspend fun execute(params: ProcessEventForPushTask.Params): Try<Unit> {
return Try {
params.events.forEach { event ->
fulfilledBingRule(event, params.rules)?.let {
Timber.v("Rule $it match for event ${event.eventId}")
defaultPushRuleService.dispatchBing(event, it)
}
}
defaultPushRuleService.dispatchFinish()
}
}
private fun fulfilledBingRule(event: Event, rules: List<PushRule>): PushRule? {
val conditionResolver = DefaultConditionResolver(event)
rules.filter { it.enabled }.forEach { rule ->
val isFullfilled = rule.conditions?.map {
it.asExecutableCondition()?.isSatisfied(conditionResolver) ?: false
}?.fold(true/*A rule with no conditions always matches*/, { acc, next ->
//All conditions must hold true for an event in order to apply the action for the event.
acc && next
}) ?: false
if (isFullfilled) {
return rule
}
}
return null
}
}

View file

@ -0,0 +1,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.PusherEntity
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.MatrixKoinComponent
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.util.WorkerParamsFactory
import org.koin.standalone.inject
class AddHttpPusherWorker(context: Context, params: WorkerParameters)
: CoroutineWorker(context, params), MatrixKoinComponent {
@JsonClass(generateAdapter = true)
internal data class Params(
val pusher: JsonPusher,
val userId: String
)
private val pushersAPI by inject<PushersAPI>()
private val monarchy by inject<Monarchy>()
override suspend fun doWork(): Result {
val params = WorkerParamsFactory.fromData<Params>(inputData)
?: return Result.failure()
val pusher = params.pusher
if (pusher.pushKey.isBlank()) {
return Result.failure()
}
val result = executeRequest<Unit> {
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?.format = pusher.data?.format
echo.data?.url = pusher.data?.url
echo.state = PusherState.REGISTERED
} else {
pusher.toEntity(params.userId).also {
it.state = PusherState.REGISTERED
realm.insertOrUpdate(it)
}
}
}
Result.success()
})
}
}

View file

@ -0,0 +1,55 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.pushers
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.pushrules.*
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.RoomService
import im.vector.matrix.android.internal.di.MatrixKoinComponent
import org.koin.standalone.inject
import timber.log.Timber
internal class DefaultConditionResolver(val event: Event) : ConditionResolver, MatrixKoinComponent {
private val roomService by inject<RoomService>()
private val sessionParams by inject<SessionParams>()
override fun resolveEventMatchCondition(eventMatchCondition: EventMatchCondition): Boolean {
return eventMatchCondition.isSatisfied(event)
}
override fun resolveRoomMemberCountCondition(roomMemberCountCondition: RoomMemberCountCondition): Boolean {
return roomMemberCountCondition.isSatisfied(event, roomService)
}
override fun resolveSenderNotificationPermissionCondition(senderNotificationPermissionCondition: SenderNotificationPermissionCondition): Boolean {
// val roomId = event.roomId ?: return false
// val room = roomService.getRoom(roomId) ?: return false
//TODO RoomState not yet managed
Timber.e("POWER LEVELS STATE NOT YET MANAGED BY RIOTX")
return false //senderNotificationPermissionCondition.isSatisfied(event, )
}
override fun resolveContainsDisplayNameCondition(containsDisplayNameCondition: ContainsDisplayNameCondition): Boolean {
val roomId = event.roomId ?: return false
val room = roomService.getRoom(roomId) ?: return false
val myDisplayName = room.getRoomMember(sessionParams.credentials.userId)?.displayName
?: return false
return containsDisplayNameCondition.isSatisfied(event, myDisplayName)
}
}

View file

@ -0,0 +1,114 @@
/*
* 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 removePusherTask: RemovePusherTask,
private val taskExecutor: TaskExecutor
) : PushersService {
override fun refreshPushers() {
getPusherTask
.configureWith(Unit)
.dispatchTo(object : MatrixCallback<GetPushersResponse> {
override fun onSuccess(data: GetPushersResponse) {
monarchy.runTransactionSync { realm ->
//clear existings?
realm.where(PusherEntity::class.java)
.equalTo(PusherEntityFields.USER_ID, sessionParam.credentials.userId)
.findAll().deleteAllFromRealm()
data.pushers?.forEach { jsonPusher ->
jsonPusher.toEntity(sessionParam.credentials.userId).also {
it.state = PusherState.REGISTERED
realm.insertOrUpdate(it)
}
}
}
}
})
.executeBy(taskExecutor)
}
override fun addHttpPusher(pushkey: String, appId: String, profileTag: String,
lang: String, appDisplayName: String, deviceDisplayName: String,
url: String, append: Boolean, withEventIdOnly: Boolean)
: UUID {
val pusher = JsonPusher(
pushKey = pushkey,
kind = "http",
appId = appId,
appDisplayName = appDisplayName,
deviceDisplayName = deviceDisplayName,
profileTag = profileTag,
lang = lang,
data = JsonPusherData(url, if (withEventIdOnly) PushersService.EVENT_ID_ONLY else null),
append = append)
val params = AddHttpPusherWorker.Params(pusher, sessionParam.credentials.userId)
val constraints = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()
val request = OneTimeWorkRequestBuilder<AddHttpPusherWorker>()
.setConstraints(constraints)
.setInputData(WorkerParamsFactory.toData(params))
.setBackoffCriteria(BackoffPolicy.LINEAR, 10_000L, TimeUnit.MILLISECONDS)
.build()
WorkManager.getInstance().enqueue(request)
return request.id
}
override fun removeHttpPusher(pushkey: String, appId: String, callback: MatrixCallback<Unit>) {
val params = RemovePusherTask.Params(sessionParam.credentials.userId,pushkey,appId)
removePusherTask
.configureWith(params)
.dispatchTo(callback)
//.enableRetry() ??
.executeBy(taskExecutor)
}
override fun livePushers(): LiveData<List<Pusher>> {
return monarchy.findAllMappedWithChanges(
{ realm -> PusherEntity.where(realm, sessionParam.credentials.userId) },
{ it.asDomain() }
)
}
}

View file

@ -0,0 +1,33 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.pushers
import arrow.core.Try
import im.vector.matrix.android.api.pushrules.rest.GetPushRulesResponse
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task
internal interface GetPushRulesTask : Task<Unit, GetPushRulesResponse>
internal class DefaultGetPushRulesTask(private val pushRulesApi: PushRulesApi) : GetPushRulesTask {
override suspend fun execute(params: Unit): Try<GetPushRulesResponse> {
return executeRequest {
apiCall = pushRulesApi.getAllRules()
}
}
}

View file

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

View file

@ -0,0 +1,31 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.pushers
import arrow.core.Try
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task
internal interface GetPushersTask : Task<Unit, GetPushersResponse>
internal class DefaultGetPusherTask(private val pushersAPI: PushersAPI) : GetPushersTask {
override suspend fun execute(params: Unit): Try<GetPushersResponse> {
return executeRequest {
apiCall = pushersAPI.getPushers()
}
}
}

View file

@ -0,0 +1,103 @@
/*
* 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
import im.vector.matrix.android.internal.di.SerializeNulls
/**
* Example:
*
* <code>
* {
* "pushers": [
* {
* "pushkey": "Xp/MzCt8/9DcSNE9cuiaoT5Ac55job3TdLSSmtmYl4A=",
* "kind": "http",
* "app_id": "face.mcapp.appy.prod",
* "app_display_name": "Appy McAppface",
* "device_display_name": "Alice's Phone",
* "profile_tag": "xyz",
* "lang": "en-US",
* "data": {
* "url": "https://example.com/_matrix/push/v1/notify"
* }
* }]
* }
* </code>
*/
@JsonClass(generateAdapter = true)
internal data class JsonPusher(
/**
* Required. This is a unique identifier for this pusher. See /set for more detail. Max length, 512 bytes.
*/
@Json(name = "pushkey")
val pushKey: String,
/**
* Required. The kind of pusher. "http" is a pusher that sends HTTP pokes.
*/
@SerializeNulls
@Json(name = "kind")
val kind: String?,
/**
* Required. This is a reverse-DNS style identifier for the application. Max length, 64 chars.
*/
@Json(name = "app_id")
val appId: String,
/**
* Required. A string that will allow the user to identify what application owns this pusher.
*/
@Json(name = "app_display_name")
val appDisplayName: String? = null,
/**
* Required. A string that will allow the user to identify what device owns this pusher.
*/
@Json(name = "device_display_name")
val deviceDisplayName: String? = null,
/**
* This string determines which set of device specific rules this pusher executes.
*/
@Json(name = "profile_tag")
val profileTag: String? = null,
/**
* Required. The preferred language for receiving notifications (e.g. 'en' or 'en-US')
*/
@Json(name = "lang")
val lang: String? = null,
/**
* Required. A dictionary of information for the pusher implementation itself.
*/
@Json(name = "data")
val data: JsonPusherData? = null,
// Only used to update add Pusher (body of api request)
// Used If true, the homeserver should add another pusher with the given pushkey and App ID in addition
// to any others with different user IDs.
// Otherwise, the homeserver must remove any other pushers with the same App ID and pushkey for different users.
// The default is false.
@Json(name = "append")
val append: Boolean? = false
)

View file

@ -0,0 +1,34 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.pushers
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class JsonPusherData(
/**
* Required if kind is http. The URL to use to send notifications to.
*/
@Json(name = "url")
val url: String? = null,
/**
* The format to use when sending notifications to the Push Gateway.
*/
@Json(name = "format")
val format: String? = null
)

View file

@ -0,0 +1,83 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.pushers
import im.vector.matrix.android.api.pushrules.rest.PushRule
import im.vector.matrix.android.api.pushrules.rest.GetPushRulesResponse
import im.vector.matrix.android.internal.network.NetworkConstants
import retrofit2.Call
import retrofit2.http.*
internal interface PushRulesApi {
/**
* Get all push rules
*/
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushrules/")
fun getAllRules(): Call<GetPushRulesResponse>
/**
* Update the ruleID enable status
*
* @param kind the notification kind (sender, room...)
* @param ruleId the ruleId
* @param enable the new enable status
*/
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushrules/global/{kind}/{ruleId}/enabled")
fun updateEnableRuleStatus(@Path("kind") kind: String,
@Path("ruleId") ruleId: String,
@Body enable: Boolean?)
: Call<Unit>
/**
* Update the ruleID action
*
* @param kind the notification kind (sender, room...)
* @param ruleId the ruleId
* @param actions the actions
*/
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushrules/global/{kind}/{ruleId}/actions")
fun updateRuleActions(@Path("kind") kind: String,
@Path("ruleId") ruleId: String,
@Body actions: Any)
: Call<Unit>
/**
* Delete a rule
*
* @param kind the notification kind (sender, room...)
* @param ruleId the ruleId
*/
@DELETE(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushrules/global/{kind}/{ruleId}")
fun deleteRule(@Path("kind") kind: String,
@Path("ruleId") ruleId: String)
: Call<Unit>
/**
* Add the ruleID enable status
*
* @param kind the notification kind (sender, room...)
* @param ruleId the ruleId.
* @param rule the rule to add.
*/
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushrules/global/{kind}/{ruleId}")
fun addRule(@Path("kind") kind: String,
@Path("ruleId") ruleId: String,
@Body rule: PushRule)
: Call<Unit>
}

View file

@ -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.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/latest#get-matrix-client-r0-pushers
*/
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushers")
fun getPushers(): Call<GetPushersResponse>
/**
* 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.
*
* Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-pushers-set
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushers/set")
fun setPusher(@Body jsonPusher: JsonPusher): Call<Unit>
}

View file

@ -0,0 +1,63 @@
package im.vector.matrix.android.internal.session.pushers
import arrow.core.Try
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.pushers.Pusher
import im.vector.matrix.android.api.session.pushers.PusherState
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.PusherEntity
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.util.tryTransactionSync
internal interface RemovePusherTask : Task<RemovePusherTask.Params, Unit> {
data class Params(val userId: String,
val pushKey: String,
val pushAppId: String)
}
internal class DefaultRemovePusherTask(
private val pushersAPI: PushersAPI,
private val monarchy: Monarchy
) : RemovePusherTask {
override suspend fun execute(params: RemovePusherTask.Params): Try<Unit> {
return Try {
var existing: Pusher? = null
monarchy.runTransactionSync {
val existingEntity = PusherEntity.where(it, params.userId, params.pushKey).findFirst()
existingEntity?.state == PusherState.UNREGISTERING
existing = existingEntity?.asDomain()
}
if (existing == null) {
throw Exception("No existing pusher")
} else {
existing!!
}
}.flatMap {
executeRequest<Unit> {
val deleteBody = JsonPusher(
pushKey = params.pushKey,
appId = params.pushAppId,
// kind null deletes the pusher
kind = null,
appDisplayName = it.appDisplayName ?: "",
deviceDisplayName = it.deviceDisplayName ?: "",
profileTag = it.profileTag ?: "",
lang = it.lang,
data = JsonPusherData(it.data.url, it.data.format),
append = false
)
apiCall = pushersAPI.setPusher(deleteBody)
}
}.flatMap {
monarchy.tryTransactionSync {
val existing = PusherEntity.where(it, params.userId, params.pushKey).findFirst()
existing?.deleteFromRealm()
}
}
}
}

View file

@ -0,0 +1,37 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.pushers
import arrow.core.Try
import im.vector.matrix.android.api.pushrules.rest.PushRule
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task
internal interface UpdatePushRuleEnableStatusTask : Task<UpdatePushRuleEnableStatusTask.Params, Unit> {
data class Params(val kind: String,
val pushRule: PushRule,
val enabled: Boolean)
}
internal class DefaultUpdatePushRuleEnableStatusTask(private val pushRulesApi: PushRulesApi) : UpdatePushRuleEnableStatusTask {
override suspend fun execute(params: UpdatePushRuleEnableStatusTask.Params): Try<Unit> {
return executeRequest {
apiCall = pushRulesApi.updateEnableRuleStatus(params.kind, params.pushRule.ruleId, params.enabled)
}
}
}

View file

@ -33,6 +33,7 @@ import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.util.fetchCopied
internal class DefaultRoom(
override val roomId: String,
@ -52,7 +53,7 @@ internal class DefaultRoom(
RelationService by relationService,
MembershipService by roomMembersService {
override val roomSummary: LiveData<RoomSummary> by lazy {
override val liveRoomSummary: LiveData<RoomSummary> by lazy {
val liveRealmData = RealmLiveData<RoomSummaryEntity>(monarchy.realmConfiguration) { realm ->
RoomSummaryEntity.where(realm, roomId).isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME)
}
@ -68,6 +69,12 @@ internal class DefaultRoom(
}
}
override val roomSummary: RoomSummary?
get() {
var sum: RoomSummaryEntity? = monarchy.fetchCopied { RoomSummaryEntity.where(it, roomId).isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME).findFirst() }
return sum?.asDomain()
}
override fun isEncrypted(): Boolean {
return cryptoService.isRoomEncrypted(roomId)
}

View file

@ -17,6 +17,7 @@
package im.vector.matrix.android.internal.session.room
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService
@ -51,7 +52,8 @@ internal class RoomFactory(private val monarchy: Monarchy,
private val cryptoService: CryptoService,
private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
private val joinRoomTask: JoinRoomTask,
private val leaveRoomTask: LeaveRoomTask) {
private val leaveRoomTask: LeaveRoomTask,
private val sessionParams: SessionParams) {
fun instantiate(roomId: String): Room {
val roomMemberExtractor = SenderRoomMemberExtractor(roomId)
@ -61,7 +63,7 @@ internal class RoomFactory(private val monarchy: Monarchy,
val sendService = DefaultSendService(roomId, eventFactory, cryptoService, monarchy)
val stateService = DefaultStateService(roomId, taskExecutor, sendStateTask)
val roomMembersService = DefaultMembershipService(roomId, monarchy, taskExecutor, loadRoomMembersTask, inviteTask, joinRoomTask, leaveRoomTask)
val readService = DefaultReadService(roomId, monarchy, taskExecutor, setReadMarkersTask)
val readService = DefaultReadService(roomId, monarchy, taskExecutor, setReadMarkersTask, sessionParams)
val reactionService = DefaultRelationService(roomId, eventFactory, findReactionEventForUndoTask, monarchy, taskExecutor)
return DefaultRoom(

View file

@ -80,7 +80,7 @@ class RoomModule {
}
scope(DefaultSession.SCOPE) {
RoomFactory(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get())
RoomFactory(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get())
}
scope(DefaultSession.SCOPE) {

View file

@ -66,6 +66,14 @@ internal class DefaultMembershipService(private val roomId: String,
)
}
override fun getNumberOfJoinedMembers(): Int {
var result = 0
monarchy.runTransactionSync {
result = RoomMembers(it, roomId).getNumberOfJoinedMembers()
}
return result
}
override fun invite(userId: String, callback: MatrixCallback<Unit>) {
val params = InviteTask.Params(roomId, userId)
inviteTask.configureWith(params)

View file

@ -18,9 +18,15 @@ package im.vector.matrix.android.internal.session.room.read
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.room.read.ReadService
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
import im.vector.matrix.android.internal.database.query.find
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
import im.vector.matrix.android.internal.database.query.latestEvent
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.fetchCopied
@ -28,7 +34,8 @@ import im.vector.matrix.android.internal.util.fetchCopied
internal class DefaultReadService(private val roomId: String,
private val monarchy: Monarchy,
private val taskExecutor: TaskExecutor,
private val setReadMarkersTask: SetReadMarkersTask) : ReadService {
private val setReadMarkersTask: SetReadMarkersTask,
private val sessionParams: SessionParams) : ReadService {
override fun markAllAsRead(callback: MatrixCallback<Unit>) {
val latestEvent = getLatestEvent()
@ -50,5 +57,20 @@ internal class DefaultReadService(private val roomId: String,
return monarchy.fetchCopied { EventEntity.latestEvent(it, roomId) }
}
override fun isEventRead(eventId: String): Boolean {
var isEventRead = false
monarchy.doWithRealm {
val readReceipt = ReadReceiptEntity.where(it, roomId, sessionParams.credentials.userId).findFirst()
?: return@doWithRealm
val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(it, roomId)
?: return@doWithRealm
val readReceiptIndex = liveChunk.events.find(readReceipt.eventId)?.displayIndex
?: Int.MIN_VALUE
val eventToCheckIndex = liveChunk.events.find(eventId)?.displayIndex
?: Int.MAX_VALUE
isEventRead = eventToCheckIndex <= readReceiptIndex
}
return isEventRead
}
}

View file

@ -20,7 +20,11 @@ import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.TimelineService
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
import im.vector.matrix.android.internal.database.query.find
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.util.fetchCopyMap
@ -45,4 +49,5 @@ internal class DefaultTimelineService(private val roomId: String,
})
}
}

View file

@ -29,7 +29,7 @@ import im.vector.matrix.android.internal.task.Task
internal interface SyncTask : Task<SyncTask.Params, SyncResponse> {
data class Params(val token: String?)
data class Params(val token: String?, var timeout: Long = 30_000L)
}
@ -42,10 +42,10 @@ internal class DefaultSyncTask(private val syncAPI: SyncAPI,
override suspend fun execute(params: SyncTask.Params): Try<SyncResponse> {
val requestParams = HashMap<String, String>()
var timeout = 0
var timeout = 0L
if (params.token != null) {
requestParams["since"] = params.token
timeout = 30000
timeout = params.timeout
}
requestParams["timeout"] = timeout.toString()
requestParams["filter"] = filterRepository.getFilter()

View file

@ -0,0 +1,243 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.sync.job
import android.app.Service
import android.content.Intent
import android.os.Binder
import android.os.IBinder
import com.squareup.moshi.JsonEncodingException
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.di.MatrixKoinComponent
import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
import im.vector.matrix.android.internal.session.sync.SyncTask
import im.vector.matrix.android.internal.session.sync.SyncTokenStore
import im.vector.matrix.android.internal.session.sync.model.SyncResponse
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.TaskThread
import im.vector.matrix.android.internal.task.configureWith
import org.koin.standalone.inject
import timber.log.Timber
import java.net.SocketTimeoutException
import java.util.*
import kotlin.collections.ArrayList
private const val DEFAULT_LONG_POOL_TIMEOUT = 10_000L
private const val BACKGROUND_LONG_POOL_TIMEOUT = 0L
/**
* Can execute periodic sync task.
* An IntentService is used in conjunction with the AlarmManager and a Broadcast Receiver
* in order to be able to perform a sync even if the app is not running.
* The <receiver> and <service> must be declared in the Manifest or the app using the SDK
*/
open class SyncService : Service(), MatrixKoinComponent {
private var mIsSelfDestroyed: Boolean = false
private var cancelableTask: Cancelable? = null
private val syncTokenStore: SyncTokenStore by inject()
private val syncTask: SyncTask by inject()
private val networkConnectivityChecker: NetworkConnectivityChecker by inject()
private val taskExecutor: TaskExecutor by inject()
private var localBinder = LocalBinder()
var timer = Timer()
var nextBatchDelay = 0L
var timeout = 10_000L
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Timber.i("onStartCommand ${intent}")
nextBatchDelay = 60_000L
timeout = 0
intent?.let {
if (cancelableTask == null) {
timer.cancel()
timer = Timer()
doSync(true)
} 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(once: Boolean = false) {
var nextBatch = syncTokenStore.getLastToken()
if (!networkConnectivityChecker.isConnected()) {
Timber.v("Sync is Paused. Waiting...")
//TODO Retry in ?
timer.schedule(object : TimerTask() {
override fun run() {
doSync()
}
}, 5_000L)
} else {
Timber.v("Execute sync request with token $nextBatch and timeout $timeout")
val params = SyncTask.Params(nextBatch, timeout)
cancelableTask = syncTask.configureWith(params)
.callbackOn(TaskThread.CALLER)
.executeOn(TaskThread.CALLER)
.dispatchTo(object : MatrixCallback<SyncResponse> {
override fun onSuccess(data: SyncResponse) {
cancelableTask = null
nextBatch = data.nextBatch
syncTokenStore.saveToken(nextBatch)
localBinder.notifySyncFinish()
if (!once) {
timer.schedule(object : TimerTask() {
override fun run() {
doSync()
}
}, nextBatchDelay)
} else {
//stop
stopMe()
}
}
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()
}
}, 5_000L)
}
if (failure !is Failure.NetworkConnection
|| failure.cause is JsonEncodingException) {
// Wait 10s before retrying
timer.schedule(object : TimerTask() {
override fun run() {
doSync()
}
}, 5_000L)
}
if (failure is Failure.ServerError
&& (failure.error.code == MatrixError.UNKNOWN_TOKEN || failure.error.code == MatrixError.MISSING_TOKEN)) {
// No token or invalid token, stop the thread
stopSelf()
}
}
})
.executeBy(taskExecutor)
}
}
override fun onBind(intent: Intent?): IBinder {
return localBinder
}
inner class LocalBinder : Binder() {
private var listeners = ArrayList<SyncListener>()
fun addListener(listener: SyncListener) {
if (!listeners.contains(listener)) {
listeners.add(listener)
}
}
fun removeListener(listener: SyncListener) {
listeners.remove(listener)
}
internal fun notifySyncFinish() {
listeners.forEach {
try {
it.onSyncFinsh()
} catch (t: Throwable) {
Timber.e("Failed to notify listener $it")
}
}
}
internal fun notifyNetworkNotAvailable() {
listeners.forEach {
try {
it.networkNotAvailable()
} catch (t: Throwable) {
Timber.e("Failed to notify listener $it")
}
}
}
internal fun notifyFailure(throwable: Throwable) {
listeners.forEach {
try {
it.onFailed(throwable)
} catch (t: Throwable) {
Timber.e("Failed to notify listener $it")
}
}
}
fun getService(): SyncService = this@SyncService
}
interface SyncListener {
fun onSyncFinsh()
fun networkNotAvailable()
fun onFailed(throwable: Throwable)
}
companion object {
fun startLongPool(delay: Long) {
}
}
}

View file

@ -0,0 +1,201 @@
package im.vector.matrix.android.internal.session.sync.job
import android.app.Service
import android.content.Intent
import android.os.Binder
import android.os.IBinder
import com.squareup.moshi.JsonEncodingException
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.di.MatrixKoinComponent
import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
import im.vector.matrix.android.internal.session.sync.SyncTask
import im.vector.matrix.android.internal.session.sync.SyncTokenStore
import im.vector.matrix.android.internal.session.sync.model.SyncResponse
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.TaskThread
import im.vector.matrix.android.internal.task.configureWith
import org.koin.standalone.inject
import timber.log.Timber
import java.net.SocketTimeoutException
import java.util.*
import kotlin.collections.ArrayList
private const val DEFAULT_LONG_POOL_TIMEOUT = 10_000L
private const val BACKGROUND_LONG_POOL_TIMEOUT = 0L
/**
* Can execute periodic sync task.
* An IntentService is used in conjunction with the AlarmManager and a Broadcast Receiver
* in order to be able to perform a sync even if the app is not running.
* The <receiver> and <service> must be declared in the Manifest or the app using the SDK
*/
open class SyncServiceOld : Service(), MatrixKoinComponent {
private var mIsSelfDestroyed: Boolean = false
private var cancelableTask: Cancelable? = null
private val syncTokenStore: SyncTokenStore by inject()
private val syncTask: SyncTask by inject()
private val networkConnectivityChecker: NetworkConnectivityChecker by inject()
private val taskExecutor: TaskExecutor by inject()
private var localBinder = LocalBinder()
val timer = Timer()
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Timber.i("onStartCommand ${intent}")
intent?.let {
if (cancelableTask == null) {
doSync(BACKGROUND_LONG_POOL_TIMEOUT)
} else {
//Already syncing ignore
Timber.i("Received a start while was already syncking... ignore")
}
}
//No intent just start the service, an alarm will should call with intent
return START_STICKY
}
override fun onDestroy() {
Timber.i("## onDestroy() : $this")
if (!mIsSelfDestroyed) {
Timber.w("## Destroy by the system : $this")
}
cancelableTask?.cancel()
super.onDestroy()
}
fun stopMe() {
cancelableTask?.cancel()
mIsSelfDestroyed = true
stopSelf()
}
fun doSync(currentLongPoolTimeoutMs: Long = DEFAULT_LONG_POOL_TIMEOUT) {
var nextBatch = syncTokenStore.getLastToken()
if (!networkConnectivityChecker.isConnected()) {
Timber.v("Sync is Paused. Waiting...")
//TODO Retry in ?
} else {
Timber.v("Execute sync request with token $nextBatch and timeout $currentLongPoolTimeoutMs")
val params = SyncTask.Params(nextBatch, currentLongPoolTimeoutMs)
cancelableTask = syncTask.configureWith(params)
.callbackOn(TaskThread.CALLER)
.executeOn(TaskThread.CALLER)
.dispatchTo(object : MatrixCallback<SyncResponse> {
override fun onSuccess(data: SyncResponse) {
cancelableTask = null
nextBatch = data.nextBatch
syncTokenStore.saveToken(nextBatch)
localBinder.notifySyncFinish()
}
override fun onFailure(failure: Throwable) {
Timber.e(failure)
cancelableTask = null
localBinder.notifyFailure(failure)
// if (failure is Failure.NetworkConnection
// && failure.cause is SocketTimeoutException) {
// // Timeout are not critical
// localBinder.notifyFailure()
// }
//
// if (failure !is Failure.NetworkConnection
// || failure.cause is JsonEncodingException) {
// // Wait 10s before retrying
//// Thread.sleep(RETRY_WAIT_TIME_MS)
// //TODO Retry in 10S?
// }
//
// if (failure is Failure.ServerError
// && (failure.error.code == MatrixError.UNKNOWN_TOKEN || failure.error.code == MatrixError.MISSING_TOKEN)) {
// // No token or invalid token, stop the thread
// stopSelf()
// }
}
})
.executeBy(taskExecutor)
//TODO return and schedule a new one?
// Timber.v("Waiting for $currentLongPoolDelayMs delay before new pool...")
// if (currentLongPoolDelayMs > 0) Thread.sleep(currentLongPoolDelayMs)
// Timber.v("...Continue")
}
}
override fun onBind(intent: Intent?): IBinder {
return localBinder
}
inner class LocalBinder : Binder() {
private var listeners = ArrayList<SyncListener>()
fun addListener(listener: SyncListener) {
if (!listeners.contains(listener)) {
listeners.add(listener)
}
}
fun removeListener(listener: SyncListener) {
listeners.remove(listener)
}
internal fun notifySyncFinish() {
listeners.forEach {
try {
it.onSyncFinsh()
} catch (t: Throwable) {
Timber.e("Failed to notify listener $it")
}
}
}
internal fun notifyNetworkNotAvailable() {
listeners.forEach {
try {
it.networkNotAvailable()
} catch (t: Throwable) {
Timber.e("Failed to notify listener $it")
}
}
}
internal fun notifyFailure(throwable: Throwable) {
listeners.forEach {
try {
it.onFailed(throwable)
} catch (t: Throwable) {
Timber.e("Failed to notify listener $it")
}
}
}
fun getService(): SyncServiceOld = this@SyncServiceOld
}
interface SyncListener {
fun onSyncFinsh()
fun networkNotAvailable()
fun onFailed(throwable: Throwable)
}
companion object {
fun startLongPool(delay: Long) {
}
}
}

View file

@ -37,6 +37,8 @@ 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 = 30_000L
private const val DEFAULT_LONG_POOL_DELAY = 0L
internal class SyncThread(private val syncTask: SyncTask,
private val networkConnectivityChecker: NetworkConnectivityChecker,
@ -91,14 +93,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 $DEFAULT_LONG_POOL_TIMEOUT")
val latch = CountDownLatch(1)
val params = SyncTask.Params(nextBatch)
val params = SyncTask.Params(nextBatch, DEFAULT_LONG_POOL_TIMEOUT)
cancelableTask = syncTask.configureWith(params)
.callbackOn(TaskThread.CALLER)
.executeOn(TaskThread.CALLER)
@ -135,10 +137,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 $DEFAULT_LONG_POOL_DELAY delay before new pool...")
if (DEFAULT_LONG_POOL_DELAY > 0) sleep(DEFAULT_LONG_POOL_DELAY)
Timber.v("...Continue")
}
}
Timber.v("Sync killed")
@ -166,7 +173,6 @@ internal class SyncThread(private val syncTask: SyncTask,
pause()
}
}

View file

@ -0,0 +1,121 @@
/*
* 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 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.util.concurrent.TimeUnit
private const val DEFAULT_LONG_POOL_TIMEOUT = 0L
internal class SyncWorker(context: Context,
workerParameters: WorkerParameters
) : CoroutineWorker(context, workerParameters), MatrixKoinComponent {
@JsonClass(generateAdapter = true)
internal data class Params(
val timeout: Long = DEFAULT_LONG_POOL_TIMEOUT,
val automaticallyRetry: Boolean = false
)
private val syncAPI by inject<SyncAPI>()
private val filterRepository by inject<FilterRepository>()
private val syncResponseHandler by inject<SyncResponseHandler>()
private val sessionParamsStore by inject<SessionParamsStore>()
private val syncTokenStore by inject<SyncTokenStore>()
override suspend fun doWork(): Result {
Timber.i("Sync work starting")
val params = WorkerParamsFactory.fromData<Params>(inputData)
?: Params()
val requestParams = HashMap<String, String>()
requestParams["timeout"] = params.timeout.toString()
requestParams["filter"] = filterRepository.getFilter()
val token = syncTokenStore.getLastToken()?.also { requestParams["since"] = it }
Timber.i("Sync work last token $token")
return executeRequest<SyncResponse> {
apiCall = syncAPI.sync(requestParams)
}.fold(
{
if (it is Failure.ServerError
&& it.error.code == MatrixError.UNKNOWN_TOKEN) {
sessionParamsStore.delete()
Result.failure()
} else {
Timber.i("Sync work failed $it")
Result.retry()
}
},
{
Timber.i("Sync work success next batch ${it.nextBatch}")
if (!isStopped) {
syncResponseHandler.handleResponse(it, token, false)
syncTokenStore.saveToken(it.nextBatch)
}
if (params.automaticallyRetry) Result.retry() else Result.success()
}
)
}
companion object {
fun requireBackgroundSync(serverTimeout: Long = 0) {
val data = WorkerParamsFactory.toData(Params(serverTimeout, false))
val workRequest = OneTimeWorkRequestBuilder<SyncWorker>()
.setInputData(data)
.setConstraints(Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build())
.setBackoffCriteria(BackoffPolicy.LINEAR, 1_000, TimeUnit.MILLISECONDS)
.build()
WorkManager.getInstance().enqueueUniqueWork("BG_SYNCP", ExistingWorkPolicy.REPLACE, workRequest)
}
fun automaticallyBackgroundSync(serverTimeout: Long = 0, delay: Long = 30_000) {
val data = WorkerParamsFactory.toData(Params(serverTimeout, true))
val workRequest = OneTimeWorkRequestBuilder<SyncWorker>()
.setInputData(data)
.setConstraints(Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build())
.setBackoffCriteria(BackoffPolicy.LINEAR, delay, TimeUnit.MILLISECONDS)
.build()
WorkManager.getInstance().enqueueUniqueWork("BG_SYNCP", ExistingWorkPolicy.REPLACE, workRequest)
}
fun stopAnyBackgroundSync() {
WorkManager.getInstance().cancelUniqueWork("BG_SYNCP")
}
}
}

View file

@ -0,0 +1,74 @@
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]"
}
],
"actions": [
"notify",
{
"set_tweak": "sound",
"value": "default"
},
{
"set_tweak": "highlight",
"value": false
}
]
}
""".trimIndent()
val pushRule = MoshiProvider.providesMoshi().adapter<PushRule>(PushRule::class.java).fromJson(rawPushRule)
Assert.assertNotNull("Should have parsed the rule", pushRule)
Assert.assertNotNull("Failed to parse actions", Action.mapFrom(pushRule!!))
val actions = Action.mapFrom(pushRule)
Assert.assertEquals(3, actions!!.size)
Assert.assertEquals("First action should be notify", Action.Type.NOTIFY, actions[0].type)
Assert.assertEquals("Second action should be tweak", Action.Type.SET_TWEAK, actions[1].type)
Assert.assertEquals("Second action tweak key should be sound", "sound", actions[1].tweak_action)
Assert.assertEquals("Second action should have default as stringValue", "default", actions[1].stringValue)
Assert.assertNull("Second action boolValue should be null", actions[1].boolValue)
Assert.assertEquals("Third action should be tweak", Action.Type.SET_TWEAK, actions[2].type)
Assert.assertEquals("Third action tweak key should be highlight", "highlight", actions[2].tweak_action)
Assert.assertEquals("Third action tweak param should be false", false, actions[2].boolValue)
Assert.assertNull("Third action stringValue should be null", actions[2].stringValue)
}
}

View file

@ -0,0 +1,294 @@
package im.vector.matrix.android.api.pushrules
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.toContent
import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.RoomService
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.util.Cancelable
import org.junit.Assert
import org.junit.Test
class PushrulesConditionTest {
@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))
}
@Test
fun test_roommember_condition() {
val conditionEqual3 = RoomMemberCountCondition("3")
val conditionEqual3Bis = RoomMemberCountCondition("==3")
val conditionLessThan3 = RoomMemberCountCondition("<3")
val session = MockRoomService()
Event(
type = "m.room.message",
eventId = "mx0",
content = MessageTextContent("m.text", "A").toContent(),
originServerTs = 0,
roomId = "2joined").also {
Assert.assertFalse("This room does not have 3 members", conditionEqual3.isSatisfied(it, session))
Assert.assertFalse("This room does not have 3 members", conditionEqual3Bis.isSatisfied(it, session))
Assert.assertTrue("This room has less than 3 members", conditionLessThan3.isSatisfied(it, session))
}
Event(
type = "m.room.message",
eventId = "mx0",
content = MessageTextContent("m.text", "A").toContent(),
originServerTs = 0,
roomId = "3joined").also {
Assert.assertTrue("This room has 3 members",conditionEqual3.isSatisfied(it, session))
Assert.assertTrue("This room has 3 members",conditionEqual3Bis.isSatisfied(it, session))
Assert.assertFalse("This room has more than 3 members",conditionLessThan3.isSatisfied(it, session))
}
}
class MockRoomService() : RoomService {
override fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback<String>) {
}
override fun getRoom(roomId: String): Room? {
return when (roomId) {
"2joined" -> MockRoom(roomId, 2)
"3joined" -> MockRoom(roomId, 3)
else -> null
}
}
override fun liveRoomSummaries(): LiveData<List<RoomSummary>> {
return MutableLiveData()
}
}
class MockRoom(override val roomId: String, val _numberOfJoinedMembers: Int) : Room {
override fun getNumberOfJoinedMembers(): Int {
return _numberOfJoinedMembers
}
override val liveRoomSummary: LiveData<RoomSummary>
get() = TODO("not implemented") //To change initializer of created properties use File | Settings | File Templates.
override val roomSummary: RoomSummary?
get() = TODO("not implemented") //To change initializer of created properties use File | Settings | File Templates.
override fun createTimeline(eventId: String?, allowedTypes: List<String>?): Timeline {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun getTimeLineEvent(eventId: String): TimelineEvent? {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun sendTextMessage(text: String, msgType: String, autoMarkdown: Boolean): Cancelable {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun sendFormattedTextMessage(text: String, formattedText: String): Cancelable {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun sendMedia(attachment: ContentAttachmentData): Cancelable {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun sendMedias(attachments: List<ContentAttachmentData>): Cancelable {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun redactEvent(event: Event, reason: String?): Cancelable {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun markAllAsRead(callback: MatrixCallback<Unit>) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun setReadReceipt(eventId: String, callback: MatrixCallback<Unit>) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun setReadMarker(fullyReadEventId: String, callback: MatrixCallback<Unit>) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun isEventRead(eventId: String): Boolean {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun loadRoomMembersIfNeeded(): Cancelable {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun getRoomMember(userId: String): RoomMember? {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun getRoomMemberIdsLive(): LiveData<List<String>> {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun invite(userId: String, callback: MatrixCallback<Unit>) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun join(callback: MatrixCallback<Unit>) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun leave(callback: MatrixCallback<Unit>) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun updateTopic(topic: String, callback: MatrixCallback<Unit>) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun sendReaction(reaction: String, targetEventId: String): Cancelable {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun undoReaction(reaction: String, targetEventId: String, myUserId: String) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun updateQuickReaction(reaction: String, oppositeReaction: String, targetEventId: String, myUserId: String) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun editTextMessage(targetEventId: String, newBodyText: String, newBodyAutoMarkdown: Boolean, compatibilityBodyText: String): Cancelable {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun replyToMessage(eventReplied: Event, replyText: String): Cancelable? {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun getEventSummaryLive(eventId: String): LiveData<List<EventAnnotationsSummary>> {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun isEncrypted(): Boolean {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun encryptionAlgorithm(): String? {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun shouldEncryptForInvitedMembers(): Boolean {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}
}

View file

@ -70,13 +70,13 @@ DO NOT COMMIT
layout_constraintRight_
layout_constraintLeft_
### Use Preference from v7 library (android.support.v7.preference.PreferenceScreen)
### Use Preference from androidx library (androidx.preference.PreferenceScreen)
<PreferenceScreen
### Use im.vector.preference.VectorSwitchPreference to support multiline of the title
### Use im.vector.riotredesign.core.preference.VectorSwitchPreference to support multiline of the title
<SwitchPreference
### Use im.vector.preference.VectorPreference to support multiline of the title
### Use im.vector.riotredesign.core.preference.VectorPreference to support multiline of the title
<Preference\n
### Will crash on API < 21. Use ?colorAccent instead

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

@ -0,0 +1,10 @@
#!/usr/bin/env bash
# Ref: https://developer.android.com/training/monitoring-device-state/doze-standby#testing_your_app_with_app_standby
echo "Standby OFF"
echo "adb shell dumpsys battery reset"
adb shell dumpsys battery reset
echo "adb shell am set-inactive im.vector.riotredesign false"
adb shell am set-inactive im.vector.riotredesign false

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

@ -0,0 +1,10 @@
#!/usr/bin/env bash
# Ref: https://developer.android.com/training/monitoring-device-state/doze-standby#testing_your_app_with_app_standby
echo "Standby ON"
echo "adb shell dumpsys battery unplug"
adb shell dumpsys battery unplug
echo "adb shell am set-inactive im.vector.riotredesign true"
adb shell am set-inactive im.vector.riotredesign true

View file

@ -0,0 +1,11 @@
#!/usr/bin/env bash
# Ref: https://developer.android.com/training/monitoring-device-state/doze-standby#testing_doze
echo "Exit doze mode"
echo "shell dumpsys deviceidle unforce"
adb shell dumpsys deviceidle unforce
echo "Reactivate device"
echo "shell dumpsys battery reset"
adb shell dumpsys battery reset

View file

@ -0,0 +1,7 @@
#!/usr/bin/env bash
# Ref: https://developer.android.com/training/monitoring-device-state/doze-standby#testing_doze
echo "Enable doze mode"
echo "adb shell dumpsys deviceidle force-idle"
adb shell dumpsys deviceidle force-idle

View file

@ -2,14 +2,24 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="im.vector.riotredesign">
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application>
<receiver android:name=".receiver.OnApplicationUpgradeReceiver">
<receiver android:name=".receiver.OnApplicationUpgradeOrRebootReceiver">
<intent-filter>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver
android:name=".core.services.AlarmSyncBroadcastReceiver"
android:enabled="true"
android:exported="false">
</receiver>
</application>
</manifest>

View file

@ -14,24 +14,24 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.push.fcm;
package im.vector.riotredesign.push.fcm
import android.app.Activity;
import android.content.Context;
import android.app.Activity
import android.content.Context
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import im.vector.riotredesign.core.pushers.PushersManager
public class FcmHelper {
object FcmHelper {
fun isPushSupported(): Boolean = false
/**
* Retrieves the FCM registration token.
*
* @return the FCM token or null if not received from FCM
*/
@Nullable
public static String getFcmToken(Context context) {
return null;
fun getFcmToken(context: Context): String? {
return null
}
/**
@ -40,8 +40,7 @@ public class FcmHelper {
* @param context android context
* @param token the token to store
*/
public static void storeFcmToken(@NonNull Context context,
@Nullable String token) {
fun storeFcmToken(context: Context, token: String?) {
// No op
}
@ -50,7 +49,7 @@ public class FcmHelper {
*
* @param activity the first launch Activity
*/
public static void ensureFcmTokenIsRetrieved(final Activity activity) {
fun ensureFcmTokenIsRetrieved(activity: Activity, pushersManager: PushersManager) {
// No op
}
}

View file

@ -16,12 +16,8 @@
package im.vector.riotredesign.push.fcm
import androidx.fragment.app.Fragment
import im.vector.fragments.troubleshoot.TestAccountSettings
import im.vector.matrix.android.api.session.Session
import im.vector.riotredesign.features.settings.troubleshoot.NotificationTroubleshootTestManager
import im.vector.riotredesign.features.settings.troubleshoot.TestBingRulesSettings
import im.vector.riotredesign.features.settings.troubleshoot.TestDeviceSettings
import im.vector.riotredesign.features.settings.troubleshoot.TestSystemSettings
import im.vector.riotredesign.features.settings.troubleshoot.*
import im.vector.riotredesign.push.fcm.troubleshoot.TestAutoStartBoot
import im.vector.riotredesign.push.fcm.troubleshoot.TestBackgroundRestrictions

View file

@ -1,5 +1,6 @@
/*
* Copyright 2018 New Vector Ltd
* 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.
@ -14,21 +15,18 @@
* limitations under the License.
*/
package im.vector.riotredesign.receiver;
package im.vector.riotredesign.receiver
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import im.vector.riotredesign.core.services.AlarmSyncBroadcastReceiver
import timber.log.Timber
import timber.log.Timber;
class OnApplicationUpgradeOrRebootReceiver : BroadcastReceiver() {
public class OnApplicationUpgradeReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Timber.v("## onReceive() : Application has been upgraded, restart event stream service.");
// Start Event stream
// TODO EventStreamServiceX.Companion.onApplicationUpgrade(context);
override fun onReceive(context: Context, intent: Intent) {
Timber.v("## onReceive() ${intent.action}")
AlarmSyncBroadcastReceiver.scheduleAlarm(context, 10)
}
}

View file

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

View file

@ -1,120 +0,0 @@
/*
* Copyright 2014 OpenMarket Ltd
* Copyright 2017 Vector Creations Ltd
* Copyright 2018 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.push.fcm;
import android.app.Activity;
import android.content.Context;
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 im.vector.riotredesign.R;
import timber.log.Timber;
/**
* This class store the FCM token in SharedPrefs and ensure this token is retrieved.
* It has an alter ego in the fdroid variant.
*/
public class FcmHelper {
private static final String LOG_TAG = FcmHelper.class.getSimpleName();
private static final String PREFS_KEY_FCM_TOKEN = "FCM_TOKEN";
/**
* Retrieves the FCM registration token.
*
* @return the FCM token or null if not received from FCM
*/
@Nullable
public static String getFcmToken(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getString(PREFS_KEY_FCM_TOKEN, null);
}
/**
* Store FCM token to the SharedPrefs
*
* @param context android context
* @param token the token to store
*/
public static void storeFcmToken(@NonNull Context context,
@Nullable String token) {
PreferenceManager.getDefaultSharedPreferences(context)
.edit()
.putString(PREFS_KEY_FCM_TOKEN, token)
.apply();
}
/**
* onNewToken may not be called on application upgrade, so ensure my shared pref is set
*
* @param activity the first launch Activity
*/
public static void ensureFcmTokenIsRetrieved(final Activity activity) {
if (TextUtils.isEmpty(getFcmToken(activity))) {
//vfe: according to firebase doc
//'app should always check the device for a compatible Google Play services APK before accessing Google Play services features'
if (checkPlayServices(activity)) {
try {
FirebaseInstanceId.getInstance().getInstanceId()
.addOnSuccessListener(activity, new OnSuccessListener<InstanceIdResult>() {
@Override
public void onSuccess(InstanceIdResult instanceIdResult) {
storeFcmToken(activity, instanceIdResult.getToken());
}
})
.addOnFailureListener(activity, new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed " + e.getMessage());
}
});
} catch (Throwable e) {
Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed " + e.getMessage());
}
} else {
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.");
}
}
}
/**
* Check the device to make sure it has the Google Play Services APK. If
* it doesn't, display a dialog that allows users to download the APK from
* the Google Play Store or enable it in the device's system settings.
*/
private static boolean checkPlayServices(Activity activity) {
GoogleApiAvailability apiAvailability = GoogleApiAvailability.getInstance();
int resultCode = apiAvailability.isGooglePlayServicesAvailable(activity);
if (resultCode != ConnectionResult.SUCCESS) {
return false;
}
return true;
}
}

View file

@ -0,0 +1,102 @@
/*
* Copyright 2014 OpenMarket Ltd
* Copyright 2017 Vector Creations Ltd
* Copyright 2018 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.push.fcm
import android.app.Activity
import android.content.Context
import android.preference.PreferenceManager
import android.widget.Toast
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
/**
* This class store the FCM token in SharedPrefs and ensure this token is retrieved.
* It has an alter ego in the fdroid variant.
*/
object FcmHelper {
private val PREFS_KEY_FCM_TOKEN = "FCM_TOKEN"
fun isPushSupported(): Boolean = true
/**
* Retrieves the FCM registration token.
*
* @return the FCM token or null if not received from FCM
*/
fun getFcmToken(context: Context): String? {
return PreferenceManager.getDefaultSharedPreferences(context).getString(PREFS_KEY_FCM_TOKEN, null)
}
/**
* Store FCM token to the SharedPrefs
* TODO Store in realm
*
* @param context android context
* @param token the token to store
*/
fun storeFcmToken(context: Context,
token: String?) {
PreferenceManager.getDefaultSharedPreferences(context)
.edit()
.putString(PREFS_KEY_FCM_TOKEN, token)
.apply()
}
/**
* onNewToken may not be called on application upgrade, so ensure my shared pref is set
*
* @param activity the first launch Activity
*/
fun ensureFcmTokenIsRetrieved(activity: Activity, pushersManager: PushersManager) {
// if (TextUtils.isEmpty(getFcmToken(activity))) {
//'app should always check the device for a compatible Google Play services APK before accessing Google Play services features'
if (checkPlayServices(activity)) {
try {
FirebaseInstanceId.getInstance().instanceId
.addOnSuccessListener(activity) { instanceIdResult ->
storeFcmToken(activity, instanceIdResult.token)
pushersManager.registerPusherWithFcmKey(instanceIdResult.token)
}
.addOnFailureListener(activity) { e -> Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed " + e.message) }
} catch (e: Throwable) {
Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed " + e.message)
}
} else {
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.")
}
}
/**
* Check the device to make sure it has the Google Play Services APK. If
* it doesn't, display a dialog that allows users to download the APK from
* the Google Play Store or enable it in the device's system settings.
*/
private fun checkPlayServices(activity: Activity): Boolean {
val apiAvailability = GoogleApiAvailability.getInstance()
val resultCode = apiAvailability.isGooglePlayServicesAvailable(activity)
return resultCode == ConnectionResult.SUCCESS
}
}

View file

@ -16,15 +16,11 @@
package im.vector.riotredesign.push.fcm
import androidx.fragment.app.Fragment
import im.vector.fragments.troubleshoot.TestAccountSettings
import im.vector.matrix.android.api.session.Session
import im.vector.riotredesign.features.settings.troubleshoot.*
import im.vector.riotredesign.push.fcm.troubleshoot.TestFirebaseToken
import im.vector.riotredesign.push.fcm.troubleshoot.TestPlayServices
import im.vector.riotredesign.push.fcm.troubleshoot.TestTokenRegistration
import im.vector.riotredesign.features.settings.troubleshoot.NotificationTroubleshootTestManager
import im.vector.riotredesign.features.settings.troubleshoot.TestBingRulesSettings
import im.vector.riotredesign.features.settings.troubleshoot.TestDeviceSettings
import im.vector.riotredesign.features.settings.troubleshoot.TestSystemSettings
class NotificationTroubleshootTestManagerFactory {

View file

@ -22,18 +22,23 @@ 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 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.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
import im.vector.riotredesign.features.notifications.NotificationDrawerManager
import im.vector.riotredesign.features.notifications.SimpleNotifiableEvent
import im.vector.riotredesign.features.settings.PreferencesManager
import org.koin.android.ext.android.inject
import timber.log.Timber
@ -42,12 +47,10 @@ import timber.log.Timber
*/
class VectorFirebaseMessagingService : FirebaseMessagingService() {
val notificationDrawerManager by inject<NotificationDrawerManager>()
private val notifiableEventResolver by lazy {
NotifiableEventResolver(this)
}
private val notificationDrawerManager by inject<NotificationDrawerManager>()
private val pusherManager by inject<PushersManager>()
private val notifiableEventResolver by inject<NotifiableEventResolver>()
// UI handler
private val mUIHandler by lazy {
Handler(Looper.getMainLooper())
@ -59,26 +62,27 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
* @param message the message
*/
override fun onMessageReceived(message: RemoteMessage?) {
if (!PreferencesManager.areNotificationEnabledForDevice(applicationContext)) {
Timber.i("Notification are disabled for this device")
return
}
if (message == null || message.data == null) {
Timber.e("## onMessageReceived() : received a null message or message with no data")
return
}
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
Timber.i("## onMessageReceived()" + message.data.toString())
Timber.i("## onMessageReceived() from FCM with priority " + message.priority)
Timber.i("## onMessageReceived() %s", message.data.toString())
Timber.i("## onMessageReceived() from FCM with priority %s", 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,24 @@ 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 {
if (PreferencesManager.areNotificationEnabledForDevice(applicationContext)) {
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,55 +122,58 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
* @param data Data map containing message data as key/value pairs.
* For Set of keys use data.keySet().
*/
private fun onMessageReceivedInternal(data: Map<String, String> /*, pushManager: PushManager*/) {
private fun onMessageReceivedInternal(data: Map<String, String>) {
try {
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
Timber.i("## onMessageReceivedInternal() : $data")
}
val eventId = data["event_id"]
val roomId = data["room_id"]
if (eventId == null || roomId == null) {
Timber.e("## onMessageReceivedInternal() missing eventId and/or roomId")
return
}
// update the badge counter
val unreadCount = data.get("unread")?.let { Integer.parseInt(it) } ?: 0
BadgeProxy.updateBadgeCount(applicationContext, unreadCount)
/* TODO
val session = Matrix.getInstance(applicationContext)?.defaultSession
val session = safeGetCurrentSession()
if (VectorApp.isAppInBackground() && !pushManager.isBackgroundSyncAllowed) {
//Notification contains metadata and maybe data information
handleNotificationWithoutSyncingMode(data, session)
if (session == null) {
Timber.w("## Can't sync from push, no current session")
} else {
// Safe guard... (race?)
if (isEventAlreadyKnown(data["event_id"], data["room_id"])) return
//Catch up!!
EventStreamServiceX.onPushReceived(this)
if (isEventAlreadyKnown(eventId, roomId)) {
Timber.i("Ignoring push, event already knwown")
} else {
Timber.v("Requesting background sync")
session.requireBackgroundSync()
}
}
*/
} 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)
Timber.e(e, "## isEventAlreadyKnown() : failed to check if the event was already defined")
}
}
@ -187,48 +209,44 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
isPushGatewayEvent = true
)
notificationDrawerManager.onNotifiableEventReceived(simpleNotifiableEvent)
notificationDrawerManager.refreshNotificationDrawer(null)
notificationDrawerManager.refreshNotificationDrawer()
return
} else {
val event = parseEvent(data)
if (event?.roomId == null) {
//unsupported event
Timber.e("Received an event with no room id")
return
val event = parseEvent(data) ?: return
val notifiableEvent = notifiableEventResolver.resolveEvent(event, session)
if (notifiableEvent == null) {
Timber.e("Unsupported notifiable event ${eventId}")
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
Timber.e("--> ${event}")
}
} else {
var notifiableEvent = notifiableEventResolver.resolveEvent(event, null, null /* TODO session.fulfillRule(event) */, session)
if (notifiableEvent == null) {
Timber.e("Unsupported notifiable event ${eventId}")
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
Timber.e("--> ${event}")
if (notifiableEvent is NotifiableMessageEvent) {
if (TextUtils.isEmpty(notifiableEvent.senderName)) {
notifiableEvent.senderName = data["sender_display_name"]
?: data["sender"] ?: ""
}
} else {
if (notifiableEvent is NotifiableMessageEvent) {
if (TextUtils.isEmpty(notifiableEvent.senderName)) {
notifiableEvent.senderName = data["sender_display_name"] ?: data["sender"] ?: ""
}
if (TextUtils.isEmpty(notifiableEvent.roomName)) {
notifiableEvent.roomName = findRoomNameBestEffort(data, session) ?: ""
}
if (TextUtils.isEmpty(notifiableEvent.roomName)) {
notifiableEvent.roomName = findRoomNameBestEffort(data, session) ?: ""
}
notifiableEvent.isPushGatewayEvent = true
notifiableEvent.matrixID = session.sessionParams.credentials.userId
notificationDrawerManager.onNotifiableEventReceived(notifiableEvent)
notificationDrawerManager.refreshNotificationDrawer(null)
}
notifiableEvent.isPushGatewayEvent = true
notifiableEvent.matrixID = session.sessionParams.credentials.userId
notificationDrawerManager.onNotifiableEventReceived(notifiableEvent)
notificationDrawerManager.refreshNotificationDrawer()
}
}
}
private fun findRoomNameBestEffort(data: Map<String, String>, session: Session?): String? {
var roomName: String? = data["room_name"]
val roomName: String? = data["room_name"]
val roomId = data["room_id"]
if (null == roomName && null != roomId) {
// Try to get the room name from our store
@ -263,7 +281,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
// TODO content = data.getValue("content"),
originServerTs = System.currentTimeMillis())
} catch (e: Exception) {
Timber.e(e, "buildEvent fails " + e.localizedMessage)
Timber.e(e, "buildEvent fails ")
}
return null

View file

@ -3,7 +3,6 @@
xmlns:tools="http://schemas.android.com/tools"
package="im.vector.riotredesign">
<uses-permission android:name="android.permission.INTERNET" />
<application
@ -32,7 +31,6 @@
</intent-filter>
</activity-alias>
<activity android:name=".features.home.HomeActivity" />
<activity android:name=".features.login.LoginActivity" />
<activity android:name=".features.media.ImageMediaViewerActivity" />
@ -58,17 +56,37 @@
android:label="@string/encryption_message_recovery" />
<activity
android:name="im.vector.riotredesign.features.reactions.EmojiReactionPickerActivity"
android:name=".features.reactions.EmojiReactionPickerActivity"
android:label="@string/title_activity_emoji_reaction_picker" />
<activity android:name=".features.roomdirectory.RoomDirectoryActivity" />
<activity android:name=".features.roomdirectory.roompreview.RoomPreviewActivity" />
<activity android:name=".features.home.room.detail.RoomDetailActivity" />
<activity android:name=".features.debug.DebugMenuActivity" />
<!-- Services -->
<service
android:name=".core.services.CallService"
android:exported="false" />
<!--<service-->
<!--android:name="im.vector.matrix.android.internal.session.sync.job.SyncService"-->
<!--android:exported="false" />-->
<service
android:name=".core.services.VectorSyncService"
android:exported="false">
</service>
<!-- Receivers -->
<!-- Exported false, should only be accessible from this app!! -->
<receiver
android:name=".features.notifications.NotificationBroadcastReceiver"
android:enabled="true"
android:exported="false" />
<!-- Providers -->
<provider
android:name="androidx.core.content.FileProvider"
@ -79,7 +97,6 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/riotx_provider_paths" />
</provider>
</application>
</manifest>

View file

@ -23,6 +23,10 @@ import android.os.Handler
import android.os.HandlerThread
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 com.airbnb.epoxy.EpoxyAsyncUtil
import com.airbnb.epoxy.EpoxyController
@ -32,14 +36,21 @@ import com.github.piasy.biv.loader.glide.GlideImageLoader
import com.jakewharton.threetenabp.AndroidThreeTen
import im.vector.matrix.android.api.Matrix
import im.vector.riotredesign.core.di.AppModule
import im.vector.riotredesign.core.services.AlarmSyncBroadcastReceiver
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.NotificationDrawerManager
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.settings.PreferencesManager
import im.vector.riotredesign.features.version.getVersion
import im.vector.riotredesign.push.fcm.FcmHelper
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
@ -47,15 +58,20 @@ import timber.log.Timber
import java.text.SimpleDateFormat
import java.util.*
class VectorApplication : Application() {
lateinit var appContext: Context
//font thread handler
private var mFontThreadHandler: Handler? = null
val vectorConfiguration: VectorConfiguration by inject()
private val notificationDrawerManager by inject<NotificationDrawerManager>()
// var slowMode = false
override fun onCreate() {
super.onCreate()
appContext = this
@ -91,10 +107,51 @@ class VectorApplication : Application() {
R.array.com_google_android_gms_fonts_certs
)
// val efp = koin.koinContext.get<EmojiCompatFontProvider>()
FontsContractCompat.requestFont(this, fontRequest, koin.koinContext.get<EmojiCompatFontProvider>(), getFontThreadHandler())
vectorConfiguration.initConfiguration()
NotificationUtils.createNotificationChannels(applicationContext)
ProcessLifecycleOwner.get().lifecycle.addObserver(object : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun entersForeground() {
AlarmSyncBroadcastReceiver.cancelAlarm(appContext)
Matrix.getInstance().currentSession?.also {
it.stopAnyBackgroundSync()
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
fun entersBackground() {
Timber.i("App entered background") // call persistInfo
notificationDrawerManager.persistInfo()
if (FcmHelper.isPushSupported()) {
//TODO FCM fallback
} else {
//TODO check if notifications are enabled for this device
//We need to use alarm in this mode
if (PreferencesManager.areNotificationEnabledForDevice(applicationContext)) {
if (Matrix.getInstance().currentSession != null) {
AlarmSyncBroadcastReceiver.scheduleAlarm(applicationContext, 4_000L)
Timber.i("Alarm scheduled to restart service")
}
}
}
}
})
Matrix.getInstance().currentSession?.let {
it.refreshPushers()
//bind to the sync service
get<PushRuleTriggerListener>().startWithSession(it)
it.fetchPushRules()
}
}
private fun logInfo() {

View file

@ -21,6 +21,8 @@ import android.content.Context.MODE_PRIVATE
import im.vector.matrix.android.api.Matrix
import im.vector.riotredesign.EmojiCompatFontProvider
import im.vector.riotredesign.core.error.ErrorFormatter
import im.vector.riotredesign.core.pushers.PushersManager
import im.vector.riotredesign.core.resources.AppNameProvider
import im.vector.riotredesign.core.resources.LocaleProvider
import im.vector.riotredesign.core.resources.StringArrayProvider
import im.vector.riotredesign.core.resources.StringProvider
@ -33,7 +35,10 @@ 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.OutdatedEventDetector
import im.vector.riotredesign.features.notifications.PushRuleTriggerListener
import org.koin.dsl.module.module
class AppModule(private val context: Context) {
@ -81,7 +86,19 @@ class AppModule(private val context: Context) {
}
single {
NotificationDrawerManager(context)
PushRuleTriggerListener(get(), get())
}
single {
OutdatedEventDetector()
}
single {
NotificationDrawerManager(context, get())
}
single {
NotifiableEventResolver(get(), get())
}
factory {
@ -103,5 +120,13 @@ class AppModule(private val context: Context) {
single {
EmojiCompatFontProvider()
}
single {
AppNameProvider(context)
}
single {
PushersManager(get(), get(), get(), get())
}
}
}

View file

@ -28,6 +28,8 @@ abstract class VectorPreferenceFragment : PreferenceFragmentCompat() {
activity as VectorBaseActivity
}
abstract var titleRes: Int
/* ==========================================================================================
* Life cycle
* ========================================================================================== */
@ -36,6 +38,7 @@ abstract class VectorPreferenceFragment : PreferenceFragmentCompat() {
override fun onResume() {
super.onResume()
(activity as? VectorBaseActivity)?.supportActionBar?.setTitle(titleRes)
Timber.v("onResume Fragment ${this.javaClass.simpleName}")
}

View file

@ -25,7 +25,7 @@ import android.widget.TextView
import androidx.preference.PreferenceViewHolder
import im.vector.riotredesign.R
// TODO Replace by real Bingrule class
// TODO Replace by real Bingrule class, then delete
class BingRule(rule: BingRule) {
fun shouldNotNotify() = false
fun shouldNotify() = false

View file

@ -39,7 +39,8 @@ open class UserAvatarPreference : Preference {
init {
widgetLayoutResource = R.layout.vector_settings_round_avatar
isIconSpaceReserved = false
// Set to false to remove the space when there is no icon
isIconSpaceReserved = true
}
override fun onBindViewHolder(holder: PreferenceViewHolder) {

View file

@ -37,7 +37,8 @@ class VectorEditTextPreference : EditTextPreference {
init {
dialogLayoutResource = R.layout.dialog_preference_edit_text
isIconSpaceReserved = false
// Set to false to remove the space when there is no icon
isIconSpaceReserved = true
}
// No single line for title

View file

@ -54,7 +54,8 @@ class VectorListPreference : ListPreference {
init {
widgetLayoutResource = R.layout.vector_settings_list_preference_with_warning
isIconSpaceReserved = false
// Set to false to remove the space when there is no icon
isIconSpaceReserved = true
}
override fun onBindViewHolder(holder: PreferenceViewHolder) {

View file

@ -75,7 +75,8 @@ open class VectorPreference : Preference {
constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle)
init {
isIconSpaceReserved = false
// Set to false to remove the space when there is no icon
isIconSpaceReserved = true
}
var isHighlighted = false
@ -156,8 +157,4 @@ open class VectorPreference : Preference {
}
}
}
companion object {
private val LOG_TAG = VectorPreference::class.java.simpleName
}
}

View file

@ -36,7 +36,8 @@ class VectorPreferenceCategory : PreferenceCategory {
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
init {
isIconSpaceReserved = false
// Set to false to remove the space when there is no icon
isIconSpaceReserved = true
}
override fun onBindViewHolder(holder: PreferenceViewHolder) {
@ -47,6 +48,8 @@ class VectorPreferenceCategory : PreferenceCategory {
titleTextView?.setTypeface(null, Typeface.BOLD)
// "isIconSpaceReserved = false" does not work for preference category, so remove the padding
(titleTextView?.parent as? ViewGroup)?.setPadding(0, 0, 0, 0)
if (!isIconSpaceReserved) {
(titleTextView?.parent as? ViewGroup)?.setPadding(0, 0, 0, 0)
}
}
}

View file

@ -37,7 +37,8 @@ class VectorSwitchPreference : SwitchPreference {
constructor(context: Context) : super(context)
init {
isIconSpaceReserved = false
// Set to false to remove the space when there is no icon
isIconSpaceReserved = true
}
override fun onBindViewHolder(holder: PreferenceViewHolder) {

View file

@ -0,0 +1,38 @@
package im.vector.riotredesign.core.pushers
import im.vector.matrix.android.api.MatrixCallback
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
)
}
fun unregisterPusher(pushKey: String, callback: MatrixCallback<Unit>) {
currentSession.removeHttpPusher(pushKey, stringProvider.getString(R.string.pusher_app_id),callback)
}
}

View file

@ -0,0 +1,26 @@
package im.vector.riotredesign.core.resources
import android.content.Context
import timber.log.Timber
class AppNameProvider(private val context: Context) {
fun getAppName(): String {
try {
val appPackageName = context.applicationContext.packageName
val pm = context.packageManager
val appInfo = pm.getApplicationInfo(appPackageName, 0)
var appName = pm.getApplicationLabel(appInfo).toString()
// Use appPackageName instead of appName if appName contains any non-ASCII character
if (!appName.matches("\\A\\p{ASCII}*\\z".toRegex())) {
appName = appPackageName
}
return appName
} catch (e: Exception) {
Timber.e(e, "## AppNameProvider() : failed " + e.message)
return "RiotXAndroid"
}
}
}

View file

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

View file

@ -0,0 +1,73 @@
package im.vector.riotredesign.core.services
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.PowerManager
import androidx.core.content.ContextCompat
import timber.log.Timber
class AlarmSyncBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
//Aquire a lock to give enough time for the sync :/
(context.getSystemService(Context.POWER_SERVICE) as PowerManager).run {
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "riotx:fdroidSynclock").apply {
acquire((10_000).toLong())
}
}
// 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 (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
ContextCompat.startForegroundService(context, intent)
} else {
context.startService(intent)
}
} catch (ex: Throwable) {
//TODO
Timber.e(ex)
}
}
scheduleAlarm(context, 30_000L)
Timber.i("Alarm scheduled to restart service")
}
companion object {
const val REQUEST_CODE = 0
fun scheduleAlarm(context: Context, delay: Long) {
//Reschedule
val intent = Intent(context, AlarmSyncBroadcastReceiver::class.java)
val pIntent = PendingIntent.getBroadcast(context, AlarmSyncBroadcastReceiver.REQUEST_CODE,
intent, PendingIntent.FLAG_UPDATE_CURRENT)
val firstMillis = System.currentTimeMillis() + delay
val alarmMgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmMgr.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, firstMillis, pIntent)
} else {
alarmMgr.set(AlarmManager.RTC_WAKEUP, firstMillis, pIntent)
}
}
fun cancelAlarm(context: Context) {
val intent = Intent(context, AlarmSyncBroadcastReceiver::class.java)
val pIntent = PendingIntent.getBroadcast(context, AlarmSyncBroadcastReceiver.REQUEST_CODE,
intent, PendingIntent.FLAG_UPDATE_CURRENT)
val alarmMgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmMgr.cancel(pIntent)
}
}
}

View file

@ -1,582 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.core.services
import android.content.Context
import android.content.Intent
import androidx.core.content.ContextCompat
import androidx.work.Constraints
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.riotredesign.R
import im.vector.riotredesign.features.notifications.NotifiableEventResolver
import im.vector.riotredesign.features.notifications.NotificationUtils
import org.koin.android.ext.android.inject
import timber.log.Timber
import java.util.concurrent.TimeUnit
/**
* A service in charge of controlling whether the event stream is running or not.
*
* It manages messages notifications displayed to the end user.
*/
class EventStreamServiceX : VectorService() {
/**
* Managed session (no multi session for Riot)
*/
private val mSession by inject<Session>()
/**
* Set to true to simulate a push immediately when service is destroyed
*/
private var mSimulatePushImmediate = false
/**
* The current state.
*/
private var serviceState = ServiceState.INIT
set(newServiceState) {
Timber.i("setServiceState from $field to $newServiceState")
field = newServiceState
}
/**
* Push manager
*/
// TODO private var mPushManager: PushManager? = null
private var mNotifiableEventResolver: NotifiableEventResolver? = null
/**
* Live events listener
*/
/* TODO
private val mEventsListener = object : MXEventListener() {
override fun onBingEvent(event: Event, roomState: RoomState, bingRule: BingRule) {
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
Timber.i("%%%%%%%% MXEventListener: the event $event")
}
Timber.i("prepareNotification : " + event.eventId + " in " + roomState.roomId)
val session = Matrix.getMXSession(applicationContext, event.matrixId)
// invalid session ?
// should never happen.
// But it could be triggered because of multi accounts management.
// The dedicated account is removing but some pushes are still received.
if (null == session || !session.isAlive) {
Timber.i("prepareNotification : don't bing - no session")
return
}
if (EventType.CALL_INVITE == event.getClearType()) {
handleCallInviteEvent(event)
return
}
val notifiableEvent = mNotifiableEventResolver!!.resolveEvent(event, roomState, bingRule, session)
if (notifiableEvent != null) {
VectorApp.getInstance().notificationDrawerManager.onNotifiableEventReceived(notifiableEvent)
}
}
override fun onLiveEventsChunkProcessed(fromToken: String, toToken: String) {
Timber.i("%%%%%%%% MXEventListener: onLiveEventsChunkProcessed[$fromToken->$toToken]")
VectorApp.getInstance().notificationDrawerManager.refreshNotificationDrawer(OutdatedEventDetector(this@EventStreamServiceX))
// do not suspend the application if there is some active calls
if (ServiceState.CATCHUP == serviceState) {
val hasActiveCalls = session?.mCallsManager?.hasActiveCalls() == true
// if there are some active calls, the catchup should not be stopped.
// because an user could answer to a call from another device.
// there will no push because it is his own message.
// so, the client has no choice to catchup until the ring is shutdown
if (hasActiveCalls) {
Timber.i("onLiveEventsChunkProcessed : Catchup again because there are active calls")
catchup(false)
} else if (ServiceState.CATCHUP == serviceState) {
Timber.i("onLiveEventsChunkProcessed : no Active call")
CallsManager.getSharedInstance().checkDeadCalls()
stop()
}
}
}
} */
/**
* Service internal state
*/
private enum class ServiceState {
// Initial state
INIT,
// Service is started for a Catchup. Once the catchup is finished the service will be stopped
CATCHUP,
// Service is started, and session is monitored
STARTED
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// Cancel any previous worker
cancelAnySimulatedPushSchedule()
// no intent : restarted by Android
if (null == intent) {
// Cannot happen anymore
Timber.e("onStartCommand : null intent")
myStopSelf()
return START_NOT_STICKY
}
val action = intent.action
Timber.i("onStartCommand with action : $action (current state $serviceState)")
// Manage foreground notification
when (action) {
ACTION_BOOT_COMPLETE,
ACTION_APPLICATION_UPGRADE,
ACTION_SIMULATED_PUSH_RECEIVED -> {
// Display foreground notification
Timber.i("startForeground")
val notification = NotificationUtils.buildForegroundServiceNotification(this, R.string.notification_sync_in_progress)
startForeground(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE, notification)
}
ACTION_GO_TO_FOREGROUND -> {
// Stop foreground notification display
Timber.i("stopForeground")
stopForeground(true)
}
}
if (null == mSession) {
Timber.e("onStartCommand : no sessions")
myStopSelf()
return START_NOT_STICKY
}
when (action) {
ACTION_START,
ACTION_GO_TO_FOREGROUND ->
when (serviceState) {
ServiceState.INIT ->
start(false)
ServiceState.CATCHUP ->
// A push has been received before, just change state, to avoid stopping the service when catchup is over
serviceState = ServiceState.STARTED
ServiceState.STARTED -> {
// Nothing to do
}
}
ACTION_STOP,
ACTION_GO_TO_BACKGROUND,
ACTION_LOGOUT ->
stop()
ACTION_PUSH_RECEIVED,
ACTION_SIMULATED_PUSH_RECEIVED ->
when (serviceState) {
ServiceState.INIT ->
start(true)
ServiceState.CATCHUP ->
catchup(true)
ServiceState.STARTED ->
// Nothing to do
Unit
}
ACTION_PUSH_UPDATE -> pushStatusUpdate()
ACTION_BOOT_COMPLETE -> {
// No FCM only
mSimulatePushImmediate = true
stop()
}
ACTION_APPLICATION_UPGRADE -> {
// FDroid only
catchup(true)
}
else -> {
// Should not happen
}
}
// We don't want the service to be restarted automatically by the System
return START_NOT_STICKY
}
override fun onDestroy() {
super.onDestroy()
// Schedule worker?
scheduleSimulatedPushIfNeeded()
}
/**
* Tell the WorkManager to cancel any schedule of push simulation
*/
private fun cancelAnySimulatedPushSchedule() {
WorkManager.getInstance().cancelAllWorkByTag(PUSH_SIMULATOR_REQUEST_TAG)
}
/**
* Configure the WorkManager to schedule a simulated push, if necessary
*/
private fun scheduleSimulatedPushIfNeeded() {
if (shouldISimulatePush()) {
val delay = if (mSimulatePushImmediate) 0 else 60_000 // TODO mPushManager?.backgroundSyncDelay ?: let { 60_000 }
Timber.i("## service is schedule to restart in $delay millis, if network is connected")
val pushSimulatorRequest = OneTimeWorkRequestBuilder<PushSimulatorWorker>()
.setInitialDelay(delay.toLong(), TimeUnit.MILLISECONDS)
.setConstraints(Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build())
.addTag(PUSH_SIMULATOR_REQUEST_TAG)
.build()
WorkManager.getInstance().let {
// Cancel any previous worker
it.cancelAllWorkByTag(PUSH_SIMULATOR_REQUEST_TAG)
it.enqueue(pushSimulatorRequest)
}
}
}
/**
* Start the even stream.
*
* @param session the session
*/
private fun startEventStream(session: Session) {
/* TODO
// resume if it was only suspended
if (null != session.currentSyncToken) {
session.resumeEventStream()
} else {
session.startEventStream(store?.eventStreamToken)
}
*/
}
/**
* Monitor the provided session.
*
* @param session the session
*/
private fun monitorSession(session: Session) {
/* TODO
session.dataHandler.addListener(mEventsListener)
CallsManager.getSharedInstance().addSession(session)
val store = session.dataHandler.store
// the store is ready (no data loading in progress...)
if (store!!.isReady) {
startEventStream(session, store)
} else {
// wait that the store is ready before starting the events stream
store.addMXStoreListener(object : MXStoreListener() {
override fun onStoreReady(accountId: String) {
startEventStream(session, store)
store.removeMXStoreListener(this)
}
override fun onStoreCorrupted(accountId: String, description: String) {
// start a new initial sync
if (null == store.eventStreamToken) {
startEventStream(session, store)
} else {
// the data are out of sync
Matrix.getInstance(applicationContext)!!.reloadSessions(applicationContext)
}
store.removeMXStoreListener(this)
}
override fun onStoreOOM(accountId: String, description: String) {
val uiHandler = Handler(mainLooper)
uiHandler.post {
Toast.makeText(applicationContext, "$accountId : $description", Toast.LENGTH_LONG).show()
Matrix.getInstance(applicationContext)!!.reloadSessions(applicationContext)
}
}
})
store.open()
}
*/
}
/**
* internal start.
*/
private fun start(forPush: Boolean) {
val applicationContext = applicationContext
// TODO mPushManager = Matrix.getInstance(applicationContext)!!.pushManager
mNotifiableEventResolver = NotifiableEventResolver(applicationContext)
monitorSession(mSession!!)
serviceState = if (forPush) {
ServiceState.CATCHUP
} else {
ServiceState.STARTED
}
}
/**
* internal stop.
*/
private fun stop() {
Timber.i("## stop(): the service is stopped")
/* TODO
if (null != session && session!!.isAlive) {
session!!.stopEventStream()
session!!.dataHandler.removeListener(mEventsListener)
CallsManager.getSharedInstance().removeSession(session)
}
session = null
*/
// Stop the service
myStopSelf()
}
/**
* internal catchup method.
*
* @param checkState true to check if the current state allow to perform a catchup
*/
private fun catchup(checkState: Boolean) {
var canCatchup = true
if (!checkState) {
Timber.i("catchup without checking serviceState ")
} else {
Timber.i("catchup with serviceState " + serviceState + " CurrentActivity ") // TODO + VectorApp.getCurrentActivity())
/* TODO
// the catchup should only be done
// 1- the serviceState is in catchup : the event stream might have gone to sleep between two catchups
// 2- the thread is suspended
// 3- the application has been launched by a push so there is no displayed activity
canCatchup = (serviceState == ServiceState.CATCHUP
//|| (serviceState == ServiceState.PAUSE)
|| ServiceState.STARTED == serviceState && null == VectorApp.getCurrentActivity())
*/
}
if (canCatchup) {
if (mSession != null) {
// TODO session!!.catchupEventStream()
} else {
Timber.i("catchup no session")
}
serviceState = ServiceState.CATCHUP
} else {
Timber.i("No catchup is triggered because there is already a running event thread")
}
}
/**
* The push status has been updated (i.e disabled or enabled).
* TODO Useless now?
*/
private fun pushStatusUpdate() {
Timber.i("## pushStatusUpdate")
}
/* ==========================================================================================
* Push simulator
* ========================================================================================== */
/**
* @return true if the FCM is disable or not setup, user allowed background sync, user wants notification
*/
private fun shouldISimulatePush(): Boolean {
return false
/* TODO
if (Matrix.getInstance(applicationContext)?.defaultSession == null) {
Timber.i("## shouldISimulatePush: NO: no session")
return false
}
mPushManager?.let { pushManager ->
if (pushManager.useFcm()
&& !TextUtils.isEmpty(pushManager.currentRegistrationToken)
&& pushManager.isServerRegistered) {
// FCM is ok
Timber.i("## shouldISimulatePush: NO: FCM is up")
return false
}
if (!pushManager.isBackgroundSyncAllowed) {
// User has disabled background sync
Timber.i("## shouldISimulatePush: NO: background sync not allowed")
return false
}
if (!pushManager.areDeviceNotificationsAllowed()) {
// User does not want notifications
Timber.i("## shouldISimulatePush: NO: user does not want notification")
return false
}
}
// Lets simulate push
Timber.i("## shouldISimulatePush: YES")
return true
*/
}
//================================================================================
// Call management
//================================================================================
private fun handleCallInviteEvent(event: Event) {
/*
TODO
val session = Matrix.getMXSession(applicationContext, event.matrixId)
// invalid session ?
// should never happen.
// But it could be triggered because of multi accounts management.
// The dedicated account is removing but some pushes are still received.
if (null == session || !session.isAlive) {
Timber.v("prepareCallNotification : don't bing - no session")
return
}
val room: Room? = session.dataHandler.getRoom(event.roomId)
// invalid room ?
if (null == room) {
Timber.i("prepareCallNotification : don't bing - the room does not exist")
return
}
var callId: String? = null
var isVideo = false
try {
callId = event.contentAsJsonObject?.get("call_id")?.asString
// Check if it is a video call
val offer = event.contentAsJsonObject?.get("offer")?.asJsonObject
val sdp = offer?.get("sdp")
val sdpValue = sdp?.asString
isVideo = sdpValue?.contains("m=video") == true
} catch (e: Exception) {
Timber.e(e, "prepareNotification : getContentAsJsonObject")
}
if (!TextUtils.isEmpty(callId)) {
CallService.onIncomingCall(this,
isVideo,
room.getRoomDisplayName(this),
room.roomId,
session.myUserId!!,
callId!!)
}
*/
}
companion object {
private const val PUSH_SIMULATOR_REQUEST_TAG = "PUSH_SIMULATOR_REQUEST_TAG"
private const val ACTION_START = "im.vector.riotredesign.core.services.EventStreamServiceX.START"
private const val ACTION_LOGOUT = "im.vector.riotredesign.core.services.EventStreamServiceX.LOGOUT"
private const val ACTION_GO_TO_FOREGROUND = "im.vector.riotredesign.core.services.EventStreamServiceX.GO_TO_FOREGROUND"
private const val ACTION_GO_TO_BACKGROUND = "im.vector.riotredesign.core.services.EventStreamServiceX.GO_TO_BACKGROUND"
private const val ACTION_PUSH_UPDATE = "im.vector.riotredesign.core.services.EventStreamServiceX.PUSH_UPDATE"
private const val ACTION_PUSH_RECEIVED = "im.vector.riotredesign.core.services.EventStreamServiceX.PUSH_RECEIVED"
private const val ACTION_SIMULATED_PUSH_RECEIVED = "im.vector.riotredesign.core.services.EventStreamServiceX.SIMULATED_PUSH_RECEIVED"
private const val ACTION_STOP = "im.vector.riotredesign.core.services.EventStreamServiceX.STOP"
private const val ACTION_BOOT_COMPLETE = "im.vector.riotredesign.core.services.EventStreamServiceX.BOOT_COMPLETE"
private const val ACTION_APPLICATION_UPGRADE = "im.vector.riotredesign.core.services.EventStreamServiceX.APPLICATION_UPGRADE"
/* ==========================================================================================
* Events sent to the service
* ========================================================================================== */
fun onApplicationStarted(context: Context) {
sendAction(context, ACTION_START)
}
fun onLogout(context: Context) {
sendAction(context, ACTION_LOGOUT)
}
fun onAppGoingToForeground(context: Context) {
sendAction(context, ACTION_GO_TO_FOREGROUND)
}
fun onAppGoingToBackground(context: Context) {
sendAction(context, ACTION_GO_TO_BACKGROUND)
}
fun onPushUpdate(context: Context) {
sendAction(context, ACTION_PUSH_UPDATE)
}
fun onPushReceived(context: Context) {
sendAction(context, ACTION_PUSH_RECEIVED)
}
fun onSimulatedPushReceived(context: Context) {
sendAction(context, ACTION_SIMULATED_PUSH_RECEIVED, true)
}
fun onApplicationStopped(context: Context) {
sendAction(context, ACTION_STOP)
}
fun onBootComplete(context: Context) {
sendAction(context, ACTION_BOOT_COMPLETE, true)
}
fun onApplicationUpgrade(context: Context) {
sendAction(context, ACTION_APPLICATION_UPGRADE, true)
}
private fun sendAction(context: Context, action: String, foreground: Boolean = false) {
Timber.i("sendAction $action")
val intent = Intent(context, EventStreamServiceX::class.java)
intent.action = action
if (foreground) {
ContextCompat.startForegroundService(context, intent)
} else {
context.startService(intent)
}
}
}
}

View file

@ -1,36 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.core.services
import android.content.Context
import androidx.work.Worker
import androidx.work.WorkerParameters
/**
* This class simulate push event when FCM is not working/disabled
*/
class PushSimulatorWorker(val context: Context,
workerParams: WorkerParameters) : Worker(context, workerParams) {
override fun doWork(): Result {
// Simulate a Push
EventStreamServiceX.onSimulatedPushReceived(context)
// Indicate whether the task finished successfully with the Result
return Result.success()
}
}

View file

@ -0,0 +1,72 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.core.services
import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.os.Build
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
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 (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notification = NotificationUtils.buildForegroundServiceNotification(applicationContext, R.string.notification_listening_for_events, false)
startForeground(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE, notification)
}
return super.onStartCommand(intent, flags, startId)
}
/**
* If the service is bounded and the service was previously started we can remove foreground notif
*/
override fun onBind(intent: Intent?): IBinder {
Timber.v("VectorSyncService - onBind ")
stopForeground(true)
return super.onBind(intent)
}
override fun onUnbind(intent: Intent?): Boolean {
Timber.v("VectorSyncService - onUnbind ")
return super.onUnbind(intent)
}
}

View file

@ -0,0 +1,62 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.core.ui.list
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
import im.vector.riotredesign.core.extensions.setTextOrHide
/**
* A generic list item.
* Displays an item with a title, and optional description.
* Can display an accessory on the right, that can be an image or an indeterminate progress.
* If provided with an action, will display a button at the bottom of the list item.
*/
@EpoxyModelClass(layout = R.layout.item_generic_footer)
abstract class GenericFooterItem : VectorEpoxyModel<GenericFooterItem.Holder>() {
@EpoxyAttribute
var text: String? = null
@EpoxyAttribute
var style: GenericItem.STYLE = GenericItem.STYLE.NORMAL_TEXT
@EpoxyAttribute
var itemClickAction: GenericItem.Action? = null
override fun bind(holder: Holder) {
holder.text.setTextOrHide(text)
when (style) {
GenericItem.STYLE.BIG_TEXT -> holder.text.textSize = 18f
GenericItem.STYLE.NORMAL_TEXT -> holder.text.textSize = 14f
}
holder.view.setOnClickListener {
itemClickAction?.perform?.run()
}
}
class Holder : VectorEpoxyHolder() {
val text by bind<TextView>(R.id.itemGenericFooterText)
}
}

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