You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
git.text/src/java/net/micode/notes/gtask/data/Task.java

561 lines
20 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/*
* 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.data;
import android.database.Cursor;
import android.text.TextUtils;
import android.util.Log;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.DataConstants;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.gtask.exception.ActionFailureException;
import net.micode.notes.tool.GTaskStringUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
/**
* <p>Google任务类继承自{@link Node}抽象类。</p>
* <p>设计意图实现Google任务的具体数据模型和同步操作处理任务的创建、更新、删除等功能。</p>
* <p>核心职责:</p>
* <ul>
* <li>封装Google任务的特有属性完成状态、备注、元信息、兄弟任务关系、父子任务关系</li>
* <li>实现同步操作方法:生成创建/更新动作、解析本地/远程JSON数据、判断同步动作类型</li>
* <li>提供任务关系管理:设置父任务列表、前一个兄弟任务</li>
* <li>实现任务元信息管理:设置和解析任务的元数据</li>
* </ul>
* <p>关键关联:</p>
* <ul>
* <li>继承自{@link Node}抽象类实现Google任务数据模型的标准化接口</li>
* <li>与{@link TaskList}类关联,任务必须属于某个任务列表</li>
* <li>与{@link MetaData}类关联,管理任务的元信息</li>
* <li>与{@link GTaskStringUtils}类配合使用处理Google任务的JSON字段名</li>
* </ul>
*/
public class Task extends Node {
private static final String TAG = Task.class.getSimpleName();
/**
* 任务完成状态标记
*/
private boolean mCompleted;
/**
* 任务备注信息
*/
private String mNotes;
/**
* 任务元信息JSON对象包含本地任务的详细信息
*/
private JSONObject mMetaInfo;
/**
* 前一个兄弟任务,用于维护任务的顺序
*/
private Task mPriorSibling;
/**
* 父任务列表,任务所属的任务列表
*/
private TaskList mParent;
/**
* 默认构造方法,初始化任务属性
*/
public Task() {
super();
mCompleted = false;
mNotes = null;
mPriorSibling = null;
mParent = null;
mMetaInfo = null;
}
/**
* <p>生成创建任务的JSON动作对象。</p>
* <p>业务逻辑构造用于向Google任务服务添加新任务的JSON数据包含任务的基本信息和关系。</p>
* <p>JSON结构说明
* <ul>
* <li>action_type: 动作类型,固定为"create"</li>
* <li>action_id: 动作ID用于标识同步任务序列</li>
* <li>index: 任务在父列表中的位置索引</li>
* <li>entity_delta: 任务实体信息包含任务名称、创建者ID、实体类型、备注等</li>
* <li>parent_id: 父任务列表ID</li>
* <li>dest_parent_type: 目标父类型,固定为"group"</li>
* <li>list_id: 任务列表ID</li>
* <li>prior_sibling_id: 前一个兄弟任务ID可选</li>
* </ul>
* </p>
*
* @param actionId 动作ID用于标识同步任务序列
* @return 创建动作的JSON对象包含任务创建所需的所有信息
* @throws ActionFailureException 如果生成JSON对象失败抛出此异常
*/
public JSONObject getCreateAction(int actionId) {
JSONObject js = new JSONObject();
try {
// action_type
js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE);
// action_id
js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId);
// index
js.put(GTaskStringUtils.GTASK_JSON_INDEX, mParent.getChildTaskIndex(this));
// entity_delta
JSONObject entity = new JSONObject();
entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName());
entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null");
entity.put(GTaskStringUtils.GTASK_JSON_ENTITY_TYPE,
GTaskStringUtils.GTASK_JSON_TYPE_TASK);
if (getNotes() != null) {
entity.put(GTaskStringUtils.GTASK_JSON_NOTES, getNotes());
}
js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity);
// parent_id
js.put(GTaskStringUtils.GTASK_JSON_PARENT_ID, mParent.getGid());
// dest_parent_type
js.put(GTaskStringUtils.GTASK_JSON_DEST_PARENT_TYPE,
GTaskStringUtils.GTASK_JSON_TYPE_GROUP);
// list_id
js.put(GTaskStringUtils.GTASK_JSON_LIST_ID, mParent.getGid());
// prior_sibling_id
if (mPriorSibling != null) {
js.put(GTaskStringUtils.GTASK_JSON_PRIOR_SIBLING_ID, mPriorSibling.getGid());
}
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to generate task-create jsonobject");
}
return js;
}
/**
* <p>生成更新任务的JSON动作对象。</p>
* <p>业务逻辑构造用于向Google任务服务更新现有任务的JSON数据包含任务的更新信息。</p>
* <p>JSON结构说明
* <ul>
* <li>action_type: 动作类型,固定为"update"</li>
* <li>action_id: 动作ID用于标识同步任务序列</li>
* <li>id: 任务ID标识要更新的任务</li>
* <li>entity_delta: 任务实体更新信息,包含任务名称、备注、删除状态等</li>
* </ul>
* </p>
*
* @param actionId 动作ID用于标识同步任务序列
* @return 更新动作的JSON对象包含任务更新所需的所有信息
* @throws ActionFailureException 如果生成JSON对象失败抛出此异常
*/
public JSONObject getUpdateAction(int actionId) {
JSONObject js = new JSONObject();
try {
// action_type
js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE);
// action_id
js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId);
// id
js.put(GTaskStringUtils.GTASK_JSON_ID, getGid());
// entity_delta
JSONObject entity = new JSONObject();
entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName());
if (getNotes() != null) {
entity.put(GTaskStringUtils.GTASK_JSON_NOTES, getNotes());
}
entity.put(GTaskStringUtils.GTASK_JSON_DELETED, getDeleted());
js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity);
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to generate task-update jsonobject");
}
return js;
}
/**
* <p>从远程JSON数据解析并设置任务内容。</p>
* <p>业务逻辑将Google任务服务返回的JSON数据转换为任务对象的属性值。</p>
* <p>解析的JSON字段
* <ul>
* <li>id: 任务ID设置为GID</li>
* <li>last_modified: 最后修改时间戳</li>
* <li>name: 任务名称</li>
* <li>notes: 任务备注</li>
* <li>deleted: 删除状态</li>
* <li>completed: 完成状态</li>
* </ul>
* </p>
*
* @param js 远程获取的JSON对象包含Google任务服务返回的任务数据
* @throws ActionFailureException 如果解析JSON对象失败抛出此异常
*/
public void setContentByRemoteJSON(JSONObject js) {
if (js != null) {
try {
// id
if (js.has(GTaskStringUtils.GTASK_JSON_ID)) {
setGid(js.getString(GTaskStringUtils.GTASK_JSON_ID));
}
// last_modified
if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) {
setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED));
}
// name
if (js.has(GTaskStringUtils.GTASK_JSON_NAME)) {
setName(js.getString(GTaskStringUtils.GTASK_JSON_NAME));
}
// notes
if (js.has(GTaskStringUtils.GTASK_JSON_NOTES)) {
setNotes(js.getString(GTaskStringUtils.GTASK_JSON_NOTES));
}
// deleted
if (js.has(GTaskStringUtils.GTASK_JSON_DELETED)) {
setDeleted(js.getBoolean(GTaskStringUtils.GTASK_JSON_DELETED));
}
// completed
if (js.has(GTaskStringUtils.GTASK_JSON_COMPLETED)) {
setCompleted(js.getBoolean(GTaskStringUtils.GTASK_JSON_COMPLETED));
}
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to get task content from jsonobject");
}
}
}
/**
* <p>从本地JSON数据解析并设置任务内容。</p>
* <p>业务逻辑将本地存储的JSON数据转换为任务对象的属性值。</p>
* <p>解析的JSON结构
* <ul>
* <li>meta_note: 包含笔记类型信息</li>
* <li>meta_data: 包含笔记内容数组其中MIME类型为NOTE的数据项的内容被设置为任务名称</li>
* </ul>
* </p>
*
* @param js 本地存储的JSON对象包含应用内持久化的任务数据
*/
public void setContentByLocalJSON(JSONObject js) {
if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE)
|| !js.has(GTaskStringUtils.META_HEAD_DATA)) {
Log.w(TAG, "setContentByLocalJSON: nothing is avaiable");
}
try {
JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA);
if (note.getInt(NoteColumns.TYPE) != Notes.TYPE_NOTE) {
Log.e(TAG, "invalid type");
return;
}
for (int i = 0; i < dataArray.length(); i++) {
JSONObject data = dataArray.getJSONObject(i);
if (TextUtils.equals(data.getString(DataColumns.MIME_TYPE), DataConstants.NOTE)) {
setName(data.getString(DataColumns.CONTENT));
break;
}
}
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
}
}
/**
* <p>从任务内容生成本地JSON对象。</p>
* <p>业务逻辑根据任务名称和元信息生成本地存储所需的JSON数据结构。</p>
* <p>JSON结构说明
* <ul>
* <li>meta_note: 包含笔记类型信息</li>
* <li>meta_data: 包含笔记内容数组其中MIME类型为NOTE的数据项存储任务名称</li>
* </ul>
* </p>
* <p>处理流程:
* <ol>
* <li>如果没有元信息生成新的JSON结构适用于从Web创建的新任务</li>
* <li>如果有元信息,更新现有结构中的任务名称(适用于已同步的任务)</li>
* </ol>
* </p>
*
* @return 本地存储的JSON对象包含任务的完整数据结构
*/
public JSONObject getLocalJSONFromContent() {
String name = getName();
try {
if (mMetaInfo == null) {
// new task created from web
if (name == null) {
Log.w(TAG, "the note seems to be an empty one");
return null;
}
JSONObject js = new JSONObject();
JSONObject note = new JSONObject();
JSONArray dataArray = new JSONArray();
JSONObject data = new JSONObject();
data.put(DataColumns.CONTENT, name);
dataArray.put(data);
js.put(GTaskStringUtils.META_HEAD_DATA, dataArray);
note.put(NoteColumns.TYPE, Notes.TYPE_NOTE);
js.put(GTaskStringUtils.META_HEAD_NOTE, note);
return js;
} else {
// synced task
JSONObject note = mMetaInfo.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
JSONArray dataArray = mMetaInfo.getJSONArray(GTaskStringUtils.META_HEAD_DATA);
for (int i = 0; i < dataArray.length(); i++) {
JSONObject data = dataArray.getJSONObject(i);
if (TextUtils.equals(data.getString(DataColumns.MIME_TYPE), DataConstants.NOTE)) {
data.put(DataColumns.CONTENT, getName());
break;
}
}
note.put(NoteColumns.TYPE, Notes.TYPE_NOTE);
return mMetaInfo;
}
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
return null;
}
}
/**
* <p>设置任务的元信息。</p>
* <p>业务逻辑从MetaData对象中解析JSON字符串获取任务的元信息。</p>
* <p>处理流程:
* <ol>
* <li>检查MetaData对象及其notes字段是否为空</li>
* <li>将notes字段的JSON字符串解析为JSONObject</li>
* <li>如果解析失败将元信息设置为null</li>
* </ol>
* </p>
*
* @param metaData 包含元信息的MetaData对象
*/
public void setMetaInfo(MetaData metaData) {
if (metaData != null && metaData.getNotes() != null) {
try {
mMetaInfo = new JSONObject(metaData.getNotes());
} catch (JSONException e) {
Log.w(TAG, e.toString());
mMetaInfo = null;
}
}
}
/**
* <p>获取同步动作类型。</p>
* <p>业务逻辑:通过比较本地任务和远程任务的状态,确定需要执行的同步动作。</p>
* <p>同步动作类型说明:
* <ul>
* <li>{@link Node#SYNC_ACTION_NONE}: 无同步动作,本地和远程数据一致</li>
* <li>{@link Node#SYNC_ACTION_UPDATE_LOCAL}: 更新本地数据,应用远程变更</li>
* <li>{@link Node#SYNC_ACTION_UPDATE_REMOTE}: 更新远程数据,应用本地变更</li>
* <li>{@link Node#SYNC_ACTION_UPDATE_CONFLICT}: 同步冲突,本地和远程都有变更</li>
* <li>{@link Node#SYNC_ACTION_ERROR}: 同步错误,数据验证失败</li>
* </ul>
* </p>
* <p>处理流程:
* <ol>
* <li>验证元信息和笔记ID的有效性</li>
* <li>比较本地和远程的最后修改时间</li>
* <li>检查本地修改标记和GTASK ID的一致性</li>
* <li>根据比较结果返回相应的同步动作类型</li>
* </ol>
* </p>
*
* @param c 包含本地任务数据的游标对象
* @return 同步动作类型对应Node类中定义的SYNC_ACTION_*常量
*/
public int getSyncAction(Cursor c) {
try {
JSONObject noteInfo = null;
if (mMetaInfo != null && mMetaInfo.has(GTaskStringUtils.META_HEAD_NOTE)) {
noteInfo = mMetaInfo.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
}
if (noteInfo == null) {
Log.w(TAG, "it seems that note meta has been deleted");
return SYNC_ACTION_UPDATE_REMOTE;
}
if (!noteInfo.has(NoteColumns.ID)) {
Log.w(TAG, "remote note id seems to be deleted");
return SYNC_ACTION_UPDATE_LOCAL;
}
// validate the note id now
if (c.getLong(SqlNote.ID_COLUMN) != noteInfo.getLong(NoteColumns.ID)) {
Log.w(TAG, "note id doesn't match");
return SYNC_ACTION_UPDATE_LOCAL;
}
if (c.getInt(SqlNote.LOCAL_MODIFIED_COLUMN) == 0) {
// there is no local update
if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) {
// no update both side
return SYNC_ACTION_NONE;
} else {
// apply remote to local
return SYNC_ACTION_UPDATE_LOCAL;
}
} else {
// validate gtask id
if (!c.getString(SqlNote.GTASK_ID_COLUMN).equals(getGid())) {
Log.e(TAG, "gtask id doesn't match");
return SYNC_ACTION_ERROR;
}
if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) {
// local modification only
return SYNC_ACTION_UPDATE_REMOTE;
} else {
return SYNC_ACTION_UPDATE_CONFLICT;
}
}
} catch (Exception e) {
Log.e(TAG, e.toString());
e.printStackTrace();
}
return SYNC_ACTION_ERROR;
}
/**
* <p>判断任务是否值得保存。</p>
* <p>业务逻辑:检查任务是否包含有价值的信息,用于决定是否需要保存到数据库或同步到远程。</p>
* <p>判断条件:满足以下任一条件则认为值得保存:
* <ul>
* <li>任务包含元信息mMetaInfo不为null</li>
* <li>任务名称不为空且长度大于0</li>
* <li>任务备注不为空且长度大于0</li>
* </ul>
* </p>
*
* @return true表示任务值得保存false表示任务为空或无价值信息
*/
public boolean isWorthSaving() {
return mMetaInfo != null || (getName() != null && getName().trim().length() > 0)
|| (getNotes() != null && getNotes().trim().length() > 0);
}
/**
* 设置任务的完成状态。
*
* @param completed 任务的完成状态true表示已完成false表示未完成
*/
public void setCompleted(boolean completed) {
this.mCompleted = completed;
}
/**
* 设置任务的备注信息。
*
* @param notes 任务的备注信息字符串
*/
public void setNotes(String notes) {
this.mNotes = notes;
}
/**
* 设置任务的前一个兄弟任务,用于维护任务的顺序。
*
* @param priorSibling 前一个兄弟任务对象
*/
public void setPriorSibling(Task priorSibling) {
this.mPriorSibling = priorSibling;
}
/**
* 设置任务的父任务列表。
*
* @param parent 父任务列表对象
*/
public void setParent(TaskList parent) {
this.mParent = parent;
}
/**
* 获取任务的完成状态。
*
* @return 任务的完成状态true表示已完成false表示未完成
*/
public boolean getCompleted() {
return this.mCompleted;
}
/**
* 获取任务的备注信息。
*
* @return 任务的备注信息字符串
*/
public String getNotes() {
return this.mNotes;
}
/**
* 获取任务的前一个兄弟任务。
*
* @return 前一个兄弟任务对象如果没有则返回null
*/
public Task getPriorSibling() {
return this.mPriorSibling;
}
/**
* 获取任务的父任务列表。
*
* @return 父任务列表对象如果没有则返回null
*/
public TaskList getParent() {
return this.mParent;
}
}