diff --git a/docs/notifications.md b/docs/notifications.md new file mode 100644 index 0000000000..404f9aac94 --- /dev/null +++ b/docs/notifications.md @@ -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 Google’s 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. + + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/Matrix.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/Matrix.kt index 87f0621303..d6665f7fa6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/Matrix.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/Matrix.kt @@ -61,9 +61,7 @@ class Matrix private constructor(context: Context) : MatrixKoinComponent { currentSession = it it.open() it.setFilter(FilterService.FilterPreset.RiotFilter) - //TODO check if using push or not (should pause if we use push) -// it.shoudPauseOnBackground(false) -// it.startSync() + it.startSync() } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt index 1ad26c2daf..ac41267870 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt @@ -60,35 +60,31 @@ interface Session : @MainThread fun open() -// /** -// * This method start the sync thread. -// */ -// @MainThread -// fun startSync() -// -// -// fun isSyncThreadAlice() : Boolean -// fun syncThreadState() : String -// -//// fun pauseSync() -//// fun resumeSync() -// -// fun shoudPauseOnBackground(shouldPause: Boolean) + /** + * Requires a one time background sync + */ + fun requireBackgroundSync() /** - * Configures the sync long pooling options - * @param timoutMS The maximum time to wait, in milliseconds, before returning the sync request. - * If no events (or other data) become available before this time elapses, the server will return a response with empty fields. - * If set to 0 the server will return immediately even if the response is empty. - * @param delayMs When the server responds to a sync request, the client waits for `longPoolDelay` before calling a new sync. + * Launches infinite periodic background syncs + * THis does not work in doze mode :/ + * If battery optimization is on it can work in app standby but that's all :/ */ -// fun configureSyncLongPooling(timoutMS : Long, delayMs : Long ) + fun startAutomaticBackgroundSync(repeatDelay: Long = 30_000L) -// /** -// * This method stop the sync thread. -// */ -// @MainThread -// fun stopSync() + fun stopAnyBackgroundSync() + + /** + * This method start the sync thread. + */ + @MainThread + fun startSync() + + /** + * This method stop the sync thread. + */ + @MainThread + fun stopSync() /** * This method allows to listen the sync state. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/ReadService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/ReadService.kt index ed67c1db4a..ab406b8e54 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/ReadService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/ReadService.kt @@ -38,4 +38,5 @@ interface ReadService { */ fun setReadMarker(fullyReadEventId: String, callback: MatrixCallback) + fun isEventRead(eventId: String): Boolean } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt index 20dc4cf79b..cef7e27581 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt @@ -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.sync.SyncModule import im.vector.matrix.android.internal.session.sync.job.SyncThread +import im.vector.matrix.android.internal.session.sync.job.SyncWorker import im.vector.matrix.android.internal.session.user.UserModule import org.koin.core.scope.Scope import org.koin.standalone.inject @@ -136,45 +137,34 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi bingRuleWatcher.start() } -// @MainThread -// override fun startSync() { -// assert(isOpen) -// if (!syncThread.isAlive) { -// syncThread.start() -// } else { -// syncThread.restart() -// Timber.w("Attempt to start an already started thread") -// } -// } -// -// override fun isSyncThreadAlice(): Boolean = syncThread.isAlive -// -// override fun syncThreadState(): String = syncThread.getSyncState() -// -// override fun shoudPauseOnBackground(shouldPause: Boolean) { -// //TODO check if using push or not (should pause if we use push) -// syncThread.shouldPauseOnBackground = shouldPause -// } + override fun requireBackgroundSync() { + SyncWorker.requireBackgroundSync() + } -// override fun resumeSync() { -// assert(isOpen) -// syncThread.restart() -// } -// -// override fun pauseSync() { -// assert(isOpen) -// syncThread.pause() -// } + override fun startAutomaticBackgroundSync(repeatDelay: Long) { + SyncWorker.automaticallyBackgroundSync(0, repeatDelay) + } -// override fun configureSyncLongPooling(timoutMS: Long, delayMs: Long) { -// syncThread.configureLongPoolingSettings(timoutMS, delayMs) -// } -// -// @MainThread -// override fun stopSync() { -// assert(isOpen) -// syncThread.kill() -// } + override fun stopAnyBackgroundSync() { + SyncWorker.stopAnyBackgroundSync() + } + + @MainThread + override fun startSync() { + assert(isOpen) + 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 override fun close() { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt index ae1c30003b..42f2299ed7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.internal.session.room import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService @@ -51,7 +52,8 @@ internal class RoomFactory(private val monarchy: Monarchy, private val cryptoService: CryptoService, private val findReactionEventForUndoTask: FindReactionEventForUndoTask, private val joinRoomTask: JoinRoomTask, - private val leaveRoomTask: LeaveRoomTask) { + private val leaveRoomTask: LeaveRoomTask, + private val sessionParams: SessionParams) { fun instantiate(roomId: String): Room { val roomMemberExtractor = SenderRoomMemberExtractor(roomId) @@ -61,7 +63,7 @@ internal class RoomFactory(private val monarchy: Monarchy, val sendService = DefaultSendService(roomId, eventFactory, cryptoService, monarchy) val stateService = DefaultStateService(roomId, taskExecutor, sendStateTask) val roomMembersService = DefaultMembershipService(roomId, monarchy, taskExecutor, loadRoomMembersTask, inviteTask, joinRoomTask, leaveRoomTask) - val readService = DefaultReadService(roomId, monarchy, taskExecutor, setReadMarkersTask) + val readService = DefaultReadService(roomId, monarchy, taskExecutor, setReadMarkersTask, sessionParams) val reactionService = DefaultRelationService(roomId, eventFactory, findReactionEventForUndoTask, monarchy, taskExecutor) return DefaultRoom( diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt index ead2c8e429..1cefe088a9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt @@ -80,7 +80,7 @@ class RoomModule { } scope(DefaultSession.SCOPE) { - RoomFactory(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get()) + RoomFactory(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get()) } scope(DefaultSession.SCOPE) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt index 30bbe30c28..46e0848203 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt @@ -18,9 +18,15 @@ package im.vector.matrix.android.internal.session.room.read import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.session.room.read.ReadService +import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.EventEntity +import im.vector.matrix.android.internal.database.model.ReadReceiptEntity +import im.vector.matrix.android.internal.database.query.find +import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom import im.vector.matrix.android.internal.database.query.latestEvent +import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.fetchCopied @@ -28,7 +34,8 @@ import im.vector.matrix.android.internal.util.fetchCopied internal class DefaultReadService(private val roomId: String, private val monarchy: Monarchy, private val taskExecutor: TaskExecutor, - private val setReadMarkersTask: SetReadMarkersTask) : ReadService { + private val setReadMarkersTask: SetReadMarkersTask, + private val sessionParams: SessionParams) : ReadService { override fun markAllAsRead(callback: MatrixCallback) { val latestEvent = getLatestEvent() @@ -50,5 +57,20 @@ internal class DefaultReadService(private val roomId: String, return monarchy.fetchCopied { EventEntity.latestEvent(it, roomId) } } + override fun isEventRead(eventId: String): Boolean { + var isEventRead = false + monarchy.doWithRealm { + val readReceipt = ReadReceiptEntity.where(it, roomId, sessionParams.credentials.userId).findFirst() + ?: return@doWithRealm + val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(it, roomId) + ?: return@doWithRealm + val readReceiptIndex = liveChunk.events.find(readReceipt.eventId)?.displayIndex + ?: Int.MIN_VALUE + val eventToCheckIndex = liveChunk.events.find(eventId)?.displayIndex + ?: Int.MAX_VALUE + isEventRead = eventToCheckIndex <= readReceiptIndex + } + return isEventRead + } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt index c8629ecce8..828964d837 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt @@ -20,7 +20,11 @@ import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineService +import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.EventEntity +import im.vector.matrix.android.internal.database.model.ReadReceiptEntity +import im.vector.matrix.android.internal.database.query.find +import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.util.fetchCopyMap @@ -45,4 +49,5 @@ internal class DefaultTimelineService(private val roomId: String, }) } + } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt index 300ae4c5cd..504c621aaf 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt @@ -72,7 +72,7 @@ open class SyncService : Service(), MatrixKoinComponent { if (cancelableTask == null) { timer.cancel() timer = Timer() - doSync() + doSync(true) } else { //Already syncing ignore Timber.i("Received a start while was already syncking... ignore") @@ -101,7 +101,7 @@ open class SyncService : Service(), MatrixKoinComponent { stopSelf() } - fun doSync() { + fun doSync(once: Boolean = false) { var nextBatch = syncTokenStore.getLastToken() if (!networkConnectivityChecker.isConnected()) { Timber.v("Sync is Paused. Waiting...") @@ -110,7 +110,7 @@ open class SyncService : Service(), MatrixKoinComponent { override fun run() { doSync() } - }, 10_000L) + }, 5_000L) } else { Timber.v("Execute sync request with token $nextBatch and timeout $timeout") val params = SyncTask.Params(nextBatch, timeout) @@ -123,11 +123,16 @@ open class SyncService : Service(), MatrixKoinComponent { nextBatch = data.nextBatch syncTokenStore.saveToken(nextBatch) localBinder.notifySyncFinish() - timer.schedule(object : TimerTask() { - override fun run() { - doSync() - } - }, nextBatchDelay) + if (!once) { + timer.schedule(object : TimerTask() { + override fun run() { + doSync() + } + }, nextBatchDelay) + } else { + //stop + stopMe() + } } override fun onFailure(failure: Throwable) { @@ -141,7 +146,7 @@ open class SyncService : Service(), MatrixKoinComponent { override fun run() { doSync() } - }, 10_000L) + }, 5_000L) } if (failure !is Failure.NetworkConnection @@ -151,7 +156,7 @@ open class SyncService : Service(), MatrixKoinComponent { override fun run() { doSync() } - }, 10_000L) + }, 5_000L) } if (failure is Failure.ServerError diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt index 6d644ca547..68d01d344d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt @@ -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_DELAY = 0L - -private const val DEFAULT_BACKGROUND_LONG_POOL_TIMEOUT = 0L -private const val DEFAULT_BACKGROUND_LONG_POOL_DELAY = 30_000L - - internal class SyncThread(private val syncTask: SyncTask, private val networkConnectivityChecker: NetworkConnectivityChecker, private val syncTokenStore: SyncTokenStore, @@ -62,27 +57,6 @@ internal class SyncThread(private val syncTask: SyncTask, updateStateTo(SyncState.IDLE) } - /** - * The maximum time to wait, in milliseconds, before returning this request. - * If no events (or other data) become available before this time elapses, the server will return a response with empty fields. - * If set to 0 the server will return immediately even if the response is empty. - */ - private var longPoolTimeoutMs = DEFAULT_LONG_POOL_TIMEOUT - /** - * When the server responds to a sync request, the client waits for `longPoolDelay` before calling a new sync. - */ - private var longPoolDelayMs = DEFAULT_LONG_POOL_DELAY - - - var shouldPauseOnBackground: Boolean = true - private var backgroundedLongPoolTimeoutMs = DEFAULT_BACKGROUND_LONG_POOL_TIMEOUT - private var backgroundedLongPoolDelayMs = DEFAULT_BACKGROUND_LONG_POOL_DELAY - - - private var currentLongPoolTimeoutMs = longPoolTimeoutMs - private var currentLongPoolDelayMs = longPoolDelayMs - - fun restart() = synchronized(lock) { if (state is SyncState.PAUSED) { Timber.v("Resume sync...") @@ -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) { if (state is SyncState.RUNNING) { Timber.v("Pause sync...") @@ -148,9 +98,9 @@ internal class SyncThread(private val syncTask: SyncTask, lock.wait() } } 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 params = SyncTask.Params(nextBatch, currentLongPoolTimeoutMs) + val params = SyncTask.Params(nextBatch, DEFAULT_LONG_POOL_TIMEOUT) cancelableTask = syncTask.configureWith(params) .callbackOn(TaskThread.CALLER) .executeOn(TaskThread.CALLER) @@ -193,8 +143,8 @@ internal class SyncThread(private val syncTask: SyncTask, updateStateTo(SyncState.RUNNING(catchingUp = false)) } - Timber.v("Waiting for $currentLongPoolDelayMs delay before new pool...") - if (currentLongPoolDelayMs > 0) sleep(currentLongPoolDelayMs) + Timber.v("Waiting for $DEFAULT_LONG_POOL_DELAY delay before new pool...") + if (DEFAULT_LONG_POOL_DELAY > 0) sleep(DEFAULT_LONG_POOL_DELAY) Timber.v("...Continue") } } @@ -216,20 +166,11 @@ internal class SyncThread(private val syncTask: SyncTask, } override fun onMoveToForeground() { - currentLongPoolTimeoutMs = longPoolTimeoutMs - currentLongPoolDelayMs = longPoolDelayMs restart() } override fun onMoveToBackground() { - if (shouldPauseOnBackground) { - pause() - } else { - Timber.v("Slower sync in background mode") - //we continue but with a slower pace - currentLongPoolTimeoutMs = backgroundedLongPoolTimeoutMs - currentLongPoolDelayMs = backgroundedLongPoolDelayMs - } + pause() } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncWorker.kt index 441f7ef770..dd5b91f688 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncWorker.kt @@ -17,8 +17,6 @@ package im.vector.matrix.android.internal.session.sync.job import android.content.Context import androidx.work.* -import arrow.core.failure -import arrow.core.recoverWith import com.squareup.moshi.JsonClass import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.MatrixError @@ -33,19 +31,19 @@ import im.vector.matrix.android.internal.session.sync.model.SyncResponse import im.vector.matrix.android.internal.util.WorkerParamsFactory import org.koin.standalone.inject import timber.log.Timber -import java.net.SocketTimeoutException import java.util.concurrent.TimeUnit private const val DEFAULT_LONG_POOL_TIMEOUT = 0L -class SyncWorker(context: Context, +internal class SyncWorker(context: Context, workerParameters: WorkerParameters ) : CoroutineWorker(context, workerParameters), MatrixKoinComponent { @JsonClass(generateAdapter = true) internal data class Params( - val timeout: Long = DEFAULT_LONG_POOL_TIMEOUT + val timeout: Long = DEFAULT_LONG_POOL_TIMEOUT, + val automaticallyRetry: Boolean = false ) private val syncAPI by inject() @@ -54,7 +52,6 @@ class SyncWorker(context: Context, private val sessionParamsStore by inject() private val syncTokenStore by inject() - val autoMode = false override suspend fun doWork(): Result { Timber.i("Sync work starting") @@ -69,51 +66,56 @@ class SyncWorker(context: Context, return executeRequest { apiCall = syncAPI.sync(requestParams) - }.recoverWith { throwable -> - // Intercept 401 - if (throwable is Failure.ServerError - && throwable.error.code == MatrixError.UNKNOWN_TOKEN) { - sessionParamsStore.delete() - } - Timber.i("Sync work failed $throwable") - // Transmit the throwable - throwable.failure() }.fold( { - Timber.i("Sync work failed $it") - again() - if (it is Failure.NetworkConnection && it.cause is SocketTimeoutException) { - // Timeout are not critical - Result.Success() + if (it is Failure.ServerError + && it.error.code == MatrixError.UNKNOWN_TOKEN) { + sessionParamsStore.delete() + Result.failure() } else { - Result.Success() + Timber.i("Sync work failed $it") + Result.retry() } }, { Timber.i("Sync work success next batch ${it.nextBatch}") - syncResponseHandler.handleResponse(it, token, false) - syncTokenStore.saveToken(it.nextBatch) - again() - Result.success() + if (!isStopped) { + syncResponseHandler.handleResponse(it, token, false) + syncTokenStore.saveToken(it.nextBatch) + } + if (params.automaticallyRetry) Result.retry() else Result.success() } ) - } - fun again() { - if (autoMode) { - Timber.i("Sync work Again!!") + companion object { + fun requireBackgroundSync(serverTimeout: Long = 0) { + val data = WorkerParamsFactory.toData(Params(serverTimeout, false)) val workRequest = OneTimeWorkRequestBuilder() - .setInitialDelay(30_000, TimeUnit.MILLISECONDS) + .setInputData(data) .setConstraints(Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build()) - .setBackoffCriteria(BackoffPolicy.LINEAR, 10_000, TimeUnit.MILLISECONDS) + .setBackoffCriteria(BackoffPolicy.LINEAR, 1_000, TimeUnit.MILLISECONDS) .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() + .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") + } } } \ No newline at end of file diff --git a/vector/src/fdroid/AndroidManifest.xml b/vector/src/fdroid/AndroidManifest.xml index 0cc39d63e8..77179c86e1 100644 --- a/vector/src/fdroid/AndroidManifest.xml +++ b/vector/src/fdroid/AndroidManifest.xml @@ -2,14 +2,24 @@ + + + - + + + + + \ No newline at end of file diff --git a/vector/src/fdroid/java/im/vector/riotredesign/push/fcm/FcmHelper.java b/vector/src/fdroid/java/im/vector/riotredesign/push/fcm/FcmHelper.kt similarity index 68% rename from vector/src/fdroid/java/im/vector/riotredesign/push/fcm/FcmHelper.java rename to vector/src/fdroid/java/im/vector/riotredesign/push/fcm/FcmHelper.kt index 3efc4990e9..11826a7b35 100755 --- a/vector/src/fdroid/java/im/vector/riotredesign/push/fcm/FcmHelper.java +++ b/vector/src/fdroid/java/im/vector/riotredesign/push/fcm/FcmHelper.kt @@ -14,24 +14,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package im.vector.riotredesign.push.fcm; +package im.vector.riotredesign.push.fcm -import android.app.Activity; -import android.content.Context; +import android.app.Activity +import android.content.Context -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; +import im.vector.riotredesign.core.pushers.PushersManager -public class FcmHelper { +object FcmHelper { + + fun isPushSupported(): Boolean = false /** * Retrieves the FCM registration token. * * @return the FCM token or null if not received from FCM */ - @Nullable - public static String getFcmToken(Context context) { - return null; + fun getFcmToken(context: Context): String? { + return null } /** @@ -40,8 +40,7 @@ public class FcmHelper { * @param context android context * @param token the token to store */ - public static void storeFcmToken(@NonNull Context context, - @Nullable String token) { + fun storeFcmToken(context: Context, token: String?) { // No op } @@ -50,7 +49,7 @@ public class FcmHelper { * * @param activity the first launch Activity */ - public static void ensureFcmTokenIsRetrieved(final Activity activity, PushersManager pushersManager) { + fun ensureFcmTokenIsRetrieved(activity: Activity, pushersManager: PushersManager) { // No op } } diff --git a/vector/src/fdroid/java/im/vector/riotredesign/receiver/OnApplicationUpgradeReceiver.java b/vector/src/fdroid/java/im/vector/riotredesign/receiver/OnApplicationUpgradeOrRebootReceiver.kt similarity index 53% rename from vector/src/fdroid/java/im/vector/riotredesign/receiver/OnApplicationUpgradeReceiver.java rename to vector/src/fdroid/java/im/vector/riotredesign/receiver/OnApplicationUpgradeOrRebootReceiver.kt index 4cb092b1a7..a13bc71ada 100644 --- a/vector/src/fdroid/java/im/vector/riotredesign/receiver/OnApplicationUpgradeReceiver.java +++ b/vector/src/fdroid/java/im/vector/riotredesign/receiver/OnApplicationUpgradeOrRebootReceiver.kt @@ -1,5 +1,6 @@ /* * Copyright 2018 New Vector Ltd + * Copyright 2019 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,21 +15,18 @@ * limitations under the License. */ -package im.vector.riotredesign.receiver; +package im.vector.riotredesign.receiver -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import im.vector.riotredesign.core.services.AlarmSyncBroadcastReceiver +import timber.log.Timber -import timber.log.Timber; +class OnApplicationUpgradeOrRebootReceiver : BroadcastReceiver() { -public class OnApplicationUpgradeReceiver extends BroadcastReceiver { - - @Override - public void onReceive(Context context, Intent intent) { - Timber.v("## onReceive() : Application has been upgraded, restart event stream service."); - - // Start Event stream - // TODO EventStreamServiceX.Companion.onApplicationUpgrade(context); + override fun onReceive(context: Context, intent: Intent) { + Timber.v("## onReceive() ${intent.action}") + AlarmSyncBroadcastReceiver.scheduleAlarm(context, 10) } } diff --git a/vector/src/gplay/java/im/vector/riotredesign/push/fcm/FcmHelper.java b/vector/src/gplay/java/im/vector/riotredesign/push/fcm/FcmHelper.java deleted file mode 100755 index 17bdc909aa..0000000000 --- a/vector/src/gplay/java/im/vector/riotredesign/push/fcm/FcmHelper.java +++ /dev/null @@ -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; - } -} diff --git a/vector/src/gplay/java/im/vector/riotredesign/push/fcm/FcmHelper.kt b/vector/src/gplay/java/im/vector/riotredesign/push/fcm/FcmHelper.kt new file mode 100755 index 0000000000..f6d93d78e3 --- /dev/null +++ b/vector/src/gplay/java/im/vector/riotredesign/push/fcm/FcmHelper.kt @@ -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 + } +} diff --git a/vector/src/gplay/java/im/vector/riotredesign/push/fcm/VectorFirebaseMessagingService.kt b/vector/src/gplay/java/im/vector/riotredesign/push/fcm/VectorFirebaseMessagingService.kt index ba942124d1..7ef1973db3 100755 --- a/vector/src/gplay/java/im/vector/riotredesign/push/fcm/VectorFirebaseMessagingService.kt +++ b/vector/src/gplay/java/im/vector/riotredesign/push/fcm/VectorFirebaseMessagingService.kt @@ -144,14 +144,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { Timber.i("Ignoring push, event already knwown") } else { Timber.v("Requesting background sync") - val workRequest = OneTimeWorkRequestBuilder() - .setInputData(Data.Builder().put("timeout", 0L).build()) - .setConstraints(Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .build()) - .setBackoffCriteria(BackoffPolicy.LINEAR, 10_000, TimeUnit.MILLISECONDS) - .build() - WorkManager.getInstance().enqueueUniqueWork("BG_SYNCP", ExistingWorkPolicy.REPLACE, workRequest) + session.requireBackgroundSync(0L) } } @@ -214,7 +207,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { isPushGatewayEvent = true ) notificationDrawerManager.onNotifiableEventReceived(simpleNotifiableEvent) - notificationDrawerManager.refreshNotificationDrawer(null) + notificationDrawerManager.refreshNotificationDrawer() return } else { @@ -249,7 +242,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { notifiableEvent.isPushGatewayEvent = true notifiableEvent.matrixID = session.sessionParams.credentials.userId notificationDrawerManager.onNotifiableEventReceived(notifiableEvent) - notificationDrawerManager.refreshNotificationDrawer(null) + notificationDrawerManager.refreshNotificationDrawer() } } } diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 18e7eb7727..722460a174 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -15,12 +15,6 @@ android:supportsRtl="true" android:theme="@style/AppTheme.Light" tools:replace="android:allowBackup"> - - - - // intent.action = "NORMAL" -// try { -// startService(intent) -// } catch (e: Throwable) { -// Timber.e("Failed to launch sync service") -// } - bindService(intent, connection, Context.BIND_AUTO_CREATE) - + AlarmSyncBroadcastReceiver.cancelAlarm(appContext) + Matrix.getInstance().currentSession?.also { + it.stopAnyBackgroundSync() } } - var isPushAvailable = true @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) fun entersBackground() { Timber.i("App entered background") - //we have here 3 modes - if (isPushAvailable) { - // PUSH IS AVAILABLE: - // Just stop the service, we will sync when a notification is received - try { - unbindService(connection) - mBinder?.getService()?.stopMe() - mBinder = null - } catch (t: Throwable) { - Timber.e(t) - } + if (FcmHelper.isPushSupported()) { + //TODO FCM fallback } else { - - // NO PUSH, and don't care about battery -// unbindService(connection) -// mBinder?.getService()?.stopMe()// kill also -// mBinder = null - //In this case we will keep a permanent - - //TODO if no push schedule reccuring alarm - -// val workRequest = PeriodicWorkRequestBuilder(1, TimeUnit.MINUTES) -// .setConstraints(Constraints.Builder() -// .setRequiredNetworkType(NetworkType.CONNECTED) -// .build()) -// .setBackoffCriteria(BackoffPolicy.LINEAR, 10_000, TimeUnit.MILLISECONDS) -// .build() -// WorkManager.getInstance().enqueueUniquePeriodicWork( -// "BG_SYNC", -// ExistingPeriodicWorkPolicy.KEEP, -// workRequest) - val workRequest = OneTimeWorkRequestBuilder() -// .setInitialDelay(30_000, TimeUnit.MILLISECONDS) - .setInputData(Data.Builder().put("timeout", 0L).build()) - .setConstraints(Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .build()) - .setBackoffCriteria(BackoffPolicy.LINEAR, 10_000, TimeUnit.MILLISECONDS) - .build() - WorkManager.getInstance().enqueueUniqueWork("BG_SYNCP", ExistingWorkPolicy.REPLACE, workRequest) - -// val intent = Intent(applicationContext, RestartBroadcastReceiver::class.java) -// // Create a PendingIntent to be triggered when the alarm goes off -// val pIntent = PendingIntent.getBroadcast(applicationContext, RestartBroadcastReceiver.REQUEST_CODE, -// intent, PendingIntent.FLAG_UPDATE_CURRENT); -// // Setup periodic alarm every every half hour from this point onwards -// val firstMillis = System.currentTimeMillis(); // alarm is set right away -// val alarmMgr = getSystemService(Context.ALARM_SERVICE) as AlarmManager -// // First parameter is the type: ELAPSED_REALTIME, ELAPSED_REALTIME_WAKEUP, RTC_WAKEUP -// // Interval can be INTERVAL_FIFTEEN_MINUTES, INTERVAL_HALF_HOUR, INTERVAL_HOUR, INTERVAL_DAY -//// alarm.setInexactRepeating(AlarmManager.RTC_WAKEUP, firstMillis, -//// 30_000L, pIntent) -// alarmMgr.set(AlarmManager.RTC_WAKEUP, firstMillis, pIntent); - + //TODO check if notifications are enabled for this device + //We need to use alarm in this mode + AlarmSyncBroadcastReceiver.scheduleAlarm(applicationContext,4_000L) Timber.i("Alarm scheduled to restart service") + } } @@ -273,36 +186,4 @@ class VectorApplication : Application(), SyncService.SyncListener { return mFontThreadHandler!! } - override fun onSyncFinsh() { - //in foreground sync right now!! - Timber.v("Sync just finished") -// mBinder?.getService()?.doSync() - } - - override fun networkNotAvailable() { - //we then want to retry in 10s? - } - - override fun onFailed(failure: Throwable) { - //stop it also? -// if (failure is Failure.NetworkConnection -// && failure.cause is SocketTimeoutException) { -// // Timeout are not critical just retry? -// //TODO -// } -// -// if (failure !is Failure.NetworkConnection -// || failure.cause is JsonEncodingException) { -// //TODO Retry in 10S? -// } -// -// if (failure is Failure.ServerError -// && (failure.error.code == MatrixError.UNKNOWN_TOKEN || failure.error.code == MatrixError.MISSING_TOKEN)) { -// // No token or invalid token, stop the thread -// mBinder?.getService()?.unbindService(connection) -// mBinder?.getService()?.stopMe() -// } - - } - } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/di/AppModule.kt b/vector/src/main/java/im/vector/riotredesign/core/di/AppModule.kt index 926a5bc718..f26d2c3eb1 100644 --- a/vector/src/main/java/im/vector/riotredesign/core/di/AppModule.kt +++ b/vector/src/main/java/im/vector/riotredesign/core/di/AppModule.kt @@ -37,6 +37,7 @@ import im.vector.riotredesign.features.navigation.DefaultNavigator import im.vector.riotredesign.features.navigation.Navigator import im.vector.riotredesign.features.notifications.NotifiableEventResolver import im.vector.riotredesign.features.notifications.NotificationDrawerManager +import im.vector.riotredesign.features.notifications.OutdatedEventDetector import im.vector.riotredesign.features.notifications.PushRuleTriggerListener import org.koin.dsl.module.module @@ -85,11 +86,15 @@ class AppModule(private val context: Context) { } single { - PushRuleTriggerListener(get(),get()) + PushRuleTriggerListener(get(), get()) } single { - NotificationDrawerManager(context) + OutdatedEventDetector(context) + } + + single { + NotificationDrawerManager(context, get()) } single { diff --git a/vector/src/main/java/im/vector/riotredesign/core/services/AlarmSyncBroadcastReceiver.kt b/vector/src/main/java/im/vector/riotredesign/core/services/AlarmSyncBroadcastReceiver.kt new file mode 100644 index 0000000000..990bf15cde --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/services/AlarmSyncBroadcastReceiver.kt @@ -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) + } + } +} diff --git a/vector/src/main/java/im/vector/riotredesign/core/services/EventStreamServiceX.kt b/vector/src/main/java/im/vector/riotredesign/core/services/EventStreamServiceX.kt deleted file mode 100644 index 108b27d5de..0000000000 --- a/vector/src/main/java/im/vector/riotredesign/core/services/EventStreamServiceX.kt +++ /dev/null @@ -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() - - /** - * 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() - .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) - } - } - } -} diff --git a/vector/src/main/java/im/vector/riotredesign/core/services/HttpLongPoolingSyncService.kt b/vector/src/main/java/im/vector/riotredesign/core/services/HttpLongPoolingSyncService.kt deleted file mode 100644 index 501a2c7763..0000000000 --- a/vector/src/main/java/im/vector/riotredesign/core/services/HttpLongPoolingSyncService.kt +++ /dev/null @@ -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() -// private var mCount = 0 -// private var lastTimeMs = System.currentTimeMillis() -// -// lateinit var myRun: () -> Unit -// override fun onCreate() { -// //Add the permanent listening notification -// super.onCreate() -// -// if (SDK_INT >= VERSION_CODES.O) { -// val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager -// val notification = NotificationUtils.buildForegroundServiceNotification(applicationContext, R.string.notification_listening_for_events, false) -// startForeground(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE, notification) -// } -// val thread = HandlerThread("My service Handler") -// thread.start() -// -// mServiceLooper = thread.looper -// mHandler = Handler(mServiceLooper) -// myRun = { -// val diff = System.currentTimeMillis() - lastTimeMs -// lastTimeMs = System.currentTimeMillis() -// val isAlive = Matrix.getInstance().currentSession?.isSyncThreadAlice() -// val state = Matrix.getInstance().currentSession?.syncThreadState() -// Timber.w(" timeDiff[${diff/1000}] Yo me here $mCount, sync thread is Alive? $isAlive, state:$state") -// mCount++ -// mHandler?.postDelayed(Runnable { myRun() }, 10_000L) -// } -// } -// -// override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { -// //START_STICKY mode makes sense for things that will be explicitly started -// //and stopped to run for arbitrary periods of time -// -// mHandler?.post { -// myRun() -// } -// return START_STICKY -// } -// -// -// override fun onDestroy() { -// //TODO test if this service should be relaunched (preference) -// Timber.i("Service is destroyed, relaunch asap") -// Intent(applicationContext, RestartBroadcastReceiver::class.java).also { sendBroadcast(it) } -// super.onDestroy() -// } -// -// companion object { -// -// fun startService(context: Context) { -// Timber.i("Start sync service") -// val intent = Intent(context, HttpLongPoolingSyncService::class.java) -// try { -// if (SDK_INT >= VERSION_CODES.O) { -// startForegroundService(context, intent) -// } else { -// context.startService(intent) -// } -// } catch (ex: Throwable) { -// //TODO -// Timber.e(ex) -// } -// } -// } -//} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/services/PushSimulatorWorker.kt b/vector/src/main/java/im/vector/riotredesign/core/services/PushSimulatorWorker.kt deleted file mode 100644 index d3f93f3280..0000000000 --- a/vector/src/main/java/im/vector/riotredesign/core/services/PushSimulatorWorker.kt +++ /dev/null @@ -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() - } -} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/services/RestartBroadcastReceiver.kt b/vector/src/main/java/im/vector/riotredesign/core/services/RestartBroadcastReceiver.kt deleted file mode 100644 index 1834a994ed..0000000000 --- a/vector/src/main/java/im/vector/riotredesign/core/services/RestartBroadcastReceiver.kt +++ /dev/null @@ -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 - } -} diff --git a/vector/src/main/java/im/vector/riotredesign/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotredesign/features/login/LoginActivity.kt index eba8a85e75..8c0be58256 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/login/LoginActivity.kt @@ -76,9 +76,7 @@ class LoginActivity : VectorBaseActivity() { Matrix.getInstance().currentSession = data data.open() data.setFilter(FilterService.FilterPreset.RiotFilter) - //TODO sync -// data.shoudPauseOnBackground(false) -// data.startSync() + data.startSync() get().startWithSession(data) goToHome() } diff --git a/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationDrawerManager.kt index 15a3ef8bac..1a352864ca 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationDrawerManager.kt @@ -36,7 +36,7 @@ import java.io.FileOutputStream * 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. */ -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 private var firstTime = true @@ -53,7 +53,7 @@ class NotificationDrawerManager(val context: Context) { object : IconLoader.IconLoaderListener { override fun onIconsLoaded() { // Force refresh - refreshNotificationDrawer(null) + refreshNotificationDrawer() } }) @@ -123,7 +123,7 @@ class NotificationDrawerManager(val context: Context) { synchronized(eventList) { eventList.clear() } - refreshNotificationDrawer(null) + refreshNotificationDrawer() } /** 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) } - refreshNotificationDrawer(null) + refreshNotificationDrawer() } /** @@ -177,7 +177,7 @@ class NotificationDrawerManager(val context: Context) { } - fun refreshNotificationDrawer(outdatedDetector: OutdatedEventDetector?) { + fun refreshNotificationDrawer() { if (myUserDisplayName.isBlank()) { // TODO // initWithSession(Matrix.getInstance(context).defaultSession) diff --git a/vector/src/main/java/im/vector/riotredesign/features/notifications/OutdatedEventDetector.kt b/vector/src/main/java/im/vector/riotredesign/features/notifications/OutdatedEventDetector.kt index 5ee0cccdd9..4ebbe68651 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/notifications/OutdatedEventDetector.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/notifications/OutdatedEventDetector.kt @@ -16,6 +16,7 @@ package im.vector.riotredesign.features.notifications import android.content.Context +import im.vector.matrix.android.api.Matrix class OutdatedEventDetector(val context: Context) { @@ -28,20 +29,9 @@ class OutdatedEventDetector(val context: Context) { if (notifiableEvent is NotifiableMessageEvent) { val eventID = notifiableEvent.eventId val roomID = notifiableEvent.roomId - /* - TODO - Matrix.getMXSession(context.applicationContext, notifiableEvent.matrixID)?.let { session -> - //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 - } - } - } - } - */ + val session = Matrix.getInstance().currentSession ?: return false + val room = session.getRoom(roomID) ?: return false + return room.isEventRead(eventID) } return false } diff --git a/vector/src/main/java/im/vector/riotredesign/features/notifications/PushRuleTriggerListener.kt b/vector/src/main/java/im/vector/riotredesign/features/notifications/PushRuleTriggerListener.kt index b94cb7f6e7..260899170f 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/notifications/PushRuleTriggerListener.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/notifications/PushRuleTriggerListener.kt @@ -26,7 +26,7 @@ class PushRuleTriggerListener( } override fun batchFinish() { - drawerManager.refreshNotificationDrawer(null) + drawerManager.refreshNotificationDrawer() } fun startWithSession(session: Session) { @@ -41,6 +41,6 @@ class PushRuleTriggerListener( session?.removePushRuleListener(this) session = null drawerManager.clearAllEvents() - drawerManager.refreshNotificationDrawer(null) + drawerManager.refreshNotificationDrawer() } } \ No newline at end of file