@Override
  public Cursor query(
      @NonNull Uri uri,
      String[] projection,
      String selection,
      String[] selectionArgs,
      String sortOrder) {
    int match = uriMatcher.match(uri);
    String table;
    String useSelection = selection;
    String[] useSelectionArgs = selectionArgs;
    switch (match) {
      case ALL_ROWS:
        table = uri.getLastPathSegment();
        break;
      case ROW_BY_ID:
        List<String> segments = uri.getPathSegments();
        table = segments.get(TABLE_SEGMENT);
        useSelection = WHERE_MATCHES_ID;
        useSelectionArgs = new String[] {segments.get(ID_SEGMENT)};
        break;
      default:
        throw new UnsupportedOperationException("Unknown uri: " + uri);
    }

    boolean distinct = false;
    String limit = null;

    // If have query parameters, see if any exist interested in.
    if (!TextUtils.isEmpty(uri.getQuery())) {
      distinct = uri.getBooleanQueryParameter(DISTINCT_PARAMETER, false);
      limit = uri.getQueryParameter(LIMIT_PARAMETER);
    }

    SQLiteDatabase db = dbHelper.getReadableDatabase();
    db.acquireReference();

    try {
      Cursor cursor =
          db.query(
              distinct,
              table,
              projection,
              useSelection,
              useSelectionArgs,
              null,
              null,
              sortOrder,
              limit);
      // Register the cursor with the requested URI so the caller will receive
      // future database change notifications. Useful for "loaders" which take advantage
      // of this concept.
      cursor.setNotificationUri(getContext().getContentResolver(), uri);
      return cursor;
    } finally {
      db.releaseReference();
    }
  }
  @Override
  public int update(
      @NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) {
    int match = uriMatcher.match(uri);
    String table;
    String useSelection = selection;
    String[] useSelectionArgs = selectionArgs;
    switch (match) {
      case ALL_ROWS:
        table = uri.getLastPathSegment();
        break;
      case ROW_BY_ID:
        List<String> segments = uri.getPathSegments();
        table = segments.get(TABLE_SEGMENT);
        useSelection = WHERE_MATCHES_ID;
        useSelectionArgs = new String[] {segments.get(ID_SEGMENT)};
        break;
      default:
        throw new UnsupportedOperationException("Unknown uri: " + uri);
    }

    int rows = 0;

    SQLiteDatabase db = dbHelper.getWritableDatabase();
    db.acquireReference();

    try {
      startTransaction(db);
      try {
        rows = db.update(table, values, useSelection, useSelectionArgs);
        db.setTransactionSuccessful();
      } finally {
        db.endTransaction();
      }
    } finally {
      db.releaseReference();
    }

    // notify change essentially indicates to any users with active cursors
    // that they need to "reload" the data
    if (rows > 0) {
      notifyChange(uri);
    }
    return rows;
  }
  @Override
  public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] valuesArray) {
    int match = uriMatcher.match(uri);
    String table;
    switch (match) {
      case ALL_ROWS:
        table = uri.getLastPathSegment();
        break;
      case ROW_BY_ID:
        throw new UnsupportedOperationException("Unable to insert by id for uri: " + uri);
      default:
        throw new UnsupportedOperationException("Unknown uri: " + uri);
    }

    SQLiteDatabase db = dbHelper.getWritableDatabase();

    int count = 0;
    db.acquireReference();
    try {
      startTransaction(db);
      try {
        final String nullColumnHack = getNullColumnHack(table);
        final int conflictAlgorithm = getConflictAlgorithm(table);

        for (ContentValues values : valuesArray) {
          long id = db.insertWithOnConflict(table, nullColumnHack, values, conflictAlgorithm);
          if (id != -1) {
            ++count;
          }
        }
        db.setTransactionSuccessful();
      } finally {
        db.endTransaction();
      }
    } finally {
      db.releaseReference();
    }

    // notify change essentially indicates to any users with active cursors
    // that they need to "reload" the data
    if (count > 0) {
      notifyChange(uri);
    }
    return count;
  }
  @Override
  public Uri insert(@NonNull Uri uri, ContentValues values) {
    int match = uriMatcher.match(uri);
    String table;
    switch (match) {
      case ALL_ROWS:
        table = uri.getLastPathSegment();
        break;
      case ROW_BY_ID:
        throw new UnsupportedOperationException("Unable to insert by id for uri: " + uri);
      default:
        throw new UnsupportedOperationException("Unknown uri: " + uri);
    }

    long id = -1;

    SQLiteDatabase db = dbHelper.getWritableDatabase();
    db.acquireReference();
    try {
      startTransaction(db);
      try {
        id =
            db.insertWithOnConflict(
                table, getNullColumnHack(table), values, getConflictAlgorithm(table));
        db.setTransactionSuccessful();
      } finally {
        db.endTransaction();
      }
    } finally {
      db.releaseReference();
    }

    if (id == -1) {
      return null;
    }

    // notify change essentially indicates to any users with active cursors
    // that they need to "reload" the data
    notifyChange(uri);
    return ContentUris.withAppendedId(uri, id);
  }
  @Override
  public void onOpen(final SQLiteDatabase db) {
    super.onOpen(db);
    new AsyncTask<Void, Void, Void>() {
      private SQLiteStatement getInsertStatementForTable(String table) {
        if ("state".equals(table))
          return db.compileStatement(
              "INSERT INTO state (state_id, state_abbrev, state_name) VALUES (?, ?, ?)");
        else if ("county".equals(table))
          return db.compileStatement(
              "INSERT INTO county (state_id, county_id, county_name) VALUES (?, ?, ?)");
        else if ("zone".equals(table))
          return db.compileStatement(
              "INSERT INTO zone (state_id, zone_id, zone_name) VALUES (?, ?, ?)");
        else if ("county_zone".equals(table))
          return db.compileStatement(
              "INSERT INTO county_zone (county_state_id, county_id, zone_state_id, zone_id) VALUES (?, ?, ?, ?)");
        else if ("database_version".equals(table))
          return db.compileStatement(
              "INSERT INTO database_version (database_version_id, schema_version, last_update_time) VALUES (?, ?, ?)");
        else return null;
      }

      private SQLiteStatement getUpdateStatementForTable(String table) {
        if ("state".equals(table))
          return db.compileStatement(
              "UPDATE state SET state_abbrev = ?2, state_name = ?3 WHERE state_id = ?1");
        else if ("county".equals(table))
          return db.compileStatement(
              "UPDATE county SET county_name = ?3 WHERE state_id = ?1 AND county_id = ?2");
        else if ("zone".equals(table))
          return db.compileStatement(
              "UPDATE zone SET zone_name = ?3 WHERE state_id = ?1 AND zone_id = ?2");
        // else if ("county_zone".equals(table))
        //    return db.compileStatement("UPDATE county_zone SET (county_state_id, county_id,
        // zone_state_id, zone_id) VALUES (?, ?, ?, ?)");
        else if ("database_version".equals(table))
          return db.compileStatement(
              "UPDATE database_version SET schema_version = ?2, last_update_time = ?3 WHERE database_version_id = ?1");
        else return null;
      }

      private SQLiteStatement getDeleteStatementForTable(String table) {
        if ("state".equals(table))
          return db.compileStatement("DELETE FROM state WHERE state_id = ?");
        else if ("county".equals(table))
          return db.compileStatement("DELETE FROM county WHERE state_id = ? and county_id = ?");
        else if ("zone".equals(table))
          return db.compileStatement("DELETE FROM zone WHERE state_id = ? AND zone_id = ?");
        else if ("county_zone".equals(table))
          return db.compileStatement(
              "DELETE FROM county_zone WHERE county_state_id = ? AND county_id = ? AND zone_state_id = ? AND zone_id = ?");
        else if ("database_version".equals(table))
          return db.compileStatement("DELETE FROM database_version WHERE database_version_id = ?");
        else return null;
      }

      @Override
      protected Void doInBackground(Void... voids) {
        HttpURLConnection conn = null;
        try {
          String versionArg = "";
          Cursor cursor = db.rawQuery("SELECT last_update_time FROM database_version", null);
          try {
            if (cursor.moveToFirst())
              versionArg = String.format(Locale.US, "&since=%s", Uri.encode(cursor.getString(0)));
          } finally {
            cursor.close();
            cursor = null;
          }
          String urlprefix = "https://captest-1180.appspot.com";
          if (BuildConfig.FLAVOR.equals("local")) urlprefix = "http://10.0.2.2:8080";
          conn =
              (HttpURLConnection)
                  new URL(
                          String.format(
                              Locale.US,
                              "%s/data.cgi?schema=%d%s",
                              urlprefix,
                              db.getVersion(),
                              versionArg))
                      .openConnection();
          int status = conn.getResponseCode();
          if (status != 200) {
            if (status == 301 || status == 302) {
              conn.getInputStream().close();
              conn.disconnect();
              conn = (HttpURLConnection) new URL(conn.getHeaderField("Location")).openConnection();
              status = conn.getResponseCode();
            }
            if (status != 200) {
              conn.getErrorStream().close();
              throw new IOException("Get failed with status code " + status);
            }
          }

          JsonParser parser =
              new JacksonFactory()
                  .createJsonParser(conn.getInputStream(), Charset.forName("UTF-8"));
          try {
            Deque<StreamState> stateDeque = new ArrayDeque<StreamState>();
            String table = null;
            int index = 0;
            SQLiteStatement stmt = null;
            db.beginTransaction();
            try {
              do {
                JsonToken token = parser.nextToken();
                switch (token) {
                  case START_OBJECT:
                    if (stateDeque.isEmpty()) stateDeque.push(StreamState.START);
                    break;
                  case END_OBJECT:
                    switch (stateDeque.peek()) {
                      case START:
                        stateDeque.pop();
                        break;
                      case TABLE:
                        stateDeque.pop();
                        table = null;
                        break;
                      case ADD:
                      case CHANGE:
                      case REMOVE:
                        if (stmt != null) stmt.close();
                        stmt = null;
                        stateDeque.pop();
                        break;
                    }
                    break;
                  case FIELD_NAME:
                    switch (stateDeque.peek()) {
                      case START:
                        table = parser.getCurrentName();
                        stateDeque.push(StreamState.TABLE);
                        break;
                      case TABLE:
                        if ("added".equals(parser.getCurrentName())) {
                          stmt = getInsertStatementForTable(table);
                          stateDeque.push(StreamState.ADD);
                        } else if ("changed".equals(parser.getCurrentName())) {
                          stmt = getUpdateStatementForTable(table);
                          stateDeque.push(StreamState.CHANGE);
                        } else if ("removed".equals(parser.getCurrentName())) {
                          stmt = getDeleteStatementForTable(table);
                          stateDeque.push(StreamState.REMOVE);
                        }
                        break;
                    }
                    break;
                  case START_ARRAY:
                    switch (stateDeque.peek()) {
                      case ADD:
                      case CHANGE:
                      case REMOVE:
                        stateDeque.push(StreamState.ROW_ARRAY);
                        break;
                      case ROW_ARRAY:
                        stateDeque.push(StreamState.ROW);
                        index = 0;
                        break;
                    }
                    break;
                  case END_ARRAY:
                    switch (stateDeque.peek()) {
                      case ROW:
                        if (stmt != null) stmt.execute();
                      case ROW_ARRAY:
                        stateDeque.pop();
                        break;
                    }
                    if (!stateDeque.isEmpty()) {
                      switch (stateDeque.peek()) {
                        case ADD:
                        case CHANGE:
                        case REMOVE:
                          if (stmt != null) stmt.close();
                          stmt = null;
                          stateDeque.pop();
                          break;
                      }
                    }
                    break;
                  case VALUE_STRING:
                    switch (stateDeque.peek()) {
                      case ROW:
                        if (stmt != null) stmt.bindString(++index, parser.getText());
                        break;
                    }
                    break;
                  case VALUE_NUMBER_INT:
                    switch (stateDeque.peek()) {
                      case ROW:
                        if (stmt != null) stmt.bindLong(++index, parser.getLongValue());
                        break;
                    }
                    break;
                  case VALUE_NUMBER_FLOAT:
                    switch (stateDeque.peek()) {
                      case ROW:
                        if (stmt != null) stmt.bindDouble(++index, parser.getDoubleValue());
                        break;
                    }
                    break;
                  case VALUE_NULL:
                    switch (stateDeque.peek()) {
                      case ROW:
                        if (stmt != null) stmt.bindNull(++index);
                        break;
                    }
                    break;
                  case VALUE_TRUE:
                    switch (stateDeque.peek()) {
                      case ROW:
                        if (stmt != null) stmt.bindLong(++index, 1);
                        break;
                    }
                    break;
                  case VALUE_FALSE:
                    switch (stateDeque.peek()) {
                      case ROW:
                        if (stmt != null) stmt.bindLong(++index, 0);
                        break;
                    }
                    break;
                }
              } while (!stateDeque.isEmpty());
              db.setTransactionSuccessful();
            } finally {
              parser.close();
            }
          } finally {
            db.endTransaction();
          }
        } catch (MalformedURLException e) {
          throw new RuntimeException(e);
        } catch (IOException e) {
          throw new RuntimeException(e);
        } finally {
          if (conn != null) conn.disconnect();
          db.releaseReference();
        }
        return null;
      }
    }.execute();
    db.acquireReference();
  }