List of usage examples for android.content ContentValues getAsString
public String getAsString(String key)
From source file:ca.zadrox.dota2esportticker.service.UpdateTeamsService.java
private void updateTopTeams() { LOGD(TAG, "starting update"); // actually, first, check for connectivity: if (!checkForConnectivity()) { LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(STATUS_NO_CONNECTIVITY)); LOGD(TAG, "returning due to no connectivity"); return;// w w w . j a va 2 s . c om } // first, check last update time long lastUpdate = PrefUtils.lastTeamsUpdate(this); long currentTime = TimeUtils.getUTCTime(); // if last update is less than 1 hour old, boot user to cursorloader op. if (currentTime - lastUpdate < 60000 * 60) { LOGD(TAG, "returnning due to too soon"); LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(STATUS_COMPLETED)); return; } // else // use local broadcast manager to show loading indicator LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(STATUS_UPDATING)); final String BASE_URL = "http://www.gosugamers.net/dota2/rankings"; final String TEAM_LINK_BASE_URL = "http://www.gosugamers.net/dota2/teams/"; // we see what teams are in top 50. (httpreq -> gosugamers) try { String rawHtml = new OkHttpClient().newCall(new Request.Builder().url(BASE_URL).build()).execute() .body().string(); String processedHtml = rawHtml.substring(rawHtml.indexOf("<div id=\"col1\" class=\"rows\">"), rawHtml.indexOf("<div id=\"col2\" class=\"rows\">")); Elements teamRows = Jsoup.parse(processedHtml).getElementsByClass("ranking-link"); ExecutorService executorService = Executors.newFixedThreadPool(10); ContentValues[] teamRanks = new ContentValues[50]; HashMap<ContentValues, Future<String>> newTeamInfo = new HashMap<ContentValues, Future<String>>(); HashMap<ContentValues, Future<String>> updateTeamInfo = new HashMap<ContentValues, Future<String>>(); int i = 0; for (Element teamRow : teamRows) { ContentValues contentValues = new ContentValues(); String teamId = teamRow.attr("data-id"); contentValues.put(MatchContract.TeamEntry._ID, teamId); String untrimmedTeamName = teamRow.getElementsByTag("h4").first().text(); String teamUrl = TEAM_LINK_BASE_URL + teamId + "-" + untrimmedTeamName.replaceAll("[\\W]?[\\W][\\W]*", "-").toLowerCase(); contentValues.put(MatchContract.TeamEntry.COLUMN_TEAM_URL, teamUrl); String teamName = untrimmedTeamName.replaceAll(" ?\\.?\\-?-?Dot[aA][\\s]?2", ""); contentValues.put(MatchContract.TeamEntry.COLUMN_TEAM_NAME, teamName); if (teamUrl.charAt(teamUrl.length() - 1) == '-') { teamUrl = teamUrl.substring(0, teamUrl.length() - 2); } // then, we query db for id of the team ( Cursor cursor = getContentResolver().query( MatchContract.TeamEntry.buildTeamUri(Long.parseLong(teamId)), new String[] { MatchContract.TeamEntry.COLUMN_TEAM_NAME, MatchContract.TeamEntry.COLUMN_TEAM_URL }, null, null, null); // -> if present, and data remains unchanged, continue. // -> if present, but data is changed, add to update queue. if (cursor.moveToFirst()) { LOGD(TAG, "Have team already?"); if (!cursor.getString(0).contentEquals(teamName) || !cursor.getString(1).contentEquals(teamUrl)) { LOGD(TAG, "Team has updated values."); updateTeamInfo.put(contentValues, executorService.submit(new TeamGetter(teamUrl))); } } // -> if not present, add to update queue. else { LOGD(TAG, "Do team update"); newTeamInfo.put(contentValues, executorService.submit(new TeamGetter(teamUrl))); } // LOGD(TAG, "\n" + // "data-id: " + teamId + "\n" + // "team-name: " + teamName + "\n" + // "team-url: " + teamUrl); teamRanks[i] = new ContentValues(); teamRanks[i].put(MatchContract.TeamRankEntry._ID, i + 1); teamRanks[i].put(MatchContract.TeamRankEntry.COLUMN_TEAM_ID, teamId); cursor.close(); i++; } executorService.shutdown(); executorService.awaitTermination(20, TimeUnit.SECONDS); for (ContentValues contentValues : newTeamInfo.keySet()) { try { String teamLogo = newTeamInfo.get(contentValues).get(); contentValues.put(MatchContract.TeamEntry.COLUMN_TEAM_LOGO_URL, teamLogo); } catch (ExecutionException e) { LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(STATUS_ERROR)); e.printStackTrace(); } } for (ContentValues contentValues : updateTeamInfo.keySet()) { try { String teamLogo = updateTeamInfo.get(contentValues).get(); contentValues.put(MatchContract.TeamEntry.COLUMN_TEAM_LOGO_URL, teamLogo); String teamId = contentValues.getAsString(MatchContract.TeamEntry._ID); contentValues.remove(MatchContract.TeamEntry._ID); int updatedRows = getContentResolver().update(MatchContract.TeamEntry.CONTENT_URI, contentValues, MatchContract.TeamEntry.TABLE_NAME + "." + MatchContract.TeamEntry._ID + " = ?", new String[] { teamId }); LOGD(TAG, "updatedRows: " + updatedRows); } catch (ExecutionException e) { LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(STATUS_ERROR)); e.printStackTrace(); } } getContentResolver().bulkInsert(MatchContract.TeamEntry.CONTENT_URI, newTeamInfo.keySet().toArray(new ContentValues[newTeamInfo.size()])); getContentResolver().bulkInsert(MatchContract.TeamRankEntry.CONTENT_URI, teamRanks); } catch (IOException e) { LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(STATUS_ERROR)); e.printStackTrace(); } catch (InterruptedException e2) { LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(STATUS_ERROR)); e2.printStackTrace(); } // String[] projection = new String[]{ // MatchContract.TeamEntry.TABLE_NAME + "." + MatchContract.TeamEntry._ID, // MatchContract.TeamEntry.COLUMN_TEAM_NAME, // MatchContract.TeamEntry.COLUMN_TEAM_URL, // MatchContract.TeamEntry.COLUMN_TEAM_LOGO_URL, // MatchContract.TeamEntry.COLUMN_TEAM_STARRED, // MatchContract.TeamRankEntry.TABLE_NAME + "." + MatchContract.TeamRankEntry._ID // }; // // String sortOrder = // MatchContract.TeamRankEntry.TABLE_NAME + "." + // MatchContract.TeamRankEntry._ID + " ASC"; // // Cursor c = getContentResolver().query( // MatchContract.TeamEntry.TOP_50_URI, // projection, // null, // null, // sortOrder // ); // // while (c.moveToNext()) { // String teamPrintOut = // "Rank: " + c.getInt(5) + "\n" + // "teamId: " + c.getInt(0) + " teamName: " + c.getString(1) + "\n" + // "teamUrl: " + c.getString(2) + "\n" + // "teamLogoUrl: " + c.getString(3) + "\n" + // "isFavourited: " + (c.getInt(4) == 0 ? "false" : "true"); // LOGD(TAG + "/UTT", teamPrintOut); // } // // c.close(); // use local broadcast manager to hide loading indicator // and signal that cursorloader for top50 can happen. PrefUtils.setLastTeamUpdate(this, currentTime); LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(STATUS_COMPLETED)); }
From source file:com.ichi2.anki.provider.CardContentProvider.java
@Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { Timber.d("CardContentProvider.update"); Collection col = CollectionHelper.getInstance().getCol(getContext()); if (col == null) { return 0; }// w w w . j ava2 s .c om // Find out what data the user is requesting int match = sUriMatcher.match(uri); int updated = 0; // Number of updated entries (return value) switch (match) { case NOTES: throw new IllegalArgumentException("Not possible to update notes directly (only through data URI)"); case NOTES_ID: { /* Direct access note details */ Note currentNote = getNoteFromUri(uri, col); // the key of the ContentValues contains the column name // the value of the ContentValues contains the row value. Set<Map.Entry<String, Object>> valueSet = values.valueSet(); for (Map.Entry<String, Object> entry : valueSet) { String key = entry.getKey(); if (key.equals(FlashCardsContract.Note.FLDS)) { // Update FLDS Timber.d("CardContentProvider: flds update..."); String newFldsEncoded = (String) entry.getValue(); String[] flds = Utils.splitFields(newFldsEncoded); // Check that correct number of flds specified if (flds.length != currentNote.getFields().length) { throw new IllegalArgumentException("Incorrect flds argument : " + newFldsEncoded); } // Update the note for (int idx = 0; idx < flds.length; idx++) { currentNote.setField(idx, flds[idx]); } updated++; } else if (key.equals(FlashCardsContract.Note.TAGS)) { // Update tags Timber.d("CardContentProvider: tags update..."); currentNote.setTagsFromStr((String) entry.getValue()); updated++; } else { // Unsupported column throw new IllegalArgumentException("Unsupported column: " + key); } } Timber.d("CardContentProvider: Saving note..."); currentNote.flush(); break; } case NOTES_ID_CARDS: // TODO: To be implemented throw new UnsupportedOperationException("Not yet implemented"); // break; case NOTES_ID_CARDS_ORD: { Card currentCard = getCardFromUri(uri, col); boolean isDeckUpdate = false; long did = -1; // the key of the ContentValues contains the column name // the value of the ContentValues contains the row value. Set<Map.Entry<String, Object>> valueSet = values.valueSet(); for (Map.Entry<String, Object> entry : valueSet) { // Only updates on deck id is supported String key = entry.getKey(); isDeckUpdate = key.equals(FlashCardsContract.Card.DECK_ID); did = values.getAsLong(key); } /* now update the card */ if ((isDeckUpdate) && (did >= 0)) { Timber.d("CardContentProvider: Moving card to other deck..."); col.getDecks().flush(); currentCard.setDid(did); currentCard.flush(); updated++; } else { // User tries an operation that is not (yet?) supported. throw new IllegalArgumentException("Currently only updates of decks are supported"); } break; } case MODELS: throw new IllegalArgumentException("Cannot update models in bulk"); case MODELS_ID: // Get the input parameters String newModelName = values.getAsString(FlashCardsContract.Model.NAME); String newCss = values.getAsString(FlashCardsContract.Model.CSS); String newDid = values.getAsString(FlashCardsContract.Model.DECK_ID); String newFieldList = values.getAsString(FlashCardsContract.Model.FIELD_NAMES); if (newFieldList != null) { // Changing the field names would require a full-sync throw new IllegalArgumentException("Field names cannot be changed via provider"); } // Get the original note JSON JSONObject model = col.getModels().get(getModelIdFromUri(uri, col)); try { // Update model name and/or css if (newModelName != null) { model.put("name", newModelName); updated++; } if (newCss != null) { model.put("css", newCss); updated++; } if (newDid != null) { model.put("did", newDid); updated++; } col.getModels().save(model); } catch (JSONException e) { Timber.e(e, "JSONException updating model"); } break; case MODELS_ID_TEMPLATES: throw new IllegalArgumentException("Cannot update templates in bulk"); case MODELS_ID_TEMPLATES_ID: Long mid = values.getAsLong(CardTemplate.MODEL_ID); Integer ord = values.getAsInteger(CardTemplate.ORD); String name = values.getAsString(CardTemplate.NAME); String qfmt = values.getAsString(CardTemplate.QUESTION_FORMAT); String afmt = values.getAsString(CardTemplate.ANSWER_FORMAT); String bqfmt = values.getAsString(CardTemplate.BROWSER_QUESTION_FORMAT); String bafmt = values.getAsString(CardTemplate.BROWSER_ANSWER_FORMAT); // Throw exception if read-only fields are included if (mid != null || ord != null) { throw new IllegalArgumentException("Can update mid or ord"); } // Update the model try { Integer templateOrd = Integer.parseInt(uri.getLastPathSegment()); JSONObject existingModel = col.getModels().get(getModelIdFromUri(uri, col)); JSONArray templates = existingModel.getJSONArray("tmpls"); JSONObject template = templates.getJSONObject(templateOrd); if (name != null) { template.put("name", name); updated++; } if (qfmt != null) { template.put("qfmt", qfmt); updated++; } if (afmt != null) { template.put("afmt", afmt); updated++; } if (bqfmt != null) { template.put("bqfmt", bqfmt); updated++; } if (bafmt != null) { template.put("bafmt", bafmt); updated++; } // Save the model templates.put(templateOrd, template); existingModel.put("tmpls", templates); col.getModels().save(existingModel, true); } catch (JSONException e) { throw new IllegalArgumentException("Model is malformed", e); } break; case SCHEDULE: { Set<Map.Entry<String, Object>> valueSet = values.valueSet(); int cardOrd = -1; long noteID = -1; int ease = -1; long timeTaken = -1; for (Map.Entry<String, Object> entry : valueSet) { String key = entry.getKey(); if (key.equals(FlashCardsContract.ReviewInfo.NOTE_ID)) { noteID = values.getAsLong(key); } else if (key.equals(FlashCardsContract.ReviewInfo.CARD_ORD)) { cardOrd = values.getAsInteger(key); } else if (key.equals(FlashCardsContract.ReviewInfo.EASE)) { ease = values.getAsInteger(key); } else if (key.equals(FlashCardsContract.ReviewInfo.TIME_TAKEN)) { timeTaken = values.getAsLong(key); } } if (cardOrd != -1 && noteID != -1) { Card cardToAnswer = getCard(noteID, cardOrd, col); if (cardToAnswer != null) { answerCard(col, col.getSched(), cardToAnswer, ease, timeTaken); updated++; } else { Timber.e( "Requested card with noteId %d and cardOrd %d was not found. Either the provided " + "noteId/cardOrd were wrong or the card has been deleted in the meantime.", noteID, cardOrd); } } break; } case DECKS: throw new IllegalArgumentException("Can't update decks in bulk"); case DECKS_ID: throw new UnsupportedOperationException("Not yet implemented"); case DECK_SELECTED: { Set<Map.Entry<String, Object>> valueSet = values.valueSet(); for (Map.Entry<String, Object> entry : valueSet) { String key = entry.getKey(); if (key.equals(FlashCardsContract.Deck.DECK_ID)) { long deckId = values.getAsLong(key); if (selectDeckWithCheck(col, deckId)) { updated++; } } } break; } default: // Unknown URI type throw new IllegalArgumentException("uri " + uri + " is not supported"); } return updated; }
From source file:edu.mit.mobile.android.locast.data.Sync.java
/** * Given a live cursor pointing to a data item and/or a set of contentValues loaded from the network, * attempt to sync.//from ww w . ja v a2 s . c o m * Either c or cvNet can be null, but not both. * @param c A cursor pointing to the data item. Null is OK here. * @param jsonObject JSON object for the item as loaded from the network. null is OK here. * @param sync An empty JsonSyncableItem object. * @param publicPath TODO * * @return True if the item has been modified on either end. * @throws IOException */ private boolean syncItem(Uri toSync, Cursor c, JSONObject jsonObject, JsonSyncableItem sync, SyncProgressNotifier syncProgress, String publicPath) throws SyncException, IOException { boolean modified = false; boolean needToCloseCursor = false; boolean toSyncIsIndex = false; final SyncMap syncMap = sync.getSyncMap(); Uri locUri = null; final Uri origToSync = toSync; ContentValues cvNet = null; final Context context = getApplicationContext(); final ContentResolver cr = context.getContentResolver(); if (jsonObject != null) { if ("http".equals(toSync.getScheme()) || "https".equals(toSync.getScheme())) { // we successfully loaded it from the 'net, but toSync is really for local URIs. Erase it. toSync = sync.getContentUri(); if (toSync == null) { if (DEBUG) { Log.w(TAG, "cannot get local URI for " + origToSync + ". Skipping..."); } return false; } } try { cvNet = JsonSyncableItem.fromJSON(context, null, jsonObject, syncMap); } catch (final Exception e) { final SyncException se = new SyncException("Problem loading JSON object."); se.initCause(e); throw se; } } final String contentType = cr.getType(toSync); if (c != null) { if (contentType.startsWith(CONTENT_TYPE_PREFIX_DIR)) { locUri = ContentUris.withAppendedId(toSync, c.getLong(c.getColumnIndex(JsonSyncableItem._ID))) .buildUpon().query(null).build(); toSyncIsIndex = true; } else { locUri = toSync; } // skip any items already sync'd if (mLastUpdated.isUpdatedRecently(locUri)) { return false; } final int draftCol = c.getColumnIndex(TaggableItem._DRAFT); if (draftCol != -1 && c.getInt(draftCol) != 0) { if (DEBUG) { Log.d(TAG, locUri + " is marked a draft. Not syncing."); } return false; } syncMap.onPreSyncItem(cr, locUri, c); } else if (contentType.startsWith(CONTENT_TYPE_PREFIX_DIR)) { // strip any query strings toSync = toSync.buildUpon().query(null).build(); } // if (c != null){ // MediaProvider.dumpCursorToLog(c, sync.getFullProjection()); // } // when the PUBLIC_URI is null, that means it's only local final int pubUriColumn = (c != null) ? c.getColumnIndex(JsonSyncableItem._PUBLIC_URI) : -1; if (c != null && (c.isNull(pubUriColumn) || c.getString(pubUriColumn) == "")) { // new content on the local side only. Gotta publish. try { jsonObject = JsonSyncableItem.toJSON(context, locUri, c, syncMap); if (publicPath == null) { publicPath = MediaProvider.getPostPath(this, locUri); } if (DEBUG) { Log.d(TAG, "Posting " + locUri + " to " + publicPath); } // The response from a post to create a new item should be the newly created item, // which contains the public ID that we need. jsonObject = nc.postJson(publicPath, jsonObject); final ContentValues cvUpdate = JsonSyncableItem.fromJSON(context, locUri, jsonObject, syncMap); if (cr.update(locUri, cvUpdate, null, null) == 1) { // at this point, server and client should be in sync. mLastUpdated.markUpdated(locUri); if (DEBUG) { Log.i(TAG, "Hooray! " + locUri + " has been posted succesfully."); } } else { Log.e(TAG, "update of " + locUri + " failed"); } modified = true; } catch (final Exception e) { final SyncException se = new SyncException(getString(R.string.error_sync_no_post)); se.initCause(e); throw se; } // only on the remote side, so pull it in. } else if (c == null && cvNet != null) { if (DEBUG) { Log.i(TAG, "Only on the remote side, using network-provided values."); } final String[] params = { cvNet.getAsString(JsonSyncableItem._PUBLIC_URI) }; c = cr.query(toSync, sync.getFullProjection(), JsonSyncableItem._PUBLIC_URI + "=?", params, null); needToCloseCursor = true; if (!c.moveToFirst()) { locUri = cr.insert(toSync, cvNet); modified = true; } else { locUri = ContentUris.withAppendedId(toSync, c.getLong(c.getColumnIndex(JsonSyncableItem._ID))) .buildUpon().query(null).build(); syncMap.onPreSyncItem(cr, locUri, c); } } // we've now found data on both sides, so sync them. if (!modified && c != null) { publicPath = c.getString(c.getColumnIndex(JsonSyncableItem._PUBLIC_URI)); try { if (cvNet == null) { try { if (publicPath == null && toSyncIsIndex && !MediaProvider.canSync(locUri)) { // At this point, we've already checked the index and it doesn't contain the item (otherwise it would be in the syncdItems). // If we can't sync individual items, it's possible that the index is paged or the item has been deleted. if (DEBUG) { Log.w(TAG, "Asked to sync " + locUri + " but item wasn't in server index and cannot sync individual entries. Skipping and hoping it is up to date."); } return false; } else { if (mLastUpdated.isUpdatedRecently(nc.getFullUri(publicPath))) { if (DEBUG) { Log.d(TAG, "already sync'd! " + publicPath); } return false; } if (jsonObject == null) { jsonObject = nc.getObject(publicPath); } cvNet = JsonSyncableItem.fromJSON(context, locUri, jsonObject, syncMap); } } catch (final HttpResponseException hre) { if (hre.getStatusCode() == HttpStatus.SC_NOT_FOUND) { final SyncItemDeletedException side = new SyncItemDeletedException(locUri); side.initCause(hre); throw side; } } } if (cvNet == null) { Log.e(TAG, "got null values from fromJSON() on item " + locUri + ": " + (jsonObject != null ? jsonObject.toString() : "<< no json object >>")); return false; } final Date netLastModified = new Date(cvNet.getAsLong(JsonSyncableItem._MODIFIED_DATE)); final Date locLastModified = new Date(c.getLong(c.getColumnIndex(JsonSyncableItem._MODIFIED_DATE))); if (netLastModified.equals(locLastModified)) { // same! yay! We don't need to do anything. if (DEBUG) { Log.d("LocastSync", locUri + " doesn't need to sync."); } } else if (netLastModified.after(locLastModified)) { // remote is more up to date, update! cr.update(locUri, cvNet, null, null); if (DEBUG) { Log.d("LocastSync", cvNet + " is newer than " + locUri); } modified = true; } else if (netLastModified.before(locLastModified)) { // local is more up to date, propagate! jsonObject = nc.putJson(publicPath, JsonSyncableItem.toJSON(context, locUri, c, syncMap)); if (DEBUG) { Log.d("LocastSync", cvNet + " is older than " + locUri); } modified = true; } mLastUpdated.markUpdated(nc.getFullUri(publicPath)); } catch (final JSONException e) { final SyncException se = new SyncException( "Item sync error for path " + publicPath + ": invalid JSON."); se.initCause(e); throw se; } catch (final NetworkProtocolException e) { final SyncException se = new SyncException( "Item sync error for path " + publicPath + ": " + e.getHttpResponseMessage()); se.initCause(e); throw se; } finally { if (needToCloseCursor) { c.close(); needToCloseCursor = false; } } } if (needToCloseCursor) { c.close(); } if (locUri == null) { throw new RuntimeException("Never got a local URI for a sync'd item."); } // two calls are made in two different contexts. Which context you use depends on the application. syncMap.onPostSyncItem(context, locUri, jsonObject, modified); sync.onPostSyncItem(context, locUri, jsonObject, modified); mLastUpdated.markUpdated(locUri); // needed for things that may have requested a sync with a different URI than what was eventually produced. if (origToSync != locUri) { mLastUpdated.markUpdated(origToSync); cr.notifyChange(origToSync, null); } return modified; }
From source file:org.opendatakit.common.android.provider.impl.FormsProviderImpl.java
@Override public int update(Uri uri, ContentValues values, String where, String[] whereArgs) { List<String> segments = uri.getPathSegments(); if (segments.size() < 1 || segments.size() > 2) { throw new IllegalArgumentException("Unknown URI (incorrect number of segments!) " + uri); }/*w w w . j a v a 2 s . c om*/ String appName = segments.get(0); ODKFileUtils.verifyExternalStorageAvailability(); ODKFileUtils.assertDirectoryStructure(appName); WebLogger log = WebLogger.getLogger(appName); String uriFormId = ((segments.size() == 2) ? segments.get(1) : null); boolean isNumericId = StringUtils.isNumeric(uriFormId); // Modify the where clause to account for the presence of // a form id. Accept either: // (1) numeric _ID value // (2) string FORM_ID value. String whereId; String[] whereIdArgs; if (uriFormId == null) { whereId = where; whereIdArgs = whereArgs; } else { if (TextUtils.isEmpty(where)) { whereId = (isNumericId ? FormsColumns._ID : FormsColumns.FORM_ID) + "=?"; whereIdArgs = new String[1]; whereIdArgs[0] = uriFormId; } else { whereId = (isNumericId ? FormsColumns._ID : FormsColumns.FORM_ID) + "=? AND (" + where + ")"; whereIdArgs = new String[whereArgs.length + 1]; whereIdArgs[0] = uriFormId; for (int i = 0; i < whereArgs.length; ++i) { whereIdArgs[i + 1] = whereArgs[i]; } } } /* * First, find out what records match this query, and if they refer to two * or more (formId,formVersion) tuples, then be sure to remove all * FORM_MEDIA_PATH references. Otherwise, if they are all for the same * tuple, and the update specifies a FORM_MEDIA_PATH, move all the * non-matching directories elsewhere. */ Integer idValue = null; String tableIdValue = null; String formIdValue = null; HashMap<File, DirType> mediaDirs = new HashMap<File, DirType>(); boolean multiset = false; Cursor c = null; try { c = this.query(uri, null, whereId, whereIdArgs, null); if (c == null) { throw new SQLException( "FAILED Update of " + uri + " -- query for existing row did not return a cursor"); } if (c.getCount() >= 1) { FormIdVersion ref = null; c.moveToPosition(-1); while (c.moveToNext()) { idValue = ODKDatabaseUtils.get().getIndexAsType(c, Integer.class, c.getColumnIndex(FormsColumns._ID)); tableIdValue = ODKDatabaseUtils.get().getIndexAsString(c, c.getColumnIndex(FormsColumns.TABLE_ID)); formIdValue = ODKDatabaseUtils.get().getIndexAsString(c, c.getColumnIndex(FormsColumns.FORM_ID)); String tableId = ODKDatabaseUtils.get().getIndexAsString(c, c.getColumnIndex(FormsColumns.TABLE_ID)); String formId = ODKDatabaseUtils.get().getIndexAsString(c, c.getColumnIndex(FormsColumns.FORM_ID)); String formVersion = ODKDatabaseUtils.get().getIndexAsString(c, c.getColumnIndex(FormsColumns.FORM_VERSION)); FormIdVersion cur = new FormIdVersion(tableId, formId, formVersion); int appRelativeMediaPathIdx = c.getColumnIndex(FormsColumns.APP_RELATIVE_FORM_MEDIA_PATH); String mediaPath = ODKDatabaseUtils.get().getIndexAsString(c, appRelativeMediaPathIdx); if (mediaPath != null) { mediaDirs.put(ODKFileUtils.asAppFile(appName, mediaPath), (tableIdValue == null) ? DirType.FRAMEWORK : DirType.FORMS); } if (ref != null && !ref.equals(cur)) { multiset = true; break; } else { ref = cur; } } } } catch (Exception e) { log.w(t, "FAILED Update of " + uri + " -- query for existing row failed: " + e.toString()); if (e instanceof SQLException) { throw (SQLException) e; } else { throw new SQLException( "FAILED Update of " + uri + " -- query for existing row failed: " + e.toString()); } } finally { if (c != null) { c.close(); } } if (multiset) { // don't let users manually update media path // we are referring to two or more (formId,formVersion) tuples. if (values.containsKey(FormsColumns.APP_RELATIVE_FORM_MEDIA_PATH)) { values.remove(FormsColumns.APP_RELATIVE_FORM_MEDIA_PATH); } } else if (values.containsKey(FormsColumns.APP_RELATIVE_FORM_MEDIA_PATH)) { // we are not a multiset and we are setting the media path // try to move all the existing non-matching media paths to // somewhere else... File mediaPath = ODKFileUtils.asAppFile(appName, values.getAsString(FormsColumns.APP_RELATIVE_FORM_MEDIA_PATH)); for (HashMap.Entry<File, DirType> entry : mediaDirs.entrySet()) { File altPath = entry.getKey(); if (!altPath.equals(mediaPath)) { try { moveDirectory(appName, entry.getValue(), altPath); } catch (IOException e) { e.printStackTrace(); log.e(t, "Attempt to move " + altPath.getAbsolutePath() + " failed: " + e.toString()); } } } // OK. we have moved the existing form definitions elsewhere. We can // proceed with update... } // ensure that all values are correct and ignore some user-supplied // values... patchUpValues(appName, values); // Make sure that the necessary fields are all set if (values.containsKey(FormsColumns.DATE) == true) { Date today = new Date(); String ts = new SimpleDateFormat(getContext().getString(R.string.added_on_date_at_time), Locale.getDefault()).format(today); values.put(FormsColumns.DISPLAY_SUBTEXT, ts); } SQLiteDatabase db = null; int count; try { // OK Finally, now do the update... db = DatabaseFactory.get().getDatabase(getContext(), appName); db.beginTransaction(); count = db.update(DatabaseConstants.FORMS_TABLE_NAME, values, whereId, whereIdArgs); db.setTransactionSuccessful(); } catch (Exception e) { e.printStackTrace(); log.w(t, "Unable to perform update " + uri); return 0; } finally { if (db != null) { db.endTransaction(); db.close(); } } if (count == 1) { Uri formUri = Uri.withAppendedPath( Uri.withAppendedPath(Uri.parse("content://" + getFormsAuthority()), appName), formIdValue); getContext().getContentResolver().notifyChange(formUri, null); Uri idUri = Uri.withAppendedPath( Uri.withAppendedPath(Uri.parse("content://" + getFormsAuthority()), appName), Long.toString(idValue)); getContext().getContentResolver().notifyChange(idUri, null); } else { getContext().getContentResolver().notifyChange(uri, null); } return count; }
From source file:org.opendatakit.services.database.utlities.ODKDatabaseImplUtils.java
/** * Delete any prior server conflict row. * Examine database and incoming server values to determine how to apply the * server values to the database. This might delete the existing row, update * it, or create a conflict row./*from w w w .ja v a 2 s .c o m*/ * * @param db * @param tableId * @param orderedColumns * @param serverValues field values for this row coming from server. * All fields must have values. The SyncState field * (a local field that does not come from the server) * should be "changed" or "deleted" * @param rowId * @param activeUser * @param rolesList passed in to determine if the current user is a privileged user * @param locale */ public void privilegedPerhapsPlaceRowIntoConflictWithId(OdkConnectionInterface db, String tableId, OrderedColumns orderedColumns, ContentValues serverValues, String rowId, String activeUser, String rolesList, String locale) { AccessContext accessContext = getAccessContext(db, tableId, activeUser, rolesList); // The rolesList of the activeUser does not impact the execution of this action. boolean dbWithinTransaction = db.inTransaction(); try { if (!dbWithinTransaction) { db.beginTransactionNonExclusive(); } // delete any existing server conflict row this.deleteServerConflictRowWithId(db, tableId, rowId); // fetch the current local (possibly-in-conflict) row BaseTable baseTable = this.privilegedGetRowsWithId(db, tableId, rowId, activeUser); if (baseTable.getNumberOfRows() == 0) { throw new IllegalArgumentException("no matching row found for server conflict"); } else if (baseTable.getNumberOfRows() != 1) { throw new IllegalArgumentException("row has checkpoints or database is corrupt"); } Row localRow = baseTable.getRowAtIndex(0); if (localRow.getDataByKey(DataTableColumns.SAVEPOINT_TYPE) == null) { throw new IllegalArgumentException("row has checkpoints"); } String strSyncState = localRow.getDataByKey(DataTableColumns.SYNC_STATE); SyncState state = SyncState.valueOf(strSyncState); int localRowConflictTypeBeforeSync = -1; if (state == SyncState.in_conflict) { // we need to remove the in_conflict records that refer to the // prior state of the server String localRowConflictTypeBeforeSyncStr = localRow.getDataByKey(DataTableColumns.CONFLICT_TYPE); if (localRowConflictTypeBeforeSyncStr == null) { // this row is in conflict. It MUST have a non-null conflict type. throw new IllegalStateException("conflict type is null on an in-conflict row"); } localRowConflictTypeBeforeSync = Integer.parseInt(localRowConflictTypeBeforeSyncStr); if (localRowConflictTypeBeforeSync == ConflictType.SERVER_DELETED_OLD_VALUES || localRowConflictTypeBeforeSync == ConflictType.SERVER_UPDATED_UPDATED_VALUES) { // should be impossible throw new IllegalStateException("only the local conflict record should remain"); } } boolean isServerRowDeleted = serverValues.getAsString(DataTableColumns.SYNC_STATE) .equals(SyncState.deleted.name()); boolean executeDropThrough = false; if (isServerRowDeleted) { if (state == SyncState.synced) { // the server's change should be applied locally. this.privilegedDeleteRowWithId(db, tableId, rowId, activeUser); } else if (state == SyncState.synced_pending_files) { // sync logic may want to UPLOAD local files up to server before calling this routine. // this would prevent loss of attachments that have not yet been pushed to server. // the server's change should be applied locally. this.privilegedDeleteRowWithId(db, tableId, rowId, activeUser); } else if ((state == SyncState.deleted) || ((state == SyncState.in_conflict) && (localRowConflictTypeBeforeSync == ConflictType.LOCAL_DELETED_OLD_VALUES))) { // this occurs if // (1) a delete request was never ACKed but it was performed // on the server. // (2) if there is an unresolved conflict held locally with the // local action being to delete the record, and the prior server // state being a value change, but the newly sync'd state now // reflects a deletion by another party. // // the server's change should be applied locally. this.privilegedDeleteRowWithId(db, tableId, rowId, activeUser); } else { // need to resolve a conflict situation... executeDropThrough = true; } } else if (state == SyncState.synced || state == SyncState.synced_pending_files) { // When a prior sync ends with conflicts, we will not update the table's "lastDataETag" // and when we next sync, we will pull the same server row updates as when the // conflicts were raised (elsewhere in the table). // // Therefore, we can expect many server row updates to have already been locally // applied (for the rows were not in conflict). Detect and ignore these already- // processed changes by testing for the server and device having identical field values. // boolean isDifferent = false; for (int i = 0; i < baseTable.getWidth(); ++i) { String colName = baseTable.getElementKey(i); if (DataTableColumns.ID.equals(colName) || DataTableColumns.CONFLICT_TYPE.equals(colName) || DataTableColumns.EFFECTIVE_ACCESS.equals(colName) || DataTableColumns.SYNC_STATE.equals(colName)) { // these values are ignored during comparisons continue; } String localValue = localRow.getDataByKey(colName); String serverValue = serverValues.containsKey(colName) ? serverValues.getAsString(colName) : null; ElementDataType dt = ElementDataType.string; try { ColumnDefinition cd = orderedColumns.find(colName); dt = cd.getType().getDataType(); } catch (IllegalArgumentException e) { // ignore } if (!identicalValue(localValue, serverValue, dt)) { isDifferent = true; break; } } if (isDifferent) { // Local row needs to be updated with server values. // // detect and handle file attachment column changes and sync state // (need to change serverRow value of this) boolean hasNonNullAttachments = false; for (ColumnDefinition cd : orderedColumns.getColumnDefinitions()) { // todo: does not handle array containing (types containing) rowpath elements if (cd.isUnitOfRetention() && cd.getType().getDataType().equals(ElementDataType.rowpath)) { String uriFragment = serverValues.getAsString(cd.getElementKey()); String localUriFragment = localRow.getDataByKey(cd.getElementKey()); if (uriFragment != null) { if (localUriFragment == null || !localUriFragment.equals(uriFragment)) { hasNonNullAttachments = true; break; } } } } // update the row from the changes on the server ContentValues values = new ContentValues(serverValues); values.put(DataTableColumns.SYNC_STATE, (hasNonNullAttachments || (state == SyncState.synced_pending_files)) ? SyncState.synced_pending_files.name() : SyncState.synced.name()); values.putNull(DataTableColumns.CONFLICT_TYPE); this.privilegedUpdateRowWithId(db, tableId, orderedColumns, values, rowId, activeUser, locale, false); } // and don't execute the drop-through in this case. } else { executeDropThrough = true; } if (executeDropThrough) { // SyncState.deleted and server is not deleting // SyncState.new_row and record exists on server // SyncState.changed and new change (or delete) on server // SyncState.in_conflict and new change (or delete) on server // ALSO: this case can occur when our prior sync attempt pulled down changes that // placed local row(s) in conflict -- which we have since resolved -- and we are now // issuing a sync to push those changes up to the server. // // This is because we do not update our local table's "lastDataETag" until we have // sync'd and applied all changes from the server and have no local conflicts or // checkpoints on the table. Because of this, when we issue a sync after resolving // a conflict, we will get the set of server row changes that include the row(s) // that were previously in conflict. This will appear as one of: // changed | changed // changed | deleted // deleted | changed // deleted | deleted // // BUT, this time, however, the local records will have the same rowETag as the // server record, indicating that they are valid changes (or deletions) on top of // the server's current version of this same row. // // If this is the case (the rowETags match), then we should not place the row into // conflict, but should instead ignore the reported content from the server and push // the local row's change or delete up to the server in the next section. // // If not, when we reach this point in the code, the rowETag of the server row // should **not match** our local row -- indicating that the server has a change from // another source, and that we should place the row into conflict. // String localRowETag = localRow.getDataByKey(DataTableColumns.ROW_ETAG); String serverRowETag = serverValues.getAsString(DataTableColumns.ROW_ETAG); boolean isDifferentRowETag = (localRowETag == null) || !localRowETag.equals(serverRowETag); if (!isDifferentRowETag) { // ignore the server record. // This is an update we will push to the server. // todo: make sure local row is not in_conflict at this point. if (state == SyncState.in_conflict) { // don't think this is logically possible at this point, but we should // transition record to changed or deleted state. ContentValues values = new ContentValues(); for (int i = 0; i < baseTable.getWidth(); ++i) { String colName = baseTable.getElementKey(i); if (DataTableColumns.EFFECTIVE_ACCESS.equals(colName)) { continue; } if (localRow.getDataByIndex(i) == null) { values.putNull(colName); } else { values.put(colName, localRow.getDataByIndex(i)); } } values.put(DataTableColumns.ID, rowId); values.putNull(DataTableColumns.CONFLICT_TYPE); values.put(DataTableColumns.SYNC_STATE, (localRowConflictTypeBeforeSync == ConflictType.LOCAL_DELETED_OLD_VALUES) ? SyncState.deleted.name() : SyncState.changed.name()); this.privilegedUpdateRowWithId(db, tableId, orderedColumns, values, rowId, activeUser, locale, false); } } else { // figure out what the localRow conflict type should be... int localRowConflictType; if (state == SyncState.changed) { // SyncState.changed and new change on server localRowConflictType = ConflictType.LOCAL_UPDATED_UPDATED_VALUES; } else if (state == SyncState.new_row) { // SyncState.new_row and record exists on server // The 'new_row' case occurs if an insert is never ACKed but // completes successfully on the server. localRowConflictType = ConflictType.LOCAL_UPDATED_UPDATED_VALUES; } else if (state == SyncState.deleted) { // SyncState.deleted and server is not deleting localRowConflictType = ConflictType.LOCAL_DELETED_OLD_VALUES; } else if (state == SyncState.in_conflict) { // SyncState.in_conflict and new change on server // leave the local conflict type unchanged (retrieve it and // use it). localRowConflictType = localRowConflictTypeBeforeSync; } else { throw new IllegalStateException("Unexpected state encountered"); } boolean isDifferentPrivilegedFields = false; { String[] privilegedColumns = new String[] { DataTableColumns.FILTER_TYPE, DataTableColumns.FILTER_VALUE }; for (int i = 0; i < privilegedColumns.length; ++i) { String colName = privilegedColumns[i]; String localValue = localRow.getDataByKey(colName); String serverValue = serverValues.containsKey(colName) ? serverValues.getAsString(colName) : null; ElementDataType dt = ElementDataType.string; try { ColumnDefinition cd = orderedColumns.find(colName); dt = cd.getType().getDataType(); } catch (IllegalArgumentException e) { // ignore } boolean sameValue = identicalValue(localValue, serverValue, dt); if (!sameValue) { isDifferentPrivilegedFields = true; break; } } } boolean isDifferentExcludingPrivilegedFields = false; for (int i = 0; i < baseTable.getWidth(); ++i) { String colName = baseTable.getElementKey(i); if (DataTableColumns.ID.equals(colName) || DataTableColumns.CONFLICT_TYPE.equals(colName) || DataTableColumns.EFFECTIVE_ACCESS.equals(colName) || DataTableColumns.SYNC_STATE.equals(colName) || DataTableColumns.ROW_ETAG.equals(colName) || DataTableColumns.FILTER_TYPE.equals(colName) || DataTableColumns.FILTER_VALUE.equals(colName)) { // these values are ignored during this comparison continue; } String localValue = localRow.getDataByKey(colName); String serverValue = serverValues.containsKey(colName) ? serverValues.getAsString(colName) : null; ElementDataType dt = ElementDataType.string; try { ColumnDefinition cd = orderedColumns.find(colName); dt = cd.getType().getDataType(); } catch (IllegalArgumentException e) { // ignore } if (serverValue != null && dt == ElementDataType.bool) { serverValue = Integer .toString(DataHelper.boolToInt(serverValues.getAsBoolean(colName))); } boolean sameValue = identicalValue(localValue, serverValue, dt); if (!sameValue) { isDifferentExcludingPrivilegedFields = true; break; } } boolean hasNonNullDifferingServerAttachments = false; if (isDifferentExcludingPrivilegedFields || (state != SyncState.synced)) { for (ColumnDefinition cd : orderedColumns.getColumnDefinitions()) { // todo: does not handle array containing (types containing) rowpath elements if (cd.isUnitOfRetention() && cd.getType().getDataType().equals(ElementDataType.rowpath)) { String uriFragment = serverValues.getAsString(cd.getElementKey()); String localUriFragment = localRow.getDataByKey(cd.getElementKey()); if (uriFragment != null) { if (localUriFragment == null || !localUriFragment.equals(uriFragment)) { hasNonNullDifferingServerAttachments = true; } } } } } ContentValues values = new ContentValues(serverValues); if (isDifferentExcludingPrivilegedFields || isDifferentPrivilegedFields || isDifferentRowETag || isServerRowDeleted) { if (isDifferentExcludingPrivilegedFields || isServerRowDeleted || (localRowConflictType == ConflictType.LOCAL_DELETED_OLD_VALUES) || (accessContext.isPrivilegedUser && isDifferentPrivilegedFields)) { this.placeRowIntoConflict(db, tableId, rowId, localRowConflictType); serverValues.put(DataTableColumns.SYNC_STATE, SyncState.in_conflict.name()); serverValues.put(DataTableColumns.CONFLICT_TYPE, (isServerRowDeleted ? ConflictType.SERVER_DELETED_OLD_VALUES : ConflictType.SERVER_UPDATED_UPDATED_VALUES)); this.privilegedInsertRowWithId(db, tableId, orderedColumns, serverValues, rowId, activeUser, locale, false); } else { // just apply the server RowETag and filterScope to the local row values.put(DataTableColumns.SYNC_STATE, hasNonNullDifferingServerAttachments ? SyncState.synced_pending_files.name() : SyncState.synced.name()); values.putNull(DataTableColumns.CONFLICT_TYPE); // move the local conflict back into the normal non-conflict (null) state // set the sync state to "changed" temporarily (otherwise we can't update) this.restoreRowFromConflict(db, tableId, rowId, SyncState.changed, localRowConflictTypeBeforeSync); this.privilegedUpdateRowWithId(db, tableId, orderedColumns, values, rowId, activeUser, locale, false); } } else { // data matches -- update row to adjust sync state and conflict type // if needed. SyncState destState = (hasNonNullDifferingServerAttachments || (state == SyncState.synced_pending_files)) ? SyncState.synced_pending_files : SyncState.synced; if ((state != destState) || isDifferentRowETag || (localRow.getDataByKey(DataTableColumns.CONFLICT_TYPE) != null)) { // todo: handle case where local row was in conflict // server has now matched the local row's state. i.e., // update rowEtag, clear conflictType and adjust syncState on row. values.put(DataTableColumns.SYNC_STATE, destState.name()); values.putNull(DataTableColumns.CONFLICT_TYPE); // move the local conflict back into the normal non-conflict (null) state // set the sync state to "changed" temporarily (otherwise we can't update) this.restoreRowFromConflict(db, tableId, rowId, SyncState.changed, localRowConflictTypeBeforeSync); this.privilegedUpdateRowWithId(db, tableId, orderedColumns, values, rowId, activeUser, locale, false); WebLogger.getLogger(db.getAppName()).w(t, "identical rows returned from server -- " + "SHOULDN'T THESE NOT HAPPEN?"); } } } } if (!dbWithinTransaction) { db.setTransactionSuccessful(); } } finally { if (!dbWithinTransaction) { db.endTransaction(); } } }
From source file:org.opendatakit.services.database.utilities.ODKDatabaseImplUtils.java
/** * Delete any prior server conflict row. * Examine database and incoming server values to determine how to apply the * server values to the database. This might delete the existing row, update * it, or create a conflict row.// ww w . j a va 2 s .c o m * * @param db * @param tableId * @param orderedColumns * @param serverValues field values for this row coming from server. * All fields must have values. The SyncState field * (a local field that does not come from the server) * should be "changed" or "deleted" * @param rowId * @param activeUser * @param rolesList passed in to determine if the current user is a privileged user * @param locale */ public void privilegedPerhapsPlaceRowIntoConflictWithId(OdkConnectionInterface db, String tableId, OrderedColumns orderedColumns, ContentValues serverValues, String rowId, String activeUser, String rolesList, String locale) { AccessContext accessContext = getAccessContext(db, tableId, activeUser, rolesList); boolean dbWithinTransaction = db.inTransaction(); try { if (!dbWithinTransaction) { db.beginTransactionNonExclusive(); } // The rolesList of the activeUser does not impact the execution of this portion of // the action. // PLAN: // (1) delete any existing server-conflict row. // (2) if the local row was synced or synced pending changes, then either delete // the row from the device (if server deleted row) or accept the // server changes and place the row into synced_pending_changes status. // (3) if the local row was in the new_row state, move it into changed (prior // to creating a conflict pair in step 5) // (4) if the local row was in conflict, restore it to its pre-conflict state // (either deleted or changed). // (5) move the local row into conflict and insert the server row, placing it // into conflict. // (6) enforce permissions on the change. This may immediately resolve conflict // by taking the server changes or may overwrite the local row's permissions // column values with those from the server. // (7) optimize the conflict -- perhaps immediately resolving it based upon // whether the user actually has the privileges to do anything other than // taking the server changes or if the changes only update the tracking // and (perhaps) the metadata fields. // Do it... // (1) delete any existing server conflict row this.deleteServerConflictRowWithId(db, tableId, rowId); // fetch the current local (possibly-in-conflict) row BaseTable baseTable = this.privilegedGetRowsWithId(db, tableId, rowId, activeUser); // throws will abort the transaction, rolling back these changes if (baseTable.getNumberOfRows() == 0) { throw new IllegalArgumentException("no matching row found for server conflict"); } else if (baseTable.getNumberOfRows() != 1) { throw new IllegalArgumentException("row has checkpoints or database is corrupt"); } Row localRow = baseTable.getRowAtIndex(0); if (localRow.getDataByKey(DataTableColumns.SAVEPOINT_TYPE) == null) { throw new IllegalArgumentException("row has checkpoints"); } boolean isServerRowDeleted = serverValues.getAsString(DataTableColumns.SYNC_STATE) .equals(SyncState.deleted.name()); SyncState state; { String strSyncState = localRow.getDataByKey(DataTableColumns.SYNC_STATE); state = SyncState.valueOf(strSyncState); } SyncState initialLocalRowState = state; if (state == SyncState.synced || state == SyncState.synced_pending_files) { // (2) if the local row was synced or synced pending changes, then either delete // the row from the device (if server deleted row) or accept the // server changes and place the row into synced_pending_changes status. // the server's change should be applied locally. if (isServerRowDeleted) { this.privilegedDeleteRowWithId(db, tableId, rowId, activeUser); } else { // Local row needs to be updated with server values. // // detect and handle file attachment column changes if (state == SyncState.synced) { // determine whether there are any changes in the columns that hold file attachments. // if there are, then we need to transition into synced_pending_files. Otherwise, we // can remain in the synced state. for (ColumnDefinition cd : orderedColumns.getColumnDefinitions()) { // todo: does not handle array containing (types containing) rowpath elements if (cd.isUnitOfRetention() && cd.getType().getDataType().equals(ElementDataType.rowpath)) { String uriFragment = serverValues.getAsString(cd.getElementKey()); String localUriFragment = localRow.getDataByKey(cd.getElementKey()); if (uriFragment != null) { if (localUriFragment == null || !localUriFragment.equals(uriFragment)) { state = SyncState.synced_pending_files; WebLogger.getLogger(db.getAppName()).i(t, "privilegedPerhapsPlaceRowIntoConflictWithId: revising from synced to " + "synced_pending_files"); break; } } } } } // update the row from the changes on the server serverValues.put(DataTableColumns.SYNC_STATE, state.name()); serverValues.putNull(DataTableColumns.CONFLICT_TYPE); this.privilegedUpdateRowWithId(db, tableId, orderedColumns, serverValues, rowId, activeUser, locale, false); } // In either case (delete or sync outcome), be sure to commit our changes!!!! if (!dbWithinTransaction) { db.setTransactionSuccessful(); } return; } else if (state == SyncState.new_row) { // (3) if the local row was in the new_row state, move it into changed (prior // to creating a conflict pair in step 5) // update the row with all of the local columns as-is, except the sync state. ContentValues values = new ContentValues(); for (int i = 0; i < baseTable.getWidth(); ++i) { String colName = baseTable.getElementKey(i); if (DataTableColumns.EFFECTIVE_ACCESS.equals(colName)) { continue; } if (localRow.getDataByIndex(i) == null) { values.putNull(colName); } else { values.put(colName, localRow.getDataByIndex(i)); } } // move this into the changed state... state = SyncState.changed; values.put(DataTableColumns.SYNC_STATE, state.name()); this.privilegedUpdateRowWithId(db, tableId, orderedColumns, values, rowId, accessContext.activeUser, locale, false); } else if (state == SyncState.in_conflict) { // (4) if the local row was in conflict, restore it to its pre-conflict state // (either deleted or changed). // we need to remove the in_conflict records that refer to the // prior state of the server String localRowConflictTypeBeforeSyncStr = localRow.getDataByKey(DataTableColumns.CONFLICT_TYPE); if (localRowConflictTypeBeforeSyncStr == null) { // this row is in conflict. It MUST have a non-null conflict type. throw new IllegalStateException("conflict type is null on an in-conflict row"); } int localRowConflictTypeBeforeSync = Integer.parseInt(localRowConflictTypeBeforeSyncStr); if (localRowConflictTypeBeforeSync == ConflictType.SERVER_DELETED_OLD_VALUES || localRowConflictTypeBeforeSync == ConflictType.SERVER_UPDATED_UPDATED_VALUES) { // should be impossible throw new IllegalStateException("only the local conflict record should remain"); } // move the local conflict back into the normal non-conflict (null) state // set the sync state to "changed" temporarily (otherwise we can't update) state = ((localRowConflictTypeBeforeSync == ConflictType.LOCAL_DELETED_OLD_VALUES) ? SyncState.deleted : SyncState.changed); this.restoreRowFromConflict(db, tableId, rowId, state, localRowConflictTypeBeforeSync); } // and drop through if SyncState is changed or deleted // (5) move the local row into conflict and insert the server row, placing it // into conflict. int localRowConflictType = (state == SyncState.deleted) ? ConflictType.LOCAL_DELETED_OLD_VALUES : ConflictType.LOCAL_UPDATED_UPDATED_VALUES; this.placeRowIntoConflict(db, tableId, rowId, localRowConflictType); serverValues.put(DataTableColumns.SYNC_STATE, SyncState.in_conflict.name()); serverValues.put(DataTableColumns.CONFLICT_TYPE, (isServerRowDeleted ? ConflictType.SERVER_DELETED_OLD_VALUES : ConflictType.SERVER_UPDATED_UPDATED_VALUES)); this.privilegedInsertRowWithId(db, tableId, orderedColumns, serverValues, rowId, activeUser, locale, false); // To get here, the original local row was in some state other than synced or // synced_pending_files. Therefore, any non-empty rowpath fields should drive // the row into the synced_pending_files state if we resolve the row early. // (6) enforce permissions on the change. This may immediately resolve conflict // by taking the server changes or may overwrite the local row's permissions // column values with those from the server. if (enforcePermissionsAndOptimizeConflictProcessing(db, tableId, orderedColumns, rowId, initialLocalRowState, accessContext, locale)) { // and... // (7) optimize the conflict -- perhaps immediately resolving it based upon // whether the user actually has the privileges to do anything other than // taking the server changes or if the changes only update the tracking // and (perhaps) the metadata fields. optimizeConflictProcessing(db, tableId, orderedColumns, rowId, initialLocalRowState, accessContext, locale); } if (!dbWithinTransaction) { db.setTransactionSuccessful(); } } finally { if (!dbWithinTransaction) { db.endTransaction(); } } }