All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.parse.OfflineStore Maven / Gradle / Ivy

Go to download

A library that gives you access to the powerful Parse cloud platform from your Android app.

There is a newer version: 1.17.3
Show newest version
/*
 * Copyright (c) 2015-present, Parse, LLC.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */
package com.parse;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.text.TextUtils;
import android.util.Pair;

import com.parse.OfflineQueryLogic.ConstraintMatcher;

import org.json.JSONException;
import org.json.JSONObject;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.WeakHashMap;

import bolts.Capture;
import bolts.Continuation;
import bolts.Task;
import bolts.TaskCompletionSource;

/** package */ class OfflineStore {

  /**
   * SQLite has a max of 999 SQL variables in a single statement.
   */
  private static final int MAX_SQL_VARIABLES = 999;

  /**
   * Extends the normal JSON -> ParseObject decoding to also deal with placeholders for new objects
   * that have been saved offline.
   */
  private class OfflineDecoder extends ParseDecoder {
    // A map of UUID -> Task that will be finished once the given ParseObject is loaded.
    // The Tasks should all be finished before decode is called.
    private Map> offlineObjects;

    private OfflineDecoder(Map> offlineObjects) {
      this.offlineObjects = offlineObjects;
    }

    @Override
    public Object decode(Object object) {
      // If we see an offline id, make sure to decode it.
      if (object instanceof JSONObject
          && ((JSONObject) object).optString("__type").equals("OfflineObject")) {
        String uuid = ((JSONObject) object).optString("uuid");
        return offlineObjects.get(uuid).getResult();
      }

      /*
       * Embedded objects can't show up here, because we never stored them that way offline.
       */

      return super.decode(object);
    }
  }

  /**
   * An encoder that can encode objects that are available offline. After using this encoder, you
   * must call whenFinished() and wait for its result to be finished before the results of the
   * encoding will be valid.
   */
  private class OfflineEncoder extends ParseEncoder {
    private ParseSQLiteDatabase db;
    private ArrayList> tasks = new ArrayList<>();
    private final Object tasksLock = new Object();

    /**
     * Creates an encoder.
     *
     * @param db
     *          A database connection to use.
     */
    public OfflineEncoder(ParseSQLiteDatabase db) {
      this.db = db;
    }

    /**
     * The results of encoding an object with this encoder will not be valid until the task returned
     * by this method is finished.
     */
    public Task whenFinished() {
      return Task.whenAll(tasks).continueWithTask(new Continuation>() {
        @Override
        public Task then(Task ignore) throws Exception {
          synchronized (tasksLock) {
            // It might be better to return an aggregate error here.
            for (Task task : tasks) {
              if (task.isFaulted() || task.isCancelled()) {
                return task;
              }
            }
            tasks.clear();
            return Task.forResult((Void) null);
          }
        }
      });
    }

    /**
     * Implements an encoding strategy for Parse Objects that uses offline ids when necessary.
     */
    @Override
    public JSONObject encodeRelatedObject(ParseObject object) {
      try {
        if (object.getObjectId() != null) {
          JSONObject result = new JSONObject();
          result.put("__type", "Pointer");
          result.put("objectId", object.getObjectId());
          result.put("className", object.getClassName());
          return result;
        }

        final JSONObject result = new JSONObject();
        result.put("__type", "OfflineObject");
        synchronized (tasksLock) {
          tasks.add(getOrCreateUUIDAsync(object, db).onSuccess(new Continuation() {
            @Override
            public Void then(Task task) throws Exception {
              result.put("uuid", task.getResult());
              return null;
            }
          }));
        }
        return result;
      } catch (JSONException e) {
        // This can literally never happen.
        throw new RuntimeException(e);
      }
    }
  }

  // Lock for all members of the store.
  final private Object lock = new Object();

  // Helper for accessing the database.
  final private OfflineSQLiteOpenHelper helper;

  /**
   * In-memory map of UUID -> ParseObject. This is used so that we can always return the same
   * instance for a given object. The only objects in this map are ones that are in the database.
   */
  final private WeakValueHashMap uuidToObjectMap = new WeakValueHashMap<>();

  /**
   * In-memory map of ParseObject -> UUID. This is used to that when we see an unsaved ParseObject
   * that's already in the database, we can update the same record in the database. It stores a Task
   * instead of the String, because one thread may want to reserve the spot. Once the task is
   * finished, there will be a row for this UUID in the database.
   */
  final private WeakHashMap> objectToUuidMap = new WeakHashMap<>();

  /**
   * In-memory set of ParseObjects that have been fetched from the local database already. If the
   * object is in the map, a fetch of it has been started. If the value is a finished task, then the
   * fetch was completed.
   */
  final private WeakHashMap> fetchedObjects = new WeakHashMap<>();

  /**
   * Used by the static method to create the singleton.
   */
  /* package */ OfflineStore(Context context) {
    this(new OfflineSQLiteOpenHelper(context));
  }

  /* package */ OfflineStore(OfflineSQLiteOpenHelper helper) {
    this.helper = helper;
  }

  /**
   * Gets the UUID for the given object, if it has one. Otherwise, creates a new UUID for the object
   * and adds a new row to the database for the object with no data.
   */
  private Task getOrCreateUUIDAsync(final ParseObject object, ParseSQLiteDatabase db) {
    final String newUUID = UUID.randomUUID().toString();
    final TaskCompletionSource tcs = new TaskCompletionSource<>();

    synchronized (lock) {
      Task uuidTask = objectToUuidMap.get(object);
      if (uuidTask != null) {
        return uuidTask;
      }

      // The object doesn't have a UUID yet, so we're gonna have to make one.
      objectToUuidMap.put(object, tcs.getTask());
      uuidToObjectMap.put(newUUID, object);
      fetchedObjects.put(object, tcs.getTask().onSuccess(new Continuation() {
        @Override
        public ParseObject then(Task task) throws Exception {
          return object;
        }
      }));
    }

    /*
     * We need to put a placeholder row in the database so that later on, the save can just be an
     * update. This could be a pointer to an object that itself never gets saved offline, in which
     * case the consumer will just have to deal with that.
     */
    ContentValues values = new ContentValues();
    values.put(OfflineSQLiteOpenHelper.KEY_UUID, newUUID);
    values.put(OfflineSQLiteOpenHelper.KEY_CLASS_NAME, object.getClassName());
    db.insertOrThrowAsync(OfflineSQLiteOpenHelper.TABLE_OBJECTS, values).continueWith(
        new Continuation() {
          @Override
          public Void then(Task task) throws Exception {
            // This will signal that the UUID does represent a row in the database.
            tcs.setResult(newUUID);
            return null;
          }
        });

    return tcs.getTask();
  }

  /**
   * Gets an unfetched pointer to an object in the db, based on its uuid. The object may or may not
   * be in memory, but it must be in the database. If it is already in memory, that instance will be
   * returned. Since this is only for creating pointers to objects that are referenced by other
   * objects in the data store, that's a fair assumption.
   *
   * @param uuid
   *          The object to retrieve.
   * @param db
   *          The database instance to retrieve from.
   * @return The object with that UUID.
   */
  private  Task getPointerAsync(final String uuid,
      ParseSQLiteDatabase db) {
    synchronized (lock) {
      @SuppressWarnings("unchecked")
      T existing = (T) uuidToObjectMap.get(uuid);
      if (existing != null) {
        return Task.forResult(existing);
      }
    }

    /*
     * We want to just return the pointer, but we have to look in the database to know if there's
     * something with this classname and object id already.
     */

    String[] select = { OfflineSQLiteOpenHelper.KEY_CLASS_NAME, OfflineSQLiteOpenHelper.KEY_OBJECT_ID };
    String where = OfflineSQLiteOpenHelper.KEY_UUID + " = ?";
    String[] args = { uuid };
    return db.queryAsync(OfflineSQLiteOpenHelper.TABLE_OBJECTS, select, where, args).onSuccess(
        new Continuation() {
          @Override
          public T then(Task task) throws Exception {
            Cursor cursor = task.getResult();
            cursor.moveToFirst();
            if (cursor.isAfterLast()) {
              cursor.close();
              throw new IllegalStateException("Attempted to find non-existent uuid " + uuid);
            }

            synchronized (lock) {
              // We need to check again since another task might have come around and added it to
              // the map.
              //TODO (grantland): Maybe we should insert a Task that is resolved when the query
              // completes like we do in getOrCreateUUIDAsync?
              @SuppressWarnings("unchecked")
              T existing = (T) uuidToObjectMap.get(uuid);
              if (existing != null) {
                return existing;
              }

              String className = cursor.getString(0);
              String objectId = cursor.getString(1);
              cursor.close();
              @SuppressWarnings("unchecked")
              T pointer = (T) ParseObject.createWithoutData(className, objectId);
              /*
               * If it doesn't have an objectId, we don't really need the UUID, and this simplifies
               * some other logic elsewhere if we only update the map for new objects.
               */
              if (objectId == null) {
                uuidToObjectMap.put(uuid, pointer);
                objectToUuidMap.put(pointer, Task.forResult(uuid));
              }
              return pointer;
            }
          }
        });
  }

  /**
   * Runs a ParseQuery against the store's contents.
   *
   * @return The objects that match the query's constraints.
   */
  /* package for OfflineQueryLogic */  Task> findAsync(
      ParseQuery.State query,
      ParseUser user,
      ParsePin pin,
      ParseSQLiteDatabase db) {
    return findAsync(query, user, pin, false, db);
  }

  /**
   * Runs a ParseQuery against the store's contents. May cause any instances of T to get fetched
   * from the offline database. TODO(klimt): Should the query consider objects that are in memory,
   * but not in the offline store?
   *
   * @param query The query.
   * @param user The user making the query.
   * @param pin (Optional) The pin we are querying across. If null, all pins.
   * @param isCount True if we are doing a count.
   * @param db The SQLiteDatabase.
   * @param  Subclass of ParseObject.
   * @return The objects that match the query's constraints.
   */
  private  Task> findAsync(
      final ParseQuery.State query,
      final ParseUser user,
      final ParsePin pin,
      final boolean isCount,
      final ParseSQLiteDatabase db) {
    /*
     * This is currently unused, but is here to allow future querying across objects that are in the
     * process of being deleted eventually.
     */
    final boolean includeIsDeletingEventually = false;

    final OfflineQueryLogic queryLogic = new OfflineQueryLogic(this);

    final List results = new ArrayList<>();

    Task queryTask;
    if (pin == null) {
      String table = OfflineSQLiteOpenHelper.TABLE_OBJECTS;
      String[] select = { OfflineSQLiteOpenHelper.KEY_UUID };
      String where = OfflineSQLiteOpenHelper.KEY_CLASS_NAME + "=?";
      if (!includeIsDeletingEventually) {
        where += " AND " + OfflineSQLiteOpenHelper.KEY_IS_DELETING_EVENTUALLY + "=0";
      }
      String[] args = { query.className() };

      queryTask = db.queryAsync(table, select, where, args);
    } else {
      Task uuidTask = objectToUuidMap.get(pin);
      if (uuidTask == null) {
        // Pin was never saved locally, therefore there won't be any results.
        return Task.forResult(results);
      }

      queryTask = uuidTask.onSuccessTask(new Continuation>() {
        @Override
        public Task then(Task task) throws Exception {
          String uuid = task.getResult();

          String table = OfflineSQLiteOpenHelper.TABLE_OBJECTS + " A " +
              " INNER JOIN " + OfflineSQLiteOpenHelper.TABLE_DEPENDENCIES + " B " +
              " ON A." + OfflineSQLiteOpenHelper.KEY_UUID + "=B." + OfflineSQLiteOpenHelper.KEY_UUID;
          String[] select = {"A." + OfflineSQLiteOpenHelper.KEY_UUID};
          String where = OfflineSQLiteOpenHelper.KEY_CLASS_NAME + "=?" +
              " AND " + OfflineSQLiteOpenHelper.KEY_KEY + "=?";
          if (!includeIsDeletingEventually) {
            where += " AND " + OfflineSQLiteOpenHelper.KEY_IS_DELETING_EVENTUALLY + "=0";
          }
          String[] args = { query.className(), uuid };

          return db.queryAsync(table, select, where, args);
        }
      });
    }

    return queryTask.onSuccessTask(new Continuation>() {
      @Override
      public Task then(Task task) throws Exception {
        Cursor cursor = task.getResult();
        List uuids = new ArrayList<>();
        for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
          uuids.add(cursor.getString(0));
        }
        cursor.close();

        // Find objects that match the where clause.
        final ConstraintMatcher matcher = queryLogic.createMatcher(query, user);

        Task checkedAllObjects = Task.forResult(null);
        for (final String uuid : uuids) {
          final Capture object = new Capture<>();

          checkedAllObjects = checkedAllObjects.onSuccessTask(new Continuation>() {
            @Override
            public Task then(Task task) throws Exception {
              return getPointerAsync(uuid, db);
            }
          }).onSuccessTask(new Continuation>() {
            @Override
            public Task then(Task task) throws Exception {
              object.set(task.getResult());
              return fetchLocallyAsync(object.get(), db);
            }
          }).onSuccessTask(new Continuation>() {
            @Override
            public Task then(Task task) throws Exception {
              if (!object.get().isDataAvailable()) {
                return Task.forResult(false);
              }
              return matcher.matchesAsync(object.get(), db);
            }
          }).onSuccess(new Continuation() {
            @Override
            public Void then(Task task) {
              if (task.getResult()) {
                results.add(object.get());
              }
              return null;
            }
          });
        }

        return checkedAllObjects;
      }
    }).onSuccessTask(new Continuation>>() {
      @Override
      public Task> then(Task task) throws Exception {
        // Sort by any sort operators.
        OfflineQueryLogic.sort(results, query);

        // Apply the skip.
        List trimmedResults = results;
        int skip = query.skip();
        if (!isCount && skip >= 0) {
          skip = Math.min(query.skip(), trimmedResults.size());
          trimmedResults = trimmedResults.subList(skip, trimmedResults.size());
        }

        // Trim to the limit.
        int limit = query.limit();
        if (!isCount && limit >= 0 && trimmedResults.size() > limit) {
          trimmedResults = trimmedResults.subList(0, limit);
        }

        // Fetch the includes.
        Task fetchedIncludesTask = Task.forResult(null);
        for (final T object : trimmedResults) {
          fetchedIncludesTask = fetchedIncludesTask.onSuccessTask(new Continuation>() {
            @Override
            public Task then(Task task) throws Exception {
              return OfflineQueryLogic.fetchIncludesAsync(OfflineStore.this, object, query, db);
            }
          });
        }

        final List finalTrimmedResults = trimmedResults;
        return fetchedIncludesTask.onSuccess(new Continuation>() {
          @Override
          public List then(Task task) throws Exception {
            return finalTrimmedResults;
          }
        });
      }
    });
  }

  /**
   * Gets the data for the given object from the offline database. Returns a task that will be
   * completed if data for the object was available. If the object is not in the cache, the task
   * will be faulted, with a CACHE_MISS error.
   *
   * @param object
   *          The object to fetch.
   * @param db
   *          A database connection to use.
   */
  /* package for OfflineQueryLogic */  Task fetchLocallyAsync(
      final T object,
      final ParseSQLiteDatabase db) {
    final TaskCompletionSource tcs = new TaskCompletionSource<>();
    Task uuidTask;

    synchronized (lock) {
      if (fetchedObjects.containsKey(object)) {
        /*
         * The object has already been fetched from the offline store, so any data that's in there
         * is already reflected in the in-memory version. There's nothing more to do.
         */
        //noinspection unchecked
        return (Task) fetchedObjects.get(object);
      }

      /*
       * Put a placeholder so that anyone else who attempts to fetch this object will just wait for
       * this call to finish doing it.
       */
      //noinspection unchecked
      fetchedObjects.put(object, (Task) tcs.getTask());

      uuidTask = objectToUuidMap.get(object);
    }
    String className = object.getClassName();
    String objectId = object.getObjectId();

    /*
     * If this gets set, then it will contain data from the offline store that needs to be merged
     * into the existing object in memory.
     */
    Task jsonStringTask = Task.forResult(null);

    if (objectId == null) {
      // This Object has never been saved to Parse.
      if (uuidTask == null) {
        /*
         * This object was not pulled from the data store or previously saved to it, so there's
         * nothing that can be fetched from it. This isn't an error, because it's really convenient
         * to try to fetch objects from the offline store just to make sure they are up-to-date, and
         * we shouldn't force developers to specially handle this case.
         */
      } else {
        /*
         * This object is a new ParseObject that is known to the data store, but hasn't been
         * fetched. The only way this could happen is if the object had previously been stored in
         * the offline store, then the object was removed from memory (maybe by rebooting), and then
         * a object with a pointer to it was fetched, so we only created the pointer. We need to
         * pull the data out of the database using the UUID.
         */
        final String[] select = { OfflineSQLiteOpenHelper.KEY_JSON };
        final String where = OfflineSQLiteOpenHelper.KEY_UUID + " = ?";
        final Capture uuid = new Capture<>();
        jsonStringTask = uuidTask.onSuccessTask(new Continuation>() {
          @Override
          public Task then(Task task) throws Exception {
            uuid.set(task.getResult());
            String[] args = { uuid.get() };
            return db.queryAsync(OfflineSQLiteOpenHelper.TABLE_OBJECTS, select, where, args);
          }
        }).onSuccess(new Continuation() {
          @Override
          public String then(Task task) throws Exception {
            Cursor cursor = task.getResult();
            cursor.moveToFirst();
            if (cursor.isAfterLast()) {
              cursor.close();
              throw new IllegalStateException("Attempted to find non-existent uuid " + uuid.get());
            }
            String json = cursor.getString(0);
            cursor.close();

            return json;
          }
        });
      }
    } else {
      if (uuidTask != null) {
        /*
         * This object is an existing ParseObject, and we must've already pulled its data out of the
         * offline store, or else we wouldn't know its UUID. This should never happen.
         */
        tcs.setError(new IllegalStateException("This object must have already been "
            + "fetched from the local datastore, but isn't marked as fetched."));
        synchronized (lock) {
          // Forget we even tried to fetch this object, so that retries will actually... retry.
          fetchedObjects.remove(object);
        }
        return tcs.getTask();
      }

      /*
       * We've got a pointer to an existing ParseObject, but we've never pulled its data out of the
       * offline store. Since fetching from the server forces a fetch from the offline store, that
       * means this is a pointer. We need to try to find any existing entry for this object in the
       * database.
       */
      String[] select = { OfflineSQLiteOpenHelper.KEY_JSON, OfflineSQLiteOpenHelper.KEY_UUID  };
      String where =
          String.format("%s = ? AND %s = ?", OfflineSQLiteOpenHelper.KEY_CLASS_NAME,
              OfflineSQLiteOpenHelper.KEY_OBJECT_ID);
      String[] args = { className, objectId };
      jsonStringTask =
          db.queryAsync(OfflineSQLiteOpenHelper.TABLE_OBJECTS, select, where, args).onSuccess(
              new Continuation() {
                @Override
                public String then(Task task) throws Exception {
                  Cursor cursor = task.getResult();
                  cursor.moveToFirst();
                  if (cursor.isAfterLast()) {
                    /*
                     * This is a pointer that came from Parse that references an object that has
                     * never been saved in the offline store before. This just means there's no data
                     * in the store that needs to be merged into the object.
                     */
                    cursor.close();
                    throw new ParseException(ParseException.CACHE_MISS,
                        "This object is not available in the offline cache.");
                  }

                  // we should fetch its data and record its UUID for future reference.
                  String jsonString = cursor.getString(0);
                  String newUUID = cursor.getString(1);
                  cursor.close();

                  synchronized (lock) {
                    /*
                     * It's okay to put this object into the uuid map. No one will try to fetch
                     * it, because it's already in the fetchedObjects map. And no one will try to
                     * save to it without fetching it first, so everything should be just fine.
                     */
                    objectToUuidMap.put(object, Task.forResult(newUUID));
                    uuidToObjectMap.put(newUUID, object);
                  }

                  return jsonString;
                }
              });
    }

    return jsonStringTask.onSuccessTask(new Continuation>() {
      @Override
      public Task then(Task task) throws Exception {
        String jsonString = task.getResult();
        if (jsonString == null) {
          /*
           * This means we tried to fetch an object from the database that was never actually saved
           * locally. This probably means that its parent object was saved locally and we just
           * created a pointer to this object. This should be considered a cache miss.
           */
          return Task.forError(new ParseException(ParseException.CACHE_MISS,
              "Attempted to fetch an object offline which was never saved to the offline cache."));
        }
        final JSONObject json;
        try {
          /*
           * We can assume that whatever is in the database is the last known server state. The only
           * things to maintain from the in-memory object are any changes since the object was last
           * put in the database.
           */
          json = new JSONObject(jsonString);
        } catch (JSONException e) {
          return Task.forError(e);
        }

        // Fetch all the offline objects before we decode.
        final Map> offlineObjects = new HashMap<>();

        (new ParseTraverser() {
          @Override
          protected boolean visit(Object object) {
            if (object instanceof JSONObject
                && ((JSONObject) object).optString("__type").equals("OfflineObject")) {
              String uuid = ((JSONObject) object).optString("uuid");
              offlineObjects.put(uuid, getPointerAsync(uuid, db));
            }
            return true;
          }
        }).setTraverseParseObjects(false).setYieldRoot(false).traverse(json);

        return Task.whenAll(offlineObjects.values()).onSuccess(new Continuation() {
          @Override
          public Void then(Task task) throws Exception {
            object.mergeREST(object.getState(), json, new OfflineDecoder(offlineObjects));
            return null;
          }
        });
      }
    }).continueWithTask(new Continuation>() {
      @Override
      public Task then(Task task) throws Exception {
        if (task.isCancelled()) {
          tcs.setCancelled();
        } else if (task.isFaulted()) {
          tcs.setError(task.getError());
        } else {
          tcs.setResult(object);
        }
        return tcs.getTask();
      }
    });
  }

  /**
   * Gets the data for the given object from the offline database. Returns a task that will be
   * completed if data for the object was available. If the object is not in the cache, the task
   * will be faulted, with a CACHE_MISS error.
   *
   * @param object
   *          The object to fetch.
   */
  /* package */  Task fetchLocallyAsync(final T object) {
    return runWithManagedConnection(new SQLiteDatabaseCallable>() {
      @Override
      public Task call(ParseSQLiteDatabase db) {
        return fetchLocallyAsync(object, db);
      }
    });
  }

  /**
   * Stores a single object in the local database. If the object is a pointer, isn't dirty, and has
   * an objectId already, it may not be saved, since it would provide no useful data.
   *
   * @param object
   *          The object to save.
   * @param db
   *          A database connection to use.
   */
  private Task saveLocallyAsync(
      final String key, final ParseObject object, final ParseSQLiteDatabase db) {
    // If this is just a clean, unfetched pointer known to Parse, then there is nothing to save.
    if (object.getObjectId() != null && !object.isDataAvailable() && !object.hasChanges()
        && !object.hasOutstandingOperations()) {
      return Task.forResult(null);
    }

    final Capture uuidCapture = new Capture<>();

    // Make sure we have a UUID for the object to be saved.
    return getOrCreateUUIDAsync(object, db).onSuccessTask(new Continuation>() {
      @Override
      public Task then(Task task) throws Exception {
        String uuid = task.getResult();
        uuidCapture.set(uuid);
        return updateDataForObjectAsync(uuid, object, db);
      }
    }).onSuccessTask(new Continuation>() {
      @Override
      public Task then(Task task) throws Exception {
        final ContentValues values = new ContentValues();
        values.put(OfflineSQLiteOpenHelper.KEY_KEY, key);
        values.put(OfflineSQLiteOpenHelper.KEY_UUID, uuidCapture.get());
        return db.insertWithOnConflict(OfflineSQLiteOpenHelper.TABLE_DEPENDENCIES, values,
            SQLiteDatabase.CONFLICT_IGNORE);
      }
    });
  }

  /**
   * Stores an object (and optionally, every object it points to recursively) in the local database.
   * If any of the objects have not been fetched from Parse, they will not be stored. However, if
   * they have changed data, the data will be retained. To get the objects back later, you can use a
   * ParseQuery with a cache policy that uses the local cache, or you can create an unfetched
   * pointer with ParseObject.createWithoutData() and then call fetchFromLocalDatastore() on it. If you modify
   * the object after saving it locally, such as by fetching it or saving it, those changes will
   * automatically be applied to the cache.
   *
   * Any objects previously stored with the same key will be removed from the local database.
   *
   * @param object Root object
   * @param includeAllChildren {@code true} to recursively save all pointers.
   * @param db DB connection
   * @return A Task that will be resolved when saving is complete
   */
  private Task saveLocallyAsync(
      final ParseObject object, final boolean includeAllChildren, final ParseSQLiteDatabase db) {
    final ArrayList objectsInTree = new ArrayList<>();
    // Fetch all objects locally in case they are being re-added
    if (!includeAllChildren) {
      objectsInTree.add(object);
    } else {
      (new ParseTraverser() {
        @Override
        protected boolean visit(Object object) {
          if (object instanceof ParseObject) {
            objectsInTree.add((ParseObject) object);
          }
          return true;
        }
      }).setYieldRoot(true).setTraverseParseObjects(true).traverse(object);
    }

    return saveLocallyAsync(object, objectsInTree, db);
  }


  private Task saveLocallyAsync(
      final ParseObject object, List children, final ParseSQLiteDatabase db) {
    final List objects = children != null
        ? new ArrayList<>(children)
        : new ArrayList();
    if (!objects.contains(object)) {
      objects.add(object);
    }

    // Call saveLocallyAsync for each of them individually.
    final List> tasks = new ArrayList<>();
    for (ParseObject obj : objects) {
      tasks.add(fetchLocallyAsync(obj, db).makeVoid());
    }

    return Task.whenAll(tasks).continueWithTask(new Continuation>() {
      @Override
      public Task then(Task task) throws Exception {
        return objectToUuidMap.get(object);
      }
    }).onSuccessTask(new Continuation>() {
      @Override
      public Task then(Task task) throws Exception {
        String uuid = task.getResult();
        if (uuid == null) {
          // The root object was never stored in the offline store, so nothing to unpin.
          return null;
        }

        // Delete all objects locally corresponding to the key we're trying to use in case it was
        // used before (overwrite)
        return unpinAsync(uuid, db);
      }
    }).onSuccessTask(new Continuation>() {
      @Override
      public Task then(Task task) throws Exception {
        return getOrCreateUUIDAsync(object, db);
      }
    }).onSuccessTask(new Continuation>() {
      @Override
      public Task then(Task task) throws Exception {
        String uuid = task.getResult();

        // Call saveLocallyAsync for each of them individually.
        final List> tasks = new ArrayList<>();
        for (ParseObject obj : objects) {
          tasks.add(saveLocallyAsync(uuid, obj, db));
        }

        return Task.whenAll(tasks);
      }
    });
  }

  private Task unpinAsync(final ParseObject object, final ParseSQLiteDatabase db) {
    Task uuidTask = objectToUuidMap.get(object);
    if (uuidTask == null) {
      // The root object was never stored in the offline store, so nothing to unpin.
      return Task.forResult(null);
    }
    return uuidTask.continueWithTask(new Continuation>() {
      @Override
      public Task then(Task task) throws Exception {
        final String uuid = task.getResult();
        if (uuid == null) {
          // The root object was never stored in the offline store, so nothing to unpin.
          return Task.forResult(null);
        }
        return unpinAsync(uuid, db);
      }
    });
  }

  private Task unpinAsync(final String key, final ParseSQLiteDatabase db) {
    final List uuidsToDelete = new LinkedList<>();
    // A continueWithTask that ends with "return task" is essentially a try-finally.
    return Task.forResult((Void) null).continueWithTask(new Continuation>() {
      @Override
      public Task then(Task task) throws Exception {
        // Fetch all uuids from Dependencies for key=? grouped by uuid having a count of 1
        String sql = "SELECT " + OfflineSQLiteOpenHelper.KEY_UUID + " FROM " + OfflineSQLiteOpenHelper.TABLE_DEPENDENCIES +
            " WHERE " + OfflineSQLiteOpenHelper.KEY_KEY + "=? AND " + OfflineSQLiteOpenHelper.KEY_UUID + " IN (" +
            " SELECT " + OfflineSQLiteOpenHelper.KEY_UUID + " FROM " + OfflineSQLiteOpenHelper.TABLE_DEPENDENCIES +
            " GROUP BY " + OfflineSQLiteOpenHelper.KEY_UUID +
            " HAVING COUNT(" + OfflineSQLiteOpenHelper.KEY_UUID + ")=1" +
            ")";
        String[] args = {key};
        return db.rawQueryAsync(sql, args);
      }
    }).onSuccessTask(new Continuation>() {
      @Override
      public Task then(Task task) throws Exception {
        // DELETE FROM Objects

        Cursor cursor = task.getResult();
        while (cursor.moveToNext()) {
          uuidsToDelete.add(cursor.getString(0));
        }
        cursor.close();

        return deleteObjects(uuidsToDelete, db);
      }
    }).onSuccessTask(new Continuation>() {
      @Override
      public Task then(Task task) throws Exception {
        // DELETE FROM Dependencies
        String where = OfflineSQLiteOpenHelper.KEY_KEY + "=?";
        String[] args = {key};
        return db.deleteAsync(OfflineSQLiteOpenHelper.TABLE_DEPENDENCIES, where, args);
      }
    }).onSuccess(new Continuation() {
      @Override
      public Void then(Task task) throws Exception {
        synchronized (lock) {
          // Remove uuids from memory
          for (String uuid : uuidsToDelete) {
            ParseObject object = uuidToObjectMap.get(uuid);
            if (object != null) {
              objectToUuidMap.remove(object);
              uuidToObjectMap.remove(uuid);
            }
          }
        }
        return null;
      }
    });
  }

  private Task deleteObjects(final List uuids, final ParseSQLiteDatabase db) {
    if (uuids.size() <= 0) {
      return Task.forResult(null);
    }

    // SQLite has a max 999 SQL variables in a statement, so we need to split it up into manageable
    // chunks. We can do this because we're already in a transaction.
    if (uuids.size() > MAX_SQL_VARIABLES) {
      return deleteObjects(uuids.subList(0, MAX_SQL_VARIABLES), db).onSuccessTask(new Continuation>() {
        @Override
        public Task then(Task task) throws Exception {
          return deleteObjects(uuids.subList(MAX_SQL_VARIABLES, uuids.size()), db);
        }
      });
    }

    String[] placeholders = new String[uuids.size()];
    for (int i = 0; i < placeholders.length; i++) {
      placeholders[i] = "?";
    }
    String where = OfflineSQLiteOpenHelper.KEY_UUID + " IN (" + TextUtils.join(",", placeholders) + ")";
    // dynamic args
    String[] args = uuids.toArray(new String[uuids.size()]);
    return db.deleteAsync(OfflineSQLiteOpenHelper.TABLE_OBJECTS, where, args);
  }

  /**
   * Takes an object that has been fetched from the database before and updates it with whatever
   * data is in memory. This will only be used when data comes back from the server after a fetch or
   * a save.
   */
  /* package */ Task updateDataForObjectAsync(final ParseObject object) {
    Task fetched;
    // Make sure the object is fetched.
    synchronized (lock) {
      fetched = fetchedObjects.get(object);
      if (fetched == null) {
        return Task.forError(new IllegalStateException(
            "An object cannot be updated if it wasn't fetched."));
      }
    }
    return fetched.continueWithTask(new Continuation>() {
      @Override
      public Task then(Task task) throws Exception {
        if (task.isFaulted()) {
          // Catch CACHE_MISS
          //noinspection ThrowableResultOfMethodCallIgnored
          if (task.getError() instanceof ParseException
              && ((ParseException) task.getError()).getCode() == ParseException.CACHE_MISS) {
            return Task.forResult(null);
          }
          return task.makeVoid();
        }

        return helper.getWritableDatabaseAsync().continueWithTask(new Continuation>() {
          @Override
          public Task then(Task task) throws Exception {
            final ParseSQLiteDatabase db = task.getResult();
            return db.beginTransactionAsync().onSuccessTask(new Continuation>() {
              @Override
              public Task then(Task task) throws Exception {
                return updateDataForObjectAsync(object, db).onSuccessTask(new Continuation>() {
                  @Override
                  public Task then(Task task) throws Exception {
                    return db.setTransactionSuccessfulAsync();
                  }
                }).continueWithTask(new Continuation>() {
                  // } finally {
                  @Override
                  public Task then(Task task) throws Exception {
                    db.endTransactionAsync();
                    db.closeAsync();
                    return task;
                  }
                });
              }
            });
          }
        });
      }
    });
  }

  private Task updateDataForObjectAsync(
      final ParseObject object,
      final ParseSQLiteDatabase db) {
    // Make sure the object has a UUID.
    Task uuidTask;
    synchronized (lock) {
      uuidTask = objectToUuidMap.get(object);
      if (uuidTask == null) {
        // It was fetched, but it has no UUID. That must mean it isn't actually in the database.
        return Task.forResult(null);
      }
    }
    return uuidTask.onSuccessTask(new Continuation>() {
      @Override
      public Task then(Task task) throws Exception {
        String uuid = task.getResult();
        return updateDataForObjectAsync(uuid, object, db);
      }
    });
  }

  private Task updateDataForObjectAsync(
      final String uuid,
      final ParseObject object,
      final ParseSQLiteDatabase db) {
    // Now actually encode the object as JSON.
    OfflineEncoder encoder = new OfflineEncoder(db);
    final JSONObject json = object.toRest(encoder);

    return encoder.whenFinished().onSuccessTask(new Continuation>() {
      @Override
      public Task then(Task task) throws Exception {
        // Put the JSON in the database.
        String className = object.getClassName();
        String objectId = object.getObjectId();
        int isDeletingEventually = json.getInt(ParseObject.KEY_IS_DELETING_EVENTUALLY);

        final ContentValues values = new ContentValues();
        values.put(OfflineSQLiteOpenHelper.KEY_CLASS_NAME, className);
        values.put(OfflineSQLiteOpenHelper.KEY_JSON, json.toString());
        if (objectId != null) {
          values.put(OfflineSQLiteOpenHelper.KEY_OBJECT_ID, objectId);
        }
        values.put(OfflineSQLiteOpenHelper.KEY_IS_DELETING_EVENTUALLY, isDeletingEventually);
        String where = OfflineSQLiteOpenHelper.KEY_UUID + " = ?";
        String[] args = {uuid};
        return db.updateAsync(OfflineSQLiteOpenHelper.TABLE_OBJECTS, values, where, args).makeVoid();
      }
    });
  }

  /* package */ Task deleteDataForObjectAsync(final ParseObject object) {
    return helper.getWritableDatabaseAsync().continueWithTask(new Continuation>() {
      @Override
      public Task then(Task task) throws Exception {
        final ParseSQLiteDatabase db = task.getResult();
        return db.beginTransactionAsync().onSuccessTask(new Continuation>() {
          @Override
          public Task then(Task task) throws Exception {
            return deleteDataForObjectAsync(object, db).onSuccessTask(new Continuation>() {
              @Override
              public Task then(Task task) throws Exception {
                return db.setTransactionSuccessfulAsync();
              }
            }).continueWithTask(new Continuation>() {
              // } finally {
              @Override
              public Task then(Task task) throws Exception {
                db.endTransactionAsync();
                db.closeAsync();
                return task;
              }
            });
          }
        });
      }
    });
  }

  private Task deleteDataForObjectAsync(final ParseObject object, final ParseSQLiteDatabase db) {
    final Capture uuid = new Capture<>();

    // Make sure the object has a UUID.
    Task uuidTask;
    synchronized (lock) {
      uuidTask = objectToUuidMap.get(object);
      if (uuidTask == null) {
        // It was fetched, but it has no UUID. That must mean it isn't actually in the database.
        return Task.forResult(null);
      }
    }
    uuidTask = uuidTask.onSuccessTask(new Continuation>() {
      @Override
      public Task then(Task task) throws Exception {
        uuid.set(task.getResult());
        return task;
      }
    });

    // If the object was the root of a pin, unpin it.
    Task unpinTask = uuidTask.onSuccessTask(new Continuation>() {
      @Override
      public Task then(Task task) throws Exception {
        // Find all the roots for this object.
        String[] select = { OfflineSQLiteOpenHelper.KEY_KEY };
        String where = OfflineSQLiteOpenHelper.KEY_UUID + "=?";
        String[] args = { uuid.get() };
        return db.queryAsync(OfflineSQLiteOpenHelper.TABLE_DEPENDENCIES, select, where, args);
      }
    }).onSuccessTask(new Continuation>() {
      @Override
      public Task then(Task task) throws Exception {
        // Try to unpin this object from the pin label if it's a root of the ParsePin.
        Cursor cursor = task.getResult();
        List uuids = new ArrayList<>();
        for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
          uuids.add(cursor.getString(0));
        }
        cursor.close();

        List> tasks = new ArrayList<>();
        for (final String uuid : uuids) {
          Task unpinTask = getPointerAsync(uuid, db).onSuccessTask(new Continuation>() {
            @Override
            public Task then(Task task) throws Exception {
              ParsePin pin = (ParsePin) task.getResult();
              return fetchLocallyAsync(pin, db);
            }
          }).continueWithTask(new Continuation>() {
            @Override
            public Task then(Task task) throws Exception {
              ParsePin pin = task.getResult();

              List modified = pin.getObjects();
              if (modified == null || !modified.contains(object)) {
                return task.makeVoid();
              }

              modified.remove(object);
              if (modified.size() == 0) {
                return unpinAsync(uuid, db);
              }

              pin.setObjects(modified);
              return saveLocallyAsync(pin, true, db);
            }
          });
          tasks.add(unpinTask);
        }

        return Task.whenAll(tasks);
      }
    });

    // Delete the object from the Local Datastore in case it wasn't the root of a pin.
    return unpinTask.onSuccessTask(new Continuation>() {
      @Override
      public Task then(Task task) throws Exception {
        String where = OfflineSQLiteOpenHelper.KEY_UUID + "=?";
        String[] args = {uuid.get()};
        return db.deleteAsync(OfflineSQLiteOpenHelper.TABLE_DEPENDENCIES, where, args);
      }
    }).onSuccessTask(new Continuation>() {
      @Override
      public Task then(Task task) throws Exception {
        String where = OfflineSQLiteOpenHelper.KEY_UUID + "=?";
        String[] args = {uuid.get()};
        return db.deleteAsync(OfflineSQLiteOpenHelper.TABLE_OBJECTS, where, args);
      }
    }).onSuccessTask(new Continuation>() {
      @Override
      public Task then(Task task) throws Exception {
        synchronized (lock) {
          // Clean up
          //TODO (grantland): we should probably clean up uuidToObjectMap and objectToUuidMap, but
          // getting the uuid requires a task and things might get a little funky...
          fetchedObjects.remove(object);
        }
        return task;
      }
    });
  }

  //region ParsePin

  private Task getParsePin(final String name, ParseSQLiteDatabase db) {
    ParseQuery.State query = new ParseQuery.State.Builder<>(ParsePin.class)
        .whereEqualTo(ParsePin.KEY_NAME, name)
        .build();

    /* We need to call directly to the OfflineStore since we don't want/need a user to query for
     * ParsePins
     */
    return findAsync(query, null, null, db).onSuccess(new Continuation, ParsePin>() {
      @Override
      public ParsePin then(Task> task) throws Exception {
        ParsePin pin = null;
        if (task.getResult() != null && task.getResult().size() > 0) {
          pin = task.getResult().get(0);
        }

        //TODO (grantland): What do we do if there are more than 1 result?

        if (pin == null) {
          pin = ParseObject.create(ParsePin.class);
          pin.setName(name);
        }
        return pin;
      }
    });
  }

  /* package */  Task pinAllObjectsAsync(
      final String name,
      final List objects,
      final boolean includeChildren) {
    return runWithManagedTransaction(new SQLiteDatabaseCallable>() {
      @Override
      public Task call(ParseSQLiteDatabase db) {
        return pinAllObjectsAsync(name, objects, includeChildren, db);
      }
    });
  }

  private  Task pinAllObjectsAsync(
      final String name,
      final List objects,
      final boolean includeChildren,
      final ParseSQLiteDatabase db) {
    if (objects == null || objects.size() == 0) {
      return Task.forResult(null);
    }

    return getParsePin(name, db).onSuccessTask(new Continuation>() {
      @Override
      public Task then(Task task) throws Exception {
        ParsePin pin = task.getResult();

        //TODO (grantland): change to use relations. currently the related PO are only getting saved
        // offline as pointers.
//        ParseRelation relation = pin.getRelation(KEY_OBJECTS);
//        relation.add(object);

        // Hack to store collections in a pin
        List modified = pin.getObjects();
        if (modified == null) {
          modified = new ArrayList(objects);
        } else {
          for (ParseObject object : objects) {
            if (!modified.contains(object)) {
              modified.add(object);
            }
          }
        }
        pin.setObjects(modified);

        if (includeChildren) {
          return saveLocallyAsync(pin, true, db);
        }
        return saveLocallyAsync(pin, pin.getObjects(), db);
      }
    });
  }

  /* package */  Task unpinAllObjectsAsync(
      final String name,
      final List objects) {
    return runWithManagedTransaction(new SQLiteDatabaseCallable>() {
      @Override
      public Task call(ParseSQLiteDatabase db) {
        return unpinAllObjectsAsync(name, objects, db);
      }
    });
  }

  private  Task unpinAllObjectsAsync(
      String name,
      final List objects,
      final ParseSQLiteDatabase db) {
    if (objects == null || objects.size() == 0) {
      return Task.forResult(null);
    }

    return getParsePin(name, db).onSuccessTask(new Continuation>() {
      @Override
      public Task then(Task task) throws Exception {
        ParsePin pin = task.getResult();

        //TODO (grantland): change to use relations. currently the related PO are only getting saved
        // offline as pointers.
//        ParseRelation relation = pin.getRelation(KEY_OBJECTS);
//        relation.remove(object);

        // Hack to store collections in a pin
        List modified = pin.getObjects();
        if (modified == null) {
          // Unpin a pin that doesn't exist. Wat?
          return Task.forResult(null);
        }

        modified.removeAll(objects);
        if (modified.size() == 0) {
          return unpinAsync(pin, db);
        }
        pin.setObjects(modified);

        return saveLocallyAsync(pin, true, db);
      }
    });
  }

  /* package */ Task unpinAllObjectsAsync(final String name) {
    return runWithManagedTransaction(new SQLiteDatabaseCallable>() {
      @Override
      public Task call(ParseSQLiteDatabase db) {
        return unpinAllObjectsAsync(name, db);
      }
    });
  }

  private Task unpinAllObjectsAsync(final String name, final ParseSQLiteDatabase db) {
    return getParsePin(name, db).continueWithTask(new Continuation>() {
      @Override
      public Task then(Task task) throws Exception {
        if (task.isFaulted()) {
          return task.makeVoid();
        }
        ParsePin pin = task.getResult();
        return unpinAsync(pin, db);
      }
    });
  }

  /* package */  Task> findFromPinAsync(
      final String name,
      final ParseQuery.State state,
      final ParseUser user) {
    return runWithManagedConnection(new SQLiteDatabaseCallable>>() {
      @Override
      public Task> call(ParseSQLiteDatabase db) {
        return findFromPinAsync(name, state, user, db);
      }
    });
  }

  private  Task> findFromPinAsync(
      final String name,
      final ParseQuery.State state,
      final ParseUser user,
      final ParseSQLiteDatabase db) {
    Task task;
    if (name != null) {
      task = getParsePin(name, db);
    } else {
      task = Task.forResult(null);
    }
    return task.onSuccessTask(new Continuation>>() {
      @Override
      public Task> then(Task task) throws Exception {
        ParsePin pin = task.getResult();
        return findAsync(state, user, pin, false, db);
      }
    });
  }

  /* package */  Task countFromPinAsync(
      final String name,
      final ParseQuery.State state,
      final ParseUser user) {
    return runWithManagedConnection(new SQLiteDatabaseCallable>() {
      @Override
      public Task call(ParseSQLiteDatabase db) {
        return countFromPinAsync(name, state, user, db);
      }
    });
  }

  private  Task countFromPinAsync(
      final String name,
      final ParseQuery.State state,
      final ParseUser user,
      final ParseSQLiteDatabase db) {
    Task task;
    if (name != null) {
      task = getParsePin(name, db);
    } else {
      task = Task.forResult(null);
    }
    return task.onSuccessTask(new Continuation>() {
      @Override
      public Task then(Task task) throws Exception {
        ParsePin pin = task.getResult();
        return findAsync(state, user, pin, true, db).onSuccess(new Continuation, Integer>() {
          @Override
          public Integer then(Task> task) throws Exception {
            return task.getResult().size();
          }
        });
      }
    });
  }

  //endregion

  //region Single Instance

  /**
   * In-memory map of (className, objectId) -> ParseObject. This is used so that we can always
   * return the same instance for a given object. Objects in this map may or may not be in the
   * database.
   */
  private final WeakValueHashMap, ParseObject>
      classNameAndObjectIdToObjectMap = new WeakValueHashMap<>();

  /**
   * This should be called by the ParseObject constructor notify the store that there is an object
   * with this className and objectId.
   */
  /* package */ void registerNewObject(ParseObject object) {
    synchronized (lock) {
      String objectId = object.getObjectId();
      if (objectId != null) {
        String className = object.getClassName();
        Pair classNameAndObjectId = Pair.create(className, objectId);
        classNameAndObjectIdToObjectMap.put(classNameAndObjectId, object);
      }
    }
  }

  /* package */ void unregisterObject(ParseObject object) {
    synchronized (lock) {
      String objectId = object.getObjectId();
      if (objectId != null) {
        classNameAndObjectIdToObjectMap.remove(Pair.create(object.getClassName(), objectId));
      }
    }
  }

  /**
   * This should only ever be called from ParseObject.createWithoutData().
   *
   * @return a pair of ParseObject and Boolean. The ParseObject is the object. The Boolean is true
   *         iff the object was newly created.
   */
  /* package */ ParseObject getObject(String className, String objectId) {
    if (objectId == null) {
      throw new IllegalStateException("objectId cannot be null.");
    }

    Pair classNameAndObjectId = Pair.create(className, objectId);
    // This lock should never be held by anyone doing disk or database access.
    synchronized (lock) {
      return classNameAndObjectIdToObjectMap.get(classNameAndObjectId);
    }
  }

  /**
   * When an object is finished saving, it gets an objectId. Then it should call this method to
   * clean up the bookeeping around ids.
   */
  /* package */ void updateObjectId(ParseObject object, String oldObjectId, String newObjectId) {
    if (oldObjectId != null) {
      if (oldObjectId.equals(newObjectId)) {
        return;
      }
      throw new RuntimeException("objectIds cannot be changed in offline mode.");
    }

    String className = object.getClassName();
    Pair classNameAndNewObjectId = Pair.create(className, newObjectId);

    synchronized (lock) {
      // See if there's already an entry for the new object id.
      ParseObject existing = classNameAndObjectIdToObjectMap.get(classNameAndNewObjectId);
      if (existing != null && existing != object) {
        throw new RuntimeException("Attempted to change an objectId to one that's "
            + "already known to the Offline Store.");
      }

      // Okay, all clear to add the new reference.
      classNameAndObjectIdToObjectMap.put(classNameAndNewObjectId, object);
    }
  }

  //endregion

  /**
   * Wraps SQLite operations with a managed SQLite connection.
   */
  private  Task runWithManagedConnection(final SQLiteDatabaseCallable> callable) {
    return helper.getWritableDatabaseAsync().onSuccessTask(new Continuation>() {
      @Override
      public Task then(Task task) throws Exception {
        final ParseSQLiteDatabase db = task.getResult();
        return callable.call(db).continueWithTask(new Continuation>() {
          @Override
          public Task then(Task task) throws Exception {
            db.closeAsync();
            return task;
          }
        });
      }
    });
  }

  /**
   * Wraps SQLite operations with a managed SQLite connection and transaction.
   */
  private Task runWithManagedTransaction(final SQLiteDatabaseCallable> callable) {
    return helper.getWritableDatabaseAsync().onSuccessTask(new Continuation>() {
      @Override
      public Task then(Task task) throws Exception {
        final ParseSQLiteDatabase db = task.getResult();
        return db.beginTransactionAsync().onSuccessTask(new Continuation>() {
          @Override
          public Task then(Task task) throws Exception {
            return callable.call(db).onSuccessTask(new Continuation>() {
              @Override
              public Task then(Task task) throws Exception {
                return db.setTransactionSuccessfulAsync();
              }
            }).continueWithTask(new Continuation>() {
              @Override
              public Task then(Task task) throws Exception {
                db.endTransactionAsync();
                db.closeAsync();
                return task;
              }
            });
          }
        });
      }
    });
  }

  private interface SQLiteDatabaseCallable {
    T call(ParseSQLiteDatabase db);
  }

  /*
   * Methods for testing.
   */

  /**
   * Clears all in-memory caches so that data must be retrieved from disk.
   */
  void simulateReboot() {
    synchronized (lock) {
      uuidToObjectMap.clear();
      objectToUuidMap.clear();
      classNameAndObjectIdToObjectMap.clear();
      fetchedObjects.clear();
    }
  }

  /**
   * Clears the database on disk.
   */
  void clearDatabase(Context context) {
    helper.clearDatabase(context);
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy