/** * Odoo, Open Source Management Solution * Copyright (C) 2012-today Odoo SA (<http:www.odoo.com>) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http:www.gnu.org/licenses/> * * Created on 1/1/15 3:17 PM */ package com.odoo.core.service; import android.accounts.Account; import android.content.AbstractThreadedSyncAdapter; import android.content.ContentProviderClient; import android.content.Context; import android.content.SyncResult; import android.os.Bundle; import android.util.Log; import com.odoo.App; import com.odoo.R; import com.odoo.base.addons.ir.IrModel; import com.odoo.base.addons.res.ResCompany; import com.odoo.core.account.OdooAccountQuickManage; import com.odoo.core.auth.OdooAccountManager; import com.odoo.core.orm.ODataRow; import com.odoo.core.orm.OModel; import com.odoo.core.orm.OValues; import com.odoo.core.orm.fields.OColumn; import com.odoo.core.support.OUser; import com.odoo.core.utils.JSONUtils; import com.odoo.core.utils.ODateUtils; import com.odoo.core.utils.OPreferenceManager; import com.odoo.core.utils.OResource; import com.odoo.core.utils.notification.ONotificationBuilder; import org.json.JSONArray; import org.json.JSONObject; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import odoo.ODomain; import odoo.Odoo; import odoo.OdooInstance; import odoo.OdooSessionExpiredException; public class OSyncAdapter extends AbstractThreadedSyncAdapter { public static final String TAG = OSyncAdapter.class.getSimpleName(); public static final Integer REQUEST_SIGN_IN_ERROR = 11244; public static final String KEY_AUTH_ERROR = "key_authentication_error"; private Context mContext; private Class<? extends OModel> mModelClass; private OModel mModel; private OSyncService mService; private OUser mUser; private Boolean checkForWriteCreateDate = true; private Integer mSyncDataLimit = 0; private HashMap<String, ODomain> mDomain = new HashMap<>(); private OPreferenceManager preferenceManager; private Odoo mOdoo; private HashMap<String, ISyncFinishListener> mSyncFinishListeners = new HashMap<>(); private App app = null; public OSyncAdapter(Context context, Class<? extends OModel> model, OSyncService service, boolean autoInitialize) { super(context, autoInitialize); init(context, model, service); } public OSyncAdapter(Context context, Class<? extends OModel> model, OSyncService service, boolean autoInitialize, boolean allowParallelSyncs) { super(context, autoInitialize, allowParallelSyncs); init(context, model, service); } private void init(Context context, Class<? extends OModel> model, OSyncService service) { mContext = context; mModelClass = model; mService = service; preferenceManager = new OPreferenceManager(mContext); app = (App) context.getApplicationContext(); } public OSyncAdapter setDomain(ODomain domain) { mDomain.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) { // Creating model Object mModel = new OModel(mContext, null, OdooAccountManager.getDetails(mContext, account.name)) .createInstance(mModelClass); mUser = mModel.getUser(); // Creating Odoo instance mOdoo = createOdooInstance(mContext, mUser); Log.i(TAG, "User : " + mModel.getUser().getAndroidName()); Log.i(TAG, "Model : " + mModel.getModelName()); Log.i(TAG, "Database : " + mModel.getDatabaseName()); Log.i(TAG, "Odoo Version: " + mUser.getVersion_number()); // Calling service callback if (mService != null) mService.performDataSync(this, extras, mUser); //Creating domain ODomain domain = (mDomain.containsKey(mModel.getModelName())) ? mDomain.get(mModel.getModelName()) : null; // Ready for sync data from server syncData(mModel, mUser, domain, syncResult, true, true); } private void syncData(OModel model, OUser user, ODomain domain_filter, SyncResult result, Boolean checkForDataLimit, Boolean createRelationRecords) { Log.v(TAG, "Sync for (" + model.getModelName() + ") Started at " + ODateUtils.getDate()); model.onSyncStarted(); try { ODomain domain = new ODomain(); domain.append(model.defaultDomain()); if (domain_filter != null) { domain.append(domain_filter); } if (checkForWriteCreateDate) { List<Integer> serverIds = model.getServerIds(); // Model Create date domain filters if (model.checkForCreateDate() && checkForDataLimit) { if (serverIds.size() > 0) { if (model.checkForWriteDate() && !model.isEmptyTable()) { domain.add("|"); } if (model.checkForWriteDate() && !model.isEmptyTable() && createRelationRecords && model.getLastSyncDateTime() != null) domain.add("&"); } int data_limit = preferenceManager.getInt("sync_data_limit", 60); domain.add("create_date", ">=", ODateUtils.getDateBefore(data_limit)); if (serverIds.size() > 0) { domain.add("id", "not in", new JSONArray(serverIds.toString())); } } // Model write date domain filters if (model.checkForWriteDate() && !model.isEmptyTable() && createRelationRecords) { String last_sync_date = model.getLastSyncDateTime(); if (last_sync_date != null) { domain.add("write_date", ">", last_sync_date); } } } // Getting data JSONObject response = mOdoo.search_read(model.getModelName(), getFields(model), domain.get(), 0, mSyncDataLimit, "create_date", "DESC"); OSyncDataUtils dataUtils = new OSyncDataUtils(mContext, mOdoo, model, user, response, result, createRelationRecords); // Updating records on server if local are latest updated. // if model allowed update record to server if (model.allowUpdateRecordOnServer()) { dataUtils.updateRecordsOnServer(this); } // Creating or updating relation records handleRelationRecords(user, dataUtils.getRelationRecordsHashMap(), result); // If model allowed to create record on server if (model.allowCreateRecordOnServer()) { createRecordsOnServer(model); } // If model allowed to delete record on server if (model.allowDeleteRecordOnServer()) { removeRecordOnServer(model); } // If model allowed to delete server removed record from local database if (model.allowDeleteRecordInLocal()) { removeNonExistRecordFromLocal(model); } Log.v(TAG, "Sync for (" + model.getModelName() + ") finished at " + ODateUtils.getDate()); if (createRelationRecords) { IrModel irModel = new IrModel(mContext, user); irModel.setLastSyncDateTimeToNow(model); } model.onSyncFinished(); } catch (OdooSessionExpiredException odooSession) { app.setOdoo(null, user); if (user.isOAauthLogin()) { // FIXME: Saas not working with multi login. Facing issue of session expired. } else { showSignInErrorNotification(user); } } catch (Exception e) { e.printStackTrace(); } // Performing next sync if any in service if (mSyncFinishListeners.containsKey(model.getModelName())) { OSyncAdapter adapter = mSyncFinishListeners.get(model.getModelName()) .performNextSync(user, result); mSyncFinishListeners.remove(model.getModelName()); if (adapter != null) { SyncResult syncResult = new SyncResult(); OModel syncModel = model.createInstance(adapter.getModelClass()); ContentProviderClient contentProviderClient = mContext.getContentResolver().acquireContentProviderClient(syncModel.authority()); adapter.onPerformSync(user.getAccount(), null, syncModel.authority(), contentProviderClient, syncResult); } } model.close(); } private void showSignInErrorNotification(OUser user) { ONotificationBuilder builder = new ONotificationBuilder(mContext, REQUEST_SIGN_IN_ERROR); builder.setTitle("Odoo authentication problem"); builder.setBigText("May be you have changed your account " + "password or your account does not exists"); builder.setIcon(R.drawable.ic_action_alert_warning); builder.setText(user.getAndroidName()); builder.allowVibrate(true); builder.withRingTone(false); builder.setOngoing(true); builder.withLargeIcon(false); builder.setColor(OResource.color(mContext, R.color.android_orange_dark)); Bundle extra = user.getAsBundle(); // Actions ONotificationBuilder.NotificationAction actionReset = new ONotificationBuilder.NotificationAction( R.drawable.ic_action_refresh, "Reset", 110, "reset_password", OdooAccountQuickManage.class, extra ); ONotificationBuilder.NotificationAction deleteAccount = new ONotificationBuilder.NotificationAction( R.drawable.ic_action_navigation_close, "Remove", 111, "remove_account", OdooAccountQuickManage.class, extra ); builder.addAction(actionReset); builder.addAction(deleteAccount); builder.build().show(); } private void handleRelationRecords(OUser user, HashMap<String, OSyncDataUtils.SyncRelationRecords> relationRecords, SyncResult result) { for (String key : relationRecords.keySet()) { OSyncDataUtils.SyncRelationRecords record = relationRecords.get(key); OModel model = record.getBaseModel(); OModel rel_model = model.createInstance(record.getRelationModel()); model.close(); ODomain domain = new ODomain(); domain.add("id", "in", record.getUniqueIds()); syncData(rel_model, user, domain, result, false, false); // Updating manyToOne record with their relation record row_id switch (record.getRelationType()) { case ManyToOne: // Nothing to do. Already added link with record relation break; case OneToMany: // Update related_column with base id's row_id for each of record ids String related_column = record.getRelatedColumn(); for (Integer id : record.getUniqueIds()) { OValues values = new OValues(); ODataRow rec = rel_model.browse(rel_model.selectRowId(id)); values.put(related_column, rec.getInt(related_column)); values.put("_is_dirty", "false"); rel_model.update(rel_model.selectRowId(id), values); } break; case ManyToMany: // Nothing to do. Already added relation records links break; } rel_model.close(); } } public static Odoo createOdooInstance(Context context, OUser user) { try { App app = (App) context.getApplicationContext(); Odoo odoo = app.getOdoo(user); if (odoo == null) { if (user.isOAauthLogin()) { odoo = new Odoo(context, user.getInstanceUrl(), user.isAllowSelfSignedSSL()); OdooInstance instance = new OdooInstance(); instance.setInstanceUrl(user.getInstanceUrl()); instance.setDatabaseName(user.getInstanceDatabase()); instance.setClientId(user.getClientId()); odoo.oauth_authenticate(instance, user.getUsername(), user.getPassword()); } else { odoo = new Odoo(context, user.getHost(), user.isAllowSelfSignedSSL()); odoo.authenticate(user.getUsername(), user.getPassword(), user.getDatabase()); } app.setOdoo(odoo, user); ResCompany company = new ResCompany(context, user); if (company.count("id = ? ", new String[]{user.getCompany_id()}) <= 0) { ODataRow company_details = new ODataRow(); company_details.put("id", user.getCompany_id()); company.quickCreateRecord(company_details); } } return odoo; } catch (Exception e) { e.printStackTrace(); } return null; } private JSONObject getFields(OModel model) { JSONObject fields = new JSONObject(); try { for (OColumn column : model.getColumns(false)) { fields.accumulate("fields", column.getName()); } } catch (Exception e) { e.printStackTrace(); } return fields; } /** * Creates locally created record on server (id with zero) * * @param model model object */ private void createRecordsOnServer(OModel model) { List<ODataRow> records = model.select(null, "(id = ? or id = ?)", new String[]{"0", "false"}); int counter = 0; for (ODataRow record : records) { if (validateRelationRecords(model, record)) { /* Need to check server id for record. It is possible that record created on server by validating main record. */ if (model.selectServerId(record.getInt(OColumn.ROW_ID)) == 0) { int id = createOnServer(model, JSONUtils.createJSONValues(model, record)); if (id != OModel.INVALID_ROW_ID) { OValues values = new OValues(); values.put("id", id); values.put("_is_dirty", "false"); values.put("_write_date", ODateUtils.getUTCDate()); model.update(record.getInt(OColumn.ROW_ID), values); counter++; } else { Log.e(TAG, "Unable to create record on server."); } } } } if (counter == records.size()) { Log.i(TAG, counter + " records created on server."); } } /** * Validate relation record for the record. And if relation record not created on server. * It will be created on server before syncing original record * * @param model * @param row * @return updatedRow */ public boolean validateRelationRecords(OModel model, ODataRow row) { Log.d(TAG, "Validating relation records for record"); // Check for relation local record for (OColumn column : model.getRelationColumns()) { OModel relModel = model.createInstance(column.getType()); switch (column.getRelationType()) { case ManyToOne: if (!row.getString(column.getName()).equals("false")) { ODataRow m2oRec = row.getM2ORecord(column.getName()).browse(); if (m2oRec.getInt("id") == 0) { int new_id = relModel.getServerDataHelper().createOnServer( JSONUtils.createJSONValues(relModel, m2oRec)); updateRecordServerId(relModel, m2oRec.getInt(OColumn.ROW_ID), new_id); } } break; case ManyToMany: List<ODataRow> m2mRecs = row.getM2MRecord(column.getName()).browseEach(); if (!m2mRecs.isEmpty()) { for (ODataRow m2mRec : m2mRecs) { if (m2mRec.getInt("id") == 0) { int new_id = relModel.getServerDataHelper().createOnServer( JSONUtils.createJSONValues(relModel, m2mRec)); updateRecordServerId(relModel, m2mRec.getInt(OColumn.ROW_ID), new_id); } } } break; case OneToMany: List<ODataRow> o2mRecs = row.getM2MRecord(column.getName()).browseEach(); if (!o2mRecs.isEmpty()) { for (ODataRow o2mRec : o2mRecs) { if (o2mRec.getInt("id") == 0) { int new_id = relModel.getServerDataHelper().createOnServer( JSONUtils.createJSONValues(relModel, o2mRec)); updateRecordServerId(relModel, o2mRec.getInt(OColumn.ROW_ID), new_id); } } } break; } } return true; } /** * Updating local record with server id * * @param model * @param row_id * @param server_id */ private void updateRecordServerId(OModel model, int row_id, int server_id) { OValues values = new OValues(); values.put("id", server_id); values.put("_is_dirty", "false"); model.update(row_id, values); } private int createOnServer(OModel model, JSONObject values) { int id = OModel.INVALID_ROW_ID; try { if (values != null) { JSONObject response = mOdoo.createNew(model.getModelName(), values); id = response.getInt("result"); } } catch (Exception e) { e.printStackTrace(); } return id; } /** * Removes record on server if local record is not active * * @param model */ private void removeRecordOnServer(OModel model) { List<ODataRow> records = model.select(new String[]{}, "id != ? and _is_active = ?", new String[]{"0", "false"}); List<Integer> serverIds = new ArrayList<>(); for (ODataRow record : records) { serverIds.add(record.getInt("id")); } if (serverIds.size() > 0) { if (removeRecordsFromServer(model, serverIds)) { int counter = model.deleteRecords(serverIds, true); Log.i(TAG, counter + " records removed from server and local database"); } else { Log.e(TAG, "Unable to remove records from server"); } } } private boolean removeRecordsFromServer(OModel model, List<Integer> serverIds) { try { mOdoo.unlink(model.getModelName(), serverIds); return true; } catch (Exception e) { e.printStackTrace(); } return false; } /** * Removes non exist record from local database * * @param model */ private void removeNonExistRecordFromLocal(OModel model) { List<Integer> ids = model.getServerIds(); try { 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++) { JSONObject record = records.getJSONObject(i); ids.remove(ids.indexOf(record.getInt("id"))); } } int removedCounter = 0; if (ids.size() > 0) { removedCounter = model.deleteRecords(ids, true); } Log.i(TAG, removedCounter + " Records removed from local database."); } catch (Exception e) { e.printStackTrace(); } } public Class<? extends OModel> getModelClass() { return mModelClass; } public void setModel(OModel model) { mModel = model; } public OModel getModel() { return mModel; } public OSyncAdapter onSyncFinish(ISyncFinishListener syncFinish) { mSyncFinishListeners.put(mModel.getModelName(), syncFinish); return this; } }