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