GTaskManager.java

    /*
     * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
     *
     * Licensed under the Apache License, Version 2.0 (the "License");
     * you may not use this file except in compliance with the License.
     * You may obtain a copy of the License at
     *
     *        http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */

    package net.micode.notes.gtask.remote;

    import android.app.Activity;
    import android.content.ContentResolver;
    import android.content.ContentUris;
    import android.content.ContentValues;
    import android.content.Context;
    import android.database.Cursor;
    import android.util.Log;

    import net.micode.notes.R;
    import net.micode.notes.data.Notes;
    import net.micode.notes.data.Notes.DataColumns;
    import net.micode.notes.data.Notes.NoteColumns;
    import net.micode.notes.gtask.data.MetaData;
    import net.micode.notes.gtask.data.Node;
    import net.micode.notes.gtask.data.SqlNote;
    import net.micode.notes.gtask.data.Task;
    import net.micode.notes.gtask.data.TaskList;
    import net.micode.notes.gtask.exception.ActionFailureException;
    import net.micode.notes.gtask.exception.NetworkFailureException;
    import net.micode.notes.tool.DataUtils;
    import net.micode.notes.tool.GTaskStringUtils;

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

    import java.util.HashMap;
    import java.util.HashSet;
    import java.util.Iterator;
    import java.util.Map;

    public class GTaskManager {
        private static final String TAG = GTaskManager.class.getSimpleName();

        public static final int STATE_SUCCESS = 0;

        public static final int STATE_NETWORK_ERROR = 1;

        public static final int STATE_INTERNAL_ERROR = 2;

        public static final int STATE_SYNC_IN_PROGRESS = 3;

        public static final int STATE_SYNC_CANCELLED = 4;

        private static GTaskManager mInstance = null;

        private Activity mActivity;

        private Context mContext;

        private ContentResolver mContentResolver;

        private boolean mSyncing;

        private boolean mCancelled;

        private HashMap<String, TaskList> mGTaskListHashMap;

        private HashMap<String, Node> mGTaskHashMap;

        private HashMap<String, MetaData> mMetaHashMap;

        private TaskList mMetaList;

        private HashSet<Long> mLocalDeleteIdMap;

        private HashMap<String, Long> mGidToNid;

        private HashMap<Long, String> mNidToGid;

        private GTaskManager() {
            mSyncing = false;
            mCancelled = false;
            mGTaskListHashMap = new HashMap<String, TaskList>();
            mGTaskHashMap = new HashMap<String, Node>();
            mMetaHashMap = new HashMap<String, MetaData>();
            mMetaList = null;
            mLocalDeleteIdMap = new HashSet<Long>();
            mGidToNid = new HashMap<String, Long>();
            mNidToGid = new HashMap<Long, String>();
        }

        public static synchronized GTaskManager getInstance() {
            if (mInstance == null) {
                mInstance = new GTaskManager();
            }
            return mInstance;
        }

        public synchronized void setActivityContext(Activity activity) {
            // used for getting authtoken
            mActivity = activity;
        }

        public int sync(Context context, GTaskASyncTask asyncTask) {
            if (mSyncing) {
                Log.d(TAG, "Sync is in progress");
                return STATE_SYNC_IN_PROGRESS;
            }
            mContext = context;
            mContentResolver = mContext.getContentResolver();
            mSyncing = true;
            mCancelled = false;
            mGTaskListHashMap.clear();
            mGTaskHashMap.clear();
            mMetaHashMap.clear();
            mLocalDeleteIdMap.clear();
            mGidToNid.clear();
            mNidToGid.clear();

            try {
                GTaskClient client = GTaskClient.getInstance();
                client.resetUpdateArray();

                // login google task
                if (!mCancelled) {
                    if (!client.login(mActivity)) {
                        throw new NetworkFailureException("login google task failed");
                    }
                }

                // get the task list from google
                asyncTask.publishProgess(mContext.getString(R.string.sync_progress_init_list));
                initGTaskList();

                // do content sync work
                asyncTask.publishProgess(mContext.getString(R.string.sync_progress_syncing));
                syncContent();
            } catch (NetworkFailureException e) {
                Log.e(TAG, e.toString());
                return STATE_NETWORK_ERROR;
            } catch (ActionFailureException e) {
                Log.e(TAG, e.toString());
                return STATE_INTERNAL_ERROR;
            } catch (Exception e) {
                Log.e(TAG, e.toString());
                e.printStackTrace();
                return STATE_INTERNAL_ERROR;
            } finally {
                mGTaskListHashMap.clear();
                mGTaskHashMap.clear();
                mMetaHashMap.clear();
                mLocalDeleteIdMap.clear();
                mGidToNid.clear();
                mNidToGid.clear();
                mSyncing = false;
            }

            return mCancelled ? STATE_SYNC_CANCELLED : STATE_SUCCESS;
        }

        private void initGTaskList() throws NetworkFailureException {
            if (mCancelled)
                return;
            GTaskClient client = GTaskClient.getInstance();
            try {
                JSONArray jsTaskLists = client.getTaskLists();

                // init meta list first
                mMetaList = null;
                for (int i = 0; i < jsTaskLists.length(); i++) {
                    JSONObject object = jsTaskLists.getJSONObject(i);
                    String gid = object.getString(GTaskStringUtils.GTASK_JSON_ID);
                    String name = object.getString(GTaskStringUtils.GTASK_JSON_NAME);

                    if (name
                            .equals(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_META)) {
                        mMetaList = new TaskList();
                        mMetaList.setContentByRemoteJSON(object);

                        // load meta data
                        JSONArray jsMetas = client.getTaskList(gid);
                        for (int j = 0; j < jsMetas.length(); j++) {
                            object = (JSONObject) jsMetas.getJSONObject(j);
                            MetaData metaData = new MetaData();
                            metaData.setContentByRemoteJSON(object);
                            if (metaData.isWorthSaving()) {
                                mMetaList.addChildTask(metaData);
                                if (metaData.getGid() != null) {
                                    mMetaHashMap.put(metaData.getRelatedGid(), metaData);
                                }
                            }
                        }
                    }
                }

                // create meta list if not existed
                if (mMetaList == null) {
                    mMetaList = new TaskList();
                    mMetaList.setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX
                            + GTaskStringUtils.FOLDER_META);
                    GTaskClient.getInstance().createTaskList(mMetaList);
                }

                // init task list
                for (int i = 0; i < jsTaskLists.length(); i++) {
                    JSONObject object = jsTaskLists.getJSONObject(i);
                    String gid = object.getString(GTaskStringUtils.GTASK_JSON_ID);
                    String name = object.getString(GTaskStringUtils.GTASK_JSON_NAME);

                    if (name.startsWith(GTaskStringUtils.MIUI_FOLDER_PREFFIX)
                            && !name.equals(GTaskStringUtils.MIUI_FOLDER_PREFFIX
                                    + GTaskStringUtils.FOLDER_META)) {
                        TaskList tasklist = new TaskList();
                        tasklist.setContentByRemoteJSON(object);
                        mGTaskListHashMap.put(gid, tasklist);
                        mGTaskHashMap.put(gid, tasklist);

                        // load tasks
                        JSONArray jsTasks = client.getTaskList(gid);
                        for (int j = 0; j < jsTasks.length(); j++) {
                            object = (JSONObject) jsTasks.getJSONObject(j);
                            gid = object.getString(GTaskStringUtils.GTASK_JSON_ID);
                            Task task = new Task();
                            task.setContentByRemoteJSON(object);
                            if (task.isWorthSaving()) {
                                task.setMetaInfo(mMetaHashMap.get(gid));
                                tasklist.addChildTask(task);
                                mGTaskHashMap.put(gid, task);
                            }
                        }
                    }
                }
            } catch (JSONException e) {
                Log.e(TAG, e.toString());
                e.printStackTrace();
                throw new ActionFailureException("initGTaskList: handing JSONObject failed");
            }
        }

        private void syncContent() throws NetworkFailureException {
            int syncType;
            Cursor c = null;
            String gid;
            Node node;

            mLocalDeleteIdMap.clear();

            if (mCancelled) {
                return;
            }

            // for local deleted note
            try {
                c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE,
                        "(type<>? AND parent_id=?)", new String[] {
                                String.valueOf(Notes.TYPE_SYSTEM), String.valueOf(Notes.ID_TRASH_FOLER)
                        }, null);
                if (c != null) {
                    while (c.moveToNext()) {
                        gid = c.getString(SqlNote.GTASK_ID_COLUMN);
                        node = mGTaskHashMap.get(gid);
                        if (node != null) {
                            mGTaskHashMap.remove(gid);
                            doContentSync(Node.SYNC_ACTION_DEL_REMOTE, node, c);
                        }

                        mLocalDeleteIdMap.add(c.getLong(SqlNote.ID_COLUMN));
                    }
                } else {
                    Log.w(TAG, "failed to query trash folder");
                }
            } finally {
                if (c != null) {
                    c.close();
                    c = null;
                }
            }

            // sync folder first
            syncFolder();

            // for note existing in database
            try {
                c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE,
                        "(type=? AND parent_id<>?)", new String[] {
                                String.valueOf(Notes.TYPE_NOTE), String.valueOf(Notes.ID_TRASH_FOLER)
                        }, NoteColumns.TYPE + " DESC");
                if (c != null) {
                    while (c.moveToNext()) {
                        gid = c.getString(SqlNote.GTASK_ID_COLUMN);
                        node = mGTaskHashMap.get(gid);
                        if (node != null) {
                            mGTaskHashMap.remove(gid);
                            mGidToNid.put(gid, c.getLong(SqlNote.ID_COLUMN));
                            mNidToGid.put(c.getLong(SqlNote.ID_COLUMN), gid);
                            syncType = node.getSyncAction(c);
                        } else {
                            if (c.getString(SqlNote.GTASK_ID_COLUMN).trim().length() == 0) {
                                // local add
                                syncType = Node.SYNC_ACTION_ADD_REMOTE;
                            } else {
                                // remote delete
                                syncType = Node.SYNC_ACTION_DEL_LOCAL;
                            }
                        }
                        doContentSync(syncType, node, c);
                    }
                } else {
                    Log.w(TAG, "failed to query existing note in database");
                }

            } finally {
                if (c != null) {
                    c.close();
                    c = null;
                }
            }

            // go through remaining items
            Iterator<Map.Entry<String, Node>> iter = mGTaskHashMap.entrySet().iterator();
            while (iter.hasNext()) {
                Map.Entry<String, Node> entry = iter.next();
                node = entry.getValue();
                doContentSync(Node.SYNC_ACTION_ADD_LOCAL, node, null);
            }

            // mCancelled can be set by another thread, so we neet to check one by
            // one
            // clear local delete table
            if (!mCancelled) {
                if (!DataUtils.batchDeleteNotes(mContentResolver, mLocalDeleteIdMap)) {
                    throw new ActionFailureException("failed to batch-delete local deleted notes");
                }
            }

            // refresh local sync id
            if (!mCancelled) {
                GTaskClient.getInstance().commitUpdate();
                refreshLocalSyncId();
            }

        }

        private void syncFolder() throws NetworkFailureException {
            Cursor c = null;
            String gid;
            Node node;
            int syncType;

            if (mCancelled) {
                return;
            }

            // for root folder
            try {
                c = mContentResolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI,
                        Notes.ID_ROOT_FOLDER), SqlNote.PROJECTION_NOTE, null, null, null);
                if (c != null) {
                    c.moveToNext();
                    gid = c.getString(SqlNote.GTASK_ID_COLUMN);
                    node = mGTaskHashMap.get(gid);
                    if (node != null) {
                        mGTaskHashMap.remove(gid);
                        mGidToNid.put(gid, (long) Notes.ID_ROOT_FOLDER);
                        mNidToGid.put((long) Notes.ID_ROOT_FOLDER, gid);
                        // for system folder, only update remote name if necessary
                        if (!node.getName().equals(
                                GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT))
                            doContentSync(Node.SYNC_ACTION_UPDATE_REMOTE, node, c);
                    } else {
                        doContentSync(Node.SYNC_ACTION_ADD_REMOTE, node, c);
                    }
                } else {
                    Log.w(TAG, "failed to query root folder");
                }
            } finally {
                if (c != null) {
                    c.close();
                    c = null;
                }
            }

            // for call-note folder
            try {
                c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, "(_id=?)",
                        new String[] {
                            String.valueOf(Notes.ID_CALL_RECORD_FOLDER)
                        }, null);
                if (c != null) {
                    if (c.moveToNext()) {
                        gid = c.getString(SqlNote.GTASK_ID_COLUMN);
                        node = mGTaskHashMap.get(gid);
                        if (node != null) {
                            mGTaskHashMap.remove(gid);
                            mGidToNid.put(gid, (long) Notes.ID_CALL_RECORD_FOLDER);
                            mNidToGid.put((long) Notes.ID_CALL_RECORD_FOLDER, gid);
                            // for system folder, only update remote name if
                            // necessary
                            if (!node.getName().equals(
                                    GTaskStringUtils.MIUI_FOLDER_PREFFIX
                                            + GTaskStringUtils.FOLDER_CALL_NOTE))
                                doContentSync(Node.SYNC_ACTION_UPDATE_REMOTE, node, c);
                        } else {
                            doContentSync(Node.SYNC_ACTION_ADD_REMOTE, node, c);
                        }
                    }
                } else {
                    Log.w(TAG, "failed to query call note folder");
                }
            } finally {
                if (c != null) {
                    c.close();
                    c = null;
                }
            }

            // for local existing folders
            try {
                c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE,
                        "(type=? AND parent_id<>?)", new String[] {
                                String.valueOf(Notes.TYPE_FOLDER), String.valueOf(Notes.ID_TRASH_FOLER)
                        }, NoteColumns.TYPE + " DESC");
                if (c != null) {
                    while (c.moveToNext()) {
                        gid = c.getString(SqlNote.GTASK_ID_COLUMN);
                        node = mGTaskHashMap.get(gid);
                        if (node != null) {
                            mGTaskHashMap.remove(gid);
                            mGidToNid.put(gid, c.getLong(SqlNote.ID_COLUMN));
                            mNidToGid.put(c.getLong(SqlNote.ID_COLUMN), gid);
                            syncType = node.getSyncAction(c);
                        } else {
                            if (c.getString(SqlNote.GTASK_ID_COLUMN).trim().length() == 0) {
                                // local add
                                syncType = Node.SYNC_ACTION_ADD_REMOTE;
                            } else {
                                // remote delete
                                syncType = Node.SYNC_ACTION_DEL_LOCAL;
                            }
                        }
                        doContentSync(syncType, node, c);
                    }
                } else {
                    Log.w(TAG, "failed to query existing folder");
                }
            } finally {
                if (c != null) {
                    c.close();
                    c = null;
                }
            }

            // for remote add folders
            Iterator<Map.Entry<String, TaskList>> iter = mGTaskListHashMap.entrySet().iterator();
            while (iter.hasNext()) {
                Map.Entry<String, TaskList> entry = iter.next();
                gid = entry.getKey();
                node = entry.getValue();
                if (mGTaskHashMap.containsKey(gid)) {
                    mGTaskHashMap.remove(gid);
                    doContentSync(Node.SYNC_ACTION_ADD_LOCAL, node, null);
                }
            }

            if (!mCancelled)
                GTaskClient.getInstance().commitUpdate();
        }

        private void doContentSync(int syncType, Node node, Cursor c) throws NetworkFailureException {
            if (mCancelled) {
                return;
            }

            MetaData meta;
            switch (syncType) {
                case Node.SYNC_ACTION_ADD_LOCAL:
                    addLocalNode(node);
                    break;
                case Node.SYNC_ACTION_ADD_REMOTE:
                    addRemoteNode(node, c);
                    break;
                case Node.SYNC_ACTION_DEL_LOCAL:
                    meta = mMetaHashMap.get(c.getString(SqlNote.GTASK_ID_COLUMN));
                    if (meta != null) {
                        GTaskClient.getInstance().deleteNode(meta);
                    }
                    mLocalDeleteIdMap.add(c.getLong(SqlNote.ID_COLUMN));
                    break;
                case Node.SYNC_ACTION_DEL_REMOTE:
                    meta = mMetaHashMap.get(node.getGid());
                    if (meta != null) {
                        GTaskClient.getInstance().deleteNode(meta);
                    }
                    GTaskClient.getInstance().deleteNode(node);
                    break;
                case Node.SYNC_ACTION_UPDATE_LOCAL:
                    updateLocalNode(node, c);
                    break;
                case Node.SYNC_ACTION_UPDATE_REMOTE:
                    updateRemoteNode(node, c);
                    break;
                case Node.SYNC_ACTION_UPDATE_CONFLICT:
                    // merging both modifications maybe a good idea
                    // right now just use local update simply
                    updateRemoteNode(node, c);
                    break;
                case Node.SYNC_ACTION_NONE:
                    break;
                case Node.SYNC_ACTION_ERROR:
                default:
                    throw new ActionFailureException("unkown sync action type");
            }
        }

        private void addLocalNode(Node node) throws NetworkFailureException {
            if (mCancelled) {
                return;
            }

            SqlNote sqlNote;
            if (node instanceof TaskList) {
                if (node.getName().equals(
                        GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT)) {
                    sqlNote = new SqlNote(mContext, Notes.ID_ROOT_FOLDER);
                } else if (node.getName().equals(
                        GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_CALL_NOTE)) {
                    sqlNote = new SqlNote(mContext, Notes.ID_CALL_RECORD_FOLDER);
                } else {
                    sqlNote = new SqlNote(mContext);
                    sqlNote.setContent(node.getLocalJSONFromContent());
                    sqlNote.setParentId(Notes.ID_ROOT_FOLDER);
                }
            } else {
                sqlNote = new SqlNote(mContext);
                JSONObject js = node.getLocalJSONFromContent();
                try {
                    if (js.has(GTaskStringUtils.META_HEAD_NOTE)) {
                        JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
                        if (note.has(NoteColumns.ID)) {
                            long id = note.getLong(NoteColumns.ID);
                            if (DataUtils.existInNoteDatabase(mContentResolver, id)) {
                                // the id is not available, have to create a new one
                                note.remove(NoteColumns.ID);
                            }
                        }
                    }

                    if (js.has(GTaskStringUtils.META_HEAD_DATA)) {
                        JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA);
                        for (int i = 0; i < dataArray.length(); i++) {
                            JSONObject data = dataArray.getJSONObject(i);
                            if (data.has(DataColumns.ID)) {
                                long dataId = data.getLong(DataColumns.ID);
                                if (DataUtils.existInDataDatabase(mContentResolver, dataId)) {
                                    // the data id is not available, have to create
                                    // a new one
                                    data.remove(DataColumns.ID);
                                }
                            }
                        }

                    }
                } catch (JSONException e) {
                    Log.w(TAG, e.toString());
                    e.printStackTrace();
                }
                sqlNote.setContent(js);

                Long parentId = mGidToNid.get(((Task) node).getParent().getGid());
                if (parentId == null) {
                    Log.e(TAG, "cannot find task's parent id locally");
                    throw new ActionFailureException("cannot add local node");
                }
                sqlNote.setParentId(parentId.longValue());
            }

            // create the local node
            sqlNote.setGtaskId(node.getGid());
            sqlNote.commit(false);

            // update gid-nid mapping
            mGidToNid.put(node.getGid(), sqlNote.getId());
            mNidToGid.put(sqlNote.getId(), node.getGid());

            // update meta
            updateRemoteMeta(node.getGid(), sqlNote);
        }

        private void updateLocalNode(Node node, Cursor c) throws NetworkFailureException {
            if (mCancelled) {
                return;
            }

            SqlNote sqlNote;
            // update the note locally
            sqlNote = new SqlNote(mContext, c);
            sqlNote.setContent(node.getLocalJSONFromContent());

            Long parentId = (node instanceof Task) ? mGidToNid.get(((Task) node).getParent().getGid())
                    : new Long(Notes.ID_ROOT_FOLDER);
            if (parentId == null) {
                Log.e(TAG, "cannot find task's parent id locally");
                throw new ActionFailureException("cannot update local node");
            }
            sqlNote.setParentId(parentId.longValue());
            sqlNote.commit(true);

            // update meta info
            updateRemoteMeta(node.getGid(), sqlNote);
        }

        private void addRemoteNode(Node node, Cursor c) throws NetworkFailureException {
            if (mCancelled) {
                return;
            }

            SqlNote sqlNote = new SqlNote(mContext, c);
            Node n;

            // update remotely
            if (sqlNote.isNoteType()) {
                Task task = new Task();
                task.setContentByLocalJSON(sqlNote.getContent());

                String parentGid = mNidToGid.get(sqlNote.getParentId());
                if (parentGid == null) {
                    Log.e(TAG, "cannot find task's parent tasklist");
                    throw new ActionFailureException("cannot add remote task");
                }
                mGTaskListHashMap.get(parentGid).addChildTask(task);

                GTaskClient.getInstance().createTask(task);
                n = (Node) task;

                // add meta
                updateRemoteMeta(task.getGid(), sqlNote);
            } else {
                TaskList tasklist = null;

                // we need to skip folder if it has already existed
                String folderName = GTaskStringUtils.MIUI_FOLDER_PREFFIX;
                if (sqlNote.getId() == Notes.ID_ROOT_FOLDER)
                    folderName += GTaskStringUtils.FOLDER_DEFAULT;
                else if (sqlNote.getId() == Notes.ID_CALL_RECORD_FOLDER)
                    folderName += GTaskStringUtils.FOLDER_CALL_NOTE;
                else
                    folderName += sqlNote.getSnippet();

                Iterator<Map.Entry<String, TaskList>> iter = mGTaskListHashMap.entrySet().iterator();
                while (iter.hasNext()) {
                    Map.Entry<String, TaskList> entry = iter.next();
                    String gid = entry.getKey();
                    TaskList list = entry.getValue();

                    if (list.getName().equals(folderName)) {
                        tasklist = list;
                        if (mGTaskHashMap.containsKey(gid)) {
                            mGTaskHashMap.remove(gid);
                        }
                        break;
                    }
                }

                // no match we can add now
                if (tasklist == null) {
                    tasklist = new TaskList();
                    tasklist.setContentByLocalJSON(sqlNote.getContent());
                    GTaskClient.getInstance().createTaskList(tasklist);
                    mGTaskListHashMap.put(tasklist.getGid(), tasklist);
                }
                n = (Node) tasklist;
            }

            // update local note
            sqlNote.setGtaskId(n.getGid());
            sqlNote.commit(false);
            sqlNote.resetLocalModified();
            sqlNote.commit(true);

            // gid-id mapping
            mGidToNid.put(n.getGid(), sqlNote.getId());
            mNidToGid.put(sqlNote.getId(), n.getGid());
        }

        private void updateRemoteNode(Node node, Cursor c) throws NetworkFailureException {
            if (mCancelled) {
                return;
            }

            SqlNote sqlNote = new SqlNote(mContext, c);

            // update remotely
            node.setContentByLocalJSON(sqlNote.getContent());
            GTaskClient.getInstance().addUpdateNode(node);

            // update meta
            updateRemoteMeta(node.getGid(), sqlNote);

            // move task if necessary
            if (sqlNote.isNoteType()) {
                Task task = (Task) node;
                TaskList preParentList = task.getParent();

                String curParentGid = mNidToGid.get(sqlNote.getParentId());
                if (curParentGid == null) {
                    Log.e(TAG, "cannot find task's parent tasklist");
                    throw new ActionFailureException("cannot update remote task");
                }
                TaskList curParentList = mGTaskListHashMap.get(curParentGid);

                if (preParentList != curParentList) {
                    preParentList.removeChildTask(task);
                    curParentList.addChildTask(task);
                    GTaskClient.getInstance().moveTask(task, preParentList, curParentList);
                }
            }

            // clear local modified flag
            sqlNote.resetLocalModified();
            sqlNote.commit(true);
        }

        private void updateRemoteMeta(String gid, SqlNote sqlNote) throws NetworkFailureException {
            if (sqlNote != null && sqlNote.isNoteType()) {
                MetaData metaData = mMetaHashMap.get(gid);
                if (metaData != null) {
                    metaData.setMeta(gid, sqlNote.getContent());
                    GTaskClient.getInstance().addUpdateNode(metaData);
                } else {
                    metaData = new MetaData();
                    metaData.setMeta(gid, sqlNote.getContent());
                    mMetaList.addChildTask(metaData);
                    mMetaHashMap.put(gid, metaData);
                    GTaskClient.getInstance().createTask(metaData);
                }
            }
        }

        private void refreshLocalSyncId() throws NetworkFailureException {
            if (mCancelled) {
                return;
            }

            // get the latest gtask list
            mGTaskHashMap.clear();
            mGTaskListHashMap.clear();
            mMetaHashMap.clear();
            initGTaskList();

            Cursor c = null;
            try {
                c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE,
                        "(type<>? AND parent_id<>?)", new String[] {
                                String.valueOf(Notes.TYPE_SYSTEM), String.valueOf(Notes.ID_TRASH_FOLER)
                        }, NoteColumns.TYPE + " DESC");
                if (c != null) {
                    while (c.moveToNext()) {
                        String gid = c.getString(SqlNote.GTASK_ID_COLUMN);
                        Node node = mGTaskHashMap.get(gid);
                        if (node != null) {
                            mGTaskHashMap.remove(gid);
                            ContentValues values = new ContentValues();
                            values.put(NoteColumns.SYNC_ID, node.getLastModified());
                            mContentResolver.update(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI,
                                    c.getLong(SqlNote.ID_COLUMN)), values, null, null);
                        } else {
                            Log.e(TAG, "something is missed");
                            throw new ActionFailureException(
                                    "some local items don't have gid after sync");
                        }
                    }
                } else {
                    Log.w(TAG, "failed to query local note to refresh sync id");
                }
            } finally {
                if (c != null) {
                    c.close();
                    c = null;
                }
            }
        }

        public String getSyncAccount() {
            return GTaskClient.getInstance().getSyncAccount().name;
        }

        public void cancelSync() {
            mCancelled = true;
        }
    }