Skip to content

Commit

Permalink
Sample that shows how effectively detect user presence (#39)
Browse files Browse the repository at this point in the history
  • Loading branch information
clementetb authored Aug 3, 2023
1 parent 5aafa76 commit 56a6864
Show file tree
Hide file tree
Showing 34 changed files with 1,125 additions and 6 deletions.
1 change: 1 addition & 0 deletions AppServicesUsageSamples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Reference app that show cases different design patterns and examples of using th
## Samples

### [Property level encryption](apps/property-encryption/README.md)
### [Presence detection](apps/presence-detection/README.md)

This demo shows the process of protecting users' sensitive data by employing end-to-end encryption techniques while guaranteeing access from any user's device.

Expand Down
53 changes: 53 additions & 0 deletions AppServicesUsageSamples/apps/presence-detection/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Presence detection

User presence detection allows to check for connectivity or activity of users or devices. It has many
applications from showing users' availability in a chat application to identify malfunctioning devices in an industrial process.

This sample shows how to implement user presence detection effectively using Realm and Atlas resources.

## Tracking local connectivity

Clients could detect whether they are connected to Atlas via the `SyncSession.connectionState`
property. The values of the connection state are `DISCONNECTED`, `CONNECTING`, or `CONNECTED`.

Realm supports listening to updates to the connection state via the function `connectionStateAsFlow`, and simplifies listening to
connectivity changes to:

```kotlin
realm.syncSession
.connectionStateAsFlow()
.collect {connectionStateChange ->
// handle connection state
}
```

The local connectivity detection is used in this sample to display whether the current user is connected or not.

## Tracking connectivity of external devices

There are different approaches on how we can propagate to other parties the connectivity status of a
client.

### Forwarding logs

This approach exclusively use App services resources to perform the detection exclusively on the server.

App services would log any user connectivity change in the logs as a `Sync` type event. App services support forwarding these logs to predefined functions.

![logs sample](logs.png "Logs")

In this sample we have configured a forward log that forward all `Sync` events to a [function](functions/logPresenceDetector.js) that detects connectivity changes. This function analyzes each log to detect if a user has started/ended a session, then it upserts the changes in a [collection](data_sources/mongodb-atlas/presence-detection/user_status/schema.json) that tracks the connectivity for that user.

This collection is part of a Sync Realm, so any changes to it would be automatically propagated all its subscriptions.

![Flow](presence-flow.svg "Flow")

The response time for this solution is up to a minute, that is what it takes to the log forwarding feature to post any new updates. Logs can be processed individually or in batches, while it does not affect the response time, doing it in batches debounces changes and eliminates redundant notification changes.

### Alternatives

Another solution that we have not covered in this sample is the clients actively notifying if they have connectivity.

The clients would send a heart beat to the server each X seconds, then the server could poll for the clients that have not send a beat in Y amount of time, deeming them as disconnected.

While this approach would improve the response time, it would use more resources and wouldn't scale as well.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"enabled": false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"anon-user": {
"name": "anon-user",
"type": "anon-user",
"disabled": false
},
"api-key": {
"name": "api-key",
"type": "api-key",
"disabled": true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "mongodb-atlas",
"type": "mongodb-atlas",
"config": {
"clusterName": "Cluster0",
"readPreference": "primary",
"wireProtocolEnabled": false
},
"version": 1
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"roles": [
{
"name": "readAll",
"apply_when": {},
"document_filters": {
"write": false,
"read": true
},
"read": true,
"write": false,
"insert": false,
"delete": false,
"search": true
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"properties": {
"_id": {
"bsonType": "objectId"
},
"owner_id": {
"bsonType": "string"
},
"present": {
"bsonType": "bool"
}
},
"required": [
"_id",
"present",
"owner_id"
],
"title": "user_status",
"type": "object"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"values": {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"values": {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"values": {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"values": {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"values": {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[
{
"name": "logPresenceDetector",
"private": false
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
exports = async function (logs) {
// logs appear in ascending order
for (let i = logs.length - 1; i >= 0; i--) {
extract_presence(logs[i])
}
};

async function extract_presence(log) {
let type = log.type
if (type !== "SYNC_SESSION_START" && type !== "SYNC_SESSION_END") return;

let user_id = log.user_id;
let present = type === "SYNC_SESSION_START";

console.log(`User ${user_id} present: ${present}`);

update_presence(user_id, present);
}

async function update_presence(user_id, present) {
const customUserDataCollection = context.services
.get("mongodb-atlas")
.db("presence-detection")
.collection("user_status");

try {
await customUserDataCollection.updateOne(
{
owner_id: user_id,
},
{
owner_id: user_id,
present: present
},
{
upsert: true
}
);
} catch (e) {
console.error(`Failed to create custom user data document for user: ${user_id}`);
throw e
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"use_natural_pluralization": true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "presenceDetector",
"log_types": [
"sync"
],
"log_statuses": [
"success"
],
"policy": {
"type": "batch"
},
"action": {
"type": "function",
"name": "logPresenceDetector"
},
"disabled": false
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
335 changes: 335 additions & 0 deletions AppServicesUsageSamples/apps/presence-detection/presence-flow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"app_id": "presence-detection",
"config_version": 20210101,
"name": "presence-detection",
"location": "IE",
"provider_region": "aws-eu-west-1",
"deployment_model": "LOCAL"
}
16 changes: 16 additions & 0 deletions AppServicesUsageSamples/apps/presence-detection/sync/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"type": "flexible",
"state": "enabled",
"development_mode_enabled": false,
"service_name": "mongodb-atlas",
"database_name": "presence-detection",
"client_max_offline_days": 30,
"is_recovery_mode_disabled": false,
"permissions": {
"rules": {},
"defaultRoles": []
},
"queryable_fields_names": [
"owner_id"
]
}
1 change: 1 addition & 0 deletions AppServicesUsageSamples/demo/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
</intent-filter>
</activity>
<activity android:name=".propertyencryption.PropertyEncryptionActivity"/>
<activity android:name=".presence.PresenceDetectionActivity"/>
</application>

</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package io.realm.appservicesusagesamples

import android.app.Application
import io.realm.appservicesusagesamples.propertyencryption.propertyEncryptionModule
import io.realm.appservicesusagesamples.presence.presenceDetectionModule
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.GlobalContext.startKoin
Expand All @@ -35,7 +36,11 @@ class AppServicesUsageSamplesApp: Application() {
// Reference Android context
androidContext(this@AppServicesUsageSamplesApp)
// Load modules
modules(mainModule, propertyEncryptionModule)
modules(
mainModule,
propertyEncryptionModule,
presenceDetectionModule,
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,11 @@
*/
package io.realm.appservicesusagesamples


// Replace any <insert-app-id> with the App Services app id sample.
// It is not mandatory to have them all replaced, only the samples to evaluate.
const val PROPERTY_ENCRYPTION_APP_ID = "field-encryption-fjrvt"
const val USER_PRESENCE_APP_ID = "presence-detection-bcsii"
const val PROPERTY_ENCRYPTION_APP_ID = "<insert-app-id>"
const val USER_PRESENCE_APP_ID = "<insert-app-id>"
const val OFFLINE_LOGIN_APP_ID = "<insert-app-id>"
const val ERROR_HANDLING_APP_ID = "error-handling-jltyk"
const val ERROR_HANDLING_APP_ID = "<insert-app-id>"
const val BUSINESS_LOGIC_APP_ID = "<insert-app-id>"
const val PURCHASE_VERIFICATION_APP_ID = "<insert-app-id>"
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package io.realm.appservicesusagesamples

import io.realm.appservicesusagesamples.propertyencryption.PropertyEncryptionActivity
import io.realm.appservicesusagesamples.ui.SampleSelectorScreenViewModel
import io.realm.appservicesusagesamples.presence.PresenceDetectionActivity
import io.realm.kotlin.mongodb.App
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.qualifier.named
Expand All @@ -38,7 +39,7 @@ enum class Demos(
),
USER_PRESENCE(
"User presence",
SampleSelectorActivity::class.java,
PresenceDetectionActivity::class.java,
USER_PRESENCE_APP_ID,
),
OFFLINE_LOGIN(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright 2023 Realm Inc.
*
* 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 io.realm.appservicesusagesamples.presence

import io.realm.appservicesusagesamples.Demos
import io.realm.appservicesusagesamples.presence.ui.UserStatusListViewModel
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module

val presenceDetectionModule = module {
viewModel {
UserStatusListViewModel(
app = get(qualifier = Demos.USER_PRESENCE.qualifier),
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright 2023 Realm Inc.
*
* 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 io.realm.appservicesusagesamples.presence

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier
import io.realm.appservicesusagesamples.presence.ui.UserStatusListScreen
import io.realm.appservicesusagesamples.presence.ui.UserStatusListViewModel
import io.realm.appservicesusagesamples.ui.theme.AppServicesUsageSamplesTheme
import org.koin.android.scope.AndroidScopeComponent
import org.koin.androidx.scope.activityRetainedScope
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.scope.Scope

class PresenceDetectionActivity : ComponentActivity(), AndroidScopeComponent {
override val scope: Scope by activityRetainedScope()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val viewModel: UserStatusListViewModel by viewModel()

setContent {
AppServicesUsageSamplesTheme {
UserStatusListScreen(
viewModel = viewModel,
modifier = Modifier
.fillMaxSize(),
onLogout = {
// close the app
finish()
}
)
}
}
}
}
Loading

0 comments on commit 56a6864

Please sign in to comment.