/* * Copyright 2015 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.content.Context; import android.os.Looper; import java.io.Closeable; import java.io.File; import java.io.FileNotFoundException; import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import io.realm.exceptions.RealmException; import io.realm.exceptions.RealmFileException; import io.realm.exceptions.RealmMigrationNeededException; import io.realm.internal.CheckedRow; import io.realm.internal.ColumnInfo; import io.realm.internal.InvalidRow; import io.realm.internal.Row; import io.realm.internal.SharedRealm; import io.realm.internal.Table; import io.realm.internal.UncheckedRow; import io.realm.internal.Util; import io.realm.internal.async.RealmThreadPoolExecutor; import io.realm.log.RealmLog; import rx.Observable; /** * Base class for all Realm instances. * * @see io.realm.Realm * @see io.realm.DynamicRealm */ @SuppressWarnings("WeakerAccess") abstract class BaseRealm implements Closeable { protected static final long UNVERSIONED = -1; private static final String INCORRECT_THREAD_CLOSE_MESSAGE = "Realm access from incorrect thread. Realm instance can only be closed on the thread it was created."; private static final String INCORRECT_THREAD_MESSAGE = "Realm access from incorrect thread. Realm objects can only be accessed on the thread they were created."; private static final String CLOSED_REALM_MESSAGE = "This Realm instance has already been closed, making it unusable."; private static final String NOT_IN_TRANSACTION_MESSAGE = "Changing Realm data can only be done from inside a transaction."; static final String LISTENER_NOT_ALLOWED_MESSAGE = "Listeners cannot be used on current thread."; static volatile Context applicationContext; // Thread pool for all async operations (Query & transaction) static final RealmThreadPoolExecutor asyncTaskExecutor = RealmThreadPoolExecutor.newDefaultExecutor(); final long threadId; protected final RealmConfiguration configuration; // Which RealmCache is this Realm associated to. It is null if the Realm instance is opened without being put into a // cache. It is also null if the Realm is closed. private RealmCache realmCache; protected SharedRealm sharedRealm; protected final RealmSchema schema; // Create a realm instance and associate it to a RealmCache. BaseRealm(RealmCache cache) { this(cache.getConfiguration()); this.realmCache = cache; } // Create a realm instance without associating it to any RealmCache. BaseRealm(RealmConfiguration configuration) { this.threadId = Thread.currentThread().getId(); this.configuration = configuration; this.realmCache = null; this.sharedRealm = SharedRealm.getInstance(configuration, !(this instanceof Realm) ? null : new SharedRealm.SchemaVersionListener() { @Override public void onSchemaVersionChanged(long currentVersion) { if (realmCache != null) { realmCache.updateSchemaCache((Realm) BaseRealm.this); } } }, true); this.schema = new StandardRealmSchema(this); } /** * Sets the auto-refresh status of the Realm instance. * <p> * Auto-refresh is a feature that enables automatic update of the current Realm instance and all its derived objects * (RealmResults and RealmObject instances) when a commit is performed on a Realm acting on the same file in * another thread. This feature is only available if the Realm instance lives on a {@link android.os.Looper} enabled * thread. * * @param autoRefresh {@code true} will turn auto-refresh on, {@code false} will turn it off. * @throws IllegalStateException if called from a non-Looper thread. */ public void setAutoRefresh(boolean autoRefresh) { checkIfValid(); sharedRealm.setAutoRefresh(autoRefresh); } /** * Retrieves the auto-refresh status of the Realm instance. * * @return the auto-refresh status. */ public boolean isAutoRefresh() { return sharedRealm.isAutoRefresh(); } /** * Refreshes the Realm instance and all the RealmResults and RealmObjects instances coming from it. * It also calls any listeners associated with the Realm if neeeded. * <p> * WARNING: Calling this on a thread with async queries will turn those queries into synchronous queries. * In most cases it is better to use {@link RealmChangeListener}s to be notified about changes to the * Realm on a given thread than it is to use this method. * * @throws IllegalStateException if attempting to refresh from within a transaction. */ public void refresh() { checkIfValid(); if (isInTransaction()) { throw new IllegalStateException("Cannot refresh a Realm instance inside a transaction."); } sharedRealm.refresh(); } /** * Checks if the Realm is currently in a transaction. * * @return {@code true} if inside a transaction, {@code false} otherwise. */ public boolean isInTransaction() { checkIfValid(); return sharedRealm.isInTransaction(); } protected <T extends BaseRealm> void addListener(RealmChangeListener<T> listener) { if (listener == null) { throw new IllegalArgumentException("Listener should not be null"); } checkIfValid(); sharedRealm.capabilities.checkCanDeliverNotification(LISTENER_NOT_ALLOWED_MESSAGE); //noinspection unchecked sharedRealm.realmNotifier.addChangeListener((T) this, 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 */ protected <T extends BaseRealm> void removeListener(RealmChangeListener<T> listener) { if (listener == null) { throw new IllegalArgumentException("Listener should not be null"); } checkIfValid(); sharedRealm.capabilities.checkCanDeliverNotification(LISTENER_NOT_ALLOWED_MESSAGE); //noinspection unchecked sharedRealm.realmNotifier.removeChangeListener((T) this, listener); } /** * Returns an RxJava Observable that monitors changes to this Realm. It will emit the current state * when subscribed to. Items will continually be emitted as the Realm is updated - * {@code onComplete} will never be called. * <p> * If you would like the {@code asObservable()} to stop emitting items, you can instruct RxJava to * only emit only the first item by using the {@code first()} operator: * <p> * <pre> * {@code * realm.asObservable().first().subscribe( ... ) // You only get the results once * } * </pre> * * @return RxJava Observable that only calls {@code onNext}. It will never call {@code onComplete} or {@code OnError}. * @throws UnsupportedOperationException if the required RxJava framework is not on the classpath. * @see <a href="https://realm.io/docs/java/latest/#rxjava">RxJava and Realm</a> */ public abstract Observable asObservable(); /** * Removes all user-defined change listeners. * * @throws IllegalStateException if you try to remove listeners from a non-Looper Thread. * @see io.realm.RealmChangeListener */ protected void removeAllListeners() { checkIfValid(); sharedRealm.capabilities.checkCanDeliverNotification("removeListener cannot be called on current thread."); sharedRealm.realmNotifier.removeChangeListeners(this); } /** * Writes a compacted copy of the Realm to the given destination File. * <p> * The destination file cannot already exist. * <p> * Note that if this is called from within a transaction it writes the current data, and not the data as it was when * the last transaction was committed. * * @param destination file to save the Realm to. * @throws RealmFileException if an error happened when accessing the underlying Realm file or writing to the * destination file. */ public void writeCopyTo(File destination) { writeEncryptedCopyTo(destination, null); } /** * Writes a compacted and encrypted copy of the Realm to the given destination File. * <p> * The destination file cannot already exist. * <p> * Note that if this is called from within a transaction it writes the current data, and not the data as it was when * the last transaction was committed. * <p> * * @param destination file to save the Realm to. * @param key a 64-byte encryption key. * @throws IllegalArgumentException if destination argument is null. * @throws RealmFileException if an error happened when accessing the underlying Realm file or writing to the * destination file. */ public void writeEncryptedCopyTo(File destination, byte[] key) { if (destination == null) { throw new IllegalArgumentException("The destination argument cannot be null"); } checkIfValid(); sharedRealm.writeCopy(destination, key); } /** * Blocks the current thread until new changes to the Realm are available or {@link #stopWaitForChange()} * is called from another thread. Once stopWaitForChange is called, all future calls to this method will * return false immediately. * * @return {@code true} if the Realm was updated to the latest version, {@code false} if it was * cancelled by calling stopWaitForChange. * @throws IllegalStateException if calling this from within a transaction or from a Looper thread. * @throws RealmMigrationNeededException on typed {@link Realm} if the latest version contains * incompatible schema changes. */ public boolean waitForChange() { checkIfValid(); if (isInTransaction()) { throw new IllegalStateException("Cannot wait for changes inside of a transaction."); } if (Looper.myLooper() != null) { throw new IllegalStateException("Cannot wait for changes inside a Looper thread. Use RealmChangeListeners instead."); } boolean hasChanged = sharedRealm.waitForChange(); if (hasChanged) { // Since this Realm instance has been waiting for change, advance realm & refresh realm. sharedRealm.refresh(); } return hasChanged; } /** * Makes any current {@link #waitForChange()} return {@code false} immediately. Once this is called, * all future calls to waitForChange will immediately return {@code false}. * <p> * This method is thread-safe and should _only_ be called from another thread than the one that * called waitForChange. * * @throws IllegalStateException if the {@link io.realm.Realm} instance has already been closed. */ public void stopWaitForChange() { if (realmCache != null) { realmCache.invokeWithLock(new RealmCache.Callback0() { @Override public void onCall() { // Checks if the Realm instance has been closed. if (sharedRealm == null || sharedRealm.isClosed()) { throw new IllegalStateException(BaseRealm.CLOSED_REALM_MESSAGE); } sharedRealm.stopWaitForChange(); } }); } else { throw new IllegalStateException(BaseRealm.CLOSED_REALM_MESSAGE); } } /** * Starts a transaction which must be closed by {@link io.realm.Realm#commitTransaction()} or aborted by * {@link io.realm.Realm#cancelTransaction()}. Transactions are used to atomically create, update and delete objects * within a Realm. * <p> * Before beginning a transaction, the Realm instance is updated to the latest version in order to include all * changes from other threads. This update does not trigger any registered {@link RealmChangeListener}. * <p> * It is therefore recommended to query for the items that should be modified from inside the transaction. Otherwise * there is a risk that some of the results have been deleted or modified when the transaction begins. * <p> * <pre> * {@code * // Don't do this * RealmResults<Person> persons = realm.where(Person.class).findAll(); * realm.beginTransaction(); * persons.first().setName("John"); * realm.commitTransaction(); * * // Do this instead * realm.beginTransaction(); * RealmResults<Person> persons = realm.where(Person.class).findAll(); * persons.first().setName("John"); * realm.commitTransaction(); * } * </pre> * <p> * Notice: it is not possible to nest transactions. If you start a transaction within a transaction an exception is * thrown. * * @throws RealmMigrationNeededException on typed {@link Realm} if the latest version contains * incompatible schema changes. */ public void beginTransaction() { beginTransaction(false); } void beginTransaction(boolean ignoreReadOnly) { checkIfValid(); sharedRealm.beginTransaction(ignoreReadOnly); } /** * All changes since {@link io.realm.Realm#beginTransaction()} are persisted to disk and the Realm reverts back to * being read-only. An event is sent to notify all other Realm instances that a change has occurred. When the event * is received, the other Realms will update their objects and {@link io.realm.RealmResults} to reflect the * changes from this commit. */ public void commitTransaction() { checkIfValid(); sharedRealm.commitTransaction(); } /** * Reverts all writes (created, updated, or deleted objects) made in the current write transaction and end the * transaction. * <p> * The Realm reverts back to read-only. * <p> * Calling this when not in a transaction will throw an exception. */ public void cancelTransaction() { checkIfValid(); sharedRealm.cancelTransaction(); } /** * Checks if a Realm's underlying resources are still available or not getting accessed from the wrong thread. */ protected void checkIfValid() { if (sharedRealm == null || sharedRealm.isClosed()) { throw new IllegalStateException(BaseRealm.CLOSED_REALM_MESSAGE); } // Checks if we are in the right thread. if (threadId != Thread.currentThread().getId()) { throw new IllegalStateException(BaseRealm.INCORRECT_THREAD_MESSAGE); } } protected void checkIfInTransaction() { if (!sharedRealm.isInTransaction()) { throw new IllegalStateException("Changing Realm data can only be done from inside a transaction."); } } /** * Checks if the Realm is valid and in a transaction. */ protected void checkIfValidAndInTransaction() { if (!isInTransaction()) { throw new IllegalStateException(NOT_IN_TRANSACTION_MESSAGE); } } /** * Checks if the Realm is not built with a SyncRealmConfiguration. */ void checkNotInSync() { if (configuration.isSyncConfiguration()) { throw new IllegalArgumentException("You cannot perform changes to a schema. " + "Please update app and restart."); } } /** * Returns the canonical path to where this Realm is persisted on disk. * * @return the canonical path to the Realm file. * @see File#getCanonicalPath() */ public String getPath() { return configuration.getPath(); } /** * Returns the {@link RealmConfiguration} for this Realm. * * @return the {@link RealmConfiguration} for this Realm. */ public RealmConfiguration getConfiguration() { return configuration; } /** * Returns the schema version for this Realm. * * @return the schema version for the Realm file backing this Realm. */ public long getVersion() { return sharedRealm.getSchemaVersion(); } /** * Closes the Realm instance and all its resources. * <p> * It's important to always remember to close Realm instances when you're done with it in order not to leak memory, * file descriptors or grow the size of Realm file out of measure. * * @throws IllegalStateException if attempting to close from another thread. */ @Override public void close() { if (this.threadId != Thread.currentThread().getId()) { throw new IllegalStateException(INCORRECT_THREAD_CLOSE_MESSAGE); } if (realmCache != null) { realmCache.release(this); } else { doClose(); } } /** * Closes the Realm instances and all its resources without checking the {@link RealmCache}. */ void doClose() { realmCache = null; if (sharedRealm != null) { sharedRealm.close(); sharedRealm = null; } if (schema != null) { schema.close(); } } /** * Checks if the {@link io.realm.Realm} instance has already been closed. * * @return {@code true} if closed, {@code false} otherwise. * @throws IllegalStateException if attempting to close from another thread. */ public boolean isClosed() { if (this.threadId != Thread.currentThread().getId()) { throw new IllegalStateException(INCORRECT_THREAD_MESSAGE); } return sharedRealm == null || sharedRealm.isClosed(); } /** * Checks if this {@link io.realm.Realm} contains any objects. * * @return {@code true} if empty, @{code false} otherwise. */ public boolean isEmpty() { checkIfValid(); return sharedRealm.isEmpty(); } // package protected so unit tests can access it void setVersion(long version) { sharedRealm.setSchemaVersion(version); } /** * Returns the schema for this Realm. * * @return The {@link RealmSchema} for this Realm. */ public RealmSchema getSchema() { return schema; } // Used by RealmList/RealmResults, to create RealmObject from a Collection. // Invariant: if dynamicClassName != null -> clazz == DynamicRealmObject <E extends RealmModel> E get(Class<E> clazz, String dynamicClassName, UncheckedRow row) { final boolean isDynamicRealmObject = dynamicClassName != null; E result; if (isDynamicRealmObject) { //noinspection unchecked result = (E) new DynamicRealmObject(this, CheckedRow.getFromRow(row)); } else { result = configuration.getSchemaMediator().newInstance(clazz, this, row, schema.getColumnInfo(clazz), false, Collections.<String>emptyList()); } return result; } <E extends RealmModel> E get(Class<E> clazz, long rowIndex, boolean acceptDefaultValue, List<String> excludeFields) { Table table = schema.getTable(clazz); UncheckedRow row = table.getUncheckedRow(rowIndex); return configuration.getSchemaMediator().newInstance(clazz, this, row, schema.getColumnInfo(clazz), acceptDefaultValue, excludeFields); } // Used by RealmList/RealmResults // Invariant: if dynamicClassName != null -> clazz == DynamicRealmObject // TODO: Remove this after RealmList is backed by OS Results. <E extends RealmModel> E get(Class<E> clazz, String dynamicClassName, long rowIndex) { final boolean isDynamicRealmObject = dynamicClassName != null; final Table table = isDynamicRealmObject ? schema.getTable(dynamicClassName) : schema.getTable(clazz); E result; if (isDynamicRealmObject) { @SuppressWarnings("unchecked") E dynamicObj = (E) new DynamicRealmObject(this, (rowIndex != Table.NO_MATCH) ? table.getCheckedRow(rowIndex) : InvalidRow.INSTANCE); result = dynamicObj; } else { result = configuration.getSchemaMediator().newInstance(clazz, this, (rowIndex != Table.NO_MATCH) ? table.getUncheckedRow(rowIndex) : InvalidRow.INSTANCE, schema.getColumnInfo(clazz), false, Collections.<String>emptyList()); } return result; } /** * Deletes all objects from this Realm. * * @throws IllegalStateException if the corresponding Realm is closed or called from an incorrect thread. */ public void deleteAll() { checkIfValid(); for (RealmObjectSchema objectSchema : schema.getAll()) { schema.getTable(objectSchema.getClassName()).clear(); } } /** * Deletes the Realm file defined by the given configuration. */ static boolean deleteRealm(final RealmConfiguration configuration) { final AtomicBoolean realmDeleted = new AtomicBoolean(true); RealmCache.invokeWithGlobalRefCount(configuration, new RealmCache.Callback() { @Override public void onResult(int count) { if (count != 0) { throw new IllegalStateException("It's not allowed to delete the file associated with an open Realm. " + "Remember to close() all the instances of the Realm before deleting its file: " + configuration.getPath()); } String canonicalPath = configuration.getPath(); File realmFolder = configuration.getRealmDirectory(); String realmFileName = configuration.getRealmFileName(); realmDeleted.set(Util.deleteRealm(canonicalPath, realmFolder, realmFileName)); } }); return realmDeleted.get(); } /** * Compacts the Realm file defined by the given configuration. * * @param configuration configuration for the Realm to compact. * @return {@code true} if compaction succeeded, {@code false} otherwise. */ static boolean compactRealm(final RealmConfiguration configuration) { SharedRealm sharedRealm = SharedRealm.getInstance(configuration); Boolean result = sharedRealm.compact(); sharedRealm.close(); return result; } /** * Migrates the Realm file defined by the given configuration using the provided migration block. * * @param configuration configuration for the Realm that should be migrated. If this is a SyncConfiguration this * method does nothing. * @param migration if set, this migration block will override what is set in {@link RealmConfiguration}. * @param callback callback for specific Realm type behaviors. * @param cause which triggers this migration. * @throws FileNotFoundException if the Realm file doesn't exist. * @throws IllegalArgumentException if the provided configuration is a {@link SyncConfiguration}. */ protected static void migrateRealm(final RealmConfiguration configuration, final RealmMigration migration, final MigrationCallback callback, final RealmMigrationNeededException cause) throws FileNotFoundException { if (configuration == null) { throw new IllegalArgumentException("RealmConfiguration must be provided"); } if (configuration.isSyncConfiguration()) { throw new IllegalArgumentException("Manual migrations are not supported for synced Realms"); } if (migration == null && configuration.getMigration() == null) { throw new RealmMigrationNeededException(configuration.getPath(), "RealmMigration must be provided", cause); } final AtomicBoolean fileNotFound = new AtomicBoolean(false); RealmCache.invokeWithGlobalRefCount(configuration, new RealmCache.Callback() { @Override public void onResult(int count) { if (count != 0) { throw new IllegalStateException("Cannot migrate a Realm file that is already open: " + configuration.getPath()); } File realmFile = new File(configuration.getPath()); if (!realmFile.exists()) { fileNotFound.set(true); return; } RealmMigration realmMigration = (migration == null) ? configuration.getMigration() : migration; DynamicRealm realm = null; try { // Create a DynamicRealm WITHOUT putting it into a RealmCache to avoid recursive locks and call init // steps multiple times (copy asset file / initialData transaction). realm = DynamicRealm.createInstance(configuration); realm.beginTransaction(); long currentVersion = realm.getVersion(); realmMigration.migrate(realm, currentVersion, configuration.getSchemaVersion()); realm.setVersion(configuration.getSchemaVersion()); realm.commitTransaction(); } catch (RuntimeException e) { if (realm != null) { realm.cancelTransaction(); } throw e; } finally { if (realm != null) { realm.close(); callback.migrationComplete(); } } } }); if (fileNotFound.get()) { throw new FileNotFoundException("Cannot migrate a Realm file which doesn't exist: " + configuration.getPath()); } } @Override protected void finalize() throws Throwable { if (sharedRealm != null && !sharedRealm.isClosed()) { RealmLog.warn("Remember to call close() on all Realm instances. " + "Realm %s is being finalized without being closed, " + "this can lead to running out of native memory.", configuration.getPath() ); if (realmCache != null) { realmCache.leak(); } } super.finalize(); } SharedRealm getSharedRealm() { return sharedRealm; } // Internal delegate for migrations. protected interface MigrationCallback { void migrationComplete(); } public static final class RealmObjectContext { private BaseRealm realm; private Row row; private ColumnInfo columnInfo; private boolean acceptDefaultValue; private List<String> excludeFields; public void set(BaseRealm realm, Row row, ColumnInfo columnInfo, boolean acceptDefaultValue, List<String> excludeFields) { this.realm = realm; this.row = row; this.columnInfo = columnInfo; this.acceptDefaultValue = acceptDefaultValue; this.excludeFields = excludeFields; } BaseRealm getRealm() { return realm; } public Row getRow() { return row; } public ColumnInfo getColumnInfo() { return columnInfo; } public boolean getAcceptDefaultValue() { return acceptDefaultValue; } public List<String> getExcludeFields() { return excludeFields; } public void clear() { realm = null; row = null; columnInfo = null; acceptDefaultValue = false; excludeFields = null; } } // FIXME: This stuff doesn't appear to be used. It should either be explained or deleted. static final class ThreadLocalRealmObjectContext extends ThreadLocal<RealmObjectContext> { @Override protected RealmObjectContext initialValue() { return new RealmObjectContext(); } } public static final ThreadLocalRealmObjectContext objectContext = new ThreadLocalRealmObjectContext(); /** * The Callback used when reporting back the result of loading a Realm asynchronously using either * {@link Realm#getInstanceAsync(RealmConfiguration, Realm.Callback)} or * {@link DynamicRealm#getInstanceAsync(RealmConfiguration, DynamicRealm.Callback)}. * <p> * Before creating the first Realm instance in a process, there are some initialization work that need to be done * such as creating or validating schemas, running migration if needed, * copy asset file if {@link RealmConfiguration.Builder#assetFile(String)} is supplied and execute the * {@link RealmConfiguration.Builder#initialData(Realm.Transaction)} if necessary. This work may take time * and block the caller thread for a while. To avoid the {@code getInstance()} call blocking the main thread, the * {@code getInstanceAsync()} can be used instead to do the initialization work in the background thread and * deliver a Realm instance to the caller thread. * <p> * In general, this method is mostly useful on the UI thread since that should be blocked as little as possible. On * any other Looper threads or other threads that don't support callbacks, using the standard {@code getInstance()} * should be fine. * <p> * Here is an example of using {@code getInstanceAsync()} when the app starts the first activity: * <pre> * public class MainActivity extends Activity { * * private Realm realm = null; * private RealmAsyncTask realmAsyncTask; * * \@Override * protected void onCreate(Bundle savedInstanceState) { * super.onCreate(savedInstanceState); * setContentView(R.layout.layout_main); * realmAsyncTask = Realm.getDefaultInstanceAsync(new Callback() { * \@Override * public void onSuccess(Realm realm) { * if (isDestroyed()) { * // If the activity is destroyed, the Realm instance should be closed immediately to avoid leaks. * // Or you can call realmAsyncTask.cancel() in onDestroy() to stop callback delivery. * realm.close(); * } else { * MainActivity.this.realm = realm; * // Remove the spinner and start the real UI. * } * } * }); * * // Show a spinner before Realm instance returned by the callback. * } * * \@Override * protected void onDestroy() { * super.onDestroy(); * if (realm != null) { * realm.close(); * realm = null; * } else { * // Calling cancel() on the thread where getInstanceAsync was called on to stop the callback delivery. * // Otherwise you need to check if the activity is destroyed to close in the onSuccess() properly. * realmAsyncTask.cancel(); * } * } * } * </pre> * * @param <T> {@link Realm} or {@link DynamicRealm}. */ public abstract static class InstanceCallback<T extends BaseRealm> { /** * Deliver a Realm instance to the caller thread. * * @param realm the Realm instance for the caller thread. */ public abstract void onSuccess(T realm); /** * Deliver an error happens when creating the Realm instance to the caller thread. The default implementation * will throw an exception on the caller thread. * * @param exception happened while initializing Realm on a background thread. */ public void onError(Throwable exception) { throw new RealmException("Exception happens when initializing Realm in the background thread.", exception); } } }