diff --git a/CHANGELOG.md b/CHANGELOG.md index 50570addbf..eeeb0031c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,11 @@ ### Breaking changes * `RealmResults.distinct()` returns a new `RealmResults` object instead of filtering on the original object (#2947). -* `RealmResults` is auto-updated continuously. Any transaction on the current thread which may have an impact on the order or elements of the `RealmResults` will change the `RealmResults` immediately instead of change it in the next event loop. The standard `RealmResults.iterator()` will continue to work as normal, which means that you can still delete or modify elements without impacting the iterator. The same is not true for simple for-loops. In some cases a simple for-loop will not work (https://realm.io/docs/java/2.3.1/api/io/realm/OrderedRealmCollection.html#loops), and you must use the new createSnapshot() method. +* `RealmResults` is auto-updated continuously. Any transaction on the current thread which may have an impact on the order or elements of the `RealmResults` will change the `RealmResults` immediately instead of change it in the next event loop. The standard `RealmResults.iterator()` will continue to work as normal, which means that you can still delete or modify elements without impacting the iterator. The same is not true for simple for-loops. In some cases a simple for-loop will not work (https://realm.io/docs/java/3.0.0/api/io/realm/OrderedRealmCollection.html#loops), and you must use the new createSnapshot() method. + +### Deprecated + +* `RealmResults.removeChangeListeners()`. Use `RealmResults.removeAllChangeListeners()` instead. ### Enhancements diff --git a/realm/realm-library/src/androidTest/java/io/realm/NotificationsTest.java b/realm/realm-library/src/androidTest/java/io/realm/NotificationsTest.java index ebfbb16f79..fd7d5d8035 100644 --- a/realm/realm-library/src/androidTest/java/io/realm/NotificationsTest.java +++ b/realm/realm-library/src/androidTest/java/io/realm/NotificationsTest.java @@ -465,7 +465,7 @@ public void onChange(Realm object) { @Override public void onChange(Realm object) { listenerBCalled.incrementAndGet(); - if (listenerACalled.get() == 1) { + if (listenerBCalled.get() == 1) { // 2. Reverse order. realm.removeAllChangeListeners(); realm.addChangeListener(this); @@ -476,6 +476,8 @@ public void onChange(Realm object) { public void execute(Realm realm) { } }); + } else if (listenerBCalled.get() == 2) { + assertEquals(1, listenerACalled.get()); } } }; diff --git a/realm/realm-library/src/androidTest/java/io/realm/OrderedCollectionChangeSetTests.java b/realm/realm-library/src/androidTest/java/io/realm/OrderedCollectionChangeSetTests.java new file mode 100644 index 0000000000..a1134f85b3 --- /dev/null +++ b/realm/realm-library/src/androidTest/java/io/realm/OrderedCollectionChangeSetTests.java @@ -0,0 +1,357 @@ +/* + * 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; + +import android.support.test.runner.AndroidJUnit4; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.CountDownLatch; + +import io.realm.entities.AllTypes; +import io.realm.rule.RunInLooperThread; +import io.realm.rule.RunTestInLooperThread; +import io.realm.rule.TestRealmConfigurationFactory; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertNull; +import static junit.framework.Assert.assertSame; +import static junit.framework.Assert.fail; +import static org.junit.Assert.assertArrayEquals; + +// Tests for the ordered collection fine grained notifications. +// This should be expanded to test the notifications for RealmList as well in the future. +@RunWith(AndroidJUnit4.class) +public class OrderedCollectionChangeSetTests { + + @Rule + public final TestRealmConfigurationFactory configFactory = new TestRealmConfigurationFactory(); + @Rule + public final RunInLooperThread looperThread = new RunInLooperThread(); + + @Before + public void setUp() { + } + + @After + public void tearDown() { + } + + private void populateData(Realm realm, int testSize) { + realm.beginTransaction(); + for (int i = 0; i < testSize; i++) { + realm.createObject(AllTypes.class).setColumnLong(i); + } + realm.commitTransaction(); + } + + // The args should be [startIndex1, length1, startIndex2, length2, ...] + private void checkRanges(OrderedCollectionChangeSet.Range[] ranges, int... indexAndLen) { + if ((indexAndLen.length % 2 != 0)) { + fail("The 'indexAndLen' array length is not an even number."); + } + if (ranges.length != indexAndLen.length / 2) { + fail("The lengths of 'ranges' and 'indexAndLen' don't match."); + } + for (int i = 0; i < ranges.length; i++) { + OrderedCollectionChangeSet.Range range = ranges[i]; + int startIndex = indexAndLen[i * 2]; + int length = indexAndLen[i * 2 + 1]; + if (range.startIndex != startIndex || range.length != length) { + fail("Range at index " + i + " doesn't match start index " + startIndex + " length " + length + "."); + } + } + } + + // Deletes AllTypes objects which's columnLong is in the indices array. + private void deleteObjects(Realm realm, int... indices) { + for (int index : indices) { + realm.where(AllTypes.class).equalTo(AllTypes.FIELD_LONG, index).findFirst().deleteFromRealm(); + } + } + + // Creates AllTypes objects with columnLong set to the value elements in indices array. + private void createObjects(Realm realm, int... indices) { + for (int index : indices) { + realm.createObject(AllTypes.class).setColumnLong(index); + } + } + + // Modifies AllTypes objects which's columnLong is in the indices array. + private void modifyObjects(Realm realm, int... indices) { + for (int index : indices) { + AllTypes obj = realm.where(AllTypes.class).equalTo(AllTypes.FIELD_LONG, index).findFirst(); + assertNotNull(obj); + obj.setColumnString("modified"); + } + } + + @Test + @RunTestInLooperThread + public void deletion() { + Realm realm = looperThread.realm; + populateData(realm, 10); + RealmResults results = realm.where(AllTypes.class).findAllSorted(AllTypes.FIELD_LONG); + results.addChangeListener(new OrderedRealmCollectionChangeListener>() { + @Override + public void onChange(RealmResults collection, OrderedCollectionChangeSet changeSet) { + checkRanges(changeSet.getDeletionRanges(), + 0, 1, + 2, 3, + 8, 2); + assertArrayEquals(changeSet.getDeletions(), new int[]{0, 2, 3, 4, 8, 9}); + assertEquals(0, changeSet.getChangeRanges().length); + assertEquals(0, changeSet.getInsertionRanges().length); + assertEquals(0, changeSet.getChanges().length); + assertEquals(0, changeSet.getInsertions().length); + looperThread.testComplete(); + } + }); + + realm.beginTransaction(); + deleteObjects(realm, + 0, + 2, 3, 4, + 8, 9); + realm.commitTransaction(); + } + + @Test + @RunTestInLooperThread + public void insertion() { + Realm realm = looperThread.realm; + realm.beginTransaction(); + createObjects(realm, 0, 2, 5, 6, 7, 9); + realm.commitTransaction(); + RealmResults results = realm.where(AllTypes.class).findAllSorted(AllTypes.FIELD_LONG); + results.addChangeListener(new OrderedRealmCollectionChangeListener>() { + @Override + public void onChange(RealmResults collection, OrderedCollectionChangeSet changeSet) { + checkRanges(changeSet.getInsertionRanges(), + 1, 1, + 3, 2, + 8, 1); + assertArrayEquals(changeSet.getInsertions(), new int[]{1, 3, 4, 8}); + assertEquals(0, changeSet.getChangeRanges().length); + assertEquals(0, changeSet.getDeletionRanges().length); + assertEquals(0, changeSet.getChanges().length); + assertEquals(0, changeSet.getDeletions().length); + looperThread.testComplete(); + } + }); + + realm.beginTransaction(); + createObjects(realm, + 1, + 3, 4, + 8); + realm.commitTransaction(); + } + + @Test + @RunTestInLooperThread + public void changes() { + Realm realm = looperThread.realm; + populateData(realm, 10); + RealmResults results = realm.where(AllTypes.class).findAllSorted(AllTypes.FIELD_LONG); + results.addChangeListener(new OrderedRealmCollectionChangeListener>() { + @Override + public void onChange(RealmResults collection, OrderedCollectionChangeSet changeSet) { + checkRanges(changeSet.getChangeRanges(), + 0, 1, + 2, 3, + 8, 2); + assertArrayEquals(changeSet.getChanges(), new int[]{0, 2, 3, 4, 8, 9}); + assertEquals(0, changeSet.getInsertionRanges().length); + assertEquals(0, changeSet.getDeletionRanges().length); + assertEquals(0, changeSet.getInsertions().length); + assertEquals(0, changeSet.getDeletions().length); + looperThread.testComplete(); + } + }); + + realm.beginTransaction(); + modifyObjects(realm, + 0, + 2, 3, 4, + 8, 9); + realm.commitTransaction(); + } + + @Test + @RunTestInLooperThread + public void moves() { + Realm realm = looperThread.realm; + populateData(realm, 10); + RealmResults results = realm.where(AllTypes.class).findAllSorted(AllTypes.FIELD_LONG); + results.addChangeListener(new OrderedRealmCollectionChangeListener>() { + @Override + public void onChange(RealmResults collection, OrderedCollectionChangeSet changeSet) { + checkRanges(changeSet.getDeletionRanges(), + 0, 1, + 9, 1); + assertArrayEquals(changeSet.getDeletions(), new int[]{0, 9}); + checkRanges(changeSet.getInsertionRanges(), + 0, 1, + 9, 1); + assertArrayEquals(changeSet.getInsertions(), new int[]{0, 9}); + assertEquals(0, changeSet.getChangeRanges().length); + assertEquals(0, changeSet.getChanges().length); + looperThread.testComplete(); + } + }); + realm.beginTransaction(); + realm.where(AllTypes.class).equalTo(AllTypes.FIELD_LONG, 0).findFirst().setColumnLong(10); + realm.where(AllTypes.class).equalTo(AllTypes.FIELD_LONG, 9).findFirst().setColumnLong(0); + realm.commitTransaction(); + } + + @Test + @RunTestInLooperThread + public void mixed_changes() { + Realm realm = looperThread.realm; + populateData(realm, 10); + RealmResults results = realm.where(AllTypes.class).findAllSorted(AllTypes.FIELD_LONG); + results.addChangeListener(new OrderedRealmCollectionChangeListener>() { + @Override + public void onChange(RealmResults collection, OrderedCollectionChangeSet changeSet) { + checkRanges(changeSet.getDeletionRanges(), + 0, 2, + 5, 1); + assertArrayEquals(changeSet.getDeletions(), new int[]{0, 1, 5}); + + checkRanges(changeSet.getInsertionRanges(), + 0, 2, + 9, 2); + assertArrayEquals(changeSet.getInsertions(), new int[]{0, 1, 9, 10}); + + checkRanges(changeSet.getChangeRanges(), + 3, 2, + 8, 1); + assertArrayEquals(changeSet.getChanges(), new int[]{3, 4, 8}); + + looperThread.testComplete(); + } + }); + + realm.beginTransaction(); + createObjects(realm, 11, 12, -1, -2); + deleteObjects(realm, 0, 1, 5); + modifyObjects(realm, 12, 3, 4, 9); + realm.commitTransaction(); + // After transaction, '*' means the object has been modified. 12 has been modified as well, but it is created + // and modified in the same transaction, should not be counted in the changes range. + // [-1, -2, 2, *3, *4, 6, 7, 8, *9, 11, 12] + } + + // Change some objects then delete them. Only deletion changes should be sent. + @Test + @RunTestInLooperThread + public void changes_then_delete() { + Realm realm = looperThread.realm; + populateData(realm, 10); + RealmResults results = realm.where(AllTypes.class).findAllSorted(AllTypes.FIELD_LONG); + results.addChangeListener(new OrderedRealmCollectionChangeListener>() { + @Override + public void onChange(RealmResults collection, OrderedCollectionChangeSet changeSet) { + checkRanges(changeSet.getDeletionRanges(), + 0, 2, + 5, 1); + assertArrayEquals(changeSet.getDeletions(), new int[]{0, 1, 5}); + + assertEquals(0, changeSet.getInsertionRanges().length); + assertEquals(0, changeSet.getInsertions().length); + assertEquals(0, changeSet.getChangeRanges().length); + assertEquals(0, changeSet.getChanges().length); + + looperThread.testComplete(); + } + }); + + realm.beginTransaction(); + modifyObjects(realm, 0, 1, 5); + deleteObjects(realm, 0, 1, 5); + realm.commitTransaction(); + } + + // Insert some objects then delete them in the same transaction, the listener should not be triggered. + @Test + @RunTestInLooperThread + public void insert_then_delete() { + Realm realm = looperThread.realm; + populateData(realm, 10); + RealmResults results = realm.where(AllTypes.class).findAllSorted(AllTypes.FIELD_LONG); + results.addChangeListener(new OrderedRealmCollectionChangeListener>() { + @Override + public void onChange(RealmResults collection, OrderedCollectionChangeSet changeSet) { + fail("The listener should not be triggered since the collection has no changes compared with before."); + } + }); + + looperThread.postRunnableDelayed(new Runnable() { + @Override + public void run() { + looperThread.testComplete(); + } + }, 1000); + + realm.beginTransaction(); + createObjects(realm, 10, 11); + deleteObjects(realm, 10, 11); + realm.commitTransaction(); + } + + // The change set should empty when the async query returns at the first time. + @Test + @RunTestInLooperThread + public void emptyChangeSet_findAllAsync(){ + Realm realm = looperThread.realm; + populateData(realm, 10); + final RealmResults results = realm.where(AllTypes.class).findAllSortedAsync(AllTypes.FIELD_LONG); + results.addChangeListener(new OrderedRealmCollectionChangeListener>() { + @Override + public void onChange(RealmResults collection, OrderedCollectionChangeSet changeSet) { + assertSame(collection, results); + assertEquals(9, collection.size()); + assertNull(changeSet); + looperThread.testComplete(); + } + }); + + final CountDownLatch bgDeletionLatch = new CountDownLatch(1); + // beginTransaction() will make the async query return immediately. So we have to delete an object in another + // thread. Also, the latch has to be counted down after transaction committed so the async query results can + // contain the modification in the background transaction. + new Thread(new Runnable() { + @Override + public void run() { + Realm realm = Realm.getInstance(looperThread.realmConfiguration) ; + realm.beginTransaction(); + realm.where(AllTypes.class).equalTo(AllTypes.FIELD_LONG, 0).findFirst().deleteFromRealm(); + realm.commitTransaction(); + realm.close(); + bgDeletionLatch.countDown(); + } + }).start(); + TestHelper.awaitOrFail(bgDeletionLatch); + } +} diff --git a/realm/realm-library/src/androidTest/java/io/realm/RealmResultsTests.java b/realm/realm-library/src/androidTest/java/io/realm/RealmResultsTests.java index a6757ee1a5..78946420d6 100644 --- a/realm/realm-library/src/androidTest/java/io/realm/RealmResultsTests.java +++ b/realm/realm-library/src/androidTest/java/io/realm/RealmResultsTests.java @@ -979,7 +979,7 @@ public void run() { @UiThreadTest public void addChangeListener_null() { try { - collection.addChangeListener(null); + collection.addChangeListener((RealmChangeListener>) null); fail(); } catch (IllegalArgumentException ignored) { } @@ -1024,7 +1024,7 @@ public void run() { @UiThreadTest public void removeChangeListener_null() { try { - collection.removeChangeListener(null); + collection.removeChangeListener((RealmChangeListener) null); fail(); } catch (IllegalArgumentException ignored) { } @@ -1053,7 +1053,7 @@ public void onChange(RealmResults object) { looperThread.keepStrongReference.add(collection); collection.addChangeListener(listenerA); collection.addChangeListener(listenerB); - collection.removeChangeListeners(); + collection.removeAllChangeListeners(); realm.beginTransaction(); realm.createObject(AllTypes.class); diff --git a/realm/realm-library/src/androidTest/java/io/realm/internal/CollectionTests.java b/realm/realm-library/src/androidTest/java/io/realm/internal/CollectionTests.java index a8473b1255..e63fd0fb1a 100644 --- a/realm/realm-library/src/androidTest/java/io/realm/internal/CollectionTests.java +++ b/realm/realm-library/src/androidTest/java/io/realm/internal/CollectionTests.java @@ -408,7 +408,7 @@ public void onChange(Collection collection1) { } private static class TestIterator extends Collection.Iterator { - public TestIterator(Collection collection) { + TestIterator(Collection collection) { super(collection); } diff --git a/realm/realm-library/src/androidTest/java/io/realm/internal/ObserverPairListTests.java b/realm/realm-library/src/androidTest/java/io/realm/internal/ObserverPairListTests.java index 8fa19fbacf..c06aa13900 100644 --- a/realm/realm-library/src/androidTest/java/io/realm/internal/ObserverPairListTests.java +++ b/realm/realm-library/src/androidTest/java/io/realm/internal/ObserverPairListTests.java @@ -132,18 +132,15 @@ public void remove() { // Create a new Integer 1 to see if the equality is checked by the same object. //noinspection UnnecessaryBoxing - pair = new TestObserverPair(new Integer(1), testListener); - observerPairs.remove(pair); + observerPairs.remove(new Integer(1), testListener); assertEquals(1, observerPairs.size()); // Different listener - pair = new TestObserverPair(ONE, new TestListener()); - observerPairs.remove(pair); + observerPairs.remove(ONE, new TestListener()); assertEquals(1, observerPairs.size()); // Should remove now - pair = new TestObserverPair(ONE, testListener); - observerPairs.remove(pair); + observerPairs.remove(ONE, testListener); assertEquals(0, observerPairs.size()); } @@ -240,7 +237,8 @@ public void onCalled(TestObserverPair pair, Object observer) { public void foreach_canRemove() { final AtomicInteger count = new AtomicInteger(0); final TestObserverPair pair1 = new TestObserverPair(ONE, new TestListener()); - final TestObserverPair pair2 = new TestObserverPair(TWO, new TestListener()); + final TestListener listener2 = new TestListener(); + final TestObserverPair pair2 = new TestObserverPair(TWO, listener2); final TestObserverPair pair3 = new TestObserverPair(THREE, new TestListener()); observerPairs.add(pair1); observerPairs.add(pair2); @@ -250,7 +248,7 @@ public void foreach_canRemove() { @Override public void onCalled(TestObserverPair pair, Object observer) { assertFalse(((Integer) observer) == 2); - observerPairs.remove(pair2); + observerPairs.remove(TWO, listener2); count.getAndIncrement(); } }); diff --git a/realm/realm-library/src/main/cpp/CMakeLists.txt b/realm/realm-library/src/main/cpp/CMakeLists.txt index 8681f94c13..58e1d1f9c3 100644 --- a/realm/realm-library/src/main/cpp/CMakeLists.txt +++ b/realm/realm-library/src/main/cpp/CMakeLists.txt @@ -38,7 +38,7 @@ set(classes_LIST io.realm.internal.TableQuery io.realm.internal.SharedRealm io.realm.internal.TestUtil io.realm.log.LogLevel io.realm.log.RealmLog io.realm.Property io.realm.RealmSchema io.realm.RealmObjectSchema io.realm.internal.Collection - io.realm.internal.NativeObjectReference + io.realm.internal.NativeObjectReference io.realm.internal.CollectionChangeSet ) # /./ is the workaround for the problem that AS cannot find the jni headers. # See https://github.com/googlesamples/android-ndk/issues/319 diff --git a/realm/realm-library/src/main/cpp/io_realm_internal_Collection.cpp b/realm/realm-library/src/main/cpp/io_realm_internal_Collection.cpp index a7350f43a4..e17d47f692 100644 --- a/realm/realm-library/src/main/cpp/io_realm_internal_Collection.cpp +++ b/realm/realm-library/src/main/cpp/io_realm_internal_Collection.cpp @@ -263,7 +263,7 @@ Java_io_realm_internal_Collection_nativeStartListening(JNIEnv* env, jobject inst { TR_ENTER_PTR(native_ptr) - static JavaMethod notify_change_listeners(env, instance, "notifyChangeListeners", "(Z)V"); + static JavaMethod notify_change_listeners(env, instance, "notifyChangeListeners", "(J)V"); try { auto wrapper = reinterpret_cast(native_ptr); @@ -271,13 +271,22 @@ Java_io_realm_internal_Collection_nativeStartListening(JNIEnv* env, jobject inst wrapper->m_collection_weak_ref = JavaGlobalWeakRef(env, instance); } - auto cb = [=](realm::CollectionChangeSet const& changes, - std::exception_ptr /*err*/) { + auto cb = [=](CollectionChangeSet const& changes, std::exception_ptr err) { // OS will call all notifiers' callback in one run, so check the Java exception first!! if (env->ExceptionCheck()) return; + if (err) { + try { + std::rethrow_exception(err); + } catch(const std::exception& e) { + realm::jni_util::Log::e("Caught exception in collection change callback %1", e.what()); + return; + } + } + wrapper->m_collection_weak_ref.call_with_local_ref(env, [&] (JNIEnv* local_env, jobject collection_obj) { - local_env->CallVoidMethod(collection_obj, notify_change_listeners, changes.empty()); + local_env->CallVoidMethod(collection_obj, notify_change_listeners, + reinterpret_cast(changes.empty() ? 0 : new CollectionChangeSet(changes))); }); }; diff --git a/realm/realm-library/src/main/cpp/io_realm_internal_CollectionChangeSet.cpp b/realm/realm-library/src/main/cpp/io_realm_internal_CollectionChangeSet.cpp new file mode 100644 index 0000000000..450324c171 --- /dev/null +++ b/realm/realm-library/src/main/cpp/io_realm_internal_CollectionChangeSet.cpp @@ -0,0 +1,127 @@ +/* + * 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. + */ + +#include "io_realm_internal_CollectionChangeSet.h" + +#include + +#include "util.hpp" + +using namespace realm; + +static void finalize_changeset(jlong ptr); +static jintArray index_set_to_jint_array(JNIEnv* env, const IndexSet& index_set); +static jintArray index_set_to_indices_array(JNIEnv* env, const IndexSet& index_set); + +static void finalize_changeset(jlong ptr) +{ + TR_ENTER_PTR(ptr); + delete reinterpret_cast(ptr); +} + +static jintArray index_set_to_jint_array(JNIEnv* env, const IndexSet& index_set) +{ + if (index_set.empty()) { + return env->NewIntArray(0); + } + + std::vector ranges_vector; + for (auto& changes : index_set) { + ranges_vector.push_back(changes.first); + ranges_vector.push_back(changes.second - changes.first); + } + + if (ranges_vector.size() > io_realm_internal_CollectionChangeSet_MAX_ARRAY_LENGTH) { + std::ostringstream error_msg; + error_msg << "There are too many ranges changed in this change set. They cannot fit into an array." << + " ranges_vector's size: " << ranges_vector.size() << + " Java array's max size: " << io_realm_internal_CollectionChangeSet_MAX_ARRAY_LENGTH << "."; + ThrowException(env, IllegalState, error_msg.str()); + return nullptr; + } + jintArray jint_array = env->NewIntArray(static_cast(ranges_vector.size())); + env->SetIntArrayRegion(jint_array, 0, ranges_vector.size(), ranges_vector.data()); + return jint_array; +} + +static jintArray index_set_to_indices_array(JNIEnv* env, const IndexSet& index_set) +{ + if (index_set.empty()) { + return env->NewIntArray(0); + } + + std::vector indices_vector; + for (auto index : index_set.as_indexes()) { + indices_vector.push_back(index); + } + if (indices_vector.size() > io_realm_internal_CollectionChangeSet_MAX_ARRAY_LENGTH) { + std::ostringstream error_msg; + error_msg << "There are too many indices in this change set. They cannot fit into an array." << + " indices_vector's size: " << indices_vector.size() << + " Java array's max size: " << io_realm_internal_CollectionChangeSet_MAX_ARRAY_LENGTH << "."; + ThrowException(env, IllegalState, error_msg.str()); + return nullptr; + } + jintArray jint_array = env->NewIntArray(static_cast(indices_vector.size())); + env->SetIntArrayRegion(jint_array, 0, indices_vector.size(), indices_vector.data()); + return jint_array; +} + +JNIEXPORT jlong JNICALL +Java_io_realm_internal_CollectionChangeSet_nativeGetFinalizerPtr(JNIEnv*, jclass) +{ + TR_ENTER() + return reinterpret_cast(&finalize_changeset); +} + +JNIEXPORT jintArray JNICALL +Java_io_realm_internal_CollectionChangeSet_nativeGetRanges(JNIEnv *env, jclass, jlong native_ptr, jint type) +{ + TR_ENTER_PTR(native_ptr) + // no throws + auto& change_set = *reinterpret_cast(native_ptr); + switch (type) { + case io_realm_internal_CollectionChangeSet_TYPE_DELETION: + return index_set_to_jint_array(env, change_set.deletions); + case io_realm_internal_CollectionChangeSet_TYPE_INSERTION: + return index_set_to_jint_array(env, change_set.insertions); + case io_realm_internal_CollectionChangeSet_TYPE_MODIFICATION: + return index_set_to_jint_array(env, change_set.modifications_new); + default: + REALM_UNREACHABLE(); + break; + } +} + +JNIEXPORT jintArray JNICALL +Java_io_realm_internal_CollectionChangeSet_nativeGetIndices(JNIEnv *env, jclass, jlong native_ptr, jint type) +{ + TR_ENTER_PTR(native_ptr) + // no throws + auto& change_set = *reinterpret_cast(native_ptr); + switch (type) { + case io_realm_internal_CollectionChangeSet_TYPE_DELETION: + return index_set_to_indices_array(env, change_set.deletions); + case io_realm_internal_CollectionChangeSet_TYPE_INSERTION: + return index_set_to_indices_array(env, change_set.insertions); + case io_realm_internal_CollectionChangeSet_TYPE_MODIFICATION: + return index_set_to_indices_array(env, change_set.modifications_new); + default: + REALM_UNREACHABLE(); + break; + } +} + diff --git a/realm/realm-library/src/main/java/io/realm/BaseRealm.java b/realm/realm-library/src/main/java/io/realm/BaseRealm.java index 64eb001e0d..9396d46c79 100644 --- a/realm/realm-library/src/main/java/io/realm/BaseRealm.java +++ b/realm/realm-library/src/main/java/io/realm/BaseRealm.java @@ -141,7 +141,7 @@ protected void addListener(RealmChangeListener listener * @throws IllegalStateException if you try to remove a listener from a non-Looper Thread. * @see io.realm.RealmChangeListener */ - public void removeChangeListener(RealmChangeListener listener) { + protected void removeListener(RealmChangeListener listener) { if (listener == null) { throw new IllegalArgumentException("Listener should not be null"); } @@ -177,7 +177,7 @@ public void removeChangeListener(RealmChangeListener li * @throws IllegalStateException if you try to remove listeners from a non-Looper Thread. * @see io.realm.RealmChangeListener */ - public void removeAllChangeListeners() { + protected void removeAllListeners() { checkIfValid(); sharedRealm.capabilities.checkCanDeliverNotification("removeListener cannot be called on current thread."); sharedRealm.realmNotifier.removeChangeListeners(this); diff --git a/realm/realm-library/src/main/java/io/realm/DynamicRealm.java b/realm/realm-library/src/main/java/io/realm/DynamicRealm.java index 1fce306c7e..86c894ca50 100644 --- a/realm/realm-library/src/main/java/io/realm/DynamicRealm.java +++ b/realm/realm-library/src/main/java/io/realm/DynamicRealm.java @@ -42,7 +42,7 @@ * @see Realm * @see RealmSchema */ -public class DynamicRealm extends BaseRealm { +public class DynamicRealm extends BaseRealm implements RealmObservable { private DynamicRealm(RealmConfiguration configuration) { super(configuration); @@ -134,10 +134,27 @@ public RealmQuery where(String className) { * @see #removeAllChangeListeners() * @see #waitForChange() */ + @Override public void addChangeListener(RealmChangeListener listener) { super.addListener(listener); } + /** + * {@inheritDoc} + */ + @Override + public void removeChangeListener(RealmChangeListener listener) { + super.removeListener(listener); + } + + /** + * {@inheritDoc} + */ + @Override + public void removeAllChangeListeners() { + super.removeAllListeners(); + } + /** * Deletes all objects of the specified class from the Realm. * diff --git a/realm/realm-library/src/main/java/io/realm/OrderedCollectionChangeSet.java b/realm/realm-library/src/main/java/io/realm/OrderedCollectionChangeSet.java new file mode 100644 index 0000000000..a162848569 --- /dev/null +++ b/realm/realm-library/src/main/java/io/realm/OrderedCollectionChangeSet.java @@ -0,0 +1,99 @@ +/* + * 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; + +/** + * This interface describes the changes made to a collection during the last update. + *

+ * {@link OrderedCollectionChangeSet} is passed to the {@link OrderedRealmCollectionChangeListener} which is registered + * by {@link RealmResults#addChangeListener(OrderedRealmCollectionChangeListener)}. + *

+ * The change information is available in two formats: a simple array of row indices in the collection for each type of + * change, or an array of {@link Range}s. + */ +public interface OrderedCollectionChangeSet { + /** + * The deleted indices in the previous version of the collection. + * + * @return the indices array. A zero-sized array will be returned if no objects were deleted. + */ + int[] getDeletions(); + + /** + * The inserted indices in the new version of the collection. + * + * @return the indices array. A zero-sized array will be returned if no objects were inserted. + */ + int[] getInsertions(); + + /** + * The modified indices in the new version of the collection. + *

+ * For {@link RealmResults}, this means that one or more of the properties of the object at the given index were + * modified (or an object linked to by that object was modified). + * + * @return the indices array. A zero-sized array will be returned if objects were modified. + */ + int[] getChanges(); + + /** + * The deleted ranges of objects in the previous version of the collection. + * + * @return the {@link Range} array. A zero-sized array will be returned if no objects were deleted. + */ + Range[] getDeletionRanges(); + + /** + * The inserted ranges of objects in the new version of the collection. + * + * @return the {@link Range} array. A zero-sized array will be returned if no objects were inserted. + */ + Range[] getInsertionRanges(); + + /** + * The modified ranges of objects in the new version of the collection. + * + * @return the {@link Range} array. A zero-sized array will be returned if no objects were modified. + */ + Range[] getChangeRanges(); + + /** + * + */ + class Range { + /** + * The start index of this change range. + */ + public final int startIndex; + + /** + * How many elements are inside this range. + */ + public final int length; + + /** + * Creates a {@link Range} with given start index and length. + * + * @param startIndex the start index of this change range. + * @param length how many elements are inside this range. + */ + public Range(int startIndex, int length) { + this.startIndex = startIndex; + this.length = length; + } + } +} diff --git a/realm/realm-library/src/main/java/io/realm/OrderedRealmCollectionChangeListener.java b/realm/realm-library/src/main/java/io/realm/OrderedRealmCollectionChangeListener.java new file mode 100644 index 0000000000..8c51f2a570 --- /dev/null +++ b/realm/realm-library/src/main/java/io/realm/OrderedRealmCollectionChangeListener.java @@ -0,0 +1,40 @@ +/* + * 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; + +/** + * {@link OrderedRealmCollectionChangeListener} can be registered with a {@link RealmResults} to receive a notification + * with a {@link OrderedCollectionChangeSet} to describe the details of what have been changed in the collection from + * last time. + *

+ * Realm instances on a thread without an {@link android.os.Looper} cannot register a + * {@link OrderedRealmCollectionChangeListener}. + *

+ * + * @see RealmResults#addChangeListener(OrderedRealmCollectionChangeListener) + */ +public interface OrderedRealmCollectionChangeListener { + + /** + * This will be called when the async query is finished the first time or the collection of objects has changed. + * + * @param collection the collection this listener is registered to. + * @param changeSet object with information about which rows in the collection were added, removed or modified. + * {@code null} is returned the first time an async query is completed. + */ + void onChange(T collection, OrderedCollectionChangeSet changeSet); +} diff --git a/realm/realm-library/src/main/java/io/realm/Realm.java b/realm/realm-library/src/main/java/io/realm/Realm.java index aca42eeb84..746dfb862b 100644 --- a/realm/realm-library/src/main/java/io/realm/Realm.java +++ b/realm/realm-library/src/main/java/io/realm/Realm.java @@ -124,7 +124,7 @@ * @see ACID * @see Examples using Realm */ -public class Realm extends BaseRealm { +public class Realm extends BaseRealm implements RealmObservable { public static final String DEFAULT_REALM_NAME = RealmConfiguration.DEFAULT_REALM_NAME; @@ -1281,10 +1281,27 @@ public RealmQuery where(Class clazz) { * @see #removeChangeListener(RealmChangeListener) * @see #removeAllChangeListeners() */ + @Override public void addChangeListener(RealmChangeListener listener) { super.addListener(listener); } + /** + * {@inheritDoc} + */ + @Override + public void removeChangeListener(RealmChangeListener listener) { + super.removeListener(listener); + } + + /** + * {@inheritDoc} + */ + @Override + public void removeAllChangeListeners() { + super.removeAllListeners(); + } + /** * Executes a given transaction on the Realm. {@link #beginTransaction()} and {@link #commitTransaction()} will be * called automatically. If any exception is thrown during the transaction {@link #cancelTransaction()} will be diff --git a/realm/realm-library/src/main/java/io/realm/RealmCollectionObservable.java b/realm/realm-library/src/main/java/io/realm/RealmCollectionObservable.java new file mode 100644 index 0000000000..dcb8a5b61e --- /dev/null +++ b/realm/realm-library/src/main/java/io/realm/RealmCollectionObservable.java @@ -0,0 +1,50 @@ +/* + * 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; + +/** + * A collection class implementing this interface is capable of reporting fine-grained notifications about how the + * collection is changed. It will report insertions, deletions and changes, but not how an individual element + * changed. When a change is detected all registered listeners will be triggered. + *

+ * This is often useful when updating UI elements, e.g. {@code RecyclerView.Adapter} can provide nicer animations and + * work more effectively if it knows exactly which elements changed. + * @see RealmObservable for information about more coarse-grained notifications. + * @see Android Adapters + */ +public interface RealmCollectionObservable + extends RealmObservable { + /** + * Adds a change listener to this {@link OrderedRealmCollection}. + * + * @param listener the change listener to be notified. + * @throws IllegalArgumentException if the change listener is {@code null}. + * @throws IllegalStateException if you try to add a listener from a non-Looper or + * {@link android.app.IntentService} thread. + */ + void addChangeListener(S listener); + + /** + * Removes the specified change listener. + * + * @param listener the change listener to be removed. + * @throws IllegalArgumentException if the change listener is {@code null}. + * @throws IllegalStateException if you try to remove a listener from a non-Looper Thread. + * @see io.realm.RealmChangeListener + */ + void removeChangeListener(S listener); +} diff --git a/realm/realm-library/src/main/java/io/realm/RealmObservable.java b/realm/realm-library/src/main/java/io/realm/RealmObservable.java new file mode 100644 index 0000000000..6d9e619b09 --- /dev/null +++ b/realm/realm-library/src/main/java/io/realm/RealmObservable.java @@ -0,0 +1,55 @@ +/* + * 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; + +/** + * A class implementing this interface is capable of reporting when the data stored by the class have changed. When that + * happens all registered {@link RealmChangeListener}'s will be triggered. + *

+ * This class will only report that something changed, not what changed. + * @see RealmCollectionObservable for information about more fine-grained collection notifications. + */ +public interface RealmObservable { + /** + * Adds a change listener to this {@link RealmResults}, {@link RealmList}, {@link Realm}, {@link DynamicRealm} or + * {@link RealmObject}. + * + * @param listener the change listener to be notified. + * @throws IllegalArgumentException if the change listener is {@code null}. + * @throws IllegalStateException if you try to add a listener from a non-Looper or + * {@link android.app.IntentService} thread. + */ + void addChangeListener(RealmChangeListener listener); + + /** + * Removes the specified change listener. + * + * @param listener the change listener to be removed. + * @throws IllegalArgumentException if the change listener is {@code null}. + * @throws IllegalStateException if you try to remove a listener from a non-Looper Thread. + * @see io.realm.RealmChangeListener + */ + void removeChangeListener(RealmChangeListener listener); + + /** + * Removes all user-defined change listeners. + * + * @throws IllegalStateException if you try to remove listeners from a non-Looper Thread. + * @see io.realm.RealmChangeListener + */ + void removeAllChangeListeners(); +} diff --git a/realm/realm-library/src/main/java/io/realm/RealmResults.java b/realm/realm-library/src/main/java/io/realm/RealmResults.java index bee6375f1c..70eb49ec00 100644 --- a/realm/realm-library/src/main/java/io/realm/RealmResults.java +++ b/realm/realm-library/src/main/java/io/realm/RealmResults.java @@ -17,21 +17,9 @@ package io.realm; -import android.app.IntentService; import android.os.Looper; -import java.util.AbstractList; -import java.util.ConcurrentModificationException; -import java.util.Date; -import java.util.Iterator; -import java.util.ListIterator; - -import io.realm.internal.InvalidRow; -import io.realm.internal.RealmObjectProxy; -import io.realm.internal.SortDescriptor; -import io.realm.internal.Table; import io.realm.internal.Collection; -import io.realm.internal.UncheckedRow; import rx.Observable; /** @@ -61,7 +49,8 @@ * @see RealmQuery#findAll() * @see Realm#executeTransaction(Realm.Transaction) */ -public class RealmResults extends OrderedRealmCollectionImpl { +public class RealmResults extends OrderedRealmCollectionImpl + implements RealmCollectionObservable, OrderedRealmCollectionChangeListener>> { RealmResults(BaseRealm realm, Collection collection, Class clazz) { super(realm, collection, clazz); @@ -117,44 +106,59 @@ public boolean load() { } /** - * Adds a change listener to this RealmResults. - * - * @param listener the change listener to be notified. - * @throws IllegalArgumentException if the change listener is {@code null}. - * @throws IllegalStateException if you try to add a listener from a non-Looper or {@link IntentService} thread. + * {@inheritDoc} */ + @Override public void addChangeListener(RealmChangeListener> listener) { + checkForAddRemoveListener(listener); + collection.addListener(this, listener); + } + + @Override + public void addChangeListener(OrderedRealmCollectionChangeListener> listener) { + checkForAddRemoveListener(listener); + collection.addListener(this, listener); + } + + private void checkForAddRemoveListener(Object listener) { if (listener == null) { throw new IllegalArgumentException("Listener should not be null"); } realm.checkIfValid(); realm.sharedRealm.capabilities.checkCanDeliverNotification(BaseRealm.LISTENER_NOT_ALLOWED_MESSAGE); - collection.addListener(this, listener); } /** - * Removes a previously registered listener. - * - * @param listener the instance to be removed. - * @throws IllegalArgumentException if the change listener is {@code null}. - * @throws IllegalStateException if you try to remove a listener from a non-Looper Thread. + * {@inheritDoc} */ - public void removeChangeListener(RealmChangeListener listener) { - if (listener == null) { - throw new IllegalArgumentException("Listener should not be null"); - } + @Override + public void removeAllChangeListeners() { realm.checkIfValid(); realm.sharedRealm.capabilities.checkCanDeliverNotification(BaseRealm.LISTENER_NOT_ALLOWED_MESSAGE); - collection.removeListener(this, listener); + collection.removeAllListeners(); } /** - * Removes all registered listeners. + * Use {@link #removeAllChangeListeners()} instead. */ + @Deprecated public void removeChangeListeners() { - realm.checkIfValid(); - realm.sharedRealm.capabilities.checkCanDeliverNotification(BaseRealm.LISTENER_NOT_ALLOWED_MESSAGE); - collection.removeAllListeners(); + removeAllChangeListeners(); + } + + /** + * {@inheritDoc} + */ + @Override + public void removeChangeListener(RealmChangeListener listener) { + checkForAddRemoveListener(listener); + collection.removeListener(this, listener); + } + + @Override + public void removeChangeListener(OrderedRealmCollectionChangeListener> listener) { + checkForAddRemoveListener(listener); + collection.removeListener(this, listener); } /** diff --git a/realm/realm-library/src/main/java/io/realm/internal/Collection.java b/realm/realm-library/src/main/java/io/realm/internal/Collection.java index 30e4794e83..358b794cb6 100644 --- a/realm/realm-library/src/main/java/io/realm/internal/Collection.java +++ b/realm/realm-library/src/main/java/io/realm/internal/Collection.java @@ -20,6 +20,8 @@ import java.util.Date; import java.util.NoSuchElementException; +import io.realm.OrderedCollectionChangeSet; +import io.realm.OrderedRealmCollectionChangeListener; import io.realm.RealmChangeListener; /** @@ -29,13 +31,59 @@ @Keep public class Collection implements NativeObject { - private class CollectionObserverPair extends ObserverPairList.ObserverPair> { - public CollectionObserverPair(T observer, RealmChangeListener listener) { + private class CollectionObserverPair extends ObserverPairList.ObserverPair { + public CollectionObserverPair(T observer, Object listener) { super(observer, listener); } - public void onChange(T observer) { - listener.onChange(observer); + public void onChange(T observer, OrderedCollectionChangeSet changes) { + if (listener instanceof OrderedRealmCollectionChangeListener) { + //noinspection unchecked + ((OrderedRealmCollectionChangeListener)listener).onChange(observer, changes); + } else if (listener instanceof RealmChangeListener) { + //noinspection unchecked + ((RealmChangeListener)listener).onChange(observer); + } else { + throw new RuntimeException("Unsupported listener type: " + listener); + } + } + } + + private static class RealmChangeListenerWrapper implements OrderedRealmCollectionChangeListener { + private final RealmChangeListener listener; + + RealmChangeListenerWrapper(RealmChangeListener listener) { + this.listener = listener; + } + + @Override + public void onChange(T collection, OrderedCollectionChangeSet changes) { + listener.onChange(collection); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof RealmChangeListenerWrapper && + listener == ((RealmChangeListenerWrapper) obj).listener; + } + + @Override + public int hashCode() { + return listener.hashCode(); + } + } + + private static class Callback implements ObserverPairList.Callback { + private final OrderedCollectionChangeSet changeSet; + + Callback(OrderedCollectionChangeSet changeSet) { + this.changeSet = changeSet; + } + + @Override + public void onCalled(CollectionObserverPair pair, Object observer) { + //noinspection unchecked + pair.onChange(observer, changeSet); } } @@ -208,14 +256,6 @@ public void set(T object) { private boolean isSnapshot = false; private final ObserverPairList observerPairs = new ObserverPairList(); - private static final ObserverPairList.Callback onChangeCallback = - new ObserverPairList.Callback() { - @Override - public void onCalled(CollectionObserverPair pair, Object observer) { - //noinspection unchecked - pair.onChange(observer); - } - }; // Public for static checking in JNI @SuppressWarnings("WeakerAccess") @@ -417,7 +457,7 @@ public boolean deleteLast() { return nativeDeleteLast(nativePtr); } - public void addListener(T observer, RealmChangeListener listener) { + public void addListener(T observer, OrderedRealmCollectionChangeListener listener) { if (observerPairs.isEmpty()) { nativeStartListening(nativePtr); } @@ -425,14 +465,21 @@ public void addListener(T observer, RealmChangeListener listener) { observerPairs.add(collectionObserverPair); } - public void removeListener(T observer, RealmChangeListener listener) { - CollectionObserverPair collectionObserverPair = new CollectionObserverPair(observer, listener); - observerPairs.remove(collectionObserverPair); + public void addListener(T observer, RealmChangeListener listener) { + addListener(observer, new RealmChangeListenerWrapper(listener)); + } + + public void removeListener(T observer, OrderedRealmCollectionChangeListener listener) { + observerPairs.remove(observer, listener); if (observerPairs.isEmpty()) { nativeStopListening(nativePtr); } } + public void removeListener(T observer, RealmChangeListener listener) { + removeListener(observer, new RealmChangeListenerWrapper(listener)); + } + public void removeAllListeners() { observerPairs.clear(); nativeStopListening(nativePtr); @@ -444,15 +491,17 @@ public boolean isValid() { // Called by JNI @SuppressWarnings("unused") - private void notifyChangeListeners(boolean emptyChanges) { - if (emptyChanges && isLoaded()) { + private void notifyChangeListeners(long nativeChangeSetPtr) { + if (nativeChangeSetPtr == 0 && isLoaded()) { return; } + boolean wasLoaded = loaded; loaded = true; - // TODO: For the fine grained notification, remember to call the callback with empty change set if the - // isLoaded() returns false even when the change set is not empty. Since in that case, it is the first time - // the listener gets called to indicate async query returns. - observerPairs.foreach(onChangeCallback); + // Object Store compute the change set between the SharedGroup versions when the query created and the latest. + // So it is possible it deliver a non-empty change set for the first async query returns. In this case, we + // return an empty change set to user since it is considered as the first time async query returns. + observerPairs.foreach(new Callback(nativeChangeSetPtr == 0 || !wasLoaded ? + null : new CollectionChangeSet(nativeChangeSetPtr))); } public Mode getMode() { @@ -478,7 +527,7 @@ public void load() { if (loaded) { return; } - notifyChangeListeners(true); + notifyChangeListeners(0); } private static native long nativeGetFinalizerPtr(); diff --git a/realm/realm-library/src/main/java/io/realm/internal/CollectionChangeSet.java b/realm/realm-library/src/main/java/io/realm/internal/CollectionChangeSet.java new file mode 100644 index 0000000000..96804a1121 --- /dev/null +++ b/realm/realm-library/src/main/java/io/realm/internal/CollectionChangeSet.java @@ -0,0 +1,129 @@ +/* + * 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.internal; + +import io.realm.OrderedCollectionChangeSet; + +/** + * Implementation of {@link OrderedCollectionChangeSet}. This class holds a pointer to the Object Store's + * CollectionChangeSet and read from it only when needed. Creating an Java object from JNI when the collection + * notification arrives, is avoided since we also support the collection listeners without a change set parameter, + * parsing the change set may not be necessary all the time. + */ +public class CollectionChangeSet implements OrderedCollectionChangeSet, NativeObject { + + // Used in JNI. + @SuppressWarnings("WeakerAccess") + public static final int TYPE_DELETION = 0; + @SuppressWarnings("WeakerAccess") + public static final int TYPE_INSERTION = 1; + @SuppressWarnings("WeakerAccess") + public static final int TYPE_MODIFICATION = 2; + // Max array length is VM dependent. This is a safe value. + // See http://stackoverflow.com/questions/3038392/do-java-arrays-have-a-maximum-size + @SuppressWarnings({"WeakerAccess", "unused"}) + public static final int MAX_ARRAY_LENGTH = Integer.MAX_VALUE - 8; + + private static long finalizerPtr = nativeGetFinalizerPtr(); + private final long nativePtr; + + public CollectionChangeSet(long nativePtr) { + this.nativePtr = nativePtr; + Context.dummyContext.addReference(this); + } + + /** + * {@inheritDoc} + */ + @Override + public int[] getDeletions() { + return nativeGetIndices(nativePtr, TYPE_DELETION); + } + + /** + * {@inheritDoc} + */ + @Override + public int[] getInsertions() { + return nativeGetIndices(nativePtr, TYPE_INSERTION); + } + + /** + * {@inheritDoc} + */ + @Override + public int[] getChanges() { + return nativeGetIndices(nativePtr, TYPE_MODIFICATION); + } + + /** + * {@inheritDoc} + */ + @Override + public Range[] getDeletionRanges() { + return longArrayToRangeArray(nativeGetRanges(nativePtr, TYPE_DELETION)); + } + + /** + * {@inheritDoc} + */ + @Override + public Range[] getInsertionRanges() { + return longArrayToRangeArray(nativeGetRanges(nativePtr, TYPE_INSERTION)); + } + + /** + * {@inheritDoc} + */ + @Override + public Range[] getChangeRanges() { + return longArrayToRangeArray(nativeGetRanges(nativePtr, TYPE_MODIFICATION)); + } + + /** + * {@inheritDoc} + */ + @Override + public long getNativePtr() { + return nativePtr; + } + + @Override + public long getNativeFinalizerPtr() { + return finalizerPtr; + } + + // Convert long array returned by the nativeGetXxxRanges() to Range array. + private Range[] longArrayToRangeArray(int[] longArray) { + if (longArray == null) { + // Returns a size 0 array so we know JNI gets called. + return new Range[0]; + } + + Range[] ranges = new Range[longArray.length / 2]; + for (int i = 0; i < ranges.length; i++) { + ranges[i] = new Range(longArray[i * 2], longArray[i * 2 + 1]); + } + return ranges; + } + + private native static long nativeGetFinalizerPtr(); + // Returns the ranges as an long array. eg.: [startIndex1, length1, startIndex2, length2, ...] + private native static int[] nativeGetRanges(long nativePtr, int type); + // Returns the indices array. + private native static int[] nativeGetIndices(long nativePtr, int type); +} diff --git a/realm/realm-library/src/main/java/io/realm/internal/Context.java b/realm/realm-library/src/main/java/io/realm/internal/Context.java index 9dc084e83f..cb9bb6ece6 100644 --- a/realm/realm-library/src/main/java/io/realm/internal/Context.java +++ b/realm/realm-library/src/main/java/io/realm/internal/Context.java @@ -28,6 +28,8 @@ public class Context { private final static ReferenceQueue referenceQueue = new ReferenceQueue(); private final static Thread finalizingThread = new Thread(new FinalizerRunnable(referenceQueue)); + // Dummy context which will be used by native objects which's destructors are always thread safe. + final static Context dummyContext = new Context(); static { finalizingThread.setName("RealmFinalizingDaemon"); diff --git a/realm/realm-library/src/main/java/io/realm/internal/ObserverPairList.java b/realm/realm-library/src/main/java/io/realm/internal/ObserverPairList.java index e3f0fa717a..aeb5f8cb0b 100644 --- a/realm/realm-library/src/main/java/io/realm/internal/ObserverPairList.java +++ b/realm/realm-library/src/main/java/io/realm/internal/ObserverPairList.java @@ -32,25 +32,24 @@ * * @param the type of {@link ObserverPair}. */ -public class ObserverPairList { +class ObserverPairList { /** * @param the type of observer. * @param the type of listener. */ - public abstract static class ObserverPair { - protected final WeakReference observerRef; + abstract static class ObserverPair { + final WeakReference observerRef; protected final S listener; // Should only be set by the outer class. To marked it as removed in case it is removed in foreach callback. boolean removed = false; - public ObserverPair(T observer, S listener) { + ObserverPair(T observer, S listener) { this.listener = listener; this.observerRef = new WeakReference(observer); } - // The two pairs will be treated as the same only when the observers are the same and the listeners are the same - // as well. + // The two pairs will be treated as the same only when the observers are the same and the listeners are equal. @Override public boolean equals(Object obj) { if (this == obj) { @@ -96,7 +95,7 @@ interface Callback { * * @param callback to be executed on the pair. */ - public void foreach(Callback callback) { + void foreach(Callback callback) { for (T pair : pairs) { if (cleared) { break; @@ -123,23 +122,28 @@ public void clear() { public void add(T pair) { if (!pairs.contains(pair)) { pairs.add(pair); + pair.removed = false; } if (cleared) { cleared = false; } } - public void remove(T pair) { - pair.removed = true; - pairs.remove(pair); + public void remove(S observer, U listener) { + for (T pair : pairs) { + if (observer == pair.observerRef.get() && listener.equals(pair.listener)) { + pair.removed = true; + pairs.remove(pair); + break; + } + } } - public void removeByObserver(Object observer) { + void removeByObserver(Object observer) { for (T pair : pairs) { Object object = pair.observerRef.get(); - if (object == null) { - pairs.remove(pair); - } else if (object == observer) { + if (object == null || object == observer) { + pair.removed = true; pairs.remove(pair); } } diff --git a/realm/realm-library/src/main/java/io/realm/internal/RealmNotifier.java b/realm/realm-library/src/main/java/io/realm/internal/RealmNotifier.java index 90ca9af48f..74726c5a05 100644 --- a/realm/realm-library/src/main/java/io/realm/internal/RealmNotifier.java +++ b/realm/realm-library/src/main/java/io/realm/internal/RealmNotifier.java @@ -127,8 +127,7 @@ public void addChangeListener(T observer, RealmChangeListener realmChange } public void removeChangeListener(E observer, RealmChangeListener realmChangeListener) { - RealmObserverPair observerPair = new RealmObserverPair(observer, realmChangeListener); - realmObserverPairs.remove(observerPair); + realmObserverPairs.remove(observer, realmChangeListener); } public void removeChangeListeners(E observer) {