diff --git a/CHANGELOG.md b/CHANGELOG.md
index dcf603b27c..ba50900c1a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,7 +21,7 @@
* Added better checks for detecting corrupted files, both before and after the file is written to disk.
### Fixed
-* [ObjectServer] Native errors sometimes mapped to the wrong Java ErrorCode. [#6364](https://github.com/realm/realm-java/issues/6364)
+* [ObjectServer] Native errors sometimes mapped to the wrong Java ErrorCode. (Issue [#6364](https://github.com/realm/realm-java/issues/6364), since 2.0.0)
* [ObjectServer] Query-based Sync queries involving LIMIT, limited the result before permissions were evaluated. This could sometimes result in the wrong number of elements being returned.
* Removed Java 8 bytecode. Resulted in errors like `D8: Invoke-customs are only supported starting with Android O (--min-api 26)` if not compiled with Java 8. (Issue [#6300](https://github.com/realm/realm-java/issues/6300), since 5.8.0).
diff --git a/Dockerfile b/Dockerfile
index 052db2bfe0..77fa0e1f09 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -46,16 +46,23 @@ RUN cd /opt && \
rm -f android-tools-linux.zip
# Grab what's needed in the SDK
-RUN mkdir "${ANDROID_HOME}/licenses" && \
- echo -e "\n8933bad161af4178b1185d1a37fbf41ea5269c55" > "${ANDROID_HOME}/licenses/android-sdk-license"
RUN sdkmanager --update
-# Accept all licenses
-RUN yes y | sdkmanager --licenses
-RUN sdkmanager 'platform-tools'
-RUN sdkmanager 'build-tools;28.0.3'
-RUN sdkmanager 'extras;android;m2repository'
-RUN sdkmanager 'platforms;android-27'
-RUN sdkmanager 'cmake;3.6.4111459'
+
+# Accept licenses before installing components, no need to echo y for each component
+# License is valid for all the standard components in versions installed from this file
+# Non-standard components: MIPS system images, preview versions, GDK (Google Glass) and Android Google TV require separate licenses, not accepted there
+RUN yes | sdkmanager --licenses
+
+# SDKs
+# Please keep these in descending order!
+# The `yes` is for accepting all non-standard tool licenses.
+# Please keep all sections in descending order!
+RUN yes | sdkmanager \
+ 'platform-tools' \
+ 'build-tools;28.0.3' \
+ 'extras;android;m2repository' \
+ 'platforms;android-27' \
+ 'cmake;3.6.4111459'
# Install the NDK
RUN mkdir /opt/android-ndk-tmp && \
diff --git a/dependencies.list b/dependencies.list
index 4216ada957..af7b3fec50 100644
--- a/dependencies.list
+++ b/dependencies.list
@@ -18,4 +18,3 @@ ndkVersion=r10e
BUILD_INFO_EXTRACTOR_GRADLE=4.7.5
GRADLE_BINTRAY_PLUGIN=1.8.4
-
diff --git a/examples/build.gradle b/examples/build.gradle
index 5e91e876da..3ed8391b72 100644
--- a/examples/build.gradle
+++ b/examples/build.gradle
@@ -1,6 +1,6 @@
def projectDependencies = new Properties()
projectDependencies.load(new FileInputStream("${rootDir}/../dependencies.list"))
-project.ext.sdkVersion = 27
+project.ext.sdkVersion = 28
project.ext.minSdkVersion = 15
project.ext.buildTools = projectDependencies.get("ANDROID_BUILD_TOOLS")
diff --git a/examples/gradle.properties b/examples/gradle.properties
index 94789e069a..d5b415dc3b 100644
--- a/examples/gradle.properties
+++ b/examples/gradle.properties
@@ -11,4 +11,4 @@ android.enableD8=true
android.enableBuildScriptClasspathCheck=false
# See https://developer.android.com/studio/build/optimize-your-build#configuration_on_demand
-org.gradle.configureondemand=false
+org.gradle.configureondemand=false
\ No newline at end of file
diff --git a/examples/objectServerActivityTrackerExample/README.md b/examples/objectServerActivityTrackerExample/README.md
new file mode 100644
index 0000000000..1ebf12f2f5
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/README.md
@@ -0,0 +1,45 @@
+# Object Server Example
+
+This project contains a demo app demonstrating how you can build a real world app using modern
+Android components and frameworks together with Realm Sync.
+
+This demo app covers the use case of managing people who have booked an activity during a vacation.
+
+It makes it possible for the person in charge of the activity to handle checkins as well.
+
+
+## Requirements
+
+Two Query-based reference Realms must exist on the server. These must be called:
+
+* `/demo1`
+* `/demo2`
+
+They can be created using an Admin user through Realm Studio.
+
+## Build project
+
+1) Edit `io.realm.examples.objectserver.activitytracker.Constants` and set the proper URL to the Realm Sync server
+2) Compile and install the app `./gradlew clean installDebug`
+
+## Technical Details
+
+The app is built using the following frameworks/libraries:
+
+* Kotlin 1.3
+* Databinding
+* Android Architecture Components: LiveData and ViewModel
+* RxJava
+* Realm Java
+
+For the UI parts the project uses package-by-feature instead of package-by-layer, so e.g. everything
+related to the Excursion selection screen should be in the `io.realm.examples.objectserver.activitytracker.ui.excursionlist`
+package.
+
+The project uses an MVVM architecture. All logic related to controlling the UI should be in the
+various `*ViewModel` classes. These classes expose data using `LiveData` which are consumed by
+the `Activity` using data binding.
+
+As this demo is relatively simple in scope, all business logic are contained within the view model
+classes, this also includes all usages of Realm.
+
diff --git a/examples/objectServerActivityTrackerExample/build.gradle b/examples/objectServerActivityTrackerExample/build.gradle
new file mode 100644
index 0000000000..12c60123b7
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/build.gradle
@@ -0,0 +1,55 @@
+buildscript {
+ ext.kotlin_version = '1.3.21'
+ repositories {
+ google()
+ jcenter()
+ mavenCentral()
+ }
+ dependencies {
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ }
+}
+
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android-extensions'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-kapt'
+apply plugin: 'realm-android'
+
+android {
+ compileSdkVersion rootProject.sdkVersion
+ buildToolsVersion rootProject.buildTools
+
+ defaultConfig {
+ applicationId 'io.realm.examples.objectserver.activitytracker'
+ targetSdkVersion rootProject.sdkVersion
+ minSdkVersion rootProject.minSdkVersion
+ versionCode 1
+ versionName "1.0"
+ }
+
+ dataBinding {
+ enabled = true
+ }
+}
+
+realm {
+ syncEnabled = true
+}
+
+def lifecycle_version = "2.0.0"
+dependencies {
+ implementation 'com.android.support.constraint:constraint-layout:1.1.3'
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+
+ implementation 'io.reactivex.rxjava2:rxjava:2.2.4'
+ implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
+ implementation 'androidx.appcompat:appcompat:1.0.2'
+ implementation 'androidx.cardview:cardview:1.0.0'
+ implementation 'com.google.android.material:material:1.1.0-alpha03'
+
+ implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
+ kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
+ implementation "androidx.lifecycle:lifecycle-reactivestreams-ktx:$lifecycle_version"
+ implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version"
+}
diff --git a/examples/objectServerActivityTrackerExample/gradle.properties b/examples/objectServerActivityTrackerExample/gradle.properties
new file mode 100644
index 0000000000..dbb7bf70d1
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/gradle.properties
@@ -0,0 +1,2 @@
+android.enableJetifier=true
+android.useAndroidX=true
diff --git a/examples/objectServerExample/lint.xml b/examples/objectServerActivityTrackerExample/lint.xml
similarity index 100%
rename from examples/objectServerExample/lint.xml
rename to examples/objectServerActivityTrackerExample/lint.xml
diff --git a/examples/objectServerActivityTrackerExample/src/main/AndroidManifest.xml b/examples/objectServerActivityTrackerExample/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..cfb004a0fc
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/AndroidManifest.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/Constants.kt b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/Constants.kt
new file mode 100644
index 0000000000..7d7d6171a3
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/Constants.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2019 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.examples.objectserver.activitytracker
+
+internal object Constants {
+ /**
+ * Realm Cloud Users:
+ * Replace INSTANCE_ADDRESS with the hostname of your cloud instance
+ * e.g., "mycoolapp.us1.cloud.realm.io"
+ *
+ * ROS On-Premises Users
+ * Replace the INSTANCE_ADDRESS with the fully qualified version of
+ * address of your ROS server, e.g.: INSTANCE_ADDRESS = "192.168.1.65:9080" and "http://" + INSTANCE_ADDRESS + "/auth"
+ * (remember to use 'http/realm' instead of 'https/realms' if you didn't setup SSL on ROS yet)
+ */
+ private val INSTANCE_ADDRESS = "cmelchior.us1.cloud.realm.io"
+ val AUTH_URL = "https://$INSTANCE_ADDRESS/auth"
+ val STANDARD_REALM_URL = "realms://$INSTANCE_ADDRESS/demo1" // Query-based Realm
+ val ORDERS_REALM_URL = "realms://$INSTANCE_ADDRESS/demo2" // Query-based Realm
+
+}
diff --git a/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/MyApplication.kt b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/MyApplication.kt
new file mode 100644
index 0000000000..d0636b0479
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/MyApplication.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2019 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.examples.objectserver.activitytracker
+
+import android.app.Application
+import io.realm.examples.objectserver.activitytracker.model.App
+
+import io.realm.Realm
+import io.realm.SyncUser
+
+class MyApplication : Application() {
+ override fun onCreate() {
+ super.onCreate()
+ Realm.init(this)
+
+ // Initialize the current Realm setup if a user is found
+ // This is required if we user should be allowed to
+ // start in the middle of the app after a crash.
+
+ // Make sure that calling Realm.getDefaultInstance() will throw
+ Realm.removeDefaultConfiguration()
+
+ SyncUser.current()?.let {
+ App.configureRealms(it)
+ }
+ }
+}
diff --git a/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/RealmBaseAdapter.java b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/RealmBaseAdapter.java
new file mode 100644
index 0000000000..619212b1fd
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/RealmBaseAdapter.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2019 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.examples.objectserver.activitytracker;
+
+import android.widget.BaseAdapter;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import io.realm.OrderedRealmCollection;
+import io.realm.RealmChangeListener;
+import io.realm.RealmList;
+import io.realm.RealmModel;
+import io.realm.RealmResults;
+
+/**
+ * The RealmBaseAdapter class is an abstract utility class for binding UI elements to Realm data, much like an
+ * {@link android.widget.CursorAdapter}.
+ *
+ * This adapter will automatically handle any updates to its data and call {@link #notifyDataSetChanged()} as
+ * appropriate.
+ *
+ * The RealmAdapter will stop receiving updates if the Realm instance providing the {@link io.realm.RealmResults} is
+ * closed. Trying to access Realm objects will at this point also result in a {@code IllegalStateException}.
+ */
+public abstract class RealmBaseAdapter extends BaseAdapter {
+ @Nullable
+ protected OrderedRealmCollection adapterData;
+ private final RealmChangeListener> listener;
+
+ public RealmBaseAdapter(@Nullable OrderedRealmCollection data) {
+ if (data != null && !data.isManaged())
+ throw new IllegalStateException("Only use this adapter with managed list, " +
+ "for un-managed lists you can just use the BaseAdapter");
+ this.adapterData = data;
+ this.listener = new RealmChangeListener>() {
+ @Override
+ public void onChange(OrderedRealmCollection results) {
+ notifyDataSetChanged();
+ }
+ };
+
+ if (isDataValid()) {
+ //noinspection ConstantConditions
+ addListener(data);
+ }
+ }
+
+ private void addListener(@NonNull OrderedRealmCollection data) {
+ if (data instanceof RealmResults) {
+ RealmResults results = (RealmResults) data;
+ //noinspection unchecked
+ results.addChangeListener((RealmChangeListener) listener);
+ } else if (data instanceof RealmList) {
+ RealmList list = (RealmList) data;
+ //noinspection unchecked
+ list.addChangeListener((RealmChangeListener) listener);
+ } else {
+ throw new IllegalArgumentException("RealmCollection not supported: " + data.getClass());
+ }
+ }
+
+ private void removeListener(@NonNull OrderedRealmCollection data) {
+ if (data instanceof RealmResults) {
+ RealmResults results = (RealmResults) data;
+ //noinspection unchecked
+ results.removeChangeListener((RealmChangeListener) listener);
+ } else if (data instanceof RealmList) {
+ RealmList list = (RealmList) data;
+ //noinspection unchecked
+ list.removeChangeListener((RealmChangeListener) listener);
+ } else {
+ throw new IllegalArgumentException("RealmCollection not supported: " + data.getClass());
+ }
+ }
+
+ /**
+ * Returns how many items are in the data set.
+ *
+ * @return the number of items.
+ */
+ @Override
+ public int getCount() {
+ //noinspection ConstantConditions
+ return isDataValid() ? adapterData.size() : 0;
+ }
+
+ /**
+ * Get the data item associated with the specified position in the data set.
+ *
+ * @param position Position of the item whose data we want within the adapter's
+ * data set.
+ * @return The data at the specified position.
+ */
+ @Override
+ @Nullable
+ public T getItem(int position) {
+ //noinspection ConstantConditions
+ return isDataValid() ? adapterData.get(position) : null;
+ }
+
+ /**
+ * Get the row value associated with the specified position in the list. Note that item IDs are not stable so you
+ * cannot rely on the item ID being the same after {@link #notifyDataSetChanged()} or
+ * {@link #updateData(OrderedRealmCollection)} has been called.
+ *
+ * @param position The position of the item within the adapter's data set whose row value we want.
+ * @return The value of the item at the specified position.
+ */
+ @Override
+ public long getItemId(int position) {
+ // TODO: find better solution once we have unique IDs
+ return position;
+ }
+
+ /**
+ * Updates the data associated with the Adapter.
+ *
+ * Note that RealmResults and RealmLists are "live" views, so they will automatically be updated to reflect the
+ * latest changes. This will also trigger {@code notifyDataSetChanged()} to be called on the adapter.
+ *
+ * This method is therefore only useful if you want to display data based on a new query without replacing the
+ * adapter.
+ *
+ * @param data the new {@link OrderedRealmCollection} to display.
+ */
+ @SuppressWarnings("WeakerAccess")
+ public void updateData(@Nullable OrderedRealmCollection data) {
+ if (listener != null) {
+ if (isDataValid()) {
+ //noinspection ConstantConditions
+ removeListener(adapterData);
+ }
+ if (data != null && data.isValid()) {
+ addListener(data);
+ }
+ }
+
+ this.adapterData = data;
+ notifyDataSetChanged();
+ }
+
+ private boolean isDataValid() {
+ return adapterData != null && adapterData.isValid();
+ }
+}
\ No newline at end of file
diff --git a/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/model/App.kt b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/model/App.kt
new file mode 100644
index 0000000000..c61727a000
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/model/App.kt
@@ -0,0 +1,46 @@
+package io.realm.examples.objectserver.activitytracker.model
+
+import io.realm.examples.objectserver.activitytracker.Constants
+import io.realm.examples.objectserver.activitytracker.model.entities.Guest
+import io.realm.examples.objectserver.activitytracker.model.entities.Order
+import io.realm.examples.objectserver.activitytracker.model.entities.Activity
+import io.realm.RealmConfiguration
+import io.realm.SyncUser
+import io.realm.kotlin.where
+
+/**
+ * App / Business logic that doesn't naturally fit elsewhere
+ */
+class App {
+
+
+
+
+ companion object {
+
+ lateinit var REALM1_CONFIG: RealmConfiguration
+ lateinit var REALM2_CONFIG: RealmConfiguration
+
+ fun configureRealms(user: SyncUser) {
+ REALM1_CONFIG = user
+ .createConfiguration(Constants.STANDARD_REALM_URL)
+ .initialData {
+ // Initial subscriptions
+ it.where().subscribe("guests")
+ it.where().subscribe("activities")
+ it.where().subscribe("orders")
+ }
+ .build()
+
+ REALM2_CONFIG = user
+ .createConfiguration(Constants.ORDERS_REALM_URL)
+ .initialData {
+ // Initial subscriptions
+ it.where().subscribe("orders")
+ }
+ .build()
+
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/model/entities/Activity.kt b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/model/entities/Activity.kt
new file mode 100644
index 0000000000..e63d9fcec5
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/model/entities/Activity.kt
@@ -0,0 +1,25 @@
+package io.realm.examples.objectserver.activitytracker.model.entities
+
+import io.realm.RealmList
+import io.realm.RealmObject
+import io.realm.annotations.PrimaryKey
+import java.util.*
+
+inline class ActivityId(val value: String) {
+ constructor(): this(UUID.randomUUID().toString())
+}
+
+/**
+ * This object describes a specific activity. An activity can happen multiple times during a day,
+ * each of these are described as a [TimeSlot]
+ *
+ * Guests will book a slot for a given [TimeSlot], not on the [Activity] which just contains
+ * the high-level details.
+ */
+open class Activity: RealmObject() {
+ @PrimaryKey
+ var id: ActivityId = ActivityId()
+ var name:String = ""
+
+ var timeslots: RealmList = RealmList()
+}
\ No newline at end of file
diff --git a/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/model/entities/Booking.kt b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/model/entities/Booking.kt
new file mode 100644
index 0000000000..903c3fcecf
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/model/entities/Booking.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2019 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.examples.objectserver.activitytracker.model.entities
+
+import io.realm.RealmObject
+import io.realm.RealmResults
+import io.realm.annotations.Ignore
+import io.realm.annotations.LinkingObjects
+import io.realm.annotations.PrimaryKey
+import java.util.*
+
+inline class BookingId(val value: String) {
+ constructor(): this(UUID.randomUUID().toString())
+}
+
+/**
+ * Booking object representing both normal and adhoc bookings.
+ */
+open class Booking: RealmObject() {
+
+ @PrimaryKey
+ var id: BookingId = BookingId()
+
+ var guest: Guest? = null
+ var checkedIn: Boolean = false
+ var adhoc: Boolean = false
+ var selected: Boolean = false; // UI-state. Storing here for now for simplicity
+
+ // Should always contain one element, the parent TimeSlot
+ @LinkingObjects("bookings")
+ private val timeslots: RealmResults? = null
+
+ @Ignore
+ var offering: TimeSlot = TimeSlot()
+ get() = if (timeslots != null) timeslots.first()!! else throw IllegalStateException("Only available on managed objects")
+ private set
+
+ @Ignore
+ var excursion: Activity = Activity()
+ get() = if (timeslots != null) timeslots.first()!!.activity else throw IllegalStateException("Only available on managed objects")
+ private set
+}
\ No newline at end of file
diff --git a/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/model/entities/Guest.kt b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/model/entities/Guest.kt
new file mode 100644
index 0000000000..57bb3b7360
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/model/entities/Guest.kt
@@ -0,0 +1,18 @@
+package io.realm.examples.objectserver.activitytracker.model.entities
+
+import io.realm.RealmObject
+import io.realm.annotations.PrimaryKey
+import java.util.*
+
+inline class GuestId(val value: String) {
+ constructor(): this(UUID.randomUUID().toString())
+}
+
+/**
+ * Object describing a Guest that can book activities.
+ */
+open class Guest: RealmObject() {
+ @PrimaryKey
+ var id: GuestId = GuestId()
+ var name: String = ""
+}
\ No newline at end of file
diff --git a/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/model/entities/Order.kt b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/model/entities/Order.kt
new file mode 100644
index 0000000000..cf5ce7b33c
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/model/entities/Order.kt
@@ -0,0 +1,45 @@
+package io.realm.examples.objectserver.activitytracker.model.entities
+
+import io.realm.RealmObject
+import io.realm.annotations.PrimaryKey
+import java.util.*
+
+inline class OrderId(val value: String) {
+ constructor(): this(UUID.randomUUID().toString())
+}
+
+/**
+ * This object represents a drink order in the bar.
+ *
+ */
+open class Order: RealmObject() {
+
+ @PrimaryKey
+ var id: TimeSlotId = TimeSlotId()
+
+ var name: String = ""
+ var createdAt: Date = Date()
+ var from: String = ""
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as Order
+
+ if (id != other.id) return false
+ if (name != other.name) return false
+ if (createdAt != other.createdAt) return false
+ if (from != other.from) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = id.hashCode()
+ result = 31 * result + name.hashCode()
+ result = 31 * result + createdAt.hashCode()
+ result = 31 * result + from.hashCode()
+ return result
+ }
+}
\ No newline at end of file
diff --git a/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/model/entities/Timeslot.kt b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/model/entities/Timeslot.kt
new file mode 100644
index 0000000000..3e63cb679b
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/model/entities/Timeslot.kt
@@ -0,0 +1,50 @@
+package io.realm.examples.objectserver.activitytracker.model.entities
+
+import io.realm.RealmList
+import io.realm.RealmObject
+import io.realm.RealmResults
+import io.realm.annotations.Ignore
+import io.realm.annotations.LinkingObjects
+import io.realm.annotations.PrimaryKey
+import java.util.*
+
+
+inline class TimeSlotId(val value: String) {
+ constructor(): this(UUID.randomUUID().toString())
+}
+
+/**
+ * An offering describes an Activity happening at a specific time.
+ * [Guest]s will book a time on a specific [TimeSlot]
+ */
+open class TimeSlot: RealmObject() {
+ @PrimaryKey
+ var id: TimeSlotId = TimeSlotId()
+
+ var time: Date = Date()
+ var totalNumberOfSlots: Int = 0
+ var bookings: RealmList = RealmList()
+
+ @LinkingObjects("timeslots")
+ val activities: RealmResults? = null
+
+ // Ignore fields cannot be queried
+ // Should always contain one element, the parent Activity. This is only true for managed
+ // objects.
+ @Ignore
+ var activity: Activity = Activity()
+ get() = if (activities != null) activities.first()!! else throw IllegalStateException("Only available on managed objects")
+ private set
+
+ fun remaining(): Int {
+ return bookings.size - bookings.where().equalTo("checkedIn", true).count().toInt()
+ }
+
+ fun checkedIn(): Int {
+ return bookings.where().equalTo("checkedIn", true).count().toInt()
+ }
+
+ fun availableSlots(): Int {
+ return totalNumberOfSlots - bookings.size
+ }
+}
diff --git a/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/BaseActivity.kt b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/BaseActivity.kt
new file mode 100644
index 0000000000..73e56d1b9e
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/BaseActivity.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2019 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.examples.objectserver.activitytracker.ui
+
+import android.content.Intent
+import android.view.Menu
+import android.view.MenuItem
+import androidx.appcompat.app.AppCompatActivity
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import io.realm.examples.objectserver.activitytracker.R
+import io.realm.examples.objectserver.activitytracker.ui.login.LoginActivity
+import io.realm.SyncUser
+
+open class BaseActivity: AppCompatActivity() {
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ menuInflater.inflate(R.menu.menu_items, menu)
+ return true
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ when(item.itemId) {
+ R.id.action_logout -> {
+ for (user in SyncUser.all().values) {
+ // Normally there is only one user present, but in case of
+ // development, you might run into multiple users being
+ // logged in at the same time. So for simplicity, just
+ // log out all users.
+ user.logOut()
+ }
+ val intent = Intent(this, LoginActivity::class.java)
+ intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ startActivity(intent)
+ return true
+ }
+ else -> {
+ return super.onOptionsItemSelected(item)
+ }
+ }
+ }
+
+ // Credit: http://www.albertgao.xyz/2018/04/13/how-to-add-additional-parameters-to-viewmodel-via-kotlin/
+ protected inline fun viewModelFactory(crossinline f: () -> VM) =
+ object : ViewModelProvider.Factory {
+ override fun create(aClass: Class):T = f() as T
+ }
+}
\ No newline at end of file
diff --git a/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/BaseViewModel.kt b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/BaseViewModel.kt
new file mode 100644
index 0000000000..d16d18bbe9
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/BaseViewModel.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2019 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.examples.objectserver.activitytracker.ui
+
+import androidx.lifecycle.ViewModel
+import io.realm.examples.objectserver.activitytracker.model.App
+import io.realm.Realm
+
+/**
+ * Realm-aware base view model class.
+ */
+open class BaseViewModel: ViewModel() {
+
+ protected val realm: Realm = Realm.getInstance(App.REALM1_CONFIG)
+
+ override fun onCleared() {
+ super.onCleared()
+ realm.close()
+ }
+
+}
\ No newline at end of file
diff --git a/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/RealmRecyclerViewAdapter.java b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/RealmRecyclerViewAdapter.java
new file mode 100644
index 0000000000..d92767e7d7
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/RealmRecyclerViewAdapter.java
@@ -0,0 +1,248 @@
+/*
+ * Copyright 2019 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.examples.objectserver.activitytracker.ui;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
+
+import io.realm.OrderedCollectionChangeSet;
+import io.realm.OrderedRealmCollection;
+import io.realm.OrderedRealmCollectionChangeListener;
+import io.realm.RealmList;
+import io.realm.RealmModel;
+import io.realm.RealmResults;
+
+/**
+ * The RealmBaseRecyclerAdapter class is an abstract utility class for binding RecyclerView UI elements to Realm data.
+ *
+ * This adapter will automatically handle any updates to its data and call {@code notifyDataSetChanged()},
+ * {@code notifyItemInserted()}, {@code notifyItemRemoved()} or {@code notifyItemRangeChanged()} as appropriate.
+ *
+ * The RealmAdapter will stop receiving updates if the Realm instance providing the {@link OrderedRealmCollection} is
+ * closed.
+ *
+ * If the adapter contains Realm model classes with a primary key that is either an {@code int} or a {@code long}, call
+ * {@code setHasStableIds(true)} in the constructor and override {@link #getItemId(int)} as described by the Javadoc in that method.
+ *
+ * @param type of {@link RealmModel} stored in the adapter.
+ * @param type of RecyclerView.ViewHolder used in the adapter.
+ * @see RecyclerView.Adapter#setHasStableIds(boolean)
+ * @see RecyclerView.Adapter#getItemId(int)
+ */
+public abstract class RealmRecyclerViewAdapter
+ extends RecyclerView.Adapter {
+
+ private final boolean hasAutoUpdates;
+ private final boolean updateOnModification;
+ private final OrderedRealmCollectionChangeListener listener;
+ @Nullable
+ private OrderedRealmCollection adapterData;
+
+ private OrderedRealmCollectionChangeListener createListener() {
+ return new OrderedRealmCollectionChangeListener() {
+ @Override
+ public void onChange(Object collection, OrderedCollectionChangeSet changeSet) {
+ if (changeSet.getState() == OrderedCollectionChangeSet.State.INITIAL) {
+ notifyDataSetChanged();
+ return;
+ }
+ // For deletions, the adapter has to be notified in reverse order.
+ OrderedCollectionChangeSet.Range[] deletions = changeSet.getDeletionRanges();
+ for (int i = deletions.length - 1; i >= 0; i--) {
+ OrderedCollectionChangeSet.Range range = deletions[i];
+ notifyItemRangeRemoved(range.startIndex + dataOffset(), range.length);
+ }
+
+ OrderedCollectionChangeSet.Range[] insertions = changeSet.getInsertionRanges();
+ for (OrderedCollectionChangeSet.Range range : insertions) {
+ notifyItemRangeInserted(range.startIndex + dataOffset(), range.length);
+ }
+
+ if (!updateOnModification) {
+ return;
+ }
+
+ OrderedCollectionChangeSet.Range[] modifications = changeSet.getChangeRanges();
+ for (OrderedCollectionChangeSet.Range range : modifications) {
+ notifyItemRangeChanged(range.startIndex + dataOffset(), range.length);
+ }
+ }
+ };
+ }
+
+ /**
+ * Returns the number of header elements before the Realm collection elements. This is needed so
+ * all indexes reported by the {@link OrderedRealmCollectionChangeListener} can be adjusted
+ * correctly. Failing to override can cause the wrong elements to animate and lead to runtime
+ * exceptions.
+ *
+ * @return The number of header elements in the RecyclerView before the collection elements. Default is {@code 0}.
+ */
+ public int dataOffset() {
+ return 0;
+ }
+
+ /**
+ * This is equivalent to {@code RealmRecyclerViewAdapter(data, autoUpdate, true)}.
+ *
+ * @param data collection data to be used by this adapter.
+ * @param autoUpdate when it is {@code false}, the adapter won't be automatically updated when collection data
+ * changes.
+ * @see #RealmRecyclerViewAdapter(OrderedRealmCollection, boolean, boolean)
+ */
+ public RealmRecyclerViewAdapter(@Nullable OrderedRealmCollection data, boolean autoUpdate) {
+ this(data, autoUpdate, true);
+ }
+
+ /**
+ * @param data collection data to be used by this adapter.
+ * @param autoUpdate when it is {@code false}, the adapter won't be automatically updated when collection data
+ * changes.
+ * @param updateOnModification when it is {@code true}, this adapter will be updated when deletions, insertions or
+ * modifications happen to the collection data. When it is {@code false}, only
+ * deletions and insertions will trigger the updates. This param will be ignored if
+ * {@code autoUpdate} is {@code false}.
+ */
+ public RealmRecyclerViewAdapter(@Nullable OrderedRealmCollection data, boolean autoUpdate,
+ boolean updateOnModification) {
+ if (data != null && !data.isManaged())
+ throw new IllegalStateException("Only use this adapter with managed RealmCollection, " +
+ "for un-managed lists you can just use the BaseRecyclerViewAdapter");
+ this.adapterData = data;
+ this.hasAutoUpdates = autoUpdate;
+ this.listener = hasAutoUpdates ? createListener() : null;
+ this.updateOnModification = updateOnModification;
+ }
+
+ @Override
+ public void onAttachedToRecyclerView(final RecyclerView recyclerView) {
+ super.onAttachedToRecyclerView(recyclerView);
+ if (hasAutoUpdates && isDataValid()) {
+ //noinspection ConstantConditions
+ addListener(adapterData);
+ }
+ }
+
+ @Override
+ public void onDetachedFromRecyclerView(final RecyclerView recyclerView) {
+ super.onDetachedFromRecyclerView(recyclerView);
+ if (hasAutoUpdates && isDataValid()) {
+ //noinspection ConstantConditions
+ removeListener(adapterData);
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ //noinspection ConstantConditions
+ return isDataValid() ? adapterData.size() : 0;
+ }
+
+ /**
+ * Returns the item in the underlying data associated with the specified position.
+ *
+ * This method will return {@code null} if the Realm instance has been closed or the index
+ * is outside the range of valid adapter data (which e.g. can happen if {@link #getItemCount()}
+ * is modified to account for header or footer views.
+ *
+ * Also, this method does not take into account any header views. If these are present, modify
+ * the {@code index} parameter accordingly first.
+ *
+ * @param index index of the item in the original collection backing this adapter.
+ * @return the item at the specified position or {@code null} if the position does not exists or
+ * the adapter data are no longer valid.
+ */
+ @SuppressWarnings("WeakerAccess")
+ @Nullable
+ public T getItem(int index) {
+ if (index < 0) {
+ throw new IllegalArgumentException("Only indexes >= 0 are allowed. Input was: " + index);
+ }
+
+ // To avoid exception, return null if there are some extra positions that the
+ // child adapter is adding in getItemCount (e.g: to display footer view in recycler view)
+ if(adapterData != null && index >= adapterData.size()) return null;
+ //noinspection ConstantConditions
+ return isDataValid() ? adapterData.get(index) : null;
+ }
+
+ /**
+ * Returns data associated with this adapter.
+ *
+ * @return adapter data.
+ */
+ @Nullable
+ public OrderedRealmCollection getData() {
+ return adapterData;
+ }
+
+ /**
+ * Updates the data associated to the Adapter. Useful when the query has been changed.
+ * If the query does not change you might consider using the automaticUpdate feature.
+ *
+ * @param data the new {@link OrderedRealmCollection} to display.
+ */
+ @SuppressWarnings("WeakerAccess")
+ public void updateData(@Nullable OrderedRealmCollection data) {
+ if (hasAutoUpdates) {
+ if (isDataValid()) {
+ //noinspection ConstantConditions
+ removeListener(adapterData);
+ }
+ if (data != null) {
+ addListener(data);
+ }
+ }
+
+ this.adapterData = data;
+ notifyDataSetChanged();
+ }
+
+ private void addListener(@NonNull OrderedRealmCollection data) {
+ if (data instanceof RealmResults) {
+ RealmResults results = (RealmResults) data;
+ //noinspection unchecked
+ results.addChangeListener(listener);
+ } else if (data instanceof RealmList) {
+ RealmList list = (RealmList) data;
+ //noinspection unchecked
+ list.addChangeListener(listener);
+ } else {
+ throw new IllegalArgumentException("RealmCollection not supported: " + data.getClass());
+ }
+ }
+
+ private void removeListener(@NonNull OrderedRealmCollection data) {
+ if (data instanceof RealmResults) {
+ RealmResults results = (RealmResults) data;
+ //noinspection unchecked
+ results.removeChangeListener(listener);
+ } else if (data instanceof RealmList) {
+ RealmList list = (RealmList) data;
+ //noinspection unchecked
+ list.removeChangeListener(listener);
+ } else {
+ throw new IllegalArgumentException("RealmCollection not supported: " + data.getClass());
+ }
+ }
+
+ private boolean isDataValid() {
+ return adapterData != null && adapterData.isValid();
+ }
+}
\ No newline at end of file
diff --git a/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/SingleLiveEvent.java b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/SingleLiveEvent.java
new file mode 100644
index 0000000000..10c7ea9122
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/SingleLiveEvent.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2019 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.examples.objectserver.activitytracker.ui;
+
+import android.util.Log;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.Observer;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * A lifecycle-aware observable that sends only new updates after subscription, used for events like
+ * navigation and Snackbar messages.
+ *
+ * This avoids a common problem with events: on configuration change (like rotation) an update
+ * can be emitted if the observer is active. This LiveData only calls the observable if there's an
+ * explicit call to setValue() or call().
+ *
+ * Note that only one observer is going to be notified of changes.
+ */
+public class SingleLiveEvent extends MutableLiveData {
+
+ private static final String TAG = "SingleLiveEvent";
+
+ private final AtomicBoolean mPending = new AtomicBoolean(false);
+
+ @MainThread
+ public void observe(LifecycleOwner owner, final Observer super T> observer) {
+
+ if (hasActiveObservers()) {
+ Log.w(TAG, "Multiple observers registered but only one will be notified of changes.");
+ }
+
+ // Observe the internal MutableLiveData
+ super.observe(owner, new Observer() {
+ @Override
+ public void onChanged(@Nullable T t) {
+ if (mPending.compareAndSet(true, false)) {
+ observer.onChanged(t);
+ }
+ }
+ });
+ }
+
+ @MainThread
+ public void setValue(@Nullable T t) {
+ mPending.set(true);
+ super.setValue(t);
+ }
+
+ /**
+ * Used for cases where T is Void, to make calls cleaner.
+ */
+ @MainThread
+ public void call() {
+ setValue(null);
+ }
+}
\ No newline at end of file
diff --git a/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/activitylist/ActivityRecyclerAdapter.kt b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/activitylist/ActivityRecyclerAdapter.kt
new file mode 100644
index 0000000000..22b9674414
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/activitylist/ActivityRecyclerAdapter.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2019 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.examples.objectserver.activitytracker.ui.activitylist
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import io.realm.examples.objectserver.activitytracker.databinding.ItemExcursionListBinding
+import io.realm.examples.objectserver.activitytracker.model.entities.Activity
+import io.realm.OrderedRealmCollection
+
+
+class ActivityRecyclerAdapter(private val viewModel: SelectActivityViewModel, data: OrderedRealmCollection)
+ : io.realm.examples.objectserver.activitytracker.ui.RealmRecyclerViewAdapter(data, true) {
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
+ val layoutInflater = LayoutInflater.from(parent.context)
+ val itemBinding = ItemExcursionListBinding.inflate(layoutInflater, parent, false)
+ return MyViewHolder(itemBinding)
+ }
+
+ override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
+ val item = getItem(position)
+ holder.bind(item!!)
+ }
+
+ // See https://medium.com/androiddevelopers/android-data-binding-recyclerview-db7c40d9f0e4
+ inner class MyViewHolder(private val binding: ItemExcursionListBinding) : RecyclerView.ViewHolder(binding.root) {
+ fun bind(item: Activity) {
+ binding.item = item
+ binding.vm = viewModel
+ binding.executePendingBindings()
+ }
+ }
+}
diff --git a/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/activitylist/SelectActivityActivity.kt b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/activitylist/SelectActivityActivity.kt
new file mode 100644
index 0000000000..b0208b44ae
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/activitylist/SelectActivityActivity.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2019 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.examples.objectserver.activitytracker.ui.activitylist
+
+import android.content.Intent
+import android.os.Bundle
+import android.view.Menu
+import android.view.MenuItem
+import androidx.databinding.DataBindingUtil
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProviders
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import io.realm.examples.objectserver.activitytracker.R
+import io.realm.examples.objectserver.activitytracker.databinding.ActivityExcursionListBinding
+import io.realm.examples.objectserver.activitytracker.ui.BaseActivity
+import io.realm.examples.objectserver.activitytracker.ui.checkin.CheckinActivity
+import io.realm.examples.objectserver.activitytracker.ui.orderlist.OrdersActivity
+
+class SelectActivityActivity : BaseActivity() {
+
+ private lateinit var viewModel: SelectActivityViewModel
+ private lateinit var binding: ActivityExcursionListBinding
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ viewModel = ViewModelProviders.of(this).get(SelectActivityViewModel::class.java)
+ binding = DataBindingUtil.setContentView(this, R.layout.activity_excursion_list)
+ binding.lifecycleOwner = this
+ binding.viewModel = viewModel
+
+ // Setup UI
+ setSupportActionBar(findViewById(R.id.toolbar))
+ val itemsRecyclerAdapter = ActivityRecyclerAdapter(viewModel, viewModel.excursions())
+ val recyclerView = findViewById(R.id.recycler_view)
+ recyclerView.layoutManager = LinearLayoutManager(this)
+ recyclerView.adapter = itemsRecyclerAdapter
+
+ // Setup navigation
+ viewModel.navigateTo.observe(this, Observer {
+ when(it.first) {
+ SelectActivityViewModel.NavigationTarget.ExcursionDetails -> {
+ val intent = Intent(this, CheckinActivity::class.java).apply {
+ putExtra(CheckinActivity.INTENT_EXTRA_ACTIVIY_ID, it.second.value)
+ }
+ startActivity(intent)
+ }
+ SelectActivityViewModel.NavigationTarget.Orders -> {
+ val intent = Intent(this, OrdersActivity::class.java)
+ startActivity(intent)
+ }
+ }
+ })
+ }
+
+ override fun onStart() {
+ super.onStart()
+ viewModel.cleanupSubscriptions()
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ super.onCreateOptionsMenu(menu)
+ menu.apply {
+ findItem(R.id.action_create_demo_data).isVisible = true
+ findItem(R.id.action_goto_orders).isVisible = true
+ }
+ return true
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return when(item.itemId) {
+ R.id.action_create_demo_data -> {
+ viewModel.createDemoData()
+ true
+ }
+ R.id.action_goto_orders -> {
+ viewModel.gotoOrdersSelected()
+ true
+ }
+ else -> {
+ super.onOptionsItemSelected(item)
+ }
+ }
+ }
+}
diff --git a/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/activitylist/SelectActivityViewModel.kt b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/activitylist/SelectActivityViewModel.kt
new file mode 100644
index 0000000000..5e3eca9bfc
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/activitylist/SelectActivityViewModel.kt
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2019 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.examples.objectserver.activitytracker.ui.activitylist
+
+import androidx.lifecycle.LiveData
+import io.realm.examples.objectserver.activitytracker.model.App
+import io.realm.examples.objectserver.activitytracker.model.entities.*
+import io.realm.examples.objectserver.activitytracker.ui.BaseViewModel
+import io.realm.Realm
+import io.realm.RealmList
+import io.realm.RealmResults
+import io.realm.Sort
+import io.realm.kotlin.where
+import java.util.*
+
+class SelectActivityViewModel: BaseViewModel() {
+
+ enum class NavigationTarget {
+ ExcursionDetails,
+ Orders
+ }
+ private val navigationTarget = io.realm.examples.objectserver.activitytracker.ui.SingleLiveEvent>()
+
+ /**
+ * Returns the list of all available activities for the day
+ */
+ fun excursions(): RealmResults {
+ // For now just assume that all activities are available for the day
+ // If not, the data model might be a bit tricky since Realm Java doesn't support
+ // sub-queries yet. So finding the Activity objects with timeslots might
+ // be a bit tricky. See https://github.com/realm/realm-java/issues/1598 and
+ // https://github.com/realm/realm-java/pull/6116
+ return realm.where()
+ .sort("name", Sort.ASCENDING)
+ .findAllAsync("activities.today")
+ }
+
+ /**
+ * Select a given activity
+ */
+ fun excursionSelected(excursion: Activity) {
+ navigationTarget.value = Pair(NavigationTarget.ExcursionDetails, excursion.id)
+ }
+
+ fun gotoOrdersSelected() {
+ navigationTarget.value = Pair(NavigationTarget.Orders, ActivityId())
+ }
+
+ /**
+ * Navigation event. Used by the Activity to navigationTarget to the selected navigationTarget
+ */
+ val navigateTo : LiveData>
+ get() = navigationTarget
+
+
+ /**
+ * This method will loop through the users subscriptions and remove those that no longer is
+ * needed.
+ */
+ fun cleanupSubscriptions() {
+ realm.executeTransactionAsync {
+
+ // All subscriptions used when searching for guests part of a Booking
+ // See `BookingsListViewModel.kt#131`
+ it.getSubscriptions("bookings.*.*.*").deleteAllFromRealm()
+
+ // Right now we keep all subscriptions for Offerings in `CheckinViewModel.kt#37`
+ // These should only live for a ~day. Until we get time-based subscription support
+ // in Realm we need to manually remove them.
+ // For this demo, just keep them, but the date should be encoded in the name
+ // e.g. `timeslots.dd-mm-yyy` so we can easily identify and remove them here.
+ // TODO()
+ }
+ }
+
+ /**
+ * Creates a number of randomized demo data
+ */
+ fun createDemoData() {
+ val random = Random()
+ realm.executeTransactionAsync { realm ->
+ // Generate guests
+ val firstNames = listOf("John", "Jane", "Sean", "Sofia", "Marisa", "Scott", "Peter", "Brian", "Julia")
+ val lastNames = listOf("Atkinson", "Reynolds", "Gibson", "Glenn", "Holland", "Brooks", "Hope", "McDonalds")
+ repeat(20) {
+ val guest = Guest()
+ guest.name = "${firstNames.random()} ${lastNames.random()}"
+ realm.insertOrUpdate(guest)
+ }
+ val guests = realm.where().findAll()
+
+ // Generate Shore Excursions
+ val titles = listOf(
+ "Snorkeling",
+ "Shark fishing",
+ "Scuba diving",
+ "Thrill Waterpark All Day Pass")
+ for (name in titles) {
+ val excursion = Activity()
+ excursion.name = name
+ excursion.timeslots = RealmList()
+
+ // Generate timeslots
+ repeat(5) {
+ val offering = TimeSlot()
+ offering.totalNumberOfSlots = random.nextInt(20) + 1 // Avoid 0 as number of slots
+ offering.time = Date()
+ offering.time.hours = offering.time.hours + it // Ignore overflow into the next day
+
+ // Add bookings to each offering
+ repeat(random.nextInt(offering.totalNumberOfSlots)) {
+ val booking = Booking()
+ booking.checkedIn = random.nextBoolean()
+ booking.guest = guests.random()
+ booking.adhoc = (random.nextInt(5) == 0)
+ offering.bookings.add(booking)
+ }
+ excursion.timeslots.add(offering)
+ }
+ realm.insertOrUpdate(excursion)
+ }
+
+ repeat(10) { i ->
+ val o = Order()
+ o.name = "Drink $i"
+ o.from = "Realm1"
+ o.createdAt = Date(Math.abs(random.nextInt()).toLong())
+ realm.insertOrUpdate(o)
+ }
+ }
+
+ val realm2 = Realm.getInstance(App.REALM2_CONFIG)
+ realm2.executeTransactionAsync(Realm.Transaction { realm ->
+ repeat(10) { i ->
+ val o = Order()
+ o.name = "Drink $i"
+ o.from = "Realm2"
+ o.createdAt = Date(Math.abs(random.nextInt()).toLong())
+ realm.insertOrUpdate(o)
+ }
+ }, Realm.Transaction.OnSuccess { realm2.close() })
+ }
+
+}
\ No newline at end of file
diff --git a/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/bookingslist/BookingsListActivity.kt b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/bookingslist/BookingsListActivity.kt
new file mode 100644
index 0000000000..82186deb69
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/bookingslist/BookingsListActivity.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2019 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.examples.objectserver.activitytracker.ui.bookingslist
+
+import android.os.Bundle
+import android.text.Editable
+import android.text.TextWatcher
+import androidx.databinding.DataBindingUtil
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProviders
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import io.realm.examples.objectserver.activitytracker.R
+import io.realm.examples.objectserver.activitytracker.databinding.ActivityBookingsListBinding
+import io.realm.examples.objectserver.activitytracker.model.entities.TimeSlotId
+import io.realm.examples.objectserver.activitytracker.ui.BaseActivity
+import java.util.*
+
+
+class BookingsListActivity : BaseActivity() {
+
+ companion object {
+ const val INTENT_EXTRA_OFFERING_ID = "BookingsListActivity.TimeSlotId"
+ const val INTENT_ACTION_CHECKIN = "BookingsListActivity.Checkin"
+ const val INTENT_ACTION_CANCEL_CHECKIN = "BookingsListActivity.CancelCheckin"
+ }
+
+ private lateinit var viewModel: BookingsListViewModel
+ private lateinit var binding: ActivityBookingsListBinding
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ val id = TimeSlotId(intent.getStringExtra(INTENT_EXTRA_OFFERING_ID))
+ viewModel = when (intent.action) {
+ INTENT_ACTION_CHECKIN -> {
+ ViewModelProviders.of(
+ this,
+ viewModelFactory { BookingsListViewModel(id, BookingsListViewModel.Mode.CheckIn) }
+ ).get(BookingsListViewModel::class.java)
+ }
+ INTENT_ACTION_CANCEL_CHECKIN -> {
+ ViewModelProviders.of(
+ this,
+ viewModelFactory { BookingsListViewModel(id, BookingsListViewModel.Mode.CancelCheckIn) }
+ ).get(BookingsListViewModel::class.java)
+ }
+ else -> throw IllegalArgumentException("Unsupported action: ${intent.action}")
+ }
+
+ binding = DataBindingUtil.setContentView(this, R.layout.activity_bookings_list)
+ binding.lifecycleOwner = this
+ binding.viewModel = viewModel
+
+ // Setup UI
+ setSupportActionBar(binding.toolbar)
+ viewModel.title().observe(this, Observer {
+ binding.toolbar.title = it
+ })
+
+ binding.textSearch.addTextChangedListener(object: TextWatcher {
+ private var timer = Timer()
+ private val DELAY: Long = 200 // milliseconds
+ override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
+ override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
+ override fun afterTextChanged(s: Editable) {
+ timer.cancel()
+ timer = Timer()
+ timer.schedule(object: TimerTask() {
+ override fun run() {
+ runOnUiThread {
+ viewModel.setSearchCriteria(binding.textSearch.text.toString())
+ }
+ }
+ }, DELAY)
+ }
+ })
+
+ val recyclerView = findViewById(R.id.recycler_view)
+ recyclerView.layoutManager = LinearLayoutManager(this)
+ viewModel.bookings().observe(this, Observer {
+ // If the query is updated we need to replace the query result completely instead
+ // of getting fine-grained animations. This is a bit sub-optimal, but the best we
+ // can do so far. See https://github.com/realm/realm-java/issues/6216
+ val bookingsAdapter = BookingsRecyclerAdapter(viewModel, it)
+ recyclerView.adapter = bookingsAdapter
+ })
+
+ // Setup navigation
+ viewModel.navigate().observe(this, Observer {
+ when(it.first) {
+ // For now, just assume we always return to the Checkin overview screen
+ BookingsListViewModel.NavigationTarget.CheckinOverview -> finish()
+ }
+ })
+ }
+}
diff --git a/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/bookingslist/BookingsListViewModel.kt b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/bookingslist/BookingsListViewModel.kt
new file mode 100644
index 0000000000..40e4db3632
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/bookingslist/BookingsListViewModel.kt
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2019 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.examples.objectserver.activitytracker.ui.bookingslist
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.LiveDataReactiveStreams
+import androidx.lifecycle.MutableLiveData
+import io.realm.examples.objectserver.activitytracker.model.entities.Booking
+import io.realm.examples.objectserver.activitytracker.model.entities.TimeSlot
+import io.realm.examples.objectserver.activitytracker.model.entities.TimeSlotId
+import io.realm.examples.objectserver.activitytracker.ui.BaseViewModel
+import io.realm.Case
+import io.realm.Realm
+import io.realm.RealmResults
+import io.realm.Sort
+import io.realm.kotlin.where
+
+class BookingsListViewModel(private val timeslotId: TimeSlotId, private val mode: Mode): BaseViewModel() {
+
+ enum class Mode {
+ CheckIn,
+ CancelCheckIn
+ }
+
+ enum class NavigationTarget {
+ CheckinOverview,
+ }
+
+ private val navigateTo = MutableLiveData>()
+ private val title: MutableLiveData = MutableLiveData()
+ private val bookings: MutableLiveData> = MutableLiveData()
+ private var selectedOffering: TimeSlot // Strong reference to keep change listener alive
+ private val actionText: LiveData
+
+ init {
+ selectedOffering = realm.where().equalTo("id", timeslotId.value).findFirstAsync()
+ selectedOffering.addChangeListener { obj ->
+ title.value = if (obj.isValid) obj.activity.name else "Activity not found"
+ }
+ bookings.value = createSearchQuery("")
+ actionText = LiveDataReactiveStreams.fromPublisher(realm.where()
+ .equalTo("timeslots.id", timeslotId.value)
+ .equalTo("selected", true)
+ .findAllAsync()
+ .asFlowable()
+ .map {
+ when(mode) {
+ Mode.CheckIn -> "Check-in (${it.count()})"
+ Mode.CancelCheckIn -> "Cancel check-in (${it.count()})"
+ }
+ })
+ }
+
+ /**
+ * Returns the list of all relevant bookings
+ */
+ fun bookings(): LiveData> {
+ return bookings
+ }
+
+ /**
+ * Returns the title of the activity
+ */
+ fun title(): LiveData {
+ return title
+ }
+
+ /**
+ * Observe navigation events from this ViewModel
+ */
+ fun navigate(): LiveData> {
+ return navigateTo
+ }
+
+ fun setSearchCriteria(searchString: String) {
+ bookings.value = createSearchQuery(searchString)
+ }
+
+ fun toggleSelected(booking: Booking) {
+ val id = booking.id
+ realm.executeTransactionAsync {
+ // We cheat a bit here and save the UI state to the Realm.
+ // Most likely we don't want to synchronize a temporary state as "selected"
+ // to the Realm, but it simplifies the logic quite a lot.
+ //
+ // Otherwise we will need to combine two streams of data: RealmResults and SelectedBookings
+ // which is a bit complex for a POC, especially if we also want fine-grained animations.
+ it.where().equalTo("id", id.value).findFirst()?.let { booking ->
+ booking.selected = !booking.selected
+ }
+ }
+ }
+
+ /**
+ * Returns the String used to describe the action performed on selected guests.
+ */
+ fun actionText(): LiveData {
+ return actionText
+ }
+
+ /**
+ * Toggle the checked in state and return to the Check-in overview screen
+ */
+ fun actionSelected() {
+ val id = timeslotId.value
+ realm.executeTransactionAsync(Realm.Transaction {
+ it.where()
+ .equalTo("timeslots.id", id)
+ .equalTo("checkedIn", mode != Mode.CheckIn)
+ .equalTo("selected", true)
+ .findAll()
+ .createSnapshot() // Updated to keep objects in query result even after they are updated
+ .forEach {obj ->
+ obj.selected = false
+ obj.checkedIn = (mode == Mode.CheckIn)
+ }
+ },
+ Realm.Transaction.OnSuccess {
+ navigateTo.value = Pair(NavigationTarget.CheckinOverview, timeslotId)
+ })
+ }
+
+ // Creates the query
+ private fun createSearchQuery(nameFilter: String): RealmResults? {
+ cleanupSubscriptions()
+ val query = realm.where()
+ .equalTo("timeslots.id", timeslotId.value)
+ .equalTo("checkedIn", mode == Mode.CancelCheckIn)
+ .like("guest.name", "*$nameFilter*", Case.INSENSITIVE)
+ .sort("guest.name", Sort.ASCENDING)
+
+ // Use a structured approach to naming subscriptions so we can easily find them later
+ return query.findAllAsync("bookings.${timeslotId.value}.$mode.$nameFilter") //
+ }
+
+ /**
+ * This method cleanup all subscriptions related to this screen
+ * It does so in an optimistic manner, so e.g. crashes or killing the app on this screen
+ * will cause subscriptions to linger.
+ *
+ * Having a large amounts of subscriptions is not a problem for the device, but can have
+ * negative performance implications for the server. Having some periodic cleanup routine
+ * is advised to catch stray subscriptions.
+ */
+ private fun cleanupSubscriptions() {
+ val id = timeslotId.value
+ realm.executeTransactionAsync {
+ it.getSubscriptions("bookings.$id.*").deleteAllFromRealm()
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/bookingslist/BookingsRecyclerAdapter.kt b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/bookingslist/BookingsRecyclerAdapter.kt
new file mode 100644
index 0000000000..da44a135fb
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/bookingslist/BookingsRecyclerAdapter.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2019 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.examples.objectserver.activitytracker.ui.bookingslist
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import io.realm.examples.objectserver.activitytracker.databinding.ItemBookingsListBinding
+import io.realm.examples.objectserver.activitytracker.model.entities.Booking
+import io.realm.examples.objectserver.activitytracker.ui.RealmRecyclerViewAdapter
+import io.realm.OrderedRealmCollection
+
+
+class BookingsRecyclerAdapter(private val viewModel: BookingsListViewModel, data: OrderedRealmCollection)
+ : io.realm.examples.objectserver.activitytracker.ui.RealmRecyclerViewAdapter(data, true) {
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
+ val layoutInflater = LayoutInflater.from(parent.context)
+ val itemBinding = ItemBookingsListBinding.inflate(layoutInflater, parent, false)
+ return MyViewHolder(itemBinding)
+ }
+
+ override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
+ val item = getItem(position)
+ holder.bind(item!!)
+ }
+
+ // See https://medium.com/androiddevelopers/android-data-binding-recyclerview-db7c40d9f0e4
+ inner class MyViewHolder(private val binding: io.realm.examples.objectserver.activitytracker.databinding.ItemBookingsListBinding) : RecyclerView.ViewHolder(binding.root) {
+ fun bind(item: Booking) {
+ binding.item = item
+ binding.vm = viewModel
+ binding.executePendingBindings()
+ }
+ }
+}
diff --git a/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/checkin/CheckinActivity.kt b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/checkin/CheckinActivity.kt
new file mode 100644
index 0000000000..7af1b154d8
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/checkin/CheckinActivity.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2019 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.examples.objectserver.activitytracker.ui.checkin
+
+import android.content.Intent
+import android.os.Bundle
+import android.view.View
+import android.widget.AdapterView
+import android.widget.Toast
+import androidx.databinding.DataBindingUtil
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProviders
+import io.realm.examples.objectserver.activitytracker.R
+import io.realm.examples.objectserver.activitytracker.databinding.ActivityCheckinBinding
+import io.realm.examples.objectserver.activitytracker.model.entities.ActivityId
+import io.realm.examples.objectserver.activitytracker.ui.BaseActivity
+import io.realm.examples.objectserver.activitytracker.ui.bookingslist.BookingsListActivity
+
+
+class CheckinActivity : BaseActivity() {
+
+ companion object {
+ const val INTENT_EXTRA_ACTIVIY_ID = "CheckinActivity.activityId"
+ }
+
+ private lateinit var viewModel: CheckinViewModel
+ private lateinit var binding: ActivityCheckinBinding
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val selectedExcursion = ActivityId(intent.getStringExtra(INTENT_EXTRA_ACTIVIY_ID))
+ viewModel = ViewModelProviders.of(
+ this,
+ viewModelFactory { CheckinViewModel(selectedExcursion) }
+ ).get(CheckinViewModel::class.java)
+ binding = DataBindingUtil.setContentView(this, R.layout.activity_checkin)
+ binding.lifecycleOwner = this
+ binding.viewModel = viewModel
+
+ // Setup UI
+ setSupportActionBar(findViewById(R.id.toolbar))
+ viewModel.title().observe(this, Observer {
+ binding.toolbar.title = it
+ })
+
+ val timeslotAdapter = TimeslotAdapter(this, viewModel.timeslots())
+ binding.timeslots.adapter = timeslotAdapter
+ binding.timeslots.onItemSelectedListener = object: AdapterView.OnItemSelectedListener {
+ override fun onNothingSelected(parent: AdapterView<*>?) {
+ viewModel.selectOffering(null)
+ }
+
+ override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
+ viewModel.selectOffering(timeslotAdapter.getItem(position)!!)
+ }
+ }
+
+ // Setup navigation
+ viewModel.navigate().observe(this, Observer { target ->
+ when(target.first) {
+ CheckinViewModel.NavigationTarget.CheckedInGuests -> {
+ val intent = Intent(this, BookingsListActivity::class.java).apply {
+ putExtra(BookingsListActivity.INTENT_EXTRA_OFFERING_ID, target.second.value)
+ action = BookingsListActivity.INTENT_ACTION_CANCEL_CHECKIN
+ }
+ startActivity(intent)
+ }
+ CheckinViewModel.NavigationTarget.RemainingGuests -> {
+ val intent = Intent(this, BookingsListActivity::class.java).apply {
+ putExtra(BookingsListActivity.INTENT_EXTRA_OFFERING_ID, target.second.value)
+ action = BookingsListActivity.INTENT_ACTION_CHECKIN
+ }
+ startActivity(intent)
+ }
+ CheckinViewModel.NavigationTarget.AdhocGuest -> {
+ Toast.makeText(this, "Not implemented", Toast.LENGTH_SHORT).show()
+ }
+ }
+ })
+
+ // Select default starting time
+ if (!timeslotAdapter.isEmpty) {
+ viewModel.selectOffering(timeslotAdapter.getItem(0)!!)
+ }
+ }
+}
diff --git a/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/checkin/CheckinViewModel.kt b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/checkin/CheckinViewModel.kt
new file mode 100644
index 0000000000..0393e69f1f
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/checkin/CheckinViewModel.kt
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2019 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.examples.objectserver.activitytracker.ui.checkin
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.Transformations
+import io.realm.examples.objectserver.activitytracker.model.entities.*
+import io.realm.examples.objectserver.activitytracker.ui.BaseViewModel
+import io.realm.RealmResults
+import io.realm.Sort
+import io.realm.kotlin.where
+
+class CheckinViewModel(private val excursionId: ActivityId): BaseViewModel() {
+
+ enum class NavigationTarget {
+ CheckedInGuests,
+ RemainingGuests,
+ AdhocGuest
+ }
+
+ private val navigationTarget = MutableLiveData>()
+ private val selectedTimeslot: MutableLiveData = MutableLiveData()
+ private val title: MutableLiveData = MutableLiveData()
+ private val selectedExcursion: Activity
+
+ init {
+ selectedExcursion = realm.where().equalTo("id", excursionId.value).findFirstAsync()
+ selectedExcursion.addChangeListener { obj ->
+ title.value = if (obj.isValid) obj.name else "Excursion not found"
+ }
+ selectedTimeslot.value = null // Set dummy value to initialize other streams
+ }
+
+ /**
+ * Returns the number of timeslots for the day
+ */
+ fun timeslots(): RealmResults {
+ return realm.where()
+ .equalTo("activities.id", excursionId.value)
+ .sort("time", Sort.ASCENDING)
+ .findAllAsync("timeslots.${excursionId.value}")
+ }
+
+ /**
+ * Returns the title of the activity
+ */
+ fun title(): LiveData {
+ return title
+ }
+
+ /**
+ * Returns the number of checked in guests
+ */
+ fun checkedIn(): LiveData {
+ return Transformations.map(selectedTimeslot) {
+ it?.checkedIn()?.toString() ?: "-"
+ }
+ }
+
+ /**
+ * Returns the number of guests not checked in yet
+ */
+ fun remaining(): LiveData {
+ return Transformations.map(selectedTimeslot) {
+ it?.remaining()?.toString() ?: "-"
+ }
+ }
+
+ /**
+ * Returns a representation of how many slots are still available
+ */
+ fun slotsAvailable(): LiveData {
+ return Transformations.map(selectedTimeslot) {
+ if (it != null) {
+ "${it.availableSlots()}/${it.totalNumberOfSlots}"
+ } else {
+ "-/-"
+ }
+ }
+ }
+
+ // Strong reference to prevent changelistener from being GC'ed
+ private lateinit var allBookings: RealmResults
+
+ fun selectOffering(offering: TimeSlot?) {
+ selectedTimeslot.value?.removeAllChangeListeners()
+
+ if (offering != null) {
+ offering.addChangeListener { obj ->
+ // The offering itself was updated
+ selectedTimeslot.value = obj
+ }
+
+ allBookings = offering.bookings.where().findAllAsync()
+ allBookings.addChangeListener { results ->
+ // Some of the bookings associated with the offering was updated
+ // The changelistener for TimeSlot is not triggered if an element in an list is modified
+ // Only if the list element is added or removed to the list. For that reason we need
+ // a seperate listener on all the bookings
+ if (!results.isEmpty()) {
+ selectedTimeslot.value = results.first()?.offering
+ }
+ }
+ }
+
+ selectedTimeslot.value = offering
+ }
+
+ /**
+ * Observe navigation events from this ViewModel
+ */
+ fun navigate(): LiveData> {
+ return navigationTarget
+ }
+
+ fun adhocCheckinSelected() {
+ val offering = selectedTimeslot.value
+ if (offering != null) {
+ navigationTarget.value = Pair(NavigationTarget.AdhocGuest, offering.id)
+ }
+ }
+
+ fun remainingGuestsSelected() {
+ val offering = selectedTimeslot.value
+ if (offering != null) {
+ navigationTarget.value = Pair(NavigationTarget.RemainingGuests, offering.id)
+ }
+ }
+
+ fun checkedInGuestsSelected() {
+ val offering = selectedTimeslot.value
+ if (offering != null) {
+ navigationTarget.value = Pair(NavigationTarget.CheckedInGuests, offering.id)
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/checkin/TimeslotAdapter.kt b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/checkin/TimeslotAdapter.kt
new file mode 100644
index 0000000000..9a004d40fa
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/checkin/TimeslotAdapter.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2019 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.examples.objectserver.activitytracker.ui.checkin
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import io.realm.examples.objectserver.activitytracker.R
+import io.realm.examples.objectserver.activitytracker.RealmBaseAdapter
+import io.realm.examples.objectserver.activitytracker.model.entities.TimeSlot
+import io.realm.RealmResults
+
+
+class TimeslotAdapter(context: Context, items: RealmResults): RealmBaseAdapter(items) {
+
+ val formatter = android.text.format.DateFormat.getTimeFormat(context)
+
+ override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
+ val itemView = LayoutInflater.from(parent?.context).inflate(R.layout.spinner_item, parent, false)
+ itemView.findViewById(android.R.id.text1).text = formatTime(getItem(position))
+ return itemView
+ }
+
+ override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup?): View {
+ val itemView = LayoutInflater.from(parent?.context).inflate(R.layout.spinner_item_drop_down, parent, false)
+ itemView.findViewById(android.R.id.text1).text = formatTime(getItem(position))
+ return itemView
+ }
+
+ private fun formatTime(item: TimeSlot?): String {
+ if (item == null) {
+ return "-"
+ } else {
+ return formatter.format(item.time)
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/login/LoginActivity.kt b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/login/LoginActivity.kt
new file mode 100644
index 0000000000..bee5d7f8b1
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/login/LoginActivity.kt
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2019 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.examples.objectserver.activitytracker.ui.login
+
+import android.app.ProgressDialog
+import android.content.Intent
+import android.os.Bundle
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.widget.AppCompatButton
+import androidx.appcompat.widget.AppCompatEditText
+import androidx.databinding.DataBindingUtil
+import io.realm.examples.objectserver.activitytracker.Constants
+import io.realm.examples.objectserver.activitytracker.R
+import io.realm.examples.objectserver.activitytracker.databinding.ActivityLoginBinding
+import io.realm.examples.objectserver.activitytracker.model.App
+import io.realm.examples.objectserver.activitytracker.ui.activitylist.SelectActivityActivity
+import io.realm.ErrorCode
+import io.realm.ObjectServerError
+import io.realm.SyncCredentials
+import io.realm.SyncUser
+
+class LoginActivity: AppCompatActivity() {
+
+ private lateinit var username: AppCompatEditText
+ private lateinit var password: AppCompatEditText
+ private lateinit var loginButton: AppCompatButton
+ private lateinit var createUserButton: AppCompatButton
+
+ lateinit private var binding: ActivityLoginBinding
+
+ public override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = DataBindingUtil.setContentView(this, R.layout.activity_login)
+ username = binding.inputUsername
+ password = binding.inputPassword
+ loginButton = binding.buttonLogin
+ createUserButton = binding.buttonCreate
+
+ loginButton.setOnClickListener { login(false) }
+ createUserButton.setOnClickListener { login(true) }
+ }
+
+ private fun login(createUser: Boolean) {
+ if (!validate()) {
+ onLoginFailed("Invalid username or password")
+ return
+ }
+
+ binding.buttonCreate.isEnabled = false
+ binding.buttonLogin.isEnabled = false
+
+ val progressDialog = ProgressDialog(this@LoginActivity)
+ progressDialog.isIndeterminate = true
+ progressDialog.setMessage("Authenticating...")
+ progressDialog.show()
+
+ val username = this.username.text.toString()
+ val password = this.password.text.toString()
+
+ val creds = SyncCredentials.usernamePassword(username, password, createUser)
+ val callback = object : SyncUser.Callback {
+ override fun onSuccess(user: SyncUser) {
+ progressDialog.dismiss()
+ onLoginSuccess(user)
+ }
+
+ override fun onError(error: ObjectServerError) {
+ progressDialog.dismiss()
+ val errorMsg: String = when (error.errorCode) {
+ ErrorCode.UNKNOWN_ACCOUNT -> getString(R.string.login_error_unknown_account)
+ ErrorCode.INVALID_CREDENTIALS -> getString(R.string.login_error_invalid_credentials)
+ else -> error.toString()
+ }
+ onLoginFailed(errorMsg)
+ }
+ }
+
+ SyncUser.logInAsync(creds, Constants.AUTH_URL, callback)
+ }
+
+ override fun onBackPressed() {
+ moveTaskToBack(true)
+ }
+
+ private fun onLoginSuccess(user: SyncUser) {
+ loginButton.isEnabled = true
+ createUserButton.isEnabled = true
+ App.configureRealms(user)
+ val intent = Intent(this, SelectActivityActivity::class.java)
+ startActivity(intent)
+ }
+
+ private fun onLoginFailed(errorMsg: String) {
+ loginButton.isEnabled = true
+ createUserButton.isEnabled = true
+ Toast.makeText(baseContext, errorMsg, Toast.LENGTH_LONG).show()
+ }
+
+ private fun validate(): Boolean = when {
+ username.text.toString().isEmpty() -> false
+ password.text.toString().isEmpty() -> false
+ else -> true
+ }
+}
diff --git a/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/orderlist/OrdersActivity.kt b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/orderlist/OrdersActivity.kt
new file mode 100644
index 0000000000..08651d7262
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/orderlist/OrdersActivity.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2019 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.examples.objectserver.activitytracker.ui.orderlist
+
+import android.os.Bundle
+import android.view.Menu
+import android.view.MenuItem
+import androidx.databinding.DataBindingUtil
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProviders
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import io.realm.examples.objectserver.activitytracker.ui.BaseActivity
+import io.realm.examples.objectserver.activitytracker.R
+
+
+class OrdersActivity : BaseActivity() {
+
+ private lateinit var viewModel: OrdersViewModel
+ private lateinit var binding: io.realm.examples.objectserver.activitytracker.databinding.ActivityOrderListBinding
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ viewModel = ViewModelProviders.of(this).get(OrdersViewModel::class.java)
+ binding = DataBindingUtil.setContentView(this, R.layout.activity_order_list)
+ binding.lifecycleOwner = this
+ binding.viewModel = viewModel
+
+ // Setup UI
+ setSupportActionBar(findViewById(R.id.toolbar))
+ val adapter = OrdersRecyclerAdapter(viewModel)
+ val recyclerView = findViewById(R.id.recycler_view)
+ recyclerView.layoutManager = LinearLayoutManager(this)
+ recyclerView.adapter = adapter
+ viewModel.orders().observe(this, Observer {
+ adapter.setData(it.first)
+ val diffResults = it.second
+ if (diffResults != null) {
+ diffResults.dispatchUpdatesTo(adapter)
+ } else {
+ adapter.notifyDataSetChanged()
+ }
+ })
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ super.onCreateOptionsMenu(menu)
+ menu.apply {
+ findItem(R.id.action_create_order).isVisible = true
+ }
+ return true
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return when(item.itemId) {
+ R.id.action_create_order -> {
+ viewModel.createOrder()
+ true
+ }
+ else -> {
+ super.onOptionsItemSelected(item)
+ }
+ }
+ }
+}
diff --git a/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/orderlist/OrdersRecyclerAdapter.kt b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/orderlist/OrdersRecyclerAdapter.kt
new file mode 100644
index 0000000000..3a36778453
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/orderlist/OrdersRecyclerAdapter.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2019 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.examples.objectserver.activitytracker.ui.orderlist
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import io.realm.examples.objectserver.activitytracker.databinding.ItemOrderListBinding
+import io.realm.examples.objectserver.activitytracker.model.entities.Order
+
+
+class OrdersRecyclerAdapter(private val viewModel: OrdersViewModel): RecyclerView.Adapter() {
+
+ private var data: List = listOf()
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
+ val layoutInflater = LayoutInflater.from(parent.context)
+ val itemBinding = ItemOrderListBinding.inflate(layoutInflater, parent, false)
+ return MyViewHolder(itemBinding)
+ }
+
+ override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
+ val item = data[position]
+ holder.bind(item)
+ }
+
+ override fun getItemCount(): Int {
+ return data.size
+ }
+
+ fun setData(data: List) {
+ this.data = data
+ }
+
+ // See https://medium.com/androiddevelopers/android-data-binding-recyclerview-db7c40d9f0e4
+ inner class MyViewHolder(private val binding: ItemOrderListBinding) : RecyclerView.ViewHolder(binding.root) {
+ fun bind(item: Order) {
+ binding.item = item
+ binding.vm = viewModel
+ binding.executePendingBindings()
+ }
+ }
+}
diff --git a/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/orderlist/OrdersViewModel.kt b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/orderlist/OrdersViewModel.kt
new file mode 100644
index 0000000000..9e982adc94
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/orderlist/OrdersViewModel.kt
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2019 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.examples.objectserver.activitytracker.ui.orderlist
+
+import android.os.HandlerThread
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.LiveDataReactiveStreams
+import androidx.recyclerview.widget.DiffUtil
+import io.realm.examples.objectserver.activitytracker.model.App
+import io.realm.examples.objectserver.activitytracker.model.entities.Order
+import io.realm.examples.objectserver.activitytracker.ui.BaseViewModel
+import io.realm.examples.objectserver.activitytracker.ui.shared.createObservableForRealm
+import io.reactivex.BackpressureStrategy
+import io.reactivex.Flowable
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.functions.BiFunction
+import io.reactivex.schedulers.Schedulers
+import io.realm.OrderedCollectionChangeSet
+import io.realm.kotlin.where
+import java.util.*
+
+class OrdersViewModel: BaseViewModel() {
+
+ private val worker1 = HandlerThread("HandlerWorker1")
+ private val worker2 = HandlerThread("HandlerWorker2")
+
+ init {
+ worker1.start()
+ worker2.start()
+ }
+
+ fun createOrder() {
+ realm.executeTransactionAsync {
+ val o = Order()
+ o.name = "Custom drink"
+ o.from = "Realm1"
+ o.createdAt = Date(Random().nextInt(1000).toLong())
+ it.insertOrUpdate(o)
+ }
+ }
+
+ fun orders(): LiveData, DiffUtil.DiffResult?>> {
+ // Create Streams from each Realm.
+ //
+ // Note especially the special `createObservableForRealm` which is required to manage
+ // the lifecycle of the Realm instance within the stream
+ // Also, be careful how results from this Realm are used. In our case we keep all
+ // managed results on the same thread until they have been copied out, after which they
+ // can be observed on other threads without issues.
+ //
+ // We ignore the INITIAL state as that is mostly used for checking the Subscription state
+ // which isn't required here. So we just wait for any UPDATE state which will follow
+ // immediately after as the Subscription is either created or updated. This way we also
+ // skip doing a lof of extra work in the streams not needed.
+ val worker1Looper = AndroidSchedulers.from(worker1.looper)
+ val realm1Orders = Flowable.just(App.REALM1_CONFIG)
+ .observeOn(worker1Looper)
+ .flatMap { createObservableForRealm(it) }
+ .flatMap { it.where().findAllAsync().asChangesetObservable().toFlowable(BackpressureStrategy.ERROR) }
+ .filter { it.changeset?.state != OrderedCollectionChangeSet.State.INITIAL }
+ .filter { it.collection.isLoaded }
+ .map { it.collection.realm.copyFromRealm(it.collection) }
+ .unsubscribeOn(worker1Looper)
+
+ val worker2Looper = AndroidSchedulers.from(worker2.looper)
+ val realm2Orders = Flowable.just(App.REALM2_CONFIG)
+ .observeOn(worker2Looper)
+ .flatMap { createObservableForRealm(it) }
+ .flatMap { it.where().findAllAsync().asChangesetObservable().toFlowable(BackpressureStrategy.ERROR) }
+ .filter { it.changeset?.state != OrderedCollectionChangeSet.State.INITIAL }
+ .filter { it.collection.isLoaded }
+ .map { it.collection.realm.copyFromRealm(it.collection) }
+ .unsubscribeOn(worker2Looper)
+
+ // Merge the two Realm streams and calculate the combined fine-grained animations using
+ // DiffUtil.
+ // Inspired by https://hellsoft.se/a-nice-combination-of-rxjava-and-diffutil-fe3807186012
+ val initialPair: Pair, DiffUtil.DiffResult?> = Pair(listOf(), null)
+ val mergedEntries = Flowable.combineLatest(realm1Orders, realm2Orders, BiFunction, List, Pair, List>> { orders1, orders2 ->
+ // Do minimal amount of of work needed to package the lists up so they can be
+ // used on a background thread.
+ Pair(orders1, orders2);
+ })
+ .observeOn(Schedulers.computation())
+ .map {
+ // Merge the two lists and sort them according to timestamp in a natural order
+ ArrayList()
+ .apply {
+ addAll(it.first)
+ addAll(it.second)
+ }
+ .sortedWith(Comparator { first, second ->
+ first.createdAt.compareTo(second.createdAt)
+ })
+ }
+ .scan(initialPair) { pair, next ->
+ // Calculate the DiffUtil results
+ val callback = MyDiffCallback(pair.first, next)
+ val result: DiffUtil.DiffResult = DiffUtil.calculateDiff(callback);
+ Pair(next, result);
+ }
+ .skip(1)
+ .subscribeOn(AndroidSchedulers.mainThread())
+
+ return LiveDataReactiveStreams.fromPublisher(mergedEntries)
+ }
+
+ override fun onCleared() {
+ super.onCleared()
+ worker1.quit()
+ worker2.quit()
+ }
+
+ private class MyDiffCallback(private val current: List, private val next: List) : DiffUtil.Callback() {
+
+ override fun getOldListSize(): Int {
+ return current.size
+ }
+
+ override fun getNewListSize(): Int {
+ return next.size
+ }
+
+ override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
+ val currentItem = current[oldItemPosition]
+ val nextItem = next[newItemPosition]
+ return currentItem.id == nextItem.id
+ }
+
+ override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
+ val currentItem = current[oldItemPosition]
+ val nextItem = next[newItemPosition]
+ return currentItem == nextItem
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/shared/ObservableExt.kt b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/shared/ObservableExt.kt
new file mode 100644
index 0000000000..7021906554
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/java/io/realm/examples/objectserver/activitytracker/ui/shared/ObservableExt.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2019 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.examples.objectserver.activitytracker.ui.shared
+
+import io.reactivex.BackpressureStrategy
+import io.reactivex.Flowable
+import io.reactivex.disposables.Disposables
+import io.realm.Realm
+import io.realm.RealmConfiguration
+
+// Creates an Observable that wraps the Realm lifecycle
+// The Realm will be opened when subscribed to and closed when the stream is disposed.
+//
+// It isn't possible to add static extension functions to Java classes: https://youtrack.jetbrains.com/issue/KT-11968
+// So this is a free function for now.
+fun createObservableForRealm(config: RealmConfiguration): Flowable {
+ return Flowable.create({ emitter ->
+ val realm = Realm.getInstance(config)
+ emitter.setDisposable(Disposables.fromRunnable {
+ realm.close()
+ })
+ emitter.onNext(realm)
+ }, BackpressureStrategy.LATEST)
+}
+
diff --git a/examples/objectServerActivityTrackerExample/src/main/res/drawable-v24/ic_launcher_foreground.xml b/examples/objectServerActivityTrackerExample/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000000..c7bd21dbd8
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/objectServerActivityTrackerExample/src/main/res/drawable/ic_launcher_background.xml b/examples/objectServerActivityTrackerExample/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000000..d5fccc538c
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/objectServerExample/src/main/res/drawable/logo.png b/examples/objectServerActivityTrackerExample/src/main/res/drawable/logo.png
similarity index 100%
rename from examples/objectServerExample/src/main/res/drawable/logo.png
rename to examples/objectServerActivityTrackerExample/src/main/res/drawable/logo.png
diff --git a/examples/objectServerActivityTrackerExample/src/main/res/layout/activity_bookings_list.xml b/examples/objectServerActivityTrackerExample/src/main/res/layout/activity_bookings_list.xml
new file mode 100644
index 0000000000..704d0951fc
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/res/layout/activity_bookings_list.xml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/objectServerActivityTrackerExample/src/main/res/layout/activity_checkin.xml b/examples/objectServerActivityTrackerExample/src/main/res/layout/activity_checkin.xml
new file mode 100644
index 0000000000..77e8bdcfe9
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/res/layout/activity_checkin.xml
@@ -0,0 +1,147 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/objectServerActivityTrackerExample/src/main/res/layout/activity_excursion_list.xml b/examples/objectServerActivityTrackerExample/src/main/res/layout/activity_excursion_list.xml
new file mode 100644
index 0000000000..abe7de6dc1
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/res/layout/activity_excursion_list.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/objectServerActivityTrackerExample/src/main/res/layout/activity_login.xml b/examples/objectServerActivityTrackerExample/src/main/res/layout/activity_login.xml
new file mode 100644
index 0000000000..cde250b7de
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/res/layout/activity_login.xml
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/objectServerActivityTrackerExample/src/main/res/layout/activity_order_list.xml b/examples/objectServerActivityTrackerExample/src/main/res/layout/activity_order_list.xml
new file mode 100644
index 0000000000..d783517a90
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/res/layout/activity_order_list.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/objectServerActivityTrackerExample/src/main/res/layout/item_bookings_list.xml b/examples/objectServerActivityTrackerExample/src/main/res/layout/item_bookings_list.xml
new file mode 100644
index 0000000000..28c4a0c1ec
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/res/layout/item_bookings_list.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/objectServerActivityTrackerExample/src/main/res/layout/item_excursion_list.xml b/examples/objectServerActivityTrackerExample/src/main/res/layout/item_excursion_list.xml
new file mode 100644
index 0000000000..f6ed887728
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/res/layout/item_excursion_list.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/objectServerActivityTrackerExample/src/main/res/layout/item_order_list.xml b/examples/objectServerActivityTrackerExample/src/main/res/layout/item_order_list.xml
new file mode 100644
index 0000000000..df1b80caad
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/res/layout/item_order_list.xml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/objectServerActivityTrackerExample/src/main/res/layout/list.xml b/examples/objectServerActivityTrackerExample/src/main/res/layout/list.xml
new file mode 100644
index 0000000000..784942510e
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/res/layout/list.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
diff --git a/examples/objectServerActivityTrackerExample/src/main/res/layout/spinner_item.xml b/examples/objectServerActivityTrackerExample/src/main/res/layout/spinner_item.xml
new file mode 100644
index 0000000000..5bf0e1bdfb
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/res/layout/spinner_item.xml
@@ -0,0 +1,13 @@
+
+
diff --git a/examples/objectServerActivityTrackerExample/src/main/res/layout/spinner_item_drop_down.xml b/examples/objectServerActivityTrackerExample/src/main/res/layout/spinner_item_drop_down.xml
new file mode 100644
index 0000000000..83cc33ea7f
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/res/layout/spinner_item_drop_down.xml
@@ -0,0 +1,15 @@
+
+
diff --git a/examples/objectServerActivityTrackerExample/src/main/res/menu/menu_items.xml b/examples/objectServerActivityTrackerExample/src/main/res/menu/menu_items.xml
new file mode 100644
index 0000000000..cab3ec9ce8
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/res/menu/menu_items.xml
@@ -0,0 +1,31 @@
+
diff --git a/examples/objectServerExample/src/main/res/mipmap-hdpi/ic_launcher.png b/examples/objectServerActivityTrackerExample/src/main/res/mipmap-hdpi/ic_launcher.png
similarity index 100%
rename from examples/objectServerExample/src/main/res/mipmap-hdpi/ic_launcher.png
rename to examples/objectServerActivityTrackerExample/src/main/res/mipmap-hdpi/ic_launcher.png
diff --git a/examples/objectServerExample/src/main/res/mipmap-mdpi/ic_launcher.png b/examples/objectServerActivityTrackerExample/src/main/res/mipmap-mdpi/ic_launcher.png
similarity index 100%
rename from examples/objectServerExample/src/main/res/mipmap-mdpi/ic_launcher.png
rename to examples/objectServerActivityTrackerExample/src/main/res/mipmap-mdpi/ic_launcher.png
diff --git a/examples/objectServerExample/src/main/res/mipmap-xhdpi/ic_launcher.png b/examples/objectServerActivityTrackerExample/src/main/res/mipmap-xhdpi/ic_launcher.png
similarity index 100%
rename from examples/objectServerExample/src/main/res/mipmap-xhdpi/ic_launcher.png
rename to examples/objectServerActivityTrackerExample/src/main/res/mipmap-xhdpi/ic_launcher.png
diff --git a/examples/objectServerExample/src/main/res/mipmap-xxhdpi/ic_launcher.png b/examples/objectServerActivityTrackerExample/src/main/res/mipmap-xxhdpi/ic_launcher.png
similarity index 100%
rename from examples/objectServerExample/src/main/res/mipmap-xxhdpi/ic_launcher.png
rename to examples/objectServerActivityTrackerExample/src/main/res/mipmap-xxhdpi/ic_launcher.png
diff --git a/examples/objectServerActivityTrackerExample/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/examples/objectServerActivityTrackerExample/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100755
index 0000000000..91826a7567
Binary files /dev/null and b/examples/objectServerActivityTrackerExample/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/examples/objectServerActivityTrackerExample/src/main/res/values/colors.xml b/examples/objectServerActivityTrackerExample/src/main/res/values/colors.xml
new file mode 100644
index 0000000000..7a896dca69
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/res/values/colors.xml
@@ -0,0 +1,13 @@
+
+
+ #3F51B5
+ #303F9F
+ #7986CB
+ #E8EAF6
+ #FF4081
+ #FFF
+ #FFFFFF
+ #313131
+ #A0A0A0
+
+
diff --git a/examples/objectServerActivityTrackerExample/src/main/res/values/dimens.xml b/examples/objectServerActivityTrackerExample/src/main/res/values/dimens.xml
new file mode 100644
index 0000000000..617af6a243
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/res/values/dimens.xml
@@ -0,0 +1,6 @@
+
+ 16dp
+
+ 16dp
+ 16dp
+
diff --git a/examples/objectServerActivityTrackerExample/src/main/res/values/strings.xml b/examples/objectServerActivityTrackerExample/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..c165a0f5f7
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/res/values/strings.xml
@@ -0,0 +1,27 @@
+
+ Activity Tracker Demo
+ Logout
+ Checkin
+ Activities
+
+
+ Username
+ Password
+ Account does not exist.
+ User name and password do not match.
+ Sign in or register
+ Sign in
+ Create demo data
+
+
+ Project description
+ Remaining
+ Checked-in
+ Spots Available
+ Create account and login
+ Login
+ Show Orders
+ Orders
+ Create Order
+
+
diff --git a/examples/objectServerActivityTrackerExample/src/main/res/values/styles.xml b/examples/objectServerActivityTrackerExample/src/main/res/values/styles.xml
new file mode 100644
index 0000000000..4616b4367b
--- /dev/null
+++ b/examples/objectServerActivityTrackerExample/src/main/res/values/styles.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/objectServerExample/src/main/java/io/realm/examples/objectserver/CounterActivity.java b/examples/objectServerExample/src/main/java/io/realm/examples/objectserver/CounterActivity.java
deleted file mode 100644
index 26d129cd2d..0000000000
--- a/examples/objectServerExample/src/main/java/io/realm/examples/objectserver/CounterActivity.java
+++ /dev/null
@@ -1,222 +0,0 @@
-/*
- * Copyright 2016 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.examples.objectserver;
-
-import android.content.Intent;
-import android.graphics.PorterDuff;
-import android.os.Bundle;
-import android.support.annotation.ColorRes;
-import android.support.v7.app.AppCompatActivity;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-import android.widget.TextView;
-
-import java.util.Locale;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-import javax.annotation.Nonnull;
-
-import butterknife.BindView;
-import butterknife.ButterKnife;
-import butterknife.OnClick;
-import io.realm.OrderedCollectionChangeSet;
-import io.realm.OrderedRealmCollectionChangeListener;
-import io.realm.Progress;
-import io.realm.ProgressListener;
-import io.realm.ProgressMode;
-import io.realm.Realm;
-import io.realm.RealmResults;
-import io.realm.SyncConfiguration;
-import io.realm.SyncManager;
-import io.realm.SyncSession;
-import io.realm.SyncUser;
-import io.realm.examples.objectserver.model.CRDTCounter;
-import me.zhanghai.android.materialprogressbar.MaterialProgressBar;
-
-public class CounterActivity extends AppCompatActivity {
-
- private final ProgressListener downloadListener = new ProgressListener() {
- @Override
- public void onChange(@Nonnull Progress progress) {
- downloadingChanges.set(!progress.isTransferComplete());
- runOnUiThread(updateProgressBar);
- }
- };
- private final ProgressListener uploadListener = new ProgressListener() {
- @Override
- public void onChange(@Nonnull Progress progress) {
- uploadingChanges.set(!progress.isTransferComplete());
- runOnUiThread(updateProgressBar);
- }
- };
- private final Runnable updateProgressBar = new Runnable() {
- @Override
- public void run() {
- updateProgressBar(downloadingChanges.get(), uploadingChanges.get());
- }
- };
-
- private final AtomicBoolean downloadingChanges = new AtomicBoolean(false);
- private final AtomicBoolean uploadingChanges = new AtomicBoolean(false);
-
- private Realm realm;
- private SyncSession session;
- private SyncUser user;
-
- @BindView(R.id.text_counter) TextView counterView;
- @BindView(R.id.progressbar) MaterialProgressBar progressBar;
- private RealmResults counters; // Keep strong reference to counter to keep change listeners alive.
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_counter);
- ButterKnife.bind(this);
- }
-
- @Override
- protected void onStart() {
- super.onStart();
- user = getLoggedInUser();
- if (user == null) { return; }
-
- // Create a RealmConfiguration for our user
- SyncConfiguration config = user.createConfiguration(BuildConfig.REALM_URL)
- .initialData(new Realm.Transaction() {
- @Override
- public void execute(@Nonnull Realm realm) {
- realm.createObject(CRDTCounter.class, user.getIdentity());
- }
- })
- .build();
-
- // This will automatically sync all changes in the background for as long as the Realm is open
- realm = Realm.getInstance(config);
-
- counterView.setText("-");
- counters = realm.where(CRDTCounter.class).equalTo("name", user.getIdentity()).findAllAsync();
- counters.addChangeListener(new OrderedRealmCollectionChangeListener>() {
- @Override
- public void onChange(RealmResults counters, OrderedCollectionChangeSet changeSet) {
- if (counters.isValid() && !counters.isEmpty()) {
- CRDTCounter counter = counters.first();
- counterView.setText(String.format(Locale.US, "%d", counter.getCount()));
- } else {
- counterView.setText("-");
- }
- }
- });
-
- // Setup progress listeners for indeterminate progress bars
- session = SyncManager.getSession(config);
- session.addDownloadProgressListener(ProgressMode.INDEFINITELY, downloadListener);
- session.addUploadProgressListener(ProgressMode.INDEFINITELY, uploadListener);
- }
-
- @Override
- protected void onStop() {
- super.onStop();
- if (session != null) {
- session.removeProgressListener(downloadListener);
- session.removeProgressListener(uploadListener);
- session = null;
- }
- closeRealm();
- user = null;
- counters = null;
- }
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- getMenuInflater().inflate(R.menu.menu_counter, menu);
- return true;
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- switch(item.getItemId()) {
- case R.id.action_logout:
- closeRealm();
- user.logOut();
- user = getLoggedInUser();
- return true;
-
- default:
- return super.onOptionsItemSelected(item);
- }
- }
-
- @OnClick(R.id.upper)
- public void incrementCounter() {
- adjustCounter(1);
- }
-
- @OnClick(R.id.lower)
- public void decrementCounter() {
- adjustCounter(-1);
- }
-
- private void updateProgressBar(boolean downloading, boolean uploading) {
- @ColorRes int color = android.R.color.black;
- int visibility = View.VISIBLE;
- if (downloading && uploading) {
- color = R.color.progress_both;
- } else if (downloading) {
- color = R.color.progress_download;
- } else if (uploading) {
- color = R.color.progress_upload;
- } else {
- visibility = View.GONE;
- }
- progressBar.getIndeterminateDrawable().setColorFilter(getResources().getColor(color), PorterDuff.Mode.SRC_IN);
- progressBar.setVisibility(visibility);
- }
-
- private void adjustCounter(final int adjustment) {
- // A synchronized Realm can get written to at any point in time, so doing synchronous writes on the UI
- // thread is HIGHLY discouraged as it might block longer than intended. Use only async transactions.
- realm.executeTransactionAsync(new Realm.Transaction() {
- @Override
- public void execute(@Nonnull Realm realm) {
- CRDTCounter counter = realm.where(CRDTCounter.class).findFirst();
- if (counter != null) {
- counter.incrementCounter(adjustment);
- }
- }
- });
- }
-
- private SyncUser getLoggedInUser() {
- SyncUser user = null;
-
- try { user = SyncUser.current(); }
- catch (IllegalStateException ignore) { }
-
- if (user == null) {
- startActivity(new Intent(this, LoginActivity.class));
- }
-
- return user;
- }
-
- private void closeRealm() {
- if (realm != null && !realm.isClosed()) {
- realm.close();
- }
- }
-}
diff --git a/examples/objectServerExample/src/main/java/io/realm/examples/objectserver/LoginActivity.java b/examples/objectServerExample/src/main/java/io/realm/examples/objectserver/LoginActivity.java
deleted file mode 100644
index d16b593802..0000000000
--- a/examples/objectServerExample/src/main/java/io/realm/examples/objectserver/LoginActivity.java
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
- * Copyright 2016 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.examples.objectserver;
-
-import android.app.ProgressDialog;
-import android.os.Bundle;
-import android.support.v7.app.AppCompatActivity;
-import android.view.View;
-import android.widget.Button;
-import android.widget.EditText;
-import android.widget.Toast;
-
-import javax.annotation.Nonnull;
-
-import butterknife.BindView;
-import butterknife.ButterKnife;
-import io.realm.SyncCredentials;
-import io.realm.ObjectServerError;
-import io.realm.SyncUser;
-
-
-public class LoginActivity extends AppCompatActivity {
- @BindView(R.id.input_username) EditText username;
- @BindView(R.id.input_password) EditText password;
- @BindView(R.id.button_login) Button loginButton;
- @BindView(R.id.button_create) Button createUserButton;
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_login);
- ButterKnife.bind(this);
- loginButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- login(false);
- }
- });
- createUserButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- login(true);
- }
- });
- }
-
- public void login(boolean createUser) {
- if (!validate()) {
- onLoginFailed("Invalid username or password");
- return;
- }
-
- createUserButton.setEnabled(false);
- loginButton.setEnabled(false);
-
- final ProgressDialog progressDialog = new ProgressDialog(LoginActivity.this);
- progressDialog.setIndeterminate(true);
- progressDialog.setMessage("Authenticating...");
- progressDialog.show();
-
- String username = this.username.getText().toString();
- String password = this.password.getText().toString();
-
- SyncCredentials creds = SyncCredentials.usernamePassword(username, password, createUser);
- SyncUser.Callback callback = new SyncUser.Callback() {
- @Override
- public void onSuccess(@Nonnull SyncUser user) {
- progressDialog.dismiss();
- onLoginSuccess();
- }
-
- @Override
- public void onError(@Nonnull ObjectServerError error) {
- progressDialog.dismiss();
- String errorMsg;
- switch (error.getErrorCode()) {
- case UNKNOWN_ACCOUNT:
- errorMsg = "Account does not exists.";
- break;
- case INVALID_CREDENTIALS:
- errorMsg = "User name and password does not match";
- break;
- default:
- errorMsg = error.toString();
- }
- onLoginFailed(errorMsg);
- }
- };
-
- SyncUser.logInAsync(creds, BuildConfig.REALM_AUTH_URL, callback);
- }
-
- @Override
- public void onBackPressed() {
- // Disable going back to the MainActivity
- moveTaskToBack(true);
- }
-
- public void onLoginSuccess() {
- loginButton.setEnabled(true);
- createUserButton.setEnabled(true);
- finish();
- }
-
- public void onLoginFailed(String errorMsg) {
- loginButton.setEnabled(true);
- createUserButton.setEnabled(true);
- Toast.makeText(getBaseContext(), errorMsg, Toast.LENGTH_LONG).show();
- }
-
- public boolean validate() {
- boolean valid = true;
- String email = username.getText().toString();
- String password = this.password.getText().toString();
-
- if (email.isEmpty()) {
- valid = false;
- }
-
- if (password.isEmpty()) {
- valid = false;
- }
-
- return valid;
- }
-}
diff --git a/examples/objectServerExample/src/main/java/io/realm/examples/objectserver/model/CRDTCounter.java b/examples/objectServerExample/src/main/java/io/realm/examples/objectserver/model/CRDTCounter.java
deleted file mode 100644
index 0bca8fd53c..0000000000
--- a/examples/objectServerExample/src/main/java/io/realm/examples/objectserver/model/CRDTCounter.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright 2017 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.examples.objectserver.model;
-
-import io.realm.MutableRealmInteger;
-import io.realm.RealmObject;
-import io.realm.annotations.PrimaryKey;
-import io.realm.annotations.Required;
-
-/**
- * A named, conflict-free replicated data-type.
- */
-public class CRDTCounter extends RealmObject {
- @PrimaryKey
- private String name;
-
- @Required
- public final MutableRealmInteger counter = MutableRealmInteger.valueOf(0L);
-
- // Required for Realm
- public CRDTCounter() {}
-
- public CRDTCounter(String name) { this.name = name; }
-
- public String getName() { return name; }
-
- public long getCount() { return counter.get().longValue(); }
- public void incrementCounter(long delta) { counter.increment(delta); }
-}
diff --git a/examples/objectServerExample/src/main/res/layout/activity_counter.xml b/examples/objectServerExample/src/main/res/layout/activity_counter.xml
deleted file mode 100644
index a1300b2123..0000000000
--- a/examples/objectServerExample/src/main/res/layout/activity_counter.xml
+++ /dev/null
@@ -1,49 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/examples/objectServerExample/src/main/res/layout/activity_login.xml b/examples/objectServerExample/src/main/res/layout/activity_login.xml
deleted file mode 100644
index 375cdbca98..0000000000
--- a/examples/objectServerExample/src/main/res/layout/activity_login.xml
+++ /dev/null
@@ -1,68 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/examples/objectServerExample/README.md b/examples/objectServerSimpleExample/README.md
similarity index 80%
rename from examples/objectServerExample/README.md
rename to examples/objectServerSimpleExample/README.md
index d3c8558940..f6630def1b 100644
--- a/examples/objectServerExample/README.md
+++ b/examples/objectServerSimpleExample/README.md
@@ -10,11 +10,15 @@ injected into the build configuration.
To use a different ObjectServer, simply put the server IP Address into
the `build.gradle`, as indicated in the comments, on the lines like this:
- buildConfigField "String", "OBJECT_SERVER_IP", "\"${host}\""
+ def rosUrl = ""
For instance:
- buildConfigField "String", "OBJECT_SERVER_IP", "192.168.0.1"
+ def rosUrl = "https://myinstance.us1.cloud.realm.io"
+
+or:
+
+ def rosUrl = "http://127.0.0.1:9080"
To read more about the Realm Object Server and how to deploy it, see
https://realm.io/news/introducing-realm-mobile-platform/
diff --git a/examples/objectServerExample/build.gradle b/examples/objectServerSimpleExample/build.gradle
similarity index 77%
rename from examples/objectServerExample/build.gradle
rename to examples/objectServerSimpleExample/build.gradle
index ff0d0d5ccd..44535c85a7 100644
--- a/examples/objectServerExample/build.gradle
+++ b/examples/objectServerSimpleExample/build.gradle
@@ -1,4 +1,19 @@
+buildscript {
+ ext.kotlin_version = '1.3.20'
+ repositories {
+ google()
+ jcenter()
+ mavenCentral()
+ }
+ dependencies {
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ }
+}
+
apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android-extensions'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-kapt'
apply plugin: 'realm-android'
android {
@@ -13,6 +28,10 @@ android {
versionName "1.0"
}
+ dataBinding {
+ enabled = true
+ }
+
buildTypes {
// Go to https://cloud.realm.io and copy the URL to your instance. Insert it below.
// It will look something like "https://test.us1.cloud.realm.io"
@@ -45,6 +64,5 @@ dependencies {
implementation 'com.android.support:appcompat-v7:27.1.1'
implementation 'com.android.support:design:27.1.1'
implementation 'me.zhanghai.android.materialprogressbar:library:1.3.0'
- implementation 'com.jakewharton:butterknife:8.8.1'//TODO:Can be refactored with Native Android Data Binding
- annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'//TODO:Can be refactored with Native Android Data Binding
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}
diff --git a/examples/objectServerSimpleExample/lint.xml b/examples/objectServerSimpleExample/lint.xml
new file mode 100644
index 0000000000..6a9810cdcb
--- /dev/null
+++ b/examples/objectServerSimpleExample/lint.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/objectServerExample/src/main/AndroidManifest.xml b/examples/objectServerSimpleExample/src/main/AndroidManifest.xml
similarity index 100%
rename from examples/objectServerExample/src/main/AndroidManifest.xml
rename to examples/objectServerSimpleExample/src/main/AndroidManifest.xml
diff --git a/examples/objectServerSimpleExample/src/main/java/io/realm/examples/objectserver/CounterActivity.kt b/examples/objectServerSimpleExample/src/main/java/io/realm/examples/objectserver/CounterActivity.kt
new file mode 100644
index 0000000000..bc069f88c6
--- /dev/null
+++ b/examples/objectServerSimpleExample/src/main/java/io/realm/examples/objectserver/CounterActivity.kt
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2019 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.examples.objectserver
+
+import android.content.Intent
+import android.databinding.DataBindingUtil
+import android.graphics.PorterDuff
+import android.os.Bundle
+import android.support.annotation.ColorRes
+import android.support.v7.app.AppCompatActivity
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import android.widget.TextView
+import io.realm.*
+import io.realm.examples.objectserver.databinding.ActivityCounterBinding
+import io.realm.examples.objectserver.model.CRDTCounter
+import io.realm.kotlin.createObject
+import io.realm.kotlin.syncSession
+import io.realm.kotlin.where
+import io.realm.log.RealmLog
+import me.zhanghai.android.materialprogressbar.MaterialProgressBar
+import java.util.*
+import java.util.concurrent.atomic.AtomicBoolean
+
+class CounterActivity : AppCompatActivity() {
+
+ private lateinit var binding: ActivityCounterBinding
+
+ private val downloadListener = ProgressListener { progress ->
+ downloadingChanges.set(!progress.isTransferComplete)
+ runOnUiThread(updateProgressBar)
+ }
+ private val uploadListener = ProgressListener { progress ->
+ uploadingChanges.set(!progress.isTransferComplete)
+ runOnUiThread(updateProgressBar)
+ }
+ private val updateProgressBar = Runnable { updateProgressBar(downloadingChanges.get(), uploadingChanges.get()) }
+
+ private val downloadingChanges = AtomicBoolean(false)
+ private val uploadingChanges = AtomicBoolean(false)
+
+ private lateinit var realm: Realm
+ private lateinit var session: SyncSession
+ private var user: SyncUser? = null
+
+ private lateinit var counterView: TextView
+ private lateinit var progressBar: MaterialProgressBar
+ private lateinit var counters: RealmResults // Keep strong reference to counter to keep change listeners alive.
+
+ private val loggedInUser: SyncUser?
+ get() {
+ var user: SyncUser? = null
+
+ try {
+ user = SyncUser.current()
+ } catch (e: IllegalStateException) {
+ RealmLog.warn(e);
+ }
+
+ if (user == null) {
+ startActivity(Intent(this, LoginActivity::class.java))
+ }
+
+ return user
+ }
+
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = DataBindingUtil.setContentView(this, R.layout.activity_counter)
+ counterView = binding.textCounter
+ progressBar = binding.progressbar
+ binding.upper.setOnClickListener { adjustCounter(1) }
+ binding.lower.setOnClickListener { adjustCounter(-1) }
+ }
+
+ override fun onStart() {
+ super.onStart()
+ user = loggedInUser
+ val user = user
+ if (user != null) {
+ // Create a RealmConfiguration for our user
+ val config = user.createConfiguration(BuildConfig.REALM_URL)
+ .initialData { realm -> realm.createObject(user.identity) }
+ .build()
+
+ // This will automatically sync all changes in the background for as long as the Realm is open
+ realm = Realm.getInstance(config)
+
+ counterView.text = "-"
+ counters = realm.where().equalTo("name", user.identity).findAllAsync()
+ counters.addChangeListener { counters, _ ->
+ if (counters.isValid && !counters.isEmpty()) {
+ val counter = counters.first()
+ counterView.text = String.format(Locale.US, "%d", counter!!.count)
+ } else {
+ counterView.text = "-"
+ }
+ }
+
+ // Setup progress listeners for indeterminate progress bars
+ session = realm.syncSession
+ session.run {
+ addDownloadProgressListener(ProgressMode.INDEFINITELY, downloadListener)
+ addUploadProgressListener(ProgressMode.INDEFINITELY, uploadListener)
+ }
+ }
+ }
+
+ override fun onStop() {
+ super.onStop()
+ user?.run {
+ session.run {
+ removeProgressListener(downloadListener)
+ removeProgressListener(uploadListener)
+ }
+ realm.close()
+ }
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ menuInflater.inflate(R.menu.menu_counter, menu)
+ return true
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return when (item.itemId) {
+ R.id.action_logout -> {
+ realm.close()
+ val user = user
+ if (user != null) {
+ user.logOut()
+ this.user = loggedInUser
+ }
+ true
+ }
+
+ else -> super.onOptionsItemSelected(item)
+ }
+ }
+
+ private fun updateProgressBar(downloading: Boolean, uploading: Boolean) {
+ @ColorRes val color = when {
+ downloading && uploading -> R.color.progress_both
+ downloading -> R.color.progress_download
+ uploading -> R.color.progress_upload
+ else -> android.R.color.black
+ }
+ progressBar.indeterminateDrawable.setColorFilter(resources.getColor(color), PorterDuff.Mode.SRC_IN)
+ progressBar.visibility = if (color == android.R.color.black) View.GONE else View.VISIBLE
+ }
+
+ private fun adjustCounter(adjustment: Int) {
+ // A synchronized Realm can get written to at any point in time, so doing synchronous writes on the UI
+ // thread is HIGHLY discouraged as it might block longer than intended. Use only async transactions.
+ realm.executeTransactionAsync { realm ->
+ val counter = realm.where().findFirst()
+ counter?.incrementCounter(adjustment.toLong())
+ }
+ }
+
+}
diff --git a/examples/objectServerSimpleExample/src/main/java/io/realm/examples/objectserver/LoginActivity.kt b/examples/objectServerSimpleExample/src/main/java/io/realm/examples/objectserver/LoginActivity.kt
new file mode 100644
index 0000000000..1038e795a8
--- /dev/null
+++ b/examples/objectServerSimpleExample/src/main/java/io/realm/examples/objectserver/LoginActivity.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2019 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.examples.objectserver
+
+import android.app.ProgressDialog
+import android.databinding.DataBindingUtil
+import android.os.Bundle
+import android.support.v7.app.AppCompatActivity
+import android.widget.Button
+import android.widget.EditText
+import android.widget.Toast
+import io.realm.ErrorCode
+import io.realm.ObjectServerError
+import io.realm.SyncCredentials
+import io.realm.SyncUser
+import io.realm.examples.objectserver.databinding.ActivityLoginBinding
+
+class LoginActivity : AppCompatActivity() {
+
+ private lateinit var username: EditText
+ private lateinit var password: EditText
+ private lateinit var loginButton: Button
+ private lateinit var createUserButton: Button
+
+ lateinit private var binding: ActivityLoginBinding
+
+ public override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = DataBindingUtil.setContentView(this, R.layout.activity_login)
+ username = binding.inputUsername
+ password = binding.inputPassword
+ loginButton = binding.buttonLogin
+ createUserButton = binding.buttonCreate
+
+ loginButton.setOnClickListener { login(false) }
+ createUserButton.setOnClickListener { login(true) }
+ }
+
+ private fun login(createUser: Boolean) {
+ if (!validate()) {
+ onLoginFailed("Invalid username or password")
+ return
+ }
+
+ binding.buttonCreate.isEnabled = false
+ binding.buttonLogin.isEnabled = false
+
+ val progressDialog = ProgressDialog(this@LoginActivity)
+ progressDialog.isIndeterminate = true
+ progressDialog.setMessage("Authenticating...")
+ progressDialog.show()
+
+ val username = this.username.text.toString()
+ val password = this.password.text.toString()
+
+ val creds = SyncCredentials.usernamePassword(username, password, createUser)
+ val callback = object : SyncUser.Callback {
+ override fun onSuccess(user: SyncUser) {
+ progressDialog.dismiss()
+ onLoginSuccess()
+ }
+
+ override fun onError(error: ObjectServerError) {
+ progressDialog.dismiss()
+ val errorMsg: String = when (error.errorCode) {
+ ErrorCode.UNKNOWN_ACCOUNT -> getString(R.string.login_error_unknown_account)
+ ErrorCode.INVALID_CREDENTIALS -> getString(R.string.login_error_invalid_credentials)
+ else -> error.toString()
+ }
+ onLoginFailed(errorMsg)
+ }
+ }
+
+ SyncUser.logInAsync(creds, BuildConfig.REALM_AUTH_URL, callback)
+ }
+
+ override fun onBackPressed() {
+ // Disable going back to the MainActivity
+ moveTaskToBack(true)
+ }
+
+ private fun onLoginSuccess() {
+ loginButton.isEnabled = true
+ createUserButton.isEnabled = true
+ finish()
+ }
+
+ private fun onLoginFailed(errorMsg: String) {
+ loginButton.isEnabled = true
+ createUserButton.isEnabled = true
+ Toast.makeText(baseContext, errorMsg, Toast.LENGTH_LONG).show()
+ }
+
+ private fun validate(): Boolean = when {
+ username.text.toString().isEmpty() -> false
+ password.text.toString().isEmpty() -> false
+ else -> true
+ }
+}
diff --git a/examples/objectServerExample/src/main/java/io/realm/examples/objectserver/MyApplication.java b/examples/objectServerSimpleExample/src/main/java/io/realm/examples/objectserver/MyApplication.kt
similarity index 62%
rename from examples/objectServerExample/src/main/java/io/realm/examples/objectserver/MyApplication.java
rename to examples/objectServerSimpleExample/src/main/java/io/realm/examples/objectserver/MyApplication.kt
index 417fd9d9f5..dc7360fb48 100644
--- a/examples/objectServerExample/src/main/java/io/realm/examples/objectserver/MyApplication.java
+++ b/examples/objectServerSimpleExample/src/main/java/io/realm/examples/objectserver/MyApplication.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2016 Realm Inc.
+ * Copyright 2019 Realm Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,24 +14,23 @@
* limitations under the License.
*/
-package io.realm.examples.objectserver;
+package io.realm.examples.objectserver
-import android.app.Application;
-import android.util.Log;
+import android.app.Application
+import android.util.Log
-import io.realm.Realm;
-import io.realm.log.RealmLog;
+import io.realm.Realm
+import io.realm.log.RealmLog
-public class MyApplication extends Application {
+class MyApplication : Application() {
- @Override
- public void onCreate() {
- super.onCreate();
- Realm.init(this, "ObjectServerExample/" + BuildConfig.VERSION_NAME);
+ override fun onCreate() {
+ super.onCreate()
+ Realm.init(this, "ObjectServerExample/" + BuildConfig.VERSION_NAME)
- // Enable full log output when debugging
+ // Enable more
if (BuildConfig.DEBUG) {
- RealmLog.setLevel(Log.DEBUG);
+ RealmLog.setLevel(Log.DEBUG)
}
}
}
diff --git a/examples/objectServerSimpleExample/src/main/java/io/realm/examples/objectserver/model/CRDTCounter.kt b/examples/objectServerSimpleExample/src/main/java/io/realm/examples/objectserver/model/CRDTCounter.kt
new file mode 100644
index 0000000000..6078bb0512
--- /dev/null
+++ b/examples/objectServerSimpleExample/src/main/java/io/realm/examples/objectserver/model/CRDTCounter.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2019 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.examples.objectserver.model
+
+import io.realm.MutableRealmInteger
+import io.realm.RealmObject
+import io.realm.annotations.PrimaryKey
+import io.realm.annotations.Required
+
+open class CRDTCounter : RealmObject() {
+
+ @PrimaryKey
+ var name: String = ""
+
+ @Required
+ private val counter = MutableRealmInteger.valueOf(0L)
+
+ val count: Long
+ get() = this.counter.get()!!.toLong()
+
+ fun incrementCounter(delta: Long) {
+ counter.increment(delta)
+ }
+
+}
diff --git a/examples/objectServerExample/src/main/res/drawable-xxhdpi/ic_exit_to_app_white_24dp.png b/examples/objectServerSimpleExample/src/main/res/drawable-xxhdpi/ic_exit_to_app_white_24dp.png
similarity index 100%
rename from examples/objectServerExample/src/main/res/drawable-xxhdpi/ic_exit_to_app_white_24dp.png
rename to examples/objectServerSimpleExample/src/main/res/drawable-xxhdpi/ic_exit_to_app_white_24dp.png
diff --git a/examples/objectServerExample/src/main/res/drawable-xxxhdpi/ic_exit_to_app_white_24dp.png b/examples/objectServerSimpleExample/src/main/res/drawable-xxxhdpi/ic_exit_to_app_white_24dp.png
similarity index 100%
rename from examples/objectServerExample/src/main/res/drawable-xxxhdpi/ic_exit_to_app_white_24dp.png
rename to examples/objectServerSimpleExample/src/main/res/drawable-xxxhdpi/ic_exit_to_app_white_24dp.png
diff --git a/examples/objectServerExample/src/main/res/drawable/button_counter.xml b/examples/objectServerSimpleExample/src/main/res/drawable/button_counter.xml
similarity index 100%
rename from examples/objectServerExample/src/main/res/drawable/button_counter.xml
rename to examples/objectServerSimpleExample/src/main/res/drawable/button_counter.xml
diff --git a/examples/objectServerSimpleExample/src/main/res/drawable/logo.png b/examples/objectServerSimpleExample/src/main/res/drawable/logo.png
new file mode 100755
index 0000000000..91826a7567
Binary files /dev/null and b/examples/objectServerSimpleExample/src/main/res/drawable/logo.png differ
diff --git a/examples/objectServerSimpleExample/src/main/res/layout/activity_counter.xml b/examples/objectServerSimpleExample/src/main/res/layout/activity_counter.xml
new file mode 100644
index 0000000000..215c9fb47f
--- /dev/null
+++ b/examples/objectServerSimpleExample/src/main/res/layout/activity_counter.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/objectServerSimpleExample/src/main/res/layout/activity_login.xml b/examples/objectServerSimpleExample/src/main/res/layout/activity_login.xml
new file mode 100644
index 0000000000..29f078ebc2
--- /dev/null
+++ b/examples/objectServerSimpleExample/src/main/res/layout/activity_login.xml
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/objectServerExample/src/main/res/menu/menu_counter.xml b/examples/objectServerSimpleExample/src/main/res/menu/menu_counter.xml
similarity index 100%
rename from examples/objectServerExample/src/main/res/menu/menu_counter.xml
rename to examples/objectServerSimpleExample/src/main/res/menu/menu_counter.xml
diff --git a/examples/objectServerSimpleExample/src/main/res/mipmap-hdpi/ic_launcher.png b/examples/objectServerSimpleExample/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100755
index 0000000000..58303aff5b
Binary files /dev/null and b/examples/objectServerSimpleExample/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/examples/objectServerSimpleExample/src/main/res/mipmap-mdpi/ic_launcher.png b/examples/objectServerSimpleExample/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100755
index 0000000000..9b29caed3d
Binary files /dev/null and b/examples/objectServerSimpleExample/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/examples/objectServerSimpleExample/src/main/res/mipmap-xhdpi/ic_launcher.png b/examples/objectServerSimpleExample/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100755
index 0000000000..15527b160e
Binary files /dev/null and b/examples/objectServerSimpleExample/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/examples/objectServerSimpleExample/src/main/res/mipmap-xxhdpi/ic_launcher.png b/examples/objectServerSimpleExample/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100755
index 0000000000..eb9ece04b2
Binary files /dev/null and b/examples/objectServerSimpleExample/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/examples/objectServerExample/src/main/res/values-w820dp/dimens.xml b/examples/objectServerSimpleExample/src/main/res/values-w820dp/dimens.xml
similarity index 100%
rename from examples/objectServerExample/src/main/res/values-w820dp/dimens.xml
rename to examples/objectServerSimpleExample/src/main/res/values-w820dp/dimens.xml
diff --git a/examples/objectServerExample/src/main/res/values/dimens.xml b/examples/objectServerSimpleExample/src/main/res/values/dimens.xml
similarity index 100%
rename from examples/objectServerExample/src/main/res/values/dimens.xml
rename to examples/objectServerSimpleExample/src/main/res/values/dimens.xml
diff --git a/examples/objectServerExample/src/main/res/values/realm_colors.xml b/examples/objectServerSimpleExample/src/main/res/values/realm_colors.xml
similarity index 100%
rename from examples/objectServerExample/src/main/res/values/realm_colors.xml
rename to examples/objectServerSimpleExample/src/main/res/values/realm_colors.xml
diff --git a/examples/objectServerExample/src/main/res/values/strings.xml b/examples/objectServerSimpleExample/src/main/res/values/strings.xml
similarity index 70%
rename from examples/objectServerExample/src/main/res/values/strings.xml
rename to examples/objectServerSimpleExample/src/main/res/values/strings.xml
index b4f90b3676..dbf98e52eb 100644
--- a/examples/objectServerExample/src/main/res/values/strings.xml
+++ b/examples/objectServerSimpleExample/src/main/res/values/strings.xml
@@ -7,4 +7,6 @@
Create account and login
Login
Logout
+ Account does not exist.
+ User name and password do not match.
diff --git a/examples/objectServerExample/src/main/res/values/styles.xml b/examples/objectServerSimpleExample/src/main/res/values/styles.xml
similarity index 100%
rename from examples/objectServerExample/src/main/res/values/styles.xml
rename to examples/objectServerSimpleExample/src/main/res/values/styles.xml
diff --git a/examples/settings.gradle b/examples/settings.gradle
index 15f6d6c37d..d8c10a93e1 100644
--- a/examples/settings.gradle
+++ b/examples/settings.gradle
@@ -13,5 +13,6 @@ include 'rxJavaExample'
include 'secureTokenAndroidKeyStore'
include 'threadExample'
include 'unitTestExample'
-include 'objectServerExample'
+include 'objectServerActivityTrackerExample'
+include 'objectServerSimpleExample'
include 'multiprocessExample'
diff --git a/version.txt b/version.txt
index 3452610e7a..ba94863e02 100644
--- a/version.txt
+++ b/version.txt
@@ -1 +1 @@
-5.9.2-SNAPSHOT
\ No newline at end of file
+5.10.0-SNAPSHOT