package com.odoo.support.service; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import odoo.ODomain; import odoo.Odoo; import org.json.JSONArray; import org.json.JSONObject; import android.accounts.Account; import android.annotation.TargetApi; import android.content.AbstractThreadedSyncAdapter; import android.content.ContentProviderClient; import android.content.ContentProviderOperation; import android.content.ContentResolver; import android.content.Context; import android.content.SyncResult; import android.database.Cursor; import android.os.Build; import android.os.Bundle; import android.text.TextUtils; import android.util.Log; import com.odoo.App; import com.odoo.auth.OdooAccountManager; import com.odoo.base.ir.IrModel; import com.odoo.orm.OColumn; import com.odoo.orm.ODataRow; import com.odoo.orm.OModel; import com.odoo.orm.ORelationRecordList; import com.odoo.orm.ORelationRecordList.ORelationRecords; import com.odoo.orm.OSyncHelper; import com.odoo.orm.OValues; import com.odoo.orm.types.OBoolean; import com.odoo.support.OUser; import com.odoo.util.ODate; import com.odoo.util.PreferenceManager; public class OSyncAdapter extends AbstractThreadedSyncAdapter { public static final String TAG = OSyncAdapter.class.getSimpleName(); private final ContentResolver mContentResolver; private Context mContext = null; private OModel mModel = null; private App mApp = null; private Odoo mOdoo = null; private HashMap<String, ODomain> mDomains = new HashMap<String, ODomain>(); private Boolean checkForWriteCreateDate = true; /** The relation record list. */ private ORelationRecordList mRelationRecordList = new ORelationRecordList(); private OSyncHelper mSync = null; private OSyncService mSyncService = null; /** The finished models. */ private List<String> mFinishedModels = new ArrayList<String>(); /** The finished rel models. */ private List<String> mFinishedRelModels = new ArrayList<String>(); /** The sync data limit. */ private Integer mSyncDataLimit = 0; private PreferenceManager mPref = null; private HashMap<String, OSyncFinishListener> mOSyncFinishListeners = new HashMap<String, OSyncFinishListener>(); public OSyncAdapter(Context context, OModel model, OSyncService service, boolean autoInitialize) { super(context, autoInitialize); mContext = context; mModel = model; mSyncService = service; mContentResolver = context.getContentResolver(); mApp = (App) mContext.getApplicationContext(); init(); } @TargetApi(Build.VERSION_CODES.HONEYCOMB) public OSyncAdapter(Context context, OModel model, OSyncService service, boolean autoInitialize, boolean allowParallelSyncs) { super(context, autoInitialize, allowParallelSyncs); mContext = context; mModel = model; mSyncService = service; mContentResolver = context.getContentResolver(); init(); } private void init() { mPref = new PreferenceManager(mContext); mSync = mModel.getSyncHelper(); mOdoo = mApp.getOdoo(); } public OSyncAdapter setDomain(ODomain domain) { mDomains.put(mModel.getModelName(), domain); return this; } public OSyncAdapter checkForWriteCreateDate(Boolean check) { checkForWriteCreateDate = check; return this; } public OSyncAdapter syncDataLimit(Integer dataLimit) { mSyncDataLimit = dataLimit; return this; } @Override public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) { final OUser user = OdooAccountManager.getAccountDetail(mContext, account.name); Log.v(TAG, "Performing sync for :" + mModel.getModelName()); Log.v(TAG, "User : " + user.getAndroidName()); Log.v(TAG, mModel.uri().toString()); mSyncService.performDataSync(this, extras, user); mModel.setUser(user); mApp.setSyncUser(user); ODomain domain = (mDomains.containsKey(mModel.getModelName())) ? mDomains .get(mModel.getModelName()) : null; performSync(mModel, domain, account, syncResult, true); } private void performSync(OModel model, ODomain domain, Account account, SyncResult syncResult, Boolean dataCheck) { if (!mFinishedModels.contains(model.getModelName()) || !checkForWriteCreateDate) { mFinishedModels.add(model.getModelName()); try { /** * Preparing domain */ if (domain == null) domain = new ODomain(); // Adding default domain to domain domain.append(model.defaultDomain()); // checking for write/create date if (checkForWriteCreateDate && dataCheck) { if (model.checkForCreateDate()) { // Adding Old data limit int data_limit = mPref.getInt("sync_data_limit", 60); List<Integer> ids = model.ids(); if (ids.size() > 0 && model.checkForWriteDate() && !model.isEmptyTable()) domain.add("|"); if (ids.size() > 0) domain.add("&"); domain.add("create_date", ">=", ODate.getDateBefore(data_limit)); if (ids.size() > 0) domain.add("id", "not in", new JSONArray(ids.toString())); } // Adding Last sync date comparing with write_date of record if (model.checkForWriteDate() && !model.isEmptyTable()) { String last_sync_date = mSync.getLastSyncDate(model); domain.add("write_date", ">", last_sync_date); } } /** * Getting data from server */ JSONObject result = mOdoo.search_read(model.getModelName(), mSync.getFields(model), domain.get(), 0, mSyncDataLimit, null, null); if (checkForWriteCreateDate && model.checkForLocalLatestUpdate() && dataCheck) { handleResult(account, syncResult, model, mSync.checkForLocalLatestUpdate(model, result)); } else { handleResult(account, syncResult, model, result); } } catch (Exception e) { e.printStackTrace(); } } } private void handleResult(Account account, SyncResult syncResult, OModel model, JSONObject result) { ArrayList<ContentProviderOperation> batch = new ArrayList<ContentProviderOperation>(); try { JSONArray records = (result.has("result")) ? result .getJSONArray("result") : result.getJSONArray("records"); for (int i = 0; i < records.length(); i++) { JSONObject record = records.getJSONObject(i); batch.clear(); batch.add(createBatch(account, model, record, syncResult)); mContentResolver.applyBatch(model.authority(), batch); } // Updating relation records for master record updateRelationRecords(account, syncResult); // Creating record on server if model allows true if (model.canCreateOnServer()) createRecordOnserver(account, model, syncResult); // Deleting record from server if model allows true if (model.canDeleteFromServer()) deleteRecordFromServer(account, model, syncResult); // Deleting record from local if model allows true if (model.canDeleteFromLocal()) deleteRecordInLocal(model, syncResult); // Updating dirty record on server if model allows true if (model.canUpdateToServer()) updateToServer(account, model, syncResult); syncFinish(account, model, syncResult); mContentResolver.notifyChange(model.uri(), null, false); } catch (Exception e) { e.printStackTrace(); } } private boolean syncFinish(Account account, OModel model, SyncResult syncResult) { String finish_date_time = ODate.getDate(); Log.v(TAG, model.getModelName() + " sync finished at " + finish_date_time + " (UTC)"); IrModel irmodel = new IrModel(mContext); OValues values = new OValues(); values.put("last_synced", finish_date_time); irmodel.update(values, "model = ?", new Object[] { model.getModelName() }); mApp.setSyncUser(null); if (mOSyncFinishListeners.containsKey(model.getModelName())) { OSyncAdapter adapter = mOSyncFinishListeners.get( model.getModelName()).performSync(syncResult); mOSyncFinishListeners.remove(model.getModelName()); if (adapter != null) { SyncResult result = new SyncResult(); OModel sync_model = adapter.getModel(); ContentProviderClient client = mContentResolver .acquireContentProviderClient(sync_model.authority()); adapter.onPerformSync(account, null, sync_model.authority(), client, result); } } return true; } public OModel getModel() { return mModel; } private void updateRelationRecords(Account account, SyncResult syncResult) { List<String> keys = new ArrayList<String>(); keys.addAll(mRelationRecordList.keys()); for (String key : keys) { if (!mFinishedRelModels.contains(key)) { mFinishedRelModels.add(key); ORelationRecords rel = mRelationRecordList.get(key); OModel base_model = rel.getBaseModel(); base_model.setSyncingDataFlag(true); OModel rel_model = rel.getRelModel(); rel_model.setSyncingDataFlag(true); ODomain rel_domain = new ODomain(); if (rel.getRelIds().size() > 0) { rel_domain.add("id", "in", rel.getRelIds()); // Removing from finished sync to sync new data int model_index = mFinishedModels.indexOf(rel_model .getModelName()); if (model_index > -1) mFinishedModels.remove(model_index); performSync(rel_model, rel_domain, account, syncResult, false); } } } } private void deleteRecordInLocal(OModel model, SyncResult syncResult) { try { List<Integer> ids = model.ids(); ODomain domain = new ODomain(); domain.add("id", "in", new JSONArray(ids.toString())); JSONObject result = mOdoo.search_read(model.getModelName(), new JSONObject(), domain.get()); JSONArray records = result.getJSONArray("records"); if (records.length() > 0) { for (int i = 0; i < records.length(); i++) { Integer server_id = records.getJSONObject(i).getInt("id"); ids.remove(ids.indexOf(server_id)); } } Log.i(TAG, "Found " + ids.size() + " entries for delete in local"); ArrayList<ContentProviderOperation> batch = new ArrayList<ContentProviderOperation>(); for (Integer id : ids) { Integer localId = model.selectRowId(id); ContentProviderOperation.Builder builder = ContentProviderOperation .newDelete(model.uri().buildUpon() .appendPath(Integer.toString(localId)).build()); syncResult.stats.numDeletes++; batch.add(builder.build()); } mContentResolver.applyBatch(model.authority(), batch); } catch (Exception e) { e.printStackTrace(); } } private void deleteRecordFromServer(Account account, OModel model, SyncResult syncResult) { Cursor c = mContentResolver.query(model.uri(), model.projection(), "is_active = ? and is_dirty = ? and odoo_name = ?", new String[] { "false", "true", account.name }, null); assert c != null; Log.i(TAG, "Found " + c.getCount() + " local entries for delete on server"); while (c.moveToNext()) { Integer recId = c.getInt(c.getColumnIndex("id")); Integer localId = c.getInt(c.getColumnIndex(OColumn.ROW_ID)); try { if (mOdoo.unlink(model.getModelName(), recId)) { ArrayList<ContentProviderOperation> batch = new ArrayList<ContentProviderOperation>(); ContentProviderOperation.Builder builder = ContentProviderOperation .newDelete(model.uri().buildUpon() .appendPath(Integer.toString(localId)) .build()); batch.add(builder.build()); mContentResolver.applyBatch(model.authority(), batch); } } catch (Exception e) { e.printStackTrace(); } } c.close(); } private void updateToServer(Account account, OModel model, SyncResult syncResult) { Cursor c = mContentResolver.query(model.uri(), model.projection(), "is_dirty = ? and odoo_name = ?", new String[] { "true", account.name }, null); assert c != null; Log.i(TAG, "Found " + c.getCount() + " local dirty entries for upload to server"); while (c.moveToNext()) { Integer recId = c.getInt(c.getColumnIndex("id")); Integer localId = c.getInt(c.getColumnIndex(OColumn.ROW_ID)); JSONObject values = createJSONValues(model, c); try { if (values != null) { mOdoo.updateValues(model.getModelName(), values, recId); ArrayList<ContentProviderOperation> batch = new ArrayList<ContentProviderOperation>(); ContentProviderOperation.Builder builder = ContentProviderOperation .newUpdate(model.uri().buildUpon() .appendPath(Integer.toString(localId)) .build()); builder.withValue("is_dirty", false); batch.add(builder.build()); mContentResolver.applyBatch(model.authority(), batch); mContentResolver.notifyChange(model.uri(), null, false); } } catch (Exception e) { e.printStackTrace(); } } c.close(); } private void createRecordOnserver(Account account, OModel model, SyncResult syncResult) { Cursor c = mContentResolver.query(model.uri(), model.projection(), "id = ? and odoo_name = ?", new String[] { "0", account.name }, null); assert c != null; Log.i(TAG, "Found " + c.getCount() + " local entries for upload to server"); while (c.moveToNext()) { create(model, c); } c.close(); } public void create(OModel model, Cursor c) { try { Integer local_id = c.getInt(c.getColumnIndex(OColumn.ROW_ID)); JSONObject values = createJSONValues(model, c); if (values != null) { Integer newId = 0; values.remove("id"); JSONObject result = mOdoo.createNew(model.getModelName(), values); newId = result.getInt("result"); ArrayList<ContentProviderOperation> batch = new ArrayList<ContentProviderOperation>(); ContentProviderOperation.Builder builder = ContentProviderOperation .newUpdate(model.uri().buildUpon() .appendPath(Integer.toString(local_id)).build()); builder.withValue("id", newId); builder.withValue("is_dirty", "false"); batch.add(builder.build()); mContentResolver.applyBatch(model.authority(), batch); mContentResolver.notifyChange(model.uri(), null, false); } } catch (Exception e) { e.printStackTrace(); } } private JSONObject createJSONValues(OModel model, Cursor c) { JSONObject values = null; try { values = new JSONObject(); for (OColumn col : model.getColumns(false)) { if (col.getRelationType() == null) { Object val = model.createRecordRow(col, c); if (val.toString().equals("false") || val == null || TextUtils.isEmpty(val.toString())) val = false; if (val.toString().equals("true")) val = true; values.put(col.getName(), val); } else { // Relation columns switch (col.getRelationType()) { case ManyToOne: OModel rel_model = model.createInstance(col.getType()); Object val = model.createRecordRow(col, c); if (val instanceof Integer) { val = rel_model.selectServerId((Integer) val); } values.put(col.getName(), val); break; case OneToMany: rel_model = model.createInstance(col.getType()); JSONArray o2mRecords = new JSONArray(); List<ODataRow> o2mRecordList = rel_model.select( col.getRelatedColumn() + " = ?", new Object[] { c.getInt(c .getColumnIndex(OColumn.ROW_ID)) }); if (o2mRecordList.size() > 0) { JSONArray rec_ids = new JSONArray(); for (ODataRow o2mR : o2mRecordList) { if (o2mR.getInt("id") != 0) rec_ids.put(o2mR.getInt("id")); } o2mRecords.put(6); o2mRecords.put(false); o2mRecords.put(rec_ids); values.put(col.getName(), new JSONArray().put(o2mRecords)); } break; case ManyToMany: rel_model = model.createInstance(col.getType()); JSONArray m2mRecords = new JSONArray(); List<ODataRow> m2mRecordList = model.selectM2MRecords( model, rel_model, c.getInt(c.getColumnIndex(OColumn.ROW_ID))); if (m2mRecordList.size() > 0) { JSONArray rec_ids = new JSONArray(); for (ODataRow o2mR : m2mRecordList) { if (o2mR.getInt("id") != 0) rec_ids.put(o2mR.getInt("id")); } m2mRecords.put(6); m2mRecords.put(false); m2mRecords.put(rec_ids); values.put(col.getName(), new JSONArray().put(m2mRecords)); } break; } } } } catch (Exception e) { e.printStackTrace(); } return values; } private ContentProviderOperation createBatch(Account account, OModel model, JSONObject original_record, SyncResult syncResult) { ContentProviderOperation.Builder batch = null; try { int id = original_record.getInt("id"); boolean update = model.hasRecord(id); batch = (update) ? ContentProviderOperation.newUpdate(model.uri() .buildUpon() .appendPath(Integer.toString(model.selectRowId(id))) .build()) : ContentProviderOperation.newInsert(model.uri()); if (update) { syncResult.stats.numUpdates++; } else { syncResult.stats.numInserts++; } List<Integer> r_ids = new ArrayList<Integer>(); for (OColumn column : model.getColumns(false)) { JSONObject record = model.beforeCreateRow(column, original_record); if (column.getRelationType() != null) { // Relation records switch (column.getRelationType()) { case ManyToOne: /* * Handling ManyToOne records */ OModel m2o = model.createInstance(column.getType()); String rel_key = m2o.getTableName() + "_" + column.getName(); if (record.get(column.getName()) instanceof JSONArray) { JSONArray m2oRecord = record.getJSONArray(column .getName()); // Local table contains only id and name so not // required // to request on server if (column.canSyncMasterRecord() && (m2o.getColumns(false).size() > 2 || (m2o .getColumns(false).size() > 4 && model .getOdooVersion() .getVersion_number() > 7))) { // Need to create list of ids for model ORelationRecords rel_record = mRelationRecordList.new ORelationRecords(); if (mRelationRecordList.contains(rel_key)) { rel_record = mRelationRecordList .get(rel_key); } else { rel_record.setRelModel(m2o); rel_record.setBaseModel(model); } rel_record.setRefColumn(column.getName()); rel_record.setRelationType(column .getRelationType()); rel_record.addBaseRelId(record.getInt("id"), m2oRecord.getInt(0)); // Creating relation ids list for relation model mRelationRecordList.add(rel_key, rel_record); } OValues m2oVals = new OValues(); m2oVals.put("id", m2oRecord.get(0)); m2oVals.put("name", m2oRecord.get(1)); m2oVals.put("is_dirty", false); Integer row_id = m2o.createORReplace(m2oVals); // Replacing original id with row_id to maintain // relation for local m2oRecord.put(0, row_id); record.put(column.getName(), m2oRecord); batch.withValue(column.getName(), m2oRecord.get(0)); } break; case ManyToMany: r_ids.clear(); OModel m2m = model.createInstance(column.getType()); rel_key = m2m.getTableName() + "_" + column.getName(); JSONArray ids_list = record.getJSONArray(column .getName()); int len = ids_list.length(); // limiting sync limit for many to many int record_len = column.getRecordSyncLimit(); if (record_len != -1 && len > record_len) len = record_len; List<Integer> row_ids = new ArrayList<Integer>(); for (int i = 0; i < len; i++) { int server_id = ids_list.getInt(i); r_ids.add(server_id); OValues vals = new OValues(); vals.put("id", server_id); int row_id = m2m.createORReplace(vals); row_ids.add(row_id); } batch.withValue(column.getName(), row_ids.toString()); ORelationRecords mrel_record = mRelationRecordList.new ORelationRecords(); if (mRelationRecordList.contains(rel_key)) { mrel_record = mRelationRecordList.get(rel_key); } else { mrel_record.setRelModel(m2m); mrel_record.setBaseModel(model); } mrel_record.addBaseRelId(record.getInt("id"), r_ids); mrel_record.setRefColumn(column.getName()); mrel_record.setRelationType(column.getRelationType()); // Creating relation ids list for relation model mRelationRecordList.add(rel_key, mrel_record); break; case OneToMany: OModel o2m = model.createInstance(column.getType()); rel_key = o2m.getTableName() + "_" + column.getName(); JSONArray o2m_ids_list = record.getJSONArray(column .getName()); r_ids.clear(); for (int i = 0; i < o2m_ids_list.length(); i++) { r_ids.add(o2m_ids_list.getInt(i)); } // Need to create list of ids for model ORelationRecords rel_record = mRelationRecordList.new ORelationRecords(); if (mRelationRecordList.contains(rel_key)) { rel_record = mRelationRecordList.get(rel_key); } else { rel_record.setRelModel(o2m); rel_record.setBaseModel(model); } rel_record.addBaseRelId(record.getInt("id"), r_ids); rel_record.setRefColumn(column.getName()); rel_record.setRelationType(column.getRelationType()); // Creating relation ids list for relation model mRelationRecordList.add(rel_key, rel_record); break; } } else { // Simple columns Object value = record.get(column.getName()); if (column.getType().isAssignableFrom(OBoolean.class)) value = value.toString(); if (value.toString().equals("false")) value = value.toString(); batch.withValue(column.getName(), value); } } for (OColumn col : model.getFunctionalColumns()) { if (col.canFunctionalStore()) { OValues values = new OValues(); if (!col.isLocal()) values.put(col.getName(), original_record.get(col.getName())); for (String dCol : col.getFunctionalStoreDepends()) { Object data = original_record.get(dCol); values.put(dCol, data); } Object value = model.getFunctionalMethodValue(col, values); batch.withValue(col.getName(), value); } } } catch (Exception e) { e.printStackTrace(); } batch.withValue("is_dirty", "false"); batch.withValue("local_write_date", ODate.getDate()); batch.withValue("odoo_name", account.name); return batch.build(); } public OSyncAdapter onSyncFinish(OSyncFinishListener syncFinish) { mOSyncFinishListeners.put(mModel.getModelName(), syncFinish); return this; } }