/*
* Copyright 2014 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.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.support.test.annotation.UiThreadTest;
import android.support.test.rule.UiThreadTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.util.Log;
import junit.framework.AssertionFailedError;
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.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import io.realm.entities.AllTypes;
import io.realm.entities.Dog;
import io.realm.log.RealmLog;
import io.realm.log.RealmLogger;
import io.realm.rule.RunInLooperThread;
import io.realm.rule.RunTestInLooperThread;
import io.realm.rule.TestRealmConfigurationFactory;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@RunWith(AndroidJUnit4.class)
public class NotificationsTest {
@Rule
public final UiThreadTestRule uiThreadTestRule = new UiThreadTestRule();
@Rule
public final RunInLooperThread looperThread = new RunInLooperThread();
@Rule
public final TestRealmConfigurationFactory configFactory = new TestRealmConfigurationFactory();
private Realm realm;
private RealmConfiguration realmConfig;
@Before
public void setUp() {
realmConfig = configFactory.createConfiguration();
}
@After
public void tearDown() {
Realm.asyncTaskExecutor.resume();
if (realm != null) {
realm.close();
}
}
@Test
public void setAutoRefresh_failsOnNonLooperThread() throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<Boolean> future = executorService.submit(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
Realm realm = Realm.getInstance(realmConfig);
boolean autoRefresh = realm.isAutoRefresh();
assertFalse(autoRefresh);
try {
realm.setAutoRefresh(true);
return false;
} catch (IllegalStateException ignored) {
return true;
} finally {
realm.close();
}
}
});
assertTrue(future.get());
RealmCache.invokeWithGlobalRefCount(realmConfig, new TestHelper.ExpectedCountCallback(0));
}
@Test
public void setAutoRefresh_onHandlerThread() throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<Boolean> future = executorService.submit(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
Looper.prepare();
Realm realm = Realm.getInstance(realmConfig);
assertTrue(realm.isAutoRefresh());
realm.setAutoRefresh(false);
assertFalse(realm.isAutoRefresh());
realm.setAutoRefresh(true);
assertTrue(realm.isAutoRefresh());
realm.close();
return true;
}
});
assertTrue(future.get());
RealmCache.invokeWithGlobalRefCount(realmConfig, new TestHelper.ExpectedCountCallback(0));
}
@Test
@UiThreadTest
public void removeChangeListener() throws InterruptedException, ExecutionException {
final AtomicInteger counter = new AtomicInteger(0);
RealmChangeListener<Realm> listener = new RealmChangeListener<Realm>() {
@Override
public void onChange(Realm object) {
counter.incrementAndGet();
}
};
realm = Realm.getInstance(realmConfig);
realm.addChangeListener(listener);
realm.removeChangeListener(listener);
realm.beginTransaction();
realm.createObject(AllTypes.class);
realm.commitTransaction();
assertEquals(0, counter.get());
}
@Test
@RunTestInLooperThread
public void addChangeListener_duplicatedListener() {
final AtomicInteger counter = new AtomicInteger(0);
RealmChangeListener<Realm> listener = new RealmChangeListener<Realm>() {
@Override
public void onChange(Realm object) {
counter.incrementAndGet();
}
};
Realm realm = looperThread.getRealm();
realm.addChangeListener(listener);
realm.addChangeListener(listener);
realm.addChangeListener(new RealmChangeListener<Realm>() {
@Override
public void onChange(Realm object) {
assertEquals(1, counter.get());
looperThread.testComplete();
}
});
realm.beginTransaction();
realm.createObject(AllTypes.class);
realm.commitTransaction();
}
@Test
public void notificationsNumber() throws InterruptedException, ExecutionException {
final CountDownLatch isReady = new CountDownLatch(1);
final CountDownLatch isRealmOpen = new CountDownLatch(1);
final AtomicInteger counter = new AtomicInteger(0);
final Looper[] looper = new Looper[1];
final RealmChangeListener<Realm> listener = new RealmChangeListener<Realm>() {
@Override
public void onChange(Realm object) {
counter.incrementAndGet();
}
};
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<Boolean> future = executorService.submit(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
Realm realm = null;
try {
Looper.prepare();
looper[0] = Looper.myLooper();
realm = Realm.getInstance(realmConfig);
realm.addChangeListener(listener);
isReady.countDown();
Looper.loop();
} finally {
if (realm != null) {
realm.close();
isRealmOpen.countDown();
}
}
return true;
}
});
// Waits until the looper in the background thread is started.
TestHelper.awaitOrFail(isReady);
// Triggers OnRealmChanged on background thread.
realm = Realm.getInstance(realmConfig);
realm.beginTransaction();
Dog dog = realm.createObject(Dog.class);
dog.setName("Rex");
realm.commitTransaction();
realm.close();
try {
future.get(1, TimeUnit.SECONDS);
} catch (TimeoutException ignore) {
} finally {
looper[0].quit();
}
// Waits until the Looper thread is actually closed.
TestHelper.awaitOrFail(isRealmOpen);
assertEquals(1, counter.get());
RealmCache.invokeWithGlobalRefCount(realmConfig, new TestHelper.ExpectedCountCallback(0));
}
@Test
public void closeClearingHandlerMessages() throws InterruptedException, TimeoutException, ExecutionException {
final int TEST_SIZE = 10;
final CountDownLatch backgroundLooperStarted = new CountDownLatch(1);
final CountDownLatch addHandlerMessages = new CountDownLatch(1);
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<Boolean> future = executorService.submit(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
Looper.prepare(); // Fake background thread with a looper, eg. a IntentService.
Realm realm = Realm.getInstance(realmConfig);
backgroundLooperStarted.countDown();
// Random operation in the client code.
final RealmResults<Dog> dogs = realm.where(Dog.class).findAll();
if (dogs.size() != 0) {
return false;
}
// Wait for main thread to add update messages.
addHandlerMessages.await(TestHelper.VERY_SHORT_WAIT_SECS, TimeUnit.SECONDS);
// Creates a Handler for the thread now. All message and references for the notification handler will be
// cleared once we call close().
Handler threadHandler = new Handler(Looper.myLooper());
realm.close(); // Close native resources + associated handlers.
// Looper now reads the update message from the main thread if the Handler was not
// cleared. This will cause an IllegalStateException and should not happen.
// If it works correctly. The looper will just block on an empty message queue.
// This is normal behavior but is bad for testing, so we add a custom quit message
// at the end so we can evaluate results faster.
// 500 ms delay is to make sure the notification daemon thread gets time to send notification.
threadHandler.postDelayed(new Runnable() {
@Override
public void run() {
TestHelper.quitLooperOrFail();
}
}, 500);
try {
Looper.loop();
} catch (IllegalStateException e) {
return false;
}
return true;
}
});
// Waits until the looper is started on a background thread.
backgroundLooperStarted.await(TestHelper.VERY_SHORT_WAIT_SECS, TimeUnit.SECONDS);
// Executes a transaction that will trigger a Realm update.
Realm realm = Realm.getInstance(realmConfig);
realm.beginTransaction();
for (int i = 0; i < TEST_SIZE; i++) {
Dog dog = realm.createObject(Dog.class);
dog.setName("Rex " + i);
}
realm.commitTransaction();
assertEquals(TEST_SIZE, realm.where(Dog.class).count());
realm.close();
addHandlerMessages.countDown();
// Checks that messages was properly cleared.
// It looks like getting this future sometimes takes a while for some reason. Setting to
// 10s. now.
Boolean result = future.get(10, TimeUnit.SECONDS);
assertTrue(result);
}
@Test
@RunTestInLooperThread
public void globalListener_looperThread_triggeredByLocalCommit() {
final AtomicInteger success = new AtomicInteger(0);
Realm realm = looperThread.getRealm();
realm.addChangeListener(new RealmChangeListener<Realm>() {
@Override
public void onChange(Realm object) {
assertEquals(0, success.getAndIncrement());
looperThread.testComplete();
}
});
realm.beginTransaction();
realm.createObject(AllTypes.class);
realm.commitTransaction();
assertEquals(1, success.get());
}
@Test
@RunTestInLooperThread
public void globalListener_looperThread_triggeredByRemoteCommit() {
final AtomicInteger success = new AtomicInteger(0);
Realm realm = looperThread.getRealm();
realm.addChangeListener(new RealmChangeListener<Realm>() {
@Override
public void onChange(Realm object) {
assertEquals(1, success.get());
looperThread.testComplete();
}
});
realm.executeTransactionAsync(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
realm.createObject(AllTypes.class);
}
});
assertEquals(0, success.getAndIncrement());
}
@Test
@RunTestInLooperThread
public void emptyCommitTriggerChangeListener() {
final RealmChangeListener<Realm> listener = new RealmChangeListener<Realm>() {
@Override
public void onChange(Realm object) {
looperThread.testComplete();
}
};
Realm realm = looperThread.getRealm();
realm.addChangeListener(listener);
realm.beginTransaction();
realm.commitTransaction();
}
@Test
@RunTestInLooperThread
public void addRemoveListenerConcurrency() {
final Realm realm = looperThread.getRealm();
final AtomicInteger counter1 = new AtomicInteger(0);
final AtomicInteger counter2 = new AtomicInteger(0);
final AtomicInteger counter3 = new AtomicInteger(0);
// At least we need 2 listeners existing in the list to make sure
// the iterator.next get called.
// This one will be added when listener2's onChange called.
final RealmChangeListener<Realm> listener1 = new RealmChangeListener<Realm>() {
@Override
public void onChange(Realm object) {
// Step 7: Last listener called. Should only be called once.
counter1.incrementAndGet();
// after listener2.onChange
// Since duplicated entries will be ignored, we still have:
// [listener2, listener1].
assertEquals(1, counter1.get());
assertEquals(2, counter2.get());
assertEquals(1, counter3.get());
looperThread.testComplete();
}
};
// This one will be existing in the list all the time.
final RealmChangeListener<Realm> listener2 = new RealmChangeListener<Realm>() {
@Override
public void onChange(Realm object) {
// Step 3: Listener2 called
// Listener state [listener2, listener3, listener1].
// Listener 1 will not be called this time around.
counter2.incrementAndGet();
realm.addChangeListener(listener1);
}
};
// This one will be removed after first transaction
RealmChangeListener<Realm> listener3 = new RealmChangeListener<Realm>() {
@Override
public void onChange(Realm object) {
// Step 4: Listener3 called
// Listener state [listener2, listener1].
counter3.incrementAndGet();
realm.removeChangeListener(this);
// Step 5: Asserts proper state
// [listener2, listener1].
assertEquals(0, counter1.get());
assertEquals(1, counter2.get());
assertEquals(1, counter3.get());
// Step 6: Triggers next round of changes on [listener2, listener1].
realm.beginTransaction();
realm.createObject(AllTypes.class);
realm.commitTransaction();
}
};
// Step 1: Adds initial listeners
// Listener state [listener2, listener3].
realm.addChangeListener(listener2);
realm.addChangeListener(listener3);
// Step 2: Triggers change listeners.
realm.beginTransaction();
realm.createObject(AllTypes.class);
realm.commitTransaction();
}
@Test
@RunTestInLooperThread
public void realmNotificationOrder() {
// Tests that global notifications are called in the order they are added
// Test both ways to check accidental ordering from unordered collections.
final AtomicInteger listenerACalled = new AtomicInteger(0);
final AtomicInteger listenerBCalled = new AtomicInteger(0);
final Realm realm = looperThread.getRealm();
final RealmChangeListener<Realm> listenerA = new RealmChangeListener<Realm>() {
@Override
public void onChange(Realm object) {
int called = listenerACalled.incrementAndGet();
if (called == 2) {
assertEquals(2, listenerBCalled.get());
looperThread.testComplete();
}
}
};
final RealmChangeListener<Realm> listenerB = new RealmChangeListener<Realm>() {
@Override
public void onChange(Realm object) {
listenerBCalled.incrementAndGet();
if (listenerBCalled.get() == 1) {
// 2. Reverse order.
realm.removeAllChangeListeners();
realm.addChangeListener(this);
realm.addChangeListener(listenerA);
// Async transaction to avoid endless recursion.
realm.executeTransactionAsync(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
}
});
} else if (listenerBCalled.get() == 2) {
assertEquals(1, listenerACalled.get());
}
}
};
// 1. Adds initial ordering.
realm.addChangeListener(listenerA);
realm.addChangeListener(listenerB);
realm.beginTransaction();
realm.commitTransaction();
}
// Tests that if the same configuration is used on 2 different Looper threads that each gets its own Handler. This
// prevents commitTransaction from accidentally posting messages to Handlers which might reference a closed Realm.
@Test
public void doNotUseClosedHandler() throws InterruptedException {
final CountDownLatch handlerNotified = new CountDownLatch(1);
final CountDownLatch backgroundThread1Started = new CountDownLatch(1);
final CountDownLatch backgroundThread2Closed = new CountDownLatch(1);
// Creates Handler on Thread1 by opening a Realm instance.
new Thread("thread1") {
@Override
public void run() {
Looper.prepare();
final Realm realm = Realm.getInstance(realmConfig);
RealmChangeListener<Realm> listener = new RealmChangeListener<Realm>() {
@Override
public void onChange(Realm object) {
realm.close();
handlerNotified.countDown();
}
};
realm.addChangeListener(listener);
backgroundThread1Started.countDown();
Looper.loop();
}
}.start();
// Creates Handler on Thread2 for the same Realm path and closes the Realm instance again.
new Thread("thread2") {
@Override
public void run() {
Looper.prepare();
Realm realm = Realm.getInstance(realmConfig);
RealmChangeListener<Realm> listener = new RealmChangeListener<Realm>() {
@Override
public void onChange(Realm object) {
fail("This handler should not be notified");
}
};
realm.addChangeListener(listener);
realm.close();
backgroundThread2Closed.countDown();
Looper.loop();
}
}.start();
TestHelper.awaitOrFail(backgroundThread1Started);
TestHelper.awaitOrFail(backgroundThread2Closed);
Realm realm = Realm.getInstance(realmConfig);
realm.beginTransaction();
realm.commitTransaction();
// Any REALM_CHANGED message should now only reach the open Handler on Thread1.
try {
// TODO: Waiting a few seconds is not a reliable condition. Figure out a better way for this.
if (!handlerNotified.await(TestHelper.SHORT_WAIT_SECS, TimeUnit.SECONDS)) {
fail("Handler didn't receive message");
}
} finally {
realm.close();
}
}
// Tests that we handle a Looper thread quiting it's looper before it is done executing the current loop ( = Realm.close()
// isn't called yet).
@Test
public void looperThreadQuitsLooperEarly() throws InterruptedException {
final CountDownLatch backgroundLooperStartedAndStopped = new CountDownLatch(1);
final CountDownLatch mainThreadCommitCompleted = new CountDownLatch(1);
final CountDownLatch backgroundThreadStopped = new CountDownLatch(1);
// Starts background looper and let it hang.
ExecutorService executorService = Executors.newSingleThreadExecutor();
//noinspection unused
final Future<?> future = executorService.submit(new Runnable() {
@Override
public void run() {
Looper.prepare(); // Fake background thread with a looper, eg. a IntentService.
Realm realm = Realm.getInstance(realmConfig);
realm.setAutoRefresh(false);
TestHelper.quitLooperOrFail();
backgroundLooperStartedAndStopped.countDown();
// This will prevent backgroundThreadStopped from being called.
TestHelper.awaitOrFail(mainThreadCommitCompleted);
realm.close();
backgroundThreadStopped.countDown();
}
});
// Creates a commit on another thread.
TestHelper.awaitOrFail(backgroundLooperStartedAndStopped);
Realm realm = Realm.getInstance(realmConfig);
RealmLogger logger = TestHelper.getFailureLogger(Log.WARN);
RealmLog.add(logger);
realm.beginTransaction();
realm.commitTransaction(); // If the Handler on the background is notified it will trigger a Log warning.
mainThreadCommitCompleted.countDown();
TestHelper.awaitOrFail(backgroundThreadStopped);
realm.close();
RealmLog.remove(logger);
}
@Test
public void handlerThreadShouldReceiveNotification() throws ExecutionException, InterruptedException {
final AssertionFailedError[] assertionFailedErrors = new AssertionFailedError[1];
final CountDownLatch backgroundThreadReady = new CountDownLatch(1);
final CountDownLatch numberOfInvocation = new CountDownLatch(1);
HandlerThread handlerThread = new HandlerThread("handlerThread");
handlerThread.start();
Handler handler = new Handler(handlerThread.getLooper());
handler.post(new Runnable() {
@Override
public void run() {
try {
assertEquals("handlerThread", Thread.currentThread().getName());
} catch (AssertionFailedError e) {
assertionFailedErrors[0] = e;
}
final Realm backgroundRealm = Realm.getInstance(realmConfig);
backgroundRealm.addChangeListener(new RealmChangeListener<Realm>() {
@Override
public void onChange(Realm object) {
backgroundRealm.close();
numberOfInvocation.countDown();
}
});
backgroundThreadReady.countDown();
}
});
TestHelper.awaitOrFail(backgroundThreadReady);
// At this point the background thread started & registered the listener.
Realm realm = Realm.getInstance(realmConfig);
realm.beginTransaction();
realm.createObject(AllTypes.class);
realm.commitTransaction();
TestHelper.awaitOrFail(numberOfInvocation);
realm.close();
handlerThread.quit();
if (assertionFailedErrors[0] != null) {
throw assertionFailedErrors[0];
}
}
@Test
public void nonLooperThreadShouldNotifyLooperThreadAboutCommit() {
final CountDownLatch mainThreadReady = new CountDownLatch(1);
final CountDownLatch backgroundThreadClosed = new CountDownLatch(1);
final CountDownLatch numberOfInvocation = new CountDownLatch(1);
Thread thread = new Thread() {
@Override
public void run() {
TestHelper.awaitOrFail(mainThreadReady);
Realm realm = Realm.getInstance(realmConfig);
realm.beginTransaction();
realm.createObject(AllTypes.class);
realm.commitTransaction();
realm.close();
backgroundThreadClosed.countDown();
}
};
thread.start();
HandlerThread mainThread = new HandlerThread("mainThread");
mainThread.start();
Handler handler = new Handler(mainThread.getLooper());
handler.post(new Runnable() {
@Override
public void run() {
final Realm mainRealm = Realm.getInstance(realmConfig);
mainRealm.addChangeListener(new RealmChangeListener<Realm>() {
@Override
public void onChange(Realm object) {
mainRealm.close();
numberOfInvocation.countDown();
}
});
mainThreadReady.countDown();
}
});
TestHelper.awaitOrFail(numberOfInvocation);
TestHelper.awaitOrFail(backgroundThreadClosed);
mainThread.quit();
}
// The presence of async RealmResults block any `REALM_CHANGE` notification causing historically the Realm
// to advance to the latest version. We make sure in this test that all Realm listeners will be notified
// regardless of the presence of an async RealmResults that will delay the `REALM_CHANGE` sometimes.
@Test
@RunTestInLooperThread
public void asyncRealmResultsShouldNotBlockBackgroundCommitNotification() {
final Realm realm = looperThread.getRealm();
final RealmResults<Dog> dogs = realm.where(Dog.class).findAllAsync();
final AtomicBoolean resultsListenerDone = new AtomicBoolean(false);
final AtomicBoolean realmListenerDone = new AtomicBoolean(false);
looperThread.keepStrongReference(dogs);
assertTrue(dogs.load());
assertEquals(0, dogs.size());
dogs.addChangeListener(new RealmChangeListener<RealmResults<Dog>>() {
@Override
public void onChange(RealmResults<Dog> results) {
if (dogs.size() == 2) {
// Results has the latest changes.
resultsListenerDone.set(true);
if (realmListenerDone.get()) {
looperThread.testComplete();
}
}
}
});
realm.addChangeListener(new RealmChangeListener<Realm>() {
@Override
public void onChange(Realm element) {
if (dogs.size() == 1) {
// Step 2. Creates the second dog.
realm.executeTransactionAsync(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
realm.createObject(Dog.class);
}
});
} else if (dogs.size() == 2) {
// Realm listener can see the latest changes.
realmListenerDone.set(true);
if (resultsListenerDone.get()) {
looperThread.testComplete();
}
}
}
});
// Step 1. Creates the first dog.
realm.executeTransactionAsync(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
realm.createObject(Dog.class);
}
});
}
// The presence of async RealmResults blocks any `REALM_CHANGE` notification . We make sure in this test that all
// Realm listeners will be notified regardless of the presence of an async RealmObject. RealmObjects are special
// in the sense that once you got a row accessor to that object, it is automatically up to date as soon as you
// call advance_read().
@Test
@RunTestInLooperThread
public void asyncRealmObjectShouldNotBlockBackgroundCommitNotification() {
final AtomicInteger numberOfRealmCallbackInvocation = new AtomicInteger(0);
final CountDownLatch signalClosedRealm = new CountDownLatch(1);
final Realm realm = looperThread.getRealm();
realm.addChangeListener(new RealmChangeListener<Realm>() {
@Override
public void onChange(final Realm realm) {
switch (numberOfRealmCallbackInvocation.incrementAndGet()) {
case 1: {
// First commit.
Dog dog = realm.where(Dog.class).findFirstAsync();
assertTrue(dog.load());
dog.addChangeListener(new RealmChangeListener<Dog>() {
@Override
public void onChange(Dog dog) {
}
});
new Thread() {
@Override
public void run() {
Realm threadRealm = Realm.getInstance(realm.getConfiguration());
threadRealm.beginTransaction();
threadRealm.createObject(Dog.class);
threadRealm.commitTransaction();
threadRealm.close();
signalClosedRealm.countDown();
}
}.start();
break;
}
case 2: {
// Finishes test.
TestHelper.awaitOrFail(signalClosedRealm);
looperThread.testComplete();
break;
}
}
}
});
looperThread.postRunnable(new Runnable() {
@Override
public void run() {
realm.beginTransaction();
realm.createObject(Dog.class);
realm.commitTransaction();
}
});
}
public static class PopulateOneAllTypes implements RunInLooperThread.RunnableBefore {
@Override
public void run(RealmConfiguration realmConfig) {
Realm realm = Realm.getInstance(realmConfig);
realm.executeTransaction(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
realm.createObject(AllTypes.class);
}
});
realm.close();
}
}
@Test
@RunTestInLooperThread(before = PopulateOneAllTypes.class)
public void realmListener_realmResultShouldBeSynced() {
final Realm realm = looperThread.getRealm();
final RealmResults<AllTypes> results = realm.where(AllTypes.class).findAll();
assertEquals(1, results.size());
realm.executeTransactionAsync(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
AllTypes allTypes = realm.where(AllTypes.class).findFirst();
assertNotNull(allTypes);
allTypes.deleteFromRealm();
assertEquals(0, realm.where(AllTypes.class).count());
}
});
realm.addChangeListener(new RealmChangeListener<Realm>() {
@Override
public void onChange(Realm element) {
// Changes event triggered by deletion in async transaction.
assertEquals(0, realm.where(AllTypes.class).count());
assertEquals(0, results.size());
looperThread.testComplete();
}
});
}
@Test
@RunTestInLooperThread
public void accessingSyncRealmResultInsideAsyncResultListener() {
final Realm realm = looperThread.getRealm();
final AtomicInteger asyncResultCallback = new AtomicInteger(0);
final RealmResults<AllTypes> syncResults = realm.where(AllTypes.class).findAll();
RealmResults<AllTypes> results = realm.where(AllTypes.class).findAllAsync();
looperThread.keepStrongReference(results);
results.addChangeListener(new RealmChangeListener<RealmResults<AllTypes>>() {
@Override
public void onChange(RealmResults<AllTypes> results) {
switch (asyncResultCallback.incrementAndGet()) {
case 1:
// Called when first async query completes.
assertEquals(0, results.size());
realm.executeTransactionAsync(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
realm.createObject(AllTypes.class);
}
});
break;
case 2:
// Called after async transaction completes, A REALM_CHANGED event has been triggered,
// async queries have rerun, and listeners are triggered again.
assertEquals(1, results.size());
assertEquals(1, syncResults.size()); // If syncResults is not in sync yet, this will fail.
looperThread.testComplete();
break;
}
}
});
}
// If RealmResults are updated just before their change listener are notified, one change listener might
// reference another RealmResults that have been advance_read, but not yet called sync_if_needed.
// This can result in accessing detached rows and other errors.
@Test
@RunTestInLooperThread
public void accessingSyncRealmResultsInsideAnotherResultListener() {
final Realm realm = looperThread.getRealm();
final RealmResults<AllTypes> syncResults1 = realm.where(AllTypes.class).findAll();
final RealmResults<AllTypes> syncResults2 = realm.where(AllTypes.class).findAll();
looperThread.keepStrongReference(syncResults1);
syncResults1.addChangeListener(new RealmChangeListener<RealmResults<AllTypes>>() {
@Override
public void onChange(RealmResults<AllTypes> element) {
assertEquals(1, syncResults1.size());
assertEquals(1, syncResults2.size()); // If syncResults2 is not in sync yet, this will fail.
looperThread.testComplete();
}
});
realm.beginTransaction();
realm.createObject(AllTypes.class);
realm.commitTransaction();
}
@Test
@RunTestInLooperThread(threadName = "IntentService[1]")
public void listenersNotAllowedOnIntentServiceThreads() {
final Realm realm = looperThread.getRealm();
realm.beginTransaction();
AllTypes obj = realm.createObject(AllTypes.class);
realm.commitTransaction();
RealmResults<AllTypes> results = realm.where(AllTypes.class).findAll();
// Global listener
try {
realm.addChangeListener(new RealmChangeListener<Realm>() {
@Override
public void onChange(Realm element) {
}
});
fail();
} catch (IllegalStateException ignored) {
}
// RealmResults listener
try {
results.addChangeListener(new RealmChangeListener<RealmResults<AllTypes>>() {
@Override
public void onChange(RealmResults<AllTypes> element) {
}
});
fail();
} catch (IllegalStateException ignored) {
}
// Object listener
try {
obj.addChangeListener(new RealmChangeListener<RealmModel>() {
@Override
public void onChange(RealmModel element) {
}
});
fail();
} catch (IllegalStateException ignored) {
}
looperThread.testComplete();
}
@Test
public void listenersNotAllowedOnNonLooperThreads() {
realm = Realm.getInstance(realmConfig);
realm.beginTransaction();
AllTypes obj = realm.createObject(AllTypes.class);
realm.commitTransaction();
RealmResults<AllTypes> results = realm.where(AllTypes.class).findAll();
// Global listener
try {
realm.addChangeListener(new RealmChangeListener<Realm>() {
@Override
public void onChange(Realm element) {
}
});
fail();
} catch (IllegalStateException ignored) {
}
// RealmResults listener
try {
results.addChangeListener(new RealmChangeListener<RealmResults<AllTypes>>() {
@Override
public void onChange(RealmResults<AllTypes> element) {
}
});
fail();
} catch (IllegalStateException ignored) {
}
// Object listener
try {
obj.addChangeListener(new RealmChangeListener<RealmModel>() {
@Override
public void onChange(RealmModel element) {
}
});
fail();
} catch (IllegalStateException ignored) {
}
}
}