Basic FCM vs fdroid mode

This commit is contained in:
Valere 2019-06-20 15:22:40 +02:00 committed by Benoit Marty
parent 0e46fc4c0a
commit 2e417a9143
30 changed files with 663 additions and 1245 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`). 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

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



View File

@ -60,35 +60,31 @@ interface Session :
@MainThread @MainThread
fun open() fun open()


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


/** /**
* Configures the sync long pooling options * Launches infinite periodic background syncs
* @param timoutMS The maximum time to wait, in milliseconds, before returning the sync request. * THis does not work in doze mode :/
* If no events (or other data) become available before this time elapses, the server will return a response with empty fields. * If battery optimization is on it can work in app standby but that's all :/
* If set to 0 the server will return immediately even if the response is empty.
* @param delayMs When the server responds to a sync request, the client waits for `longPoolDelay` before calling a new sync.
*/ */
// fun configureSyncLongPooling(timoutMS : Long, delayMs : Long ) fun startAutomaticBackgroundSync(repeatDelay: Long = 30_000L)


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

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


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

View File

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


fun isEventRead(eventId: String): Boolean
} }

View File

@ -73,6 +73,7 @@ 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.signout.SignOutModule
import im.vector.matrix.android.internal.session.sync.SyncModule 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.SyncThread
import im.vector.matrix.android.internal.session.sync.job.SyncWorker
import im.vector.matrix.android.internal.session.user.UserModule import im.vector.matrix.android.internal.session.user.UserModule
import org.koin.core.scope.Scope import org.koin.core.scope.Scope
import org.koin.standalone.inject import org.koin.standalone.inject
@ -136,45 +137,34 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi
bingRuleWatcher.start() bingRuleWatcher.start()
} }


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


// override fun resumeSync() { override fun startAutomaticBackgroundSync(repeatDelay: Long) {
// assert(isOpen) SyncWorker.automaticallyBackgroundSync(0, repeatDelay)
// syncThread.restart() }
// }
//
// override fun pauseSync() {
// assert(isOpen)
// syncThread.pause()
// }


// override fun configureSyncLongPooling(timoutMS: Long, delayMs: Long) { override fun stopAnyBackgroundSync() {
// syncThread.configureLongPoolingSettings(timoutMS, delayMs) SyncWorker.stopAnyBackgroundSync()
// } }
//
// @MainThread @MainThread
// override fun stopSync() { override fun startSync() {
// assert(isOpen) assert(isOpen)
// syncThread.kill() if (!syncThread.isAlive) {
// } syncThread.start()
} else {
syncThread.restart()
Timber.w("Attempt to start an already started thread")
}
}

@MainThread
override fun stopSync() {
assert(isOpen)
syncThread.kill()
}


@MainThread @MainThread
override fun close() { override fun close() {

View File

@ -17,6 +17,7 @@
package im.vector.matrix.android.internal.session.room package im.vector.matrix.android.internal.session.room


import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService 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 cryptoService: CryptoService,
private val findReactionEventForUndoTask: FindReactionEventForUndoTask, private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
private val joinRoomTask: JoinRoomTask, private val joinRoomTask: JoinRoomTask,
private val leaveRoomTask: LeaveRoomTask) { private val leaveRoomTask: LeaveRoomTask,
private val sessionParams: SessionParams) {


fun instantiate(roomId: String): Room { fun instantiate(roomId: String): Room {
val roomMemberExtractor = SenderRoomMemberExtractor(roomId) val roomMemberExtractor = SenderRoomMemberExtractor(roomId)
@ -61,7 +63,7 @@ internal class RoomFactory(private val monarchy: Monarchy,
val sendService = DefaultSendService(roomId, eventFactory, cryptoService, monarchy) val sendService = DefaultSendService(roomId, eventFactory, cryptoService, monarchy)
val stateService = DefaultStateService(roomId, taskExecutor, sendStateTask) val stateService = DefaultStateService(roomId, taskExecutor, sendStateTask)
val roomMembersService = DefaultMembershipService(roomId, monarchy, taskExecutor, loadRoomMembersTask, inviteTask, joinRoomTask, leaveRoomTask) 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) val reactionService = DefaultRelationService(roomId, eventFactory, findReactionEventForUndoTask, monarchy, taskExecutor)


return DefaultRoom( return DefaultRoom(

View File

@ -80,7 +80,7 @@ class RoomModule {
} }


scope(DefaultSession.SCOPE) { 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) { scope(DefaultSession.SCOPE) {

View File

@ -18,9 +18,15 @@ package im.vector.matrix.android.internal.session.room.read


import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback 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.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.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.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.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.fetchCopied 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, internal class DefaultReadService(private val roomId: String,
private val monarchy: Monarchy, private val monarchy: Monarchy,
private val taskExecutor: TaskExecutor, private val taskExecutor: TaskExecutor,
private val setReadMarkersTask: SetReadMarkersTask) : ReadService { private val setReadMarkersTask: SetReadMarkersTask,
private val sessionParams: SessionParams) : ReadService {


override fun markAllAsRead(callback: MatrixCallback<Unit>) { override fun markAllAsRead(callback: MatrixCallback<Unit>) {
val latestEvent = getLatestEvent() val latestEvent = getLatestEvent()
@ -50,5 +57,20 @@ internal class DefaultReadService(private val roomId: String,
return monarchy.fetchCopied { EventEntity.latestEvent(it, roomId) } 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.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent 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.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.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.database.query.where
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.util.fetchCopyMap import im.vector.matrix.android.internal.util.fetchCopyMap
@ -45,4 +49,5 @@ internal class DefaultTimelineService(private val roomId: String,
}) })
} }



} }

View File

@ -72,7 +72,7 @@ open class SyncService : Service(), MatrixKoinComponent {
if (cancelableTask == null) { if (cancelableTask == null) {
timer.cancel() timer.cancel()
timer = Timer() timer = Timer()
doSync() doSync(true)
} else { } else {
//Already syncing ignore //Already syncing ignore
Timber.i("Received a start while was already syncking... ignore") Timber.i("Received a start while was already syncking... ignore")
@ -101,7 +101,7 @@ open class SyncService : Service(), MatrixKoinComponent {
stopSelf() stopSelf()
} }


fun doSync() { fun doSync(once: Boolean = false) {
var nextBatch = syncTokenStore.getLastToken() var nextBatch = syncTokenStore.getLastToken()
if (!networkConnectivityChecker.isConnected()) { if (!networkConnectivityChecker.isConnected()) {
Timber.v("Sync is Paused. Waiting...") Timber.v("Sync is Paused. Waiting...")
@ -110,7 +110,7 @@ open class SyncService : Service(), MatrixKoinComponent {
override fun run() { override fun run() {
doSync() doSync()
} }
}, 10_000L) }, 5_000L)
} else { } else {
Timber.v("Execute sync request with token $nextBatch and timeout $timeout") Timber.v("Execute sync request with token $nextBatch and timeout $timeout")
val params = SyncTask.Params(nextBatch, timeout) val params = SyncTask.Params(nextBatch, timeout)
@ -123,11 +123,16 @@ open class SyncService : Service(), MatrixKoinComponent {
nextBatch = data.nextBatch nextBatch = data.nextBatch
syncTokenStore.saveToken(nextBatch) syncTokenStore.saveToken(nextBatch)
localBinder.notifySyncFinish() localBinder.notifySyncFinish()
if (!once) {
timer.schedule(object : TimerTask() { timer.schedule(object : TimerTask() {
override fun run() { override fun run() {
doSync() doSync()
} }
}, nextBatchDelay) }, nextBatchDelay)
} else {
//stop
stopMe()
}
} }


override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
@ -141,7 +146,7 @@ open class SyncService : Service(), MatrixKoinComponent {
override fun run() { override fun run() {
doSync() doSync()
} }
}, 10_000L) }, 5_000L)
} }


if (failure !is Failure.NetworkConnection if (failure !is Failure.NetworkConnection
@ -151,7 +156,7 @@ open class SyncService : Service(), MatrixKoinComponent {
override fun run() { override fun run() {
doSync() doSync()
} }
}, 10_000L) }, 5_000L)
} }


if (failure is Failure.ServerError if (failure is Failure.ServerError

View File

@ -40,11 +40,6 @@ private const val RETRY_WAIT_TIME_MS = 10_000L
private const val DEFAULT_LONG_POOL_TIMEOUT = 10_000L private const val DEFAULT_LONG_POOL_TIMEOUT = 10_000L
private const val DEFAULT_LONG_POOL_DELAY = 0L private const val DEFAULT_LONG_POOL_DELAY = 0L



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


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


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


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


private var currentLongPoolTimeoutMs = longPoolTimeoutMs
private var currentLongPoolDelayMs = longPoolDelayMs


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


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

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


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

fun pause() = synchronized(lock) { fun pause() = synchronized(lock) {
if (state is SyncState.RUNNING) { if (state is SyncState.RUNNING) {
Timber.v("Pause sync...") Timber.v("Pause sync...")
@ -148,9 +98,9 @@ internal class SyncThread(private val syncTask: SyncTask,
lock.wait() lock.wait()
} }
} else { } else {
Timber.v("Execute sync request with token $nextBatch and timeout $currentLongPoolTimeoutMs") Timber.v("Execute sync request with token $nextBatch and timeout $DEFAULT_LONG_POOL_TIMEOUT")
val latch = CountDownLatch(1) val latch = CountDownLatch(1)
val params = SyncTask.Params(nextBatch, currentLongPoolTimeoutMs) val params = SyncTask.Params(nextBatch, DEFAULT_LONG_POOL_TIMEOUT)
cancelableTask = syncTask.configureWith(params) cancelableTask = syncTask.configureWith(params)
.callbackOn(TaskThread.CALLER) .callbackOn(TaskThread.CALLER)
.executeOn(TaskThread.CALLER) .executeOn(TaskThread.CALLER)
@ -193,8 +143,8 @@ internal class SyncThread(private val syncTask: SyncTask,
updateStateTo(SyncState.RUNNING(catchingUp = false)) updateStateTo(SyncState.RUNNING(catchingUp = false))
} }


Timber.v("Waiting for $currentLongPoolDelayMs delay before new pool...") Timber.v("Waiting for $DEFAULT_LONG_POOL_DELAY delay before new pool...")
if (currentLongPoolDelayMs > 0) sleep(currentLongPoolDelayMs) if (DEFAULT_LONG_POOL_DELAY > 0) sleep(DEFAULT_LONG_POOL_DELAY)
Timber.v("...Continue") Timber.v("...Continue")
} }
} }
@ -216,20 +166,11 @@ internal class SyncThread(private val syncTask: SyncTask,
} }


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


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


} }

View File

@ -17,8 +17,6 @@ package im.vector.matrix.android.internal.session.sync.job


import android.content.Context import android.content.Context
import androidx.work.* import androidx.work.*
import arrow.core.failure
import arrow.core.recoverWith
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError import im.vector.matrix.android.api.failure.MatrixError
@ -33,19 +31,19 @@ import im.vector.matrix.android.internal.session.sync.model.SyncResponse
import im.vector.matrix.android.internal.util.WorkerParamsFactory import im.vector.matrix.android.internal.util.WorkerParamsFactory
import org.koin.standalone.inject import org.koin.standalone.inject
import timber.log.Timber import timber.log.Timber
import java.net.SocketTimeoutException
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit




private const val DEFAULT_LONG_POOL_TIMEOUT = 0L private const val DEFAULT_LONG_POOL_TIMEOUT = 0L


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


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


private val syncAPI by inject<SyncAPI>() private val syncAPI by inject<SyncAPI>()
@ -54,7 +52,6 @@ class SyncWorker(context: Context,
private val sessionParamsStore by inject<SessionParamsStore>() private val sessionParamsStore by inject<SessionParamsStore>()
private val syncTokenStore by inject<SyncTokenStore>() private val syncTokenStore by inject<SyncTokenStore>()


val autoMode = false


override suspend fun doWork(): Result { override suspend fun doWork(): Result {
Timber.i("Sync work starting") Timber.i("Sync work starting")
@ -69,51 +66,56 @@ class SyncWorker(context: Context,


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

} }


fun again() { companion object {
if (autoMode) { fun requireBackgroundSync(serverTimeout: Long = 0) {
Timber.i("Sync work Again!!") val data = WorkerParamsFactory.toData(Params(serverTimeout, false))
val workRequest = OneTimeWorkRequestBuilder<SyncWorker>() val workRequest = OneTimeWorkRequestBuilder<SyncWorker>()
.setInitialDelay(30_000, TimeUnit.MILLISECONDS) .setInputData(data)
.setConstraints(Constraints.Builder() .setConstraints(Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED) .setRequiredNetworkType(NetworkType.CONNECTED)
.build()) .build())
.setBackoffCriteria(BackoffPolicy.LINEAR, 10_000, TimeUnit.MILLISECONDS) .setBackoffCriteria(BackoffPolicy.LINEAR, 1_000, TimeUnit.MILLISECONDS)
.build() .build()
WorkManager.getInstance().enqueueUniqueWork("BG_SYNCP", ExistingWorkPolicy.APPEND, workRequest) 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

@ -2,14 +2,24 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="im.vector.riotredesign"> package="im.vector.riotredesign">


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

<application> <application>


<receiver android:name=".receiver.OnApplicationUpgradeReceiver"> <receiver android:name=".receiver.OnApplicationUpgradeOrRebootReceiver">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" /> <action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter> </intent-filter>
</receiver> </receiver>


<receiver
android:name=".core.services.AlarmSyncBroadcastReceiver"
android:enabled="true"
android:exported="false">
</receiver>

</application> </application>


</manifest> </manifest>

View File

@ -14,24 +14,24 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package im.vector.riotredesign.push.fcm; package im.vector.riotredesign.push.fcm


import android.app.Activity; import android.app.Activity
import android.content.Context; import android.content.Context


import androidx.annotation.NonNull; import im.vector.riotredesign.core.pushers.PushersManager
import androidx.annotation.Nullable;


public class FcmHelper { object FcmHelper {

fun isPushSupported(): Boolean = false


/** /**
* Retrieves the FCM registration token. * Retrieves the FCM registration token.
* *
* @return the FCM token or null if not received from FCM * @return the FCM token or null if not received from FCM
*/ */
@Nullable fun getFcmToken(context: Context): String? {
public static String getFcmToken(Context context) { return null
return null;
} }


/** /**
@ -40,8 +40,7 @@ public class FcmHelper {
* @param context android context * @param context android context
* @param token the token to store * @param token the token to store
*/ */
public static void storeFcmToken(@NonNull Context context, fun storeFcmToken(context: Context, token: String?) {
@Nullable String token) {
// No op // No op
} }


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

View File

@ -1,5 +1,6 @@
/* /*
* Copyright 2018 New Vector Ltd * Copyright 2018 New Vector Ltd
* Copyright 2019 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,21 +15,18 @@
* limitations under the License. * limitations under the License.
*/ */


package im.vector.riotredesign.receiver; package im.vector.riotredesign.receiver


import android.content.BroadcastReceiver; import android.content.BroadcastReceiver
import android.content.Context; import android.content.Context
import android.content.Intent; 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 fun onReceive(context: Context, intent: Intent) {

Timber.v("## onReceive() ${intent.action}")
@Override AlarmSyncBroadcastReceiver.scheduleAlarm(context, 10)
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);
} }
} }

View File

@ -1,113 +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 androidx.annotation.NonNull;
import androidx.annotation.Nullable;

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

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

/**
* 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, PushersManager pushersManager) {
// 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, instanceIdResult -> {
storeFcmToken(activity, instanceIdResult.getToken());
pushersManager.registerPusherWithFcmKey(instanceIdResult.getToken());
})
.addOnFailureListener(activity, 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,103 @@
/*
* 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 LOG_TAG = FcmHelper::class.java.simpleName

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
*
* @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

@ -144,14 +144,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
Timber.i("Ignoring push, event already knwown") Timber.i("Ignoring push, event already knwown")
} else { } else {
Timber.v("Requesting background sync") Timber.v("Requesting background sync")
val workRequest = OneTimeWorkRequestBuilder<SyncWorker>() session.requireBackgroundSync(0L)
.setInputData(Data.Builder().put("timeout", 0L).build())
.setConstraints(Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build())
.setBackoffCriteria(BackoffPolicy.LINEAR, 10_000, TimeUnit.MILLISECONDS)
.build()
WorkManager.getInstance().enqueueUniqueWork("BG_SYNCP", ExistingWorkPolicy.REPLACE, workRequest)
} }
} }


@ -214,7 +207,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
isPushGatewayEvent = true isPushGatewayEvent = true
) )
notificationDrawerManager.onNotifiableEventReceived(simpleNotifiableEvent) notificationDrawerManager.onNotifiableEventReceived(simpleNotifiableEvent)
notificationDrawerManager.refreshNotificationDrawer(null) notificationDrawerManager.refreshNotificationDrawer()


return return
} else { } else {
@ -249,7 +242,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
notifiableEvent.isPushGatewayEvent = true notifiableEvent.isPushGatewayEvent = true
notifiableEvent.matrixID = session.sessionParams.credentials.userId notifiableEvent.matrixID = session.sessionParams.credentials.userId
notificationDrawerManager.onNotifiableEventReceived(notifiableEvent) notificationDrawerManager.onNotifiableEventReceived(notifiableEvent)
notificationDrawerManager.refreshNotificationDrawer(null) notificationDrawerManager.refreshNotificationDrawer()
} }
} }
} }

View File

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

</receiver>


<activity <activity
android:name=".features.MainActivity" android:name=".features.MainActivity"

View File

@ -34,7 +34,6 @@ import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent import androidx.lifecycle.OnLifecycleEvent
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import androidx.multidex.MultiDex import androidx.multidex.MultiDex
import androidx.work.*
import com.airbnb.epoxy.EpoxyAsyncUtil import com.airbnb.epoxy.EpoxyAsyncUtil
import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyController
import com.facebook.stetho.Stetho import com.facebook.stetho.Stetho
@ -43,9 +42,8 @@ import com.github.piasy.biv.loader.glide.GlideImageLoader
import com.jakewharton.threetenabp.AndroidThreeTen import com.jakewharton.threetenabp.AndroidThreeTen
import im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.internal.session.sync.job.SyncService import im.vector.matrix.android.internal.session.sync.job.SyncService
import im.vector.matrix.android.internal.session.sync.job.SyncWorker
import im.vector.riotredesign.core.di.AppModule import im.vector.riotredesign.core.di.AppModule
import im.vector.riotredesign.core.services.RestartBroadcastReceiver import im.vector.riotredesign.core.services.AlarmSyncBroadcastReceiver
import im.vector.riotredesign.core.services.VectorSyncService import im.vector.riotredesign.core.services.VectorSyncService
import im.vector.riotredesign.features.configuration.VectorConfiguration import im.vector.riotredesign.features.configuration.VectorConfiguration
import im.vector.riotredesign.features.crypto.keysbackup.KeysBackupModule import im.vector.riotredesign.features.crypto.keysbackup.KeysBackupModule
@ -57,6 +55,7 @@ import im.vector.riotredesign.features.rageshake.VectorFileLogger
import im.vector.riotredesign.features.rageshake.VectorUncaughtExceptionHandler import im.vector.riotredesign.features.rageshake.VectorUncaughtExceptionHandler
import im.vector.riotredesign.features.roomdirectory.RoomDirectoryModule import im.vector.riotredesign.features.roomdirectory.RoomDirectoryModule
import im.vector.riotredesign.features.version.getVersion 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.get
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.koin.log.EmptyLogger import org.koin.log.EmptyLogger
@ -66,7 +65,7 @@ import java.text.SimpleDateFormat
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit


class VectorApplication : Application(), SyncService.SyncListener { class VectorApplication : Application() {




lateinit var appContext: Context lateinit var appContext: Context
@ -75,26 +74,6 @@ class VectorApplication : Application(), SyncService.SyncListener {


val vectorConfiguration: VectorConfiguration by inject() val vectorConfiguration: VectorConfiguration by inject()


private var mBinder: SyncService.LocalBinder? = null

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

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

}

// var slowMode = false // var slowMode = false




@ -141,92 +120,26 @@ class VectorApplication : Application(), SyncService.SyncListener {


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


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

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

} }
} }


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


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

//TODO check if notifications are enabled for this device
// NO PUSH, and don't care about battery //We need to use alarm in this mode
// unbindService(connection) AlarmSyncBroadcastReceiver.scheduleAlarm(applicationContext,4_000L)
// mBinder?.getService()?.stopMe()// kill also
// mBinder = null
//In this case we will keep a permanent

//TODO if no push schedule reccuring alarm

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

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

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

} }
} }


@ -273,36 +186,4 @@ class VectorApplication : Application(), SyncService.SyncListener {
return mFontThreadHandler!! return mFontThreadHandler!!
} }


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

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

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

}

} }

View File

@ -37,6 +37,7 @@ import im.vector.riotredesign.features.navigation.DefaultNavigator
import im.vector.riotredesign.features.navigation.Navigator import im.vector.riotredesign.features.navigation.Navigator
import im.vector.riotredesign.features.notifications.NotifiableEventResolver import im.vector.riotredesign.features.notifications.NotifiableEventResolver
import im.vector.riotredesign.features.notifications.NotificationDrawerManager import im.vector.riotredesign.features.notifications.NotificationDrawerManager
import im.vector.riotredesign.features.notifications.OutdatedEventDetector
import im.vector.riotredesign.features.notifications.PushRuleTriggerListener import im.vector.riotredesign.features.notifications.PushRuleTriggerListener
import org.koin.dsl.module.module import org.koin.dsl.module.module


@ -89,7 +90,11 @@ class AppModule(private val context: Context) {
} }


single { single {
NotificationDrawerManager(context) OutdatedEventDetector(context)
}

single {
NotificationDrawerManager(context, get())
} }


single { single {

View File

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

View File

@ -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

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

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

class RestartBroadcastReceiver : BroadcastReceiver() {

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

companion object {
const val REQUEST_CODE = 0
}
}

View File

@ -76,9 +76,7 @@ class LoginActivity : VectorBaseActivity() {
Matrix.getInstance().currentSession = data Matrix.getInstance().currentSession = data
data.open() data.open()
data.setFilter(FilterService.FilterPreset.RiotFilter) data.setFilter(FilterService.FilterPreset.RiotFilter)
//TODO sync data.startSync()
// data.shoudPauseOnBackground(false)
// data.startSync()
get<PushRuleTriggerListener>().startWithSession(data) get<PushRuleTriggerListener>().startWithSession(data)
goToHome() goToHome()
} }

View File

@ -36,7 +36,7 @@ import java.io.FileOutputStream
* organise them in order to display them in the notification drawer. * organise them in order to display them in the notification drawer.
* Events can be grouped into the same notification, old (already read) events can be removed to do some cleaning. * Events can be grouped into the same notification, old (already read) events can be removed to do some cleaning.
*/ */
class NotificationDrawerManager(val context: Context) { class NotificationDrawerManager(val context: Context, private val outdatedDetector: OutdatedEventDetector?) {


//The first time the notification drawer is refreshed, we force re-render of all notifications //The first time the notification drawer is refreshed, we force re-render of all notifications
private var firstTime = true private var firstTime = true
@ -53,7 +53,7 @@ class NotificationDrawerManager(val context: Context) {
object : IconLoader.IconLoaderListener { object : IconLoader.IconLoaderListener {
override fun onIconsLoaded() { override fun onIconsLoaded() {
// Force refresh // Force refresh
refreshNotificationDrawer(null) refreshNotificationDrawer()
} }
}) })


@ -123,7 +123,7 @@ class NotificationDrawerManager(val context: Context) {
synchronized(eventList) { synchronized(eventList) {
eventList.clear() eventList.clear()
} }
refreshNotificationDrawer(null) refreshNotificationDrawer()
} }


/** Clear all known message events for this room and refresh the notification drawer */ /** Clear all known message events for this room and refresh the notification drawer */
@ -139,7 +139,7 @@ class NotificationDrawerManager(val context: Context) {
} }
NotificationUtils.cancelNotificationMessage(context, roomId, ROOM_MESSAGES_NOTIFICATION_ID) NotificationUtils.cancelNotificationMessage(context, roomId, ROOM_MESSAGES_NOTIFICATION_ID)
} }
refreshNotificationDrawer(null) refreshNotificationDrawer()
} }


/** /**
@ -177,7 +177,7 @@ class NotificationDrawerManager(val context: Context) {
} }




fun refreshNotificationDrawer(outdatedDetector: OutdatedEventDetector?) { fun refreshNotificationDrawer() {
if (myUserDisplayName.isBlank()) { if (myUserDisplayName.isBlank()) {
// TODO // TODO
// initWithSession(Matrix.getInstance(context).defaultSession) // initWithSession(Matrix.getInstance(context).defaultSession)

View File

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


import android.content.Context import android.content.Context
import im.vector.matrix.android.api.Matrix


class OutdatedEventDetector(val context: Context) { class OutdatedEventDetector(val context: Context) {


@ -28,20 +29,9 @@ class OutdatedEventDetector(val context: Context) {
if (notifiableEvent is NotifiableMessageEvent) { if (notifiableEvent is NotifiableMessageEvent) {
val eventID = notifiableEvent.eventId val eventID = notifiableEvent.eventId
val roomID = notifiableEvent.roomId val roomID = notifiableEvent.roomId
/* val session = Matrix.getInstance().currentSession ?: return false
TODO val room = session.getRoom(roomID) ?: return false
Matrix.getMXSession(context.applicationContext, notifiableEvent.matrixID)?.let { session -> return room.isEventRead(eventID)
//find the room
if (session.isAlive) {
session.dataHandler.getRoom(roomID)?.let { room ->
if (room.isEventRead(eventID)) {
Timber.v("Notifiable Event $eventID is read, and should be removed")
return true
}
}
}
}
*/
} }
return false return false
} }

View File

@ -26,7 +26,7 @@ class PushRuleTriggerListener(
} }


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


fun startWithSession(session: Session) { fun startWithSession(session: Session) {
@ -41,6 +41,6 @@ class PushRuleTriggerListener(
session?.removePushRuleListener(this) session?.removePushRuleListener(this)
session = null session = null
drawerManager.clearAllEvents() drawerManager.clearAllEvents()
drawerManager.refreshNotificationDrawer(null) drawerManager.refreshNotificationDrawer()
} }
} }