diff --git a/README.md b/README.md index 9ce45f8..0a99726 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ # XiaoMiNotes +123 + diff --git a/doc/小米便签开源代码泛读报告.docx b/doc/小米便签开源代码泛读报告.docx deleted file mode 100644 index da8741c..0000000 Binary files a/doc/小米便签开源代码泛读报告.docx and /dev/null differ diff --git a/doc/小米便签泛读报告.docx b/doc/小米便签泛读报告.docx new file mode 100644 index 0000000..7bb2377 Binary files /dev/null and b/doc/小米便签泛读报告.docx differ diff --git a/src/data/Contact.java b/src/data/Contact.java deleted file mode 100644 index d97ac5d..0000000 --- a/src/data/Contact.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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.data; - -import android.content.Context; -import android.database.Cursor; -import android.provider.ContactsContract.CommonDataKinds.Phone; -import android.provider.ContactsContract.Data; -import android.telephony.PhoneNumberUtils; -import android.util.Log; - -import java.util.HashMap; - -public class Contact { - private static HashMap sContactCache; - private static final String TAG = "Contact"; - - private static final String CALLER_ID_SELECTION = "PHONE_NUMBERS_EQUAL(" + Phone.NUMBER - + ",?) AND " + Data.MIMETYPE + "='" + Phone.CONTENT_ITEM_TYPE + "'" - + " AND " + Data.RAW_CONTACT_ID + " IN " - + "(SELECT raw_contact_id " - + " FROM phone_lookup" - + " WHERE min_match = '+')"; - - public static String getContact(Context context, String phoneNumber) { - if(sContactCache == null) { - sContactCache = new HashMap(); - } - - if(sContactCache.containsKey(phoneNumber)) { - return sContactCache.get(phoneNumber); - } - - String selection = CALLER_ID_SELECTION.replace("+", - PhoneNumberUtils.toCallerIDMinMatch(phoneNumber)); - Cursor cursor = context.getContentResolver().query( - Data.CONTENT_URI, - new String [] { Phone.DISPLAY_NAME }, - selection, - new String[] { phoneNumber }, - null); - - if (cursor != null && cursor.moveToFirst()) { - try { - String name = cursor.getString(0); - sContactCache.put(phoneNumber, name); - return name; - } catch (IndexOutOfBoundsException e) { - Log.e(TAG, " Cursor get string error " + e.toString()); - return null; - } finally { - cursor.close(); - } - } else { - Log.d(TAG, "No contact matched with number:" + phoneNumber); - return null; - } - } -} diff --git a/src/data/NotesDatabaseHelper.java b/src/data/NotesDatabaseHelper.java deleted file mode 100644 index ffe5d57..0000000 --- a/src/data/NotesDatabaseHelper.java +++ /dev/null @@ -1,362 +0,0 @@ -/* - * 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.data; - -import android.content.ContentValues; -import android.content.Context; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteOpenHelper; -import android.util.Log; - -import net.micode.notes.data.Notes.DataColumns; -import net.micode.notes.data.Notes.DataConstants; -import net.micode.notes.data.Notes.NoteColumns; - - -public class NotesDatabaseHelper extends SQLiteOpenHelper { - private static final String DB_NAME = "note.db"; - - private static final int DB_VERSION = 4; - - public interface TABLE { - public static final String NOTE = "note"; - - public static final String DATA = "data"; - } - - private static final String TAG = "NotesDatabaseHelper"; - - private static NotesDatabaseHelper mInstance; - - private static final String CREATE_NOTE_TABLE_SQL = - "CREATE TABLE " + TABLE.NOTE + "(" + - NoteColumns.ID + " INTEGER PRIMARY KEY," + - NoteColumns.PARENT_ID + " INTEGER NOT NULL DEFAULT 0," + - NoteColumns.ALERTED_DATE + " INTEGER NOT NULL DEFAULT 0," + - NoteColumns.BG_COLOR_ID + " INTEGER NOT NULL DEFAULT 0," + - NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + - NoteColumns.HAS_ATTACHMENT + " INTEGER NOT NULL DEFAULT 0," + - NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + - NoteColumns.NOTES_COUNT + " INTEGER NOT NULL DEFAULT 0," + - NoteColumns.SNIPPET + " TEXT NOT NULL DEFAULT ''," + - NoteColumns.TYPE + " INTEGER NOT NULL DEFAULT 0," + - NoteColumns.WIDGET_ID + " INTEGER NOT NULL DEFAULT 0," + - NoteColumns.WIDGET_TYPE + " INTEGER NOT NULL DEFAULT -1," + - NoteColumns.SYNC_ID + " INTEGER NOT NULL DEFAULT 0," + - NoteColumns.LOCAL_MODIFIED + " INTEGER NOT NULL DEFAULT 0," + - NoteColumns.ORIGIN_PARENT_ID + " INTEGER NOT NULL DEFAULT 0," + - NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''," + - NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0" + - ")"; - - private static final String CREATE_DATA_TABLE_SQL = - "CREATE TABLE " + TABLE.DATA + "(" + - DataColumns.ID + " INTEGER PRIMARY KEY," + - DataColumns.MIME_TYPE + " TEXT NOT NULL," + - DataColumns.NOTE_ID + " INTEGER NOT NULL DEFAULT 0," + - NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + - NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + - DataColumns.CONTENT + " TEXT NOT NULL DEFAULT ''," + - DataColumns.DATA1 + " INTEGER," + - DataColumns.DATA2 + " INTEGER," + - DataColumns.DATA3 + " TEXT NOT NULL DEFAULT ''," + - DataColumns.DATA4 + " TEXT NOT NULL DEFAULT ''," + - DataColumns.DATA5 + " TEXT NOT NULL DEFAULT ''" + - ")"; - - private static final String CREATE_DATA_NOTE_ID_INDEX_SQL = - "CREATE INDEX IF NOT EXISTS note_id_index ON " + - TABLE.DATA + "(" + DataColumns.NOTE_ID + ");"; - - /** - * Increase folder's note count when move note to the folder - */ - private static final String NOTE_INCREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER = - "CREATE TRIGGER increase_folder_count_on_update "+ - " AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE + - " BEGIN " + - " UPDATE " + TABLE.NOTE + - " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" + - " WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" + - " END"; - - /** - * Decrease folder's note count when move note from folder - */ - private static final String NOTE_DECREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER = - "CREATE TRIGGER decrease_folder_count_on_update " + - " AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE + - " BEGIN " + - " UPDATE " + TABLE.NOTE + - " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" + - " WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID + - " AND " + NoteColumns.NOTES_COUNT + ">0" + ";" + - " END"; - - /** - * Increase folder's note count when insert new note to the folder - */ - private static final String NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER = - "CREATE TRIGGER increase_folder_count_on_insert " + - " AFTER INSERT ON " + TABLE.NOTE + - " BEGIN " + - " UPDATE " + TABLE.NOTE + - " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" + - " WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" + - " END"; - - /** - * Decrease folder's note count when delete note from the folder - */ - private static final String NOTE_DECREASE_FOLDER_COUNT_ON_DELETE_TRIGGER = - "CREATE TRIGGER decrease_folder_count_on_delete " + - " AFTER DELETE ON " + TABLE.NOTE + - " BEGIN " + - " UPDATE " + TABLE.NOTE + - " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" + - " WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID + - " AND " + NoteColumns.NOTES_COUNT + ">0;" + - " END"; - - /** - * Update note's content when insert data with type {@link DataConstants#NOTE} - */ - private static final String DATA_UPDATE_NOTE_CONTENT_ON_INSERT_TRIGGER = - "CREATE TRIGGER update_note_content_on_insert " + - " AFTER INSERT ON " + TABLE.DATA + - " WHEN new." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" + - " BEGIN" + - " UPDATE " + TABLE.NOTE + - " SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT + - " WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" + - " END"; - - /** - * Update note's content when data with {@link DataConstants#NOTE} type has changed - */ - private static final String DATA_UPDATE_NOTE_CONTENT_ON_UPDATE_TRIGGER = - "CREATE TRIGGER update_note_content_on_update " + - " AFTER UPDATE ON " + TABLE.DATA + - " WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" + - " BEGIN" + - " UPDATE " + TABLE.NOTE + - " SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT + - " WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" + - " END"; - - /** - * Update note's content when data with {@link DataConstants#NOTE} type has deleted - */ - private static final String DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER = - "CREATE TRIGGER update_note_content_on_delete " + - " AFTER delete ON " + TABLE.DATA + - " WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" + - " BEGIN" + - " UPDATE " + TABLE.NOTE + - " SET " + NoteColumns.SNIPPET + "=''" + - " WHERE " + NoteColumns.ID + "=old." + DataColumns.NOTE_ID + ";" + - " END"; - - /** - * Delete datas belong to note which has been deleted - */ - private static final String NOTE_DELETE_DATA_ON_DELETE_TRIGGER = - "CREATE TRIGGER delete_data_on_delete " + - " AFTER DELETE ON " + TABLE.NOTE + - " BEGIN" + - " DELETE FROM " + TABLE.DATA + - " WHERE " + DataColumns.NOTE_ID + "=old." + NoteColumns.ID + ";" + - " END"; - - /** - * Delete notes belong to folder which has been deleted - */ - private static final String FOLDER_DELETE_NOTES_ON_DELETE_TRIGGER = - "CREATE TRIGGER folder_delete_notes_on_delete " + - " AFTER DELETE ON " + TABLE.NOTE + - " BEGIN" + - " DELETE FROM " + TABLE.NOTE + - " WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" + - " END"; - - /** - * Move notes belong to folder which has been moved to trash folder - */ - private static final String FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER = - "CREATE TRIGGER folder_move_notes_on_trash " + - " AFTER UPDATE ON " + TABLE.NOTE + - " WHEN new." + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER + - " BEGIN" + - " UPDATE " + TABLE.NOTE + - " SET " + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER + - " WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" + - " END"; - - public NotesDatabaseHelper(Context context) { - super(context, DB_NAME, null, DB_VERSION); - } - - public void createNoteTable(SQLiteDatabase db) { - db.execSQL(CREATE_NOTE_TABLE_SQL); - reCreateNoteTableTriggers(db); - createSystemFolder(db); - Log.d(TAG, "note table has been created"); - } - - private void reCreateNoteTableTriggers(SQLiteDatabase db) { - db.execSQL("DROP TRIGGER IF EXISTS increase_folder_count_on_update"); - db.execSQL("DROP TRIGGER IF EXISTS decrease_folder_count_on_update"); - db.execSQL("DROP TRIGGER IF EXISTS decrease_folder_count_on_delete"); - db.execSQL("DROP TRIGGER IF EXISTS delete_data_on_delete"); - db.execSQL("DROP TRIGGER IF EXISTS increase_folder_count_on_insert"); - db.execSQL("DROP TRIGGER IF EXISTS folder_delete_notes_on_delete"); - db.execSQL("DROP TRIGGER IF EXISTS folder_move_notes_on_trash"); - - db.execSQL(NOTE_INCREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER); - db.execSQL(NOTE_DECREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER); - db.execSQL(NOTE_DECREASE_FOLDER_COUNT_ON_DELETE_TRIGGER); - db.execSQL(NOTE_DELETE_DATA_ON_DELETE_TRIGGER); - db.execSQL(NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER); - db.execSQL(FOLDER_DELETE_NOTES_ON_DELETE_TRIGGER); - db.execSQL(FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER); - } - - private void createSystemFolder(SQLiteDatabase db) { - ContentValues values = new ContentValues(); - - /** - * call record foler for call notes - */ - values.put(NoteColumns.ID, Notes.ID_CALL_RECORD_FOLDER); - values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); - db.insert(TABLE.NOTE, null, values); - - /** - * root folder which is default folder - */ - values.clear(); - values.put(NoteColumns.ID, Notes.ID_ROOT_FOLDER); - values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); - db.insert(TABLE.NOTE, null, values); - - /** - * temporary folder which is used for moving note - */ - values.clear(); - values.put(NoteColumns.ID, Notes.ID_TEMPARAY_FOLDER); - values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); - db.insert(TABLE.NOTE, null, values); - - /** - * create trash folder - */ - values.clear(); - values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER); - values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); - db.insert(TABLE.NOTE, null, values); - } - - public void createDataTable(SQLiteDatabase db) { - db.execSQL(CREATE_DATA_TABLE_SQL); - reCreateDataTableTriggers(db); - db.execSQL(CREATE_DATA_NOTE_ID_INDEX_SQL); - Log.d(TAG, "data table has been created"); - } - - private void reCreateDataTableTriggers(SQLiteDatabase db) { - db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_insert"); - db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_update"); - db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_delete"); - - db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_INSERT_TRIGGER); - db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_UPDATE_TRIGGER); - db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER); - } - - static synchronized NotesDatabaseHelper getInstance(Context context) { - if (mInstance == null) { - mInstance = new NotesDatabaseHelper(context); - } - return mInstance; - } - - @Override - public void onCreate(SQLiteDatabase db) { - createNoteTable(db); - createDataTable(db); - } - - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - boolean reCreateTriggers = false; - boolean skipV2 = false; - - if (oldVersion == 1) { - upgradeToV2(db); - skipV2 = true; // this upgrade including the upgrade from v2 to v3 - oldVersion++; - } - - if (oldVersion == 2 && !skipV2) { - upgradeToV3(db); - reCreateTriggers = true; - oldVersion++; - } - - if (oldVersion == 3) { - upgradeToV4(db); - oldVersion++; - } - - if (reCreateTriggers) { - reCreateNoteTableTriggers(db); - reCreateDataTableTriggers(db); - } - - if (oldVersion != newVersion) { - throw new IllegalStateException("Upgrade notes database to version " + newVersion - + "fails"); - } - } - - private void upgradeToV2(SQLiteDatabase db) { - db.execSQL("DROP TABLE IF EXISTS " + TABLE.NOTE); - db.execSQL("DROP TABLE IF EXISTS " + TABLE.DATA); - createNoteTable(db); - createDataTable(db); - } - - private void upgradeToV3(SQLiteDatabase db) { - // drop unused triggers - db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_insert"); - db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_delete"); - db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_update"); - // add a column for gtask id - db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.GTASK_ID - + " TEXT NOT NULL DEFAULT ''"); - // add a trash system folder - ContentValues values = new ContentValues(); - values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER); - values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); - db.insert(TABLE.NOTE, null, values); - } - - private void upgradeToV4(SQLiteDatabase db) { - db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.VERSION - + " INTEGER NOT NULL DEFAULT 0"); - } -} diff --git a/src/gtask/data/MetaData.java b/src/gtask/data/MetaData.java deleted file mode 100644 index 3a2050b..0000000 --- a/src/gtask/data/MetaData.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * 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.util.Log; - -import net.micode.notes.tool.GTaskStringUtils; - -import org.json.JSONException; -import org.json.JSONObject; - - -public class MetaData extends Task { - private final static String TAG = MetaData.class.getSimpleName(); - - private String mRelatedGid = null; - - public void setMeta(String gid, JSONObject metaInfo) { - try { - metaInfo.put(GTaskStringUtils.META_HEAD_GTASK_ID, gid); - } catch (JSONException e) { - Log.e(TAG, "failed to put related gid"); - } - setNotes(metaInfo.toString()); - setName(GTaskStringUtils.META_NOTE_NAME); - } - - public String getRelatedGid() { - return mRelatedGid; - } - - @Override - public boolean isWorthSaving() { - return getNotes() != null; - } - - @Override - public void setContentByRemoteJSON(JSONObject js) { - super.setContentByRemoteJSON(js); - if (getNotes() != null) { - try { - JSONObject metaInfo = new JSONObject(getNotes().trim()); - mRelatedGid = metaInfo.getString(GTaskStringUtils.META_HEAD_GTASK_ID); - } catch (JSONException e) { - Log.w(TAG, "failed to get related gid"); - mRelatedGid = null; - } - } - } - - @Override - public void setContentByLocalJSON(JSONObject js) { - // this function should not be called - throw new IllegalAccessError("MetaData:setContentByLocalJSON should not be called"); - } - - @Override - public JSONObject getLocalJSONFromContent() { - throw new IllegalAccessError("MetaData:getLocalJSONFromContent should not be called"); - } - - @Override - public int getSyncAction(Cursor c) { - throw new IllegalAccessError("MetaData:getSyncAction should not be called"); - } - -} diff --git a/src/gtask/data/Node.java b/src/gtask/data/Node.java deleted file mode 100644 index 63950e0..0000000 --- a/src/gtask/data/Node.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * 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 org.json.JSONObject; - -public abstract class Node { - public static final int SYNC_ACTION_NONE = 0; - - public static final int SYNC_ACTION_ADD_REMOTE = 1; - - public static final int SYNC_ACTION_ADD_LOCAL = 2; - - public static final int SYNC_ACTION_DEL_REMOTE = 3; - - public static final int SYNC_ACTION_DEL_LOCAL = 4; - - public static final int SYNC_ACTION_UPDATE_REMOTE = 5; - - public static final int SYNC_ACTION_UPDATE_LOCAL = 6; - - public static final int SYNC_ACTION_UPDATE_CONFLICT = 7; - - public static final int SYNC_ACTION_ERROR = 8; - - private String mGid; - - private String mName; - - private long mLastModified; - - private boolean mDeleted; - - public Node() { - mGid = null; - mName = ""; - mLastModified = 0; - mDeleted = false; - } - - public abstract JSONObject getCreateAction(int actionId); - - public abstract JSONObject getUpdateAction(int actionId); - - public abstract void setContentByRemoteJSON(JSONObject js); - - public abstract void setContentByLocalJSON(JSONObject js); - - public abstract JSONObject getLocalJSONFromContent(); - - public abstract int getSyncAction(Cursor c); - - public void setGid(String gid) { - this.mGid = gid; - } - - public void setName(String name) { - this.mName = name; - } - - public void setLastModified(long lastModified) { - this.mLastModified = lastModified; - } - - public void setDeleted(boolean deleted) { - this.mDeleted = deleted; - } - - public String getGid() { - return this.mGid; - } - - public String getName() { - return this.mName; - } - - public long getLastModified() { - return this.mLastModified; - } - - public boolean getDeleted() { - return this.mDeleted; - } - -} diff --git a/src/gtask/data/SqlNote.java b/src/gtask/data/SqlNote.java deleted file mode 100644 index 79a4095..0000000 --- a/src/gtask/data/SqlNote.java +++ /dev/null @@ -1,505 +0,0 @@ -/* - * 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.appwidget.AppWidgetManager; -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.util.Log; - -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.exception.ActionFailureException; -import net.micode.notes.tool.GTaskStringUtils; -import net.micode.notes.tool.ResourceParser; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; - - -public class SqlNote { - private static final String TAG = SqlNote.class.getSimpleName(); - - private static final int INVALID_ID = -99999; - - public static final String[] PROJECTION_NOTE = new String[] { - NoteColumns.ID, NoteColumns.ALERTED_DATE, NoteColumns.BG_COLOR_ID, - NoteColumns.CREATED_DATE, NoteColumns.HAS_ATTACHMENT, NoteColumns.MODIFIED_DATE, - NoteColumns.NOTES_COUNT, NoteColumns.PARENT_ID, NoteColumns.SNIPPET, NoteColumns.TYPE, - NoteColumns.WIDGET_ID, NoteColumns.WIDGET_TYPE, NoteColumns.SYNC_ID, - NoteColumns.LOCAL_MODIFIED, NoteColumns.ORIGIN_PARENT_ID, NoteColumns.GTASK_ID, - NoteColumns.VERSION - }; - - public static final int ID_COLUMN = 0; - - public static final int ALERTED_DATE_COLUMN = 1; - - public static final int BG_COLOR_ID_COLUMN = 2; - - public static final int CREATED_DATE_COLUMN = 3; - - public static final int HAS_ATTACHMENT_COLUMN = 4; - - public static final int MODIFIED_DATE_COLUMN = 5; - - public static final int NOTES_COUNT_COLUMN = 6; - - public static final int PARENT_ID_COLUMN = 7; - - public static final int SNIPPET_COLUMN = 8; - - public static final int TYPE_COLUMN = 9; - - public static final int WIDGET_ID_COLUMN = 10; - - public static final int WIDGET_TYPE_COLUMN = 11; - - public static final int SYNC_ID_COLUMN = 12; - - public static final int LOCAL_MODIFIED_COLUMN = 13; - - public static final int ORIGIN_PARENT_ID_COLUMN = 14; - - public static final int GTASK_ID_COLUMN = 15; - - public static final int VERSION_COLUMN = 16; - - private Context mContext; - - private ContentResolver mContentResolver; - - private boolean mIsCreate; - - private long mId; - - private long mAlertDate; - - private int mBgColorId; - - private long mCreatedDate; - - private int mHasAttachment; - - private long mModifiedDate; - - private long mParentId; - - private String mSnippet; - - private int mType; - - private int mWidgetId; - - private int mWidgetType; - - private long mOriginParent; - - private long mVersion; - - private ContentValues mDiffNoteValues; - - private ArrayList mDataList; - - public SqlNote(Context context) { - mContext = context; - mContentResolver = context.getContentResolver(); - mIsCreate = true; - mId = INVALID_ID; - mAlertDate = 0; - mBgColorId = ResourceParser.getDefaultBgId(context); - mCreatedDate = System.currentTimeMillis(); - mHasAttachment = 0; - mModifiedDate = System.currentTimeMillis(); - mParentId = 0; - mSnippet = ""; - mType = Notes.TYPE_NOTE; - mWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID; - mWidgetType = Notes.TYPE_WIDGET_INVALIDE; - mOriginParent = 0; - mVersion = 0; - mDiffNoteValues = new ContentValues(); - mDataList = new ArrayList(); - } - - public SqlNote(Context context, Cursor c) { - mContext = context; - mContentResolver = context.getContentResolver(); - mIsCreate = false; - loadFromCursor(c); - mDataList = new ArrayList(); - if (mType == Notes.TYPE_NOTE) - loadDataContent(); - mDiffNoteValues = new ContentValues(); - } - - public SqlNote(Context context, long id) { - mContext = context; - mContentResolver = context.getContentResolver(); - mIsCreate = false; - loadFromCursor(id); - mDataList = new ArrayList(); - if (mType == Notes.TYPE_NOTE) - loadDataContent(); - mDiffNoteValues = new ContentValues(); - - } - - private void loadFromCursor(long id) { - Cursor c = null; - try { - c = mContentResolver.query(Notes.CONTENT_NOTE_URI, PROJECTION_NOTE, "(_id=?)", - new String[] { - String.valueOf(id) - }, null); - if (c != null) { - c.moveToNext(); - loadFromCursor(c); - } else { - Log.w(TAG, "loadFromCursor: cursor = null"); - } - } finally { - if (c != null) - c.close(); - } - } - - private void loadFromCursor(Cursor c) { - mId = c.getLong(ID_COLUMN); - mAlertDate = c.getLong(ALERTED_DATE_COLUMN); - mBgColorId = c.getInt(BG_COLOR_ID_COLUMN); - mCreatedDate = c.getLong(CREATED_DATE_COLUMN); - mHasAttachment = c.getInt(HAS_ATTACHMENT_COLUMN); - mModifiedDate = c.getLong(MODIFIED_DATE_COLUMN); - mParentId = c.getLong(PARENT_ID_COLUMN); - mSnippet = c.getString(SNIPPET_COLUMN); - mType = c.getInt(TYPE_COLUMN); - mWidgetId = c.getInt(WIDGET_ID_COLUMN); - mWidgetType = c.getInt(WIDGET_TYPE_COLUMN); - mVersion = c.getLong(VERSION_COLUMN); - } - - private void loadDataContent() { - Cursor c = null; - mDataList.clear(); - try { - c = mContentResolver.query(Notes.CONTENT_DATA_URI, SqlData.PROJECTION_DATA, - "(note_id=?)", new String[] { - String.valueOf(mId) - }, null); - if (c != null) { - if (c.getCount() == 0) { - Log.w(TAG, "it seems that the note has not data"); - return; - } - while (c.moveToNext()) { - SqlData data = new SqlData(mContext, c); - mDataList.add(data); - } - } else { - Log.w(TAG, "loadDataContent: cursor = null"); - } - } finally { - if (c != null) - c.close(); - } - } - - public boolean setContent(JSONObject js) { - try { - JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); - if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_SYSTEM) { - Log.w(TAG, "cannot set system folder"); - } else if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_FOLDER) { - // for folder we can only update the snnipet and type - String snippet = note.has(NoteColumns.SNIPPET) ? note - .getString(NoteColumns.SNIPPET) : ""; - if (mIsCreate || !mSnippet.equals(snippet)) { - mDiffNoteValues.put(NoteColumns.SNIPPET, snippet); - } - mSnippet = snippet; - - int type = note.has(NoteColumns.TYPE) ? note.getInt(NoteColumns.TYPE) - : Notes.TYPE_NOTE; - if (mIsCreate || mType != type) { - mDiffNoteValues.put(NoteColumns.TYPE, type); - } - mType = type; - } else if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_NOTE) { - JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA); - long id = note.has(NoteColumns.ID) ? note.getLong(NoteColumns.ID) : INVALID_ID; - if (mIsCreate || mId != id) { - mDiffNoteValues.put(NoteColumns.ID, id); - } - mId = id; - - long alertDate = note.has(NoteColumns.ALERTED_DATE) ? note - .getLong(NoteColumns.ALERTED_DATE) : 0; - if (mIsCreate || mAlertDate != alertDate) { - mDiffNoteValues.put(NoteColumns.ALERTED_DATE, alertDate); - } - mAlertDate = alertDate; - - int bgColorId = note.has(NoteColumns.BG_COLOR_ID) ? note - .getInt(NoteColumns.BG_COLOR_ID) : ResourceParser.getDefaultBgId(mContext); - if (mIsCreate || mBgColorId != bgColorId) { - mDiffNoteValues.put(NoteColumns.BG_COLOR_ID, bgColorId); - } - mBgColorId = bgColorId; - - long createDate = note.has(NoteColumns.CREATED_DATE) ? note - .getLong(NoteColumns.CREATED_DATE) : System.currentTimeMillis(); - if (mIsCreate || mCreatedDate != createDate) { - mDiffNoteValues.put(NoteColumns.CREATED_DATE, createDate); - } - mCreatedDate = createDate; - - int hasAttachment = note.has(NoteColumns.HAS_ATTACHMENT) ? note - .getInt(NoteColumns.HAS_ATTACHMENT) : 0; - if (mIsCreate || mHasAttachment != hasAttachment) { - mDiffNoteValues.put(NoteColumns.HAS_ATTACHMENT, hasAttachment); - } - mHasAttachment = hasAttachment; - - long modifiedDate = note.has(NoteColumns.MODIFIED_DATE) ? note - .getLong(NoteColumns.MODIFIED_DATE) : System.currentTimeMillis(); - if (mIsCreate || mModifiedDate != modifiedDate) { - mDiffNoteValues.put(NoteColumns.MODIFIED_DATE, modifiedDate); - } - mModifiedDate = modifiedDate; - - long parentId = note.has(NoteColumns.PARENT_ID) ? note - .getLong(NoteColumns.PARENT_ID) : 0; - if (mIsCreate || mParentId != parentId) { - mDiffNoteValues.put(NoteColumns.PARENT_ID, parentId); - } - mParentId = parentId; - - String snippet = note.has(NoteColumns.SNIPPET) ? note - .getString(NoteColumns.SNIPPET) : ""; - if (mIsCreate || !mSnippet.equals(snippet)) { - mDiffNoteValues.put(NoteColumns.SNIPPET, snippet); - } - mSnippet = snippet; - - int type = note.has(NoteColumns.TYPE) ? note.getInt(NoteColumns.TYPE) - : Notes.TYPE_NOTE; - if (mIsCreate || mType != type) { - mDiffNoteValues.put(NoteColumns.TYPE, type); - } - mType = type; - - int widgetId = note.has(NoteColumns.WIDGET_ID) ? note.getInt(NoteColumns.WIDGET_ID) - : AppWidgetManager.INVALID_APPWIDGET_ID; - if (mIsCreate || mWidgetId != widgetId) { - mDiffNoteValues.put(NoteColumns.WIDGET_ID, widgetId); - } - mWidgetId = widgetId; - - int widgetType = note.has(NoteColumns.WIDGET_TYPE) ? note - .getInt(NoteColumns.WIDGET_TYPE) : Notes.TYPE_WIDGET_INVALIDE; - if (mIsCreate || mWidgetType != widgetType) { - mDiffNoteValues.put(NoteColumns.WIDGET_TYPE, widgetType); - } - mWidgetType = widgetType; - - long originParent = note.has(NoteColumns.ORIGIN_PARENT_ID) ? note - .getLong(NoteColumns.ORIGIN_PARENT_ID) : 0; - if (mIsCreate || mOriginParent != originParent) { - mDiffNoteValues.put(NoteColumns.ORIGIN_PARENT_ID, originParent); - } - mOriginParent = originParent; - - for (int i = 0; i < dataArray.length(); i++) { - JSONObject data = dataArray.getJSONObject(i); - SqlData sqlData = null; - if (data.has(DataColumns.ID)) { - long dataId = data.getLong(DataColumns.ID); - for (SqlData temp : mDataList) { - if (dataId == temp.getId()) { - sqlData = temp; - } - } - } - - if (sqlData == null) { - sqlData = new SqlData(mContext); - mDataList.add(sqlData); - } - - sqlData.setContent(data); - } - } - } catch (JSONException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - return false; - } - return true; - } - - public JSONObject getContent() { - try { - JSONObject js = new JSONObject(); - - if (mIsCreate) { - Log.e(TAG, "it seems that we haven't created this in database yet"); - return null; - } - - JSONObject note = new JSONObject(); - if (mType == Notes.TYPE_NOTE) { - note.put(NoteColumns.ID, mId); - note.put(NoteColumns.ALERTED_DATE, mAlertDate); - note.put(NoteColumns.BG_COLOR_ID, mBgColorId); - note.put(NoteColumns.CREATED_DATE, mCreatedDate); - note.put(NoteColumns.HAS_ATTACHMENT, mHasAttachment); - note.put(NoteColumns.MODIFIED_DATE, mModifiedDate); - note.put(NoteColumns.PARENT_ID, mParentId); - note.put(NoteColumns.SNIPPET, mSnippet); - note.put(NoteColumns.TYPE, mType); - note.put(NoteColumns.WIDGET_ID, mWidgetId); - note.put(NoteColumns.WIDGET_TYPE, mWidgetType); - note.put(NoteColumns.ORIGIN_PARENT_ID, mOriginParent); - js.put(GTaskStringUtils.META_HEAD_NOTE, note); - - JSONArray dataArray = new JSONArray(); - for (SqlData sqlData : mDataList) { - JSONObject data = sqlData.getContent(); - if (data != null) { - dataArray.put(data); - } - } - js.put(GTaskStringUtils.META_HEAD_DATA, dataArray); - } else if (mType == Notes.TYPE_FOLDER || mType == Notes.TYPE_SYSTEM) { - note.put(NoteColumns.ID, mId); - note.put(NoteColumns.TYPE, mType); - note.put(NoteColumns.SNIPPET, mSnippet); - js.put(GTaskStringUtils.META_HEAD_NOTE, note); - } - - return js; - } catch (JSONException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - } - return null; - } - - public void setParentId(long id) { - mParentId = id; - mDiffNoteValues.put(NoteColumns.PARENT_ID, id); - } - - public void setGtaskId(String gid) { - mDiffNoteValues.put(NoteColumns.GTASK_ID, gid); - } - - public void setSyncId(long syncId) { - mDiffNoteValues.put(NoteColumns.SYNC_ID, syncId); - } - - public void resetLocalModified() { - mDiffNoteValues.put(NoteColumns.LOCAL_MODIFIED, 0); - } - - public long getId() { - return mId; - } - - public long getParentId() { - return mParentId; - } - - public String getSnippet() { - return mSnippet; - } - - public boolean isNoteType() { - return mType == Notes.TYPE_NOTE; - } - - public void commit(boolean validateVersion) { - if (mIsCreate) { - if (mId == INVALID_ID && mDiffNoteValues.containsKey(NoteColumns.ID)) { - mDiffNoteValues.remove(NoteColumns.ID); - } - - Uri uri = mContentResolver.insert(Notes.CONTENT_NOTE_URI, mDiffNoteValues); - try { - mId = Long.valueOf(uri.getPathSegments().get(1)); - } catch (NumberFormatException e) { - Log.e(TAG, "Get note id error :" + e.toString()); - throw new ActionFailureException("create note failed"); - } - if (mId == 0) { - throw new IllegalStateException("Create thread id failed"); - } - - if (mType == Notes.TYPE_NOTE) { - for (SqlData sqlData : mDataList) { - sqlData.commit(mId, false, -1); - } - } - } else { - if (mId <= 0 && mId != Notes.ID_ROOT_FOLDER && mId != Notes.ID_CALL_RECORD_FOLDER) { - Log.e(TAG, "No such note"); - throw new IllegalStateException("Try to update note with invalid id"); - } - if (mDiffNoteValues.size() > 0) { - mVersion ++; - int result = 0; - if (!validateVersion) { - result = mContentResolver.update(Notes.CONTENT_NOTE_URI, mDiffNoteValues, "(" - + NoteColumns.ID + "=?)", new String[] { - String.valueOf(mId) - }); - } else { - result = mContentResolver.update(Notes.CONTENT_NOTE_URI, mDiffNoteValues, "(" - + NoteColumns.ID + "=?) AND (" + NoteColumns.VERSION + "<=?)", - new String[] { - String.valueOf(mId), String.valueOf(mVersion) - }); - } - if (result == 0) { - Log.w(TAG, "there is no update. maybe user updates note when syncing"); - } - } - - if (mType == Notes.TYPE_NOTE) { - for (SqlData sqlData : mDataList) { - sqlData.commit(mId, validateVersion, mVersion); - } - } - } - - // refresh local info - loadFromCursor(mId); - if (mType == Notes.TYPE_NOTE) - loadDataContent(); - - mDiffNoteValues.clear(); - mIsCreate = false; - } -} diff --git a/src/net/micode/notes/data/Contact.java b/src/net/micode/notes/data/Contact.java new file mode 100644 index 0000000..df62799 --- /dev/null +++ b/src/net/micode/notes/data/Contact.java @@ -0,0 +1,114 @@ +/* + * 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.data; // 属于小米便签的数据层包,专门处理数据相关操作 + +import android.content.Context; // Android上下文,用于访问应用资源 +import android.database.Cursor; // 数据库查询结果集,用于遍历查询结果 +import android.provider.ContactsContract.CommonDataKinds.Phone; // 通讯录电话号码相关常量 +import android.provider.ContactsContract.Data; // 通讯录数据相关常量 +import android.telephony.PhoneNumberUtils; // 电话号码工具类,用于格式化比较号码 +import android.util.Log; // Android日志工具 + +import java.util.HashMap; // HashMap集合,用于缓存联系人信息 + +/** + * Contact 类 - 联系人工具类 + * 功能:根据电话号码查询通讯录获取联系人姓名 + * 设计模式:使用单例模式和缓存机制优化性能 + * 技术特点: + * 1. 使用Android ContentProvider访问系统通讯录 + * 2. 实现LRU缓存机制避免重复查询 + * 3. 使用电话号码模糊匹配算法 + */ +public class Contact { + // 静态联系人缓存:使用HashMap存储<电话号码, 联系人姓名>键值对 + // 使用静态变量实现应用级别的缓存,减少对通讯录的频繁访问 + private static HashMap sContactCache; + + // 日志标签,用于调试和问题追踪 + private static final String TAG = "Contact"; + + // 查询条件语句模板:用于构建查询通讯录的SQL WHERE条件 + // 注意:这里使用"+"作为占位符,后面会被替换为最小匹配位数 + // PHONE_NUMBERS_EQUAL()是Android提供的特殊函数,用于电话号码匹配 + private static final String CALLER_ID_SELECTION = "PHONE_NUMBERS_EQUAL(" + Phone.NUMBER + + ",?) AND " + Data.MIMETYPE + "='" + Phone.CONTENT_ITEM_TYPE + "'" + + " AND " + Data.RAW_CONTACT_ID + " IN " + + "(SELECT raw_contact_id " + + " FROM phone_lookup" // phone_lookup是Android系统的电话号码查询优化表 + + " WHERE min_match = '+')"; // "+"是占位符,会被替换为实际的最小匹配位数 + + /** + * 根据电话号码获取联系人姓名 + * @param context Android上下文,用于访问ContentResolver + * @param phoneNumber 要查询的电话号码 + * @return 联系人姓名,如果未找到则返回null + * 方法逻辑: + * 1. 首先检查缓存中是否已有该号码对应的联系人 + * 2. 如果缓存未命中,查询系统通讯录 + * 3. 将查询结果存入缓存 + * 4. 返回查询结果 + */ + public static String getContact(Context context, String phoneNumber) { + // 延迟初始化缓存:第一次使用时才创建HashMap + if(sContactCache == null) { + sContactCache = new HashMap(); + } + + // 检查缓存:如果缓存中已有该号码,直接返回缓存的联系人姓名 + if(sContactCache.containsKey(phoneNumber)) { + return sContactCache.get(phoneNumber); + } + + // 构建查询条件:替换占位符"+"为电话号码的最小匹配位数 + // PhoneNumberUtils.toCallerIDMinMatch()计算电话号码的最小匹配位数 + String selection = CALLER_ID_SELECTION.replace("+", + PhoneNumberUtils.toCallerIDMinMatch(phoneNumber)); + + // 通过ContentResolver查询系统通讯录 + // Data.CONTENT_URI:通讯录数据的统一资源标识符 + // 查询条件:电话号码匹配且数据类型为电话号码 + Cursor cursor = context.getContentResolver().query( + Data.CONTENT_URI, // 通讯录数据URI + new String [] { Phone.DISPLAY_NAME }, // 只查询显示名字段 + selection, // 查询条件 + new String[] { phoneNumber }, // 查询参数:电话号码 + null); // 排序条件:不排序 + + // 处理查询结果 + if (cursor != null && cursor.moveToFirst()) { + try { + // 获取第一条结果的联系人姓名 + String name = cursor.getString(0); + // 将结果存入缓存 + sContactCache.put(phoneNumber, name); + return name; + } catch (IndexOutOfBoundsException e) { + // 捕获数组越界异常,记录错误日志 + Log.e(TAG, " Cursor get string error " + e.toString()); + return null; + } finally { + // 无论是否成功,都必须关闭Cursor释放资源 + cursor.close(); + } + } else { + // 没有找到匹配的联系人,记录调试日志 + Log.d(TAG, "No contact matched with number:" + phoneNumber); + return null; + } + } +} \ No newline at end of file diff --git a/src/data/Notes.java b/src/net/micode/notes/data/Notes.java similarity index 54% rename from src/data/Notes.java rename to src/net/micode/notes/data/Notes.java index f240604..24c05c0 100644 --- a/src/data/Notes.java +++ b/src/net/micode/notes/data/Notes.java @@ -17,24 +17,28 @@ package net.micode.notes.data; import android.net.Uri; + +/** + * Notes 类 - 便签应用的核心常量定义类 + * 包含数据库表定义、ContentProvider URI、Intent键、便签类型等常量 + * 采用接口定义列名和嵌套类分层设计 + */ public class Notes { public static final String AUTHORITY = "micode_notes"; public static final String TAG = "Notes"; + + // 便签类型 public static final int TYPE_NOTE = 0; public static final int TYPE_FOLDER = 1; public static final int TYPE_SYSTEM = 2; - /** - * Following IDs are system folders' identifiers - * {@link Notes#ID_ROOT_FOLDER } is default folder - * {@link Notes#ID_TEMPARAY_FOLDER } is for notes belonging no folder - * {@link Notes#ID_CALL_RECORD_FOLDER} is to store call records - */ + // 系统文件夹ID public static final int ID_ROOT_FOLDER = 0; public static final int ID_TEMPARAY_FOLDER = -1; public static final int ID_CALL_RECORD_FOLDER = -2; public static final int ID_TRASH_FOLER = -3; + // Intent Extra键 public static final String INTENT_EXTRA_ALERT_DATE = "net.micode.notes.alert_date"; public static final String INTENT_EXTRA_BACKGROUND_ID = "net.micode.notes.background_color_id"; public static final String INTENT_EXTRA_WIDGET_ID = "net.micode.notes.widget_id"; @@ -42,238 +46,83 @@ public class Notes { public static final String INTENT_EXTRA_FOLDER_ID = "net.micode.notes.folder_id"; public static final String INTENT_EXTRA_CALL_DATE = "net.micode.notes.call_date"; + // 小部件类型 public static final int TYPE_WIDGET_INVALIDE = -1; public static final int TYPE_WIDGET_2X = 0; public static final int TYPE_WIDGET_4X = 1; + /** + * DataConstants - 数据类型常量 + */ public static class DataConstants { public static final String NOTE = TextNote.CONTENT_ITEM_TYPE; public static final String CALL_NOTE = CallNote.CONTENT_ITEM_TYPE; } - /** - * Uri to query all notes and folders - */ public static final Uri CONTENT_NOTE_URI = Uri.parse("content://" + AUTHORITY + "/note"); + public static final Uri CONTENT_DATA_URI = Uri.parse("content://" + AUTHORITY + "/data"); /** - * Uri to query data + * NoteColumns - 便签表列定义 */ - public static final Uri CONTENT_DATA_URI = Uri.parse("content://" + AUTHORITY + "/data"); - public interface NoteColumns { - /** - * The unique ID for a row - *

Type: INTEGER (long)

- */ public static final String ID = "_id"; - - /** - * The parent's id for note or folder - *

Type: INTEGER (long)

- */ public static final String PARENT_ID = "parent_id"; - - /** - * Created data for note or folder - *

Type: INTEGER (long)

- */ public static final String CREATED_DATE = "created_date"; - - /** - * Latest modified date - *

Type: INTEGER (long)

- */ public static final String MODIFIED_DATE = "modified_date"; - - - /** - * Alert date - *

Type: INTEGER (long)

- */ public static final String ALERTED_DATE = "alert_date"; - - /** - * Folder's name or text content of note - *

Type: TEXT

- */ public static final String SNIPPET = "snippet"; - - /** - * Note's widget id - *

Type: INTEGER (long)

- */ public static final String WIDGET_ID = "widget_id"; - - /** - * Note's widget type - *

Type: INTEGER (long)

- */ public static final String WIDGET_TYPE = "widget_type"; - - /** - * Note's background color's id - *

Type: INTEGER (long)

- */ public static final String BG_COLOR_ID = "bg_color_id"; - - /** - * For text note, it doesn't has attachment, for multi-media - * note, it has at least one attachment - *

Type: INTEGER

- */ public static final String HAS_ATTACHMENT = "has_attachment"; - - /** - * Folder's count of notes - *

Type: INTEGER (long)

- */ public static final String NOTES_COUNT = "notes_count"; - - /** - * The file type: folder or note - *

Type: INTEGER

- */ public static final String TYPE = "type"; - - /** - * The last sync id - *

Type: INTEGER (long)

- */ public static final String SYNC_ID = "sync_id"; - - /** - * Sign to indicate local modified or not - *

Type: INTEGER

- */ public static final String LOCAL_MODIFIED = "local_modified"; - - /** - * Original parent id before moving into temporary folder - *

Type : INTEGER

- */ public static final String ORIGIN_PARENT_ID = "origin_parent_id"; - - /** - * The gtask id - *

Type : TEXT

- */ public static final String GTASK_ID = "gtask_id"; - - /** - * The version code - *

Type : INTEGER (long)

- */ public static final String VERSION = "version"; } + /** + * DataColumns - 便签数据表列定义,采用EAV模型 + */ public interface DataColumns { - /** - * The unique ID for a row - *

Type: INTEGER (long)

- */ public static final String ID = "_id"; - - /** - * The MIME type of the item represented by this row. - *

Type: Text

- */ public static final String MIME_TYPE = "mime_type"; - - /** - * The reference id to note that this data belongs to - *

Type: INTEGER (long)

- */ public static final String NOTE_ID = "note_id"; - - /** - * Created data for note or folder - *

Type: INTEGER (long)

- */ public static final String CREATED_DATE = "created_date"; - - /** - * Latest modified date - *

Type: INTEGER (long)

- */ public static final String MODIFIED_DATE = "modified_date"; - - /** - * Data's content - *

Type: TEXT

- */ public static final String CONTENT = "content"; - - - /** - * Generic data column, the meaning is {@link #MIMETYPE} specific, used for - * integer data type - *

Type: INTEGER

- */ public static final String DATA1 = "data1"; - - /** - * Generic data column, the meaning is {@link #MIMETYPE} specific, used for - * integer data type - *

Type: INTEGER

- */ public static final String DATA2 = "data2"; - - /** - * Generic data column, the meaning is {@link #MIMETYPE} specific, used for - * TEXT data type - *

Type: TEXT

- */ public static final String DATA3 = "data3"; - - /** - * Generic data column, the meaning is {@link #MIMETYPE} specific, used for - * TEXT data type - *

Type: TEXT

- */ public static final String DATA4 = "data4"; - - /** - * Generic data column, the meaning is {@link #MIMETYPE} specific, used for - * TEXT data type - *

Type: TEXT

- */ public static final String DATA5 = "data5"; } + /** + * TextNote - 文本便签的特定列定义 + */ public static final class TextNote implements DataColumns { - /** - * Mode to indicate the text in check list mode or not - *

Type: Integer 1:check list mode 0: normal mode

- */ - public static final String MODE = DATA1; - + public static final String MODE = DATA1; // 1:清单模式 0:普通模式 public static final int MODE_CHECK_LIST = 1; public static final String CONTENT_TYPE = "vnd.android.cursor.dir/text_note"; - public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/text_note"; - public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/text_note"); } + /** + * CallNote - 通话记录便签的特定列定义 + */ public static final class CallNote implements DataColumns { - /** - * Call date for this record - *

Type: INTEGER (long)

- */ - public static final String CALL_DATE = DATA1; - - /** - * Phone number for this record - *

Type: TEXT

- */ - public static final String PHONE_NUMBER = DATA3; + public static final String CALL_DATE = DATA1; // 通话日期 + public static final String PHONE_NUMBER = DATA3; // 电话号码 public static final String CONTENT_TYPE = "vnd.android.cursor.dir/call_note"; - public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/call_note"; - public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/call_note"); } -} +} \ No newline at end of file diff --git a/src/net/micode/notes/data/NotesDatabaseHelper.java b/src/net/micode/notes/data/NotesDatabaseHelper.java new file mode 100644 index 0000000..826bdf2 --- /dev/null +++ b/src/net/micode/notes/data/NotesDatabaseHelper.java @@ -0,0 +1,403 @@ +/* + * 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.data; + +import android.content.ContentValues; +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.util.Log; + +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.DataConstants; +import net.micode.notes.data.Notes.NoteColumns; + +/** + * NotesDatabaseHelper 类 - 便签数据库辅助类 + * + * 功能:管理便签应用的SQLite数据库创建和版本升级 + * 继承自SQLiteOpenHelper,实现数据库的创建、升级和初始化 + * 包含note表和data表,以及相关的触发器定义 + */ +public class NotesDatabaseHelper extends SQLiteOpenHelper { + // 数据库名称 + private static final String DB_NAME = "note.db"; + // 数据库版本号,用于数据库升级管理 + private static final int DB_VERSION = 4; + + // 表名常量接口 + public interface TABLE { + public static final String NOTE = "note"; // 便签主表 + public static final String DATA = "data"; // 便签数据表 + } + + private static final String TAG = "NotesDatabaseHelper"; // 日志标签 + + private static NotesDatabaseHelper mInstance; // 单例实例 + + // 创建note表的SQL语句 + private static final String CREATE_NOTE_TABLE_SQL = + "CREATE TABLE " + TABLE.NOTE + "(" + + NoteColumns.ID + " INTEGER PRIMARY KEY," + + NoteColumns.PARENT_ID + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.ALERTED_DATE + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.BG_COLOR_ID + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + + NoteColumns.HAS_ATTACHMENT + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + + NoteColumns.NOTES_COUNT + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.SNIPPET + " TEXT NOT NULL DEFAULT ''," + + NoteColumns.TYPE + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.WIDGET_ID + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.WIDGET_TYPE + " INTEGER NOT NULL DEFAULT -1," + + NoteColumns.SYNC_ID + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.LOCAL_MODIFIED + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.ORIGIN_PARENT_ID + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''," + + NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0" + + ")"; + + // 创建data表的SQL语句 + private static final String CREATE_DATA_TABLE_SQL = + "CREATE TABLE " + TABLE.DATA + "(" + + DataColumns.ID + " INTEGER PRIMARY KEY," + + DataColumns.MIME_TYPE + " TEXT NOT NULL," + + DataColumns.NOTE_ID + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + + NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + + DataColumns.CONTENT + " TEXT NOT NULL DEFAULT ''," + + DataColumns.DATA1 + " INTEGER," + + DataColumns.DATA2 + " INTEGER," + + DataColumns.DATA3 + " TEXT NOT NULL DEFAULT ''," + + DataColumns.DATA4 + " TEXT NOT NULL DEFAULT ''," + + DataColumns.DATA5 + " TEXT NOT NULL DEFAULT ''" + + ")"; + + // 创建data表note_id索引的SQL语句 + private static final String CREATE_DATA_NOTE_ID_INDEX_SQL = + "CREATE INDEX IF NOT EXISTS note_id_index ON " + + TABLE.DATA + "(" + DataColumns.NOTE_ID + ");"; + + // 触发器:当更新便签的父文件夹时,增加目标文件夹的便签计数 + private static final String NOTE_INCREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER = + "CREATE TRIGGER increase_folder_count_on_update "+ + " AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE + + " BEGIN " + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" + + " WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" + + " END"; + + // 触发器:当更新便签的父文件夹时,减少原文件夹的便签计数 + private static final String NOTE_DECREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER = + "CREATE TRIGGER decrease_folder_count_on_update " + + " AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE + + " BEGIN " + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" + + " WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID + + " AND " + NoteColumns.NOTES_COUNT + ">0" + ";" + + " END"; + + // 触发器:当插入新便签时,增加父文件夹的便签计数 + private static final String NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER = + "CREATE TRIGGER increase_folder_count_on_insert " + + " AFTER INSERT ON " + TABLE.NOTE + + " BEGIN " + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" + + " WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" + + " END"; + + // 触发器:当删除便签时,减少父文件夹的便签计数 + private static final String NOTE_DECREASE_FOLDER_COUNT_ON_DELETE_TRIGGER = + "CREATE TRIGGER decrease_folder_count_on_delete " + + " AFTER DELETE ON " + TABLE.NOTE + + " BEGIN " + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" + + " WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID + + " AND " + NoteColumns.NOTES_COUNT + ">0;" + + " END"; + + // 触发器:当插入数据且类型为普通便签时,更新便签的snippet字段 + private static final String DATA_UPDATE_NOTE_CONTENT_ON_INSERT_TRIGGER = + "CREATE TRIGGER update_note_content_on_insert " + + " AFTER INSERT ON " + TABLE.DATA + + " WHEN new." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" + + " BEGIN" + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT + + " WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" + + " END"; + + // 触发器:当更新数据且类型为普通便签时,更新便签的snippet字段 + private static final String DATA_UPDATE_NOTE_CONTENT_ON_UPDATE_TRIGGER = + "CREATE TRIGGER update_note_content_on_update " + + " AFTER UPDATE ON " + TABLE.DATA + + " WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" + + " BEGIN" + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT + + " WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" + + " END"; + + // 触发器:当删除数据且类型为普通便签时,清空便签的snippet字段 + private static final String DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER = + "CREATE TRIGGER update_note_content_on_delete " + + " AFTER delete ON " + TABLE.DATA + + " WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" + + " BEGIN" + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.SNIPPET + "=''" + + " WHERE " + NoteColumns.ID + "=old." + DataColumns.NOTE_ID + ";" + + " END"; + + // 触发器:当删除便签时,同步删除该便签的所有数据 + private static final String NOTE_DELETE_DATA_ON_DELETE_TRIGGER = + "CREATE TRIGGER delete_data_on_delete " + + " AFTER DELETE ON " + TABLE.NOTE + + " BEGIN" + + " DELETE FROM " + TABLE.DATA + + " WHERE " + DataColumns.NOTE_ID + "=old." + NoteColumns.ID + ";" + + " END"; + + // 触发器:当删除文件夹时,同步删除该文件夹下的所有便签(级联删除) + private static final String FOLDER_DELETE_NOTES_ON_DELETE_TRIGGER = + "CREATE TRIGGER folder_delete_notes_on_delete " + + " AFTER DELETE ON " + TABLE.NOTE + + " BEGIN" + + " DELETE FROM " + TABLE.NOTE + + " WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" + + " END"; + + // 触发器:当文件夹被移动到回收站时,将该文件夹下的所有便签也移动到回收站 + private static final String FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER = + "CREATE TRIGGER folder_move_notes_on_trash " + + " AFTER UPDATE ON " + TABLE.NOTE + + " WHEN new." + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER + + " BEGIN" + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER + + " WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" + + " END"; + + /** + * 构造方法 + * @param context Android上下文 + */ + public NotesDatabaseHelper(Context context) { + super(context, DB_NAME, null, DB_VERSION); + } + + /** + * 创建note表及其触发器和系统文件夹 + * @param db SQLiteDatabase对象 + */ + public void createNoteTable(SQLiteDatabase db) { + db.execSQL(CREATE_NOTE_TABLE_SQL); + reCreateNoteTableTriggers(db); + createSystemFolder(db); + Log.d(TAG, "note table has been created"); + } + + /** + * 重新创建note表的所有触发器(先删除后创建) + * @param db SQLiteDatabase对象 + */ + private void reCreateNoteTableTriggers(SQLiteDatabase db) { + db.execSQL("DROP TRIGGER IF EXISTS increase_folder_count_on_update"); + db.execSQL("DROP TRIGGER IF EXISTS decrease_folder_count_on_update"); + db.execSQL("DROP TRIGGER IF EXISTS decrease_folder_count_on_delete"); + db.execSQL("DROP TRIGGER IF EXISTS delete_data_on_delete"); + db.execSQL("DROP TRIGGER IF EXISTS increase_folder_count_on_insert"); + db.execSQL("DROP TRIGGER IF EXISTS folder_delete_notes_on_delete"); + db.execSQL("DROP TRIGGER IF EXISTS folder_move_notes_on_trash"); + + db.execSQL(NOTE_INCREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER); + db.execSQL(NOTE_DECREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER); + db.execSQL(NOTE_DECREASE_FOLDER_COUNT_ON_DELETE_TRIGGER); + db.execSQL(NOTE_DELETE_DATA_ON_DELETE_TRIGGER); + db.execSQL(NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER); + db.execSQL(FOLDER_DELETE_NOTES_ON_DELETE_TRIGGER); + db.execSQL(FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER); + } + + /** + * 创建系统文件夹(通话记录文件夹、根文件夹、临时文件夹、回收站文件夹) + * @param db SQLiteDatabase对象 + */ + private void createSystemFolder(SQLiteDatabase db) { + ContentValues values = new ContentValues(); + + // 通话记录文件夹 + values.put(NoteColumns.ID, Notes.ID_CALL_RECORD_FOLDER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + + // 根文件夹(默认文件夹) + values.clear(); + values.put(NoteColumns.ID, Notes.ID_ROOT_FOLDER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + + // 临时文件夹(用于移动便签时的暂存) + values.clear(); + values.put(NoteColumns.ID, Notes.ID_TEMPARAY_FOLDER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + + // 回收站文件夹 + values.clear(); + values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + } + + /** + * 创建data表及其触发器和索引 + * @param db SQLiteDatabase对象 + */ + public void createDataTable(SQLiteDatabase db) { + db.execSQL(CREATE_DATA_TABLE_SQL); + reCreateDataTableTriggers(db); + db.execSQL(CREATE_DATA_NOTE_ID_INDEX_SQL); + Log.d(TAG, "data table has been created"); + } + + /** + * 重新创建data表的所有触发器(先删除后创建) + * @param db SQLiteDatabase对象 + */ + private void reCreateDataTableTriggers(SQLiteDatabase db) { + db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_insert"); + db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_update"); + db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_delete"); + + db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_INSERT_TRIGGER); + db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_UPDATE_TRIGGER); + db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER); + } + + /** + * 获取单例实例(线程安全) + * @param context Android上下文 + * @return NotesDatabaseHelper单例实例 + */ + static synchronized NotesDatabaseHelper getInstance(Context context) { + if (mInstance == null) { + mInstance = new NotesDatabaseHelper(context); + } + return mInstance; + } + + /** + * 数据库首次创建时调用,创建所有表和触发器 + * @param db SQLiteDatabase对象 + */ + @Override + public void onCreate(SQLiteDatabase db) { + createNoteTable(db); + createDataTable(db); + } + + /** + * 数据库升级时调用,根据旧版本号逐步升级 + * @param db SQLiteDatabase对象 + * @param oldVersion 旧版本号 + * @param newVersion 新版本号 + */ + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + boolean reCreateTriggers = false; + boolean skipV2 = false; + + // 从版本1升级到版本2 + if (oldVersion == 1) { + upgradeToV2(db); + skipV2 = true; // 此次升级已包含从v2到v3的升级 + oldVersion++; + } + + // 从版本2升级到版本3(如果未跳过) + if (oldVersion == 2 && !skipV2) { + upgradeToV3(db); + reCreateTriggers = true; + oldVersion++; + } + + // 从版本3升级到版本4 + if (oldVersion == 3) { + upgradeToV4(db); + oldVersion++; + } + + // 如果需要,重新创建触发器 + if (reCreateTriggers) { + reCreateNoteTableTriggers(db); + reCreateDataTableTriggers(db); + } + + // 如果版本号不匹配,抛出异常 + if (oldVersion != newVersion) { + throw new IllegalStateException("Upgrade notes database to version " + newVersion + + "fails"); + } + } + + /** + * 升级到版本2:删除旧表并重新创建 + * @param db SQLiteDatabase对象 + */ + private void upgradeToV2(SQLiteDatabase db) { + db.execSQL("DROP TABLE IF EXISTS " + TABLE.NOTE); + db.execSQL("DROP TABLE IF EXISTS " + TABLE.DATA); + createNoteTable(db); + createDataTable(db); + } + + /** + * 升级到版本3: + * 1. 删除不再使用的触发器 + * 2. 添加gtask_id列 + * 3. 添加回收站系统文件夹 + * @param db SQLiteDatabase对象 + */ + private void upgradeToV3(SQLiteDatabase db) { + // 删除不再使用的触发器 + db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_insert"); + db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_delete"); + db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_update"); + // 添加gtask_id列 + db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.GTASK_ID + + " TEXT NOT NULL DEFAULT ''"); + // 添加回收站系统文件夹 + ContentValues values = new ContentValues(); + values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + } + + /** + * 升级到版本4:添加version列 + * @param db SQLiteDatabase对象 + */ + private void upgradeToV4(SQLiteDatabase db) { + db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.VERSION + + " INTEGER NOT NULL DEFAULT 0"); + } +} \ No newline at end of file diff --git a/src/data/NotesProvider.java b/src/net/micode/notes/data/NotesProvider.java similarity index 62% rename from src/data/NotesProvider.java rename to src/net/micode/notes/data/NotesProvider.java index edb0a60..6e71154 100644 --- a/src/data/NotesProvider.java +++ b/src/net/micode/notes/data/NotesProvider.java @@ -16,7 +16,6 @@ package net.micode.notes.data; - import android.app.SearchManager; import android.content.ContentProvider; import android.content.ContentUris; @@ -34,22 +33,28 @@ import net.micode.notes.data.Notes.DataColumns; import net.micode.notes.data.Notes.NoteColumns; import net.micode.notes.data.NotesDatabaseHelper.TABLE; - +/** + * NotesProvider 类 - 便签应用的ContentProvider + * + * 功能:为便签应用提供数据访问接口,通过ContentProvider机制实现数据共享 + * 继承自ContentProvider,实现对note表和data表的CRUD操作,并支持搜索功能 + */ public class NotesProvider extends ContentProvider { - private static final UriMatcher mMatcher; - - private NotesDatabaseHelper mHelper; + private static final UriMatcher mMatcher; // URI匹配器,用于匹配不同的URI请求 - private static final String TAG = "NotesProvider"; + private NotesDatabaseHelper mHelper; // 数据库帮助类实例 - private static final int URI_NOTE = 1; - private static final int URI_NOTE_ITEM = 2; - private static final int URI_DATA = 3; - private static final int URI_DATA_ITEM = 4; + private static final String TAG = "NotesProvider"; // 日志标签 - private static final int URI_SEARCH = 5; - private static final int URI_SEARCH_SUGGEST = 6; + // URI匹配常量定义 + private static final int URI_NOTE = 1; // 匹配note表所有记录 + private static final int URI_NOTE_ITEM = 2; // 匹配note表单条记录 + private static final int URI_DATA = 3; // 匹配data表所有记录 + private static final int URI_DATA_ITEM = 4; // 匹配data表单条记录 + private static final int URI_SEARCH = 5; // 匹配搜索请求 + private static final int URI_SEARCH_SUGGEST = 6; // 匹配搜索建议请求 + // 静态初始化块:初始化URI匹配器,添加各种URI模式 static { mMatcher = new UriMatcher(UriMatcher.NO_MATCH); mMatcher.addURI(Notes.AUTHORITY, "note", URI_NOTE); @@ -62,76 +67,92 @@ public class NotesProvider extends ContentProvider { } /** - * x'0A' represents the '\n' character in sqlite. For title and content in the search result, - * we will trim '\n' and white space in order to show more information. + * 搜索投影字段定义: + * x'0A' 表示SQLite中的换行符'\n' + * 对于搜索结果中的标题和内容,我们将修剪'\n'和空格以显示更多信息 */ private static final String NOTES_SEARCH_PROJECTION = NoteColumns.ID + "," - + NoteColumns.ID + " AS " + SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA + "," - + "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_1 + "," - + "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_2 + "," - + R.drawable.search_result + " AS " + SearchManager.SUGGEST_COLUMN_ICON_1 + "," - + "'" + Intent.ACTION_VIEW + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_ACTION + "," - + "'" + Notes.TextNote.CONTENT_TYPE + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA; + + NoteColumns.ID + " AS " + SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA + "," + + "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_1 + "," + + "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_2 + "," + + R.drawable.search_result + " AS " + SearchManager.SUGGEST_COLUMN_ICON_1 + "," + + "'" + Intent.ACTION_VIEW + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_ACTION + "," + + "'" + Notes.TextNote.CONTENT_TYPE + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA; + // 便签片段搜索查询语句 private static String NOTES_SNIPPET_SEARCH_QUERY = "SELECT " + NOTES_SEARCH_PROJECTION - + " FROM " + TABLE.NOTE - + " WHERE " + NoteColumns.SNIPPET + " LIKE ?" - + " AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER - + " AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE; + + " FROM " + TABLE.NOTE + + " WHERE " + NoteColumns.SNIPPET + " LIKE ?" // 模糊匹配搜索条件 + + " AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER // 排除回收站中的便签 + + " AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE; // 只搜索普通便签类型 + /** + * ContentProvider创建时调用,初始化数据库帮助类 + * @return true表示初始化成功 + */ @Override public boolean onCreate() { mHelper = NotesDatabaseHelper.getInstance(getContext()); return true; } + /** + * 查询方法,根据URI匹配不同的查询逻辑 + * @param uri 请求的URI + * @param projection 要返回的列 + * @param selection 查询条件 + * @param selectionArgs 查询参数 + * @param sortOrder 排序方式 + * @return 查询结果的Cursor对象 + */ @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, - String sortOrder) { + String sortOrder) { Cursor c = null; SQLiteDatabase db = mHelper.getReadableDatabase(); String id = null; switch (mMatcher.match(uri)) { - case URI_NOTE: + case URI_NOTE: // 查询note表所有记录 c = db.query(TABLE.NOTE, projection, selection, selectionArgs, null, null, sortOrder); break; - case URI_NOTE_ITEM: - id = uri.getPathSegments().get(1); + case URI_NOTE_ITEM: // 查询note表单条记录 + id = uri.getPathSegments().get(1); // 从URI路径段中获取ID c = db.query(TABLE.NOTE, projection, NoteColumns.ID + "=" + id + parseSelection(selection), selectionArgs, null, null, sortOrder); break; - case URI_DATA: + case URI_DATA: // 查询data表所有记录 c = db.query(TABLE.DATA, projection, selection, selectionArgs, null, null, sortOrder); break; - case URI_DATA_ITEM: + case URI_DATA_ITEM: // 查询data表单条记录 id = uri.getPathSegments().get(1); c = db.query(TABLE.DATA, projection, DataColumns.ID + "=" + id + parseSelection(selection), selectionArgs, null, null, sortOrder); break; case URI_SEARCH: - case URI_SEARCH_SUGGEST: + case URI_SEARCH_SUGGEST: // 处理搜索和搜索建议请求 if (sortOrder != null || projection != null) { throw new IllegalArgumentException( "do not specify sortOrder, selection, selectionArgs, or projection" + "with this query"); } String searchString = null; + // 根据URI类型获取搜索字符串 if (mMatcher.match(uri) == URI_SEARCH_SUGGEST) { if (uri.getPathSegments().size() > 1) { searchString = uri.getPathSegments().get(1); } } else { - searchString = uri.getQueryParameter("pattern"); + searchString = uri.getQueryParameter("pattern"); // 从查询参数获取搜索模式 } if (TextUtils.isEmpty(searchString)) { - return null; + return null; // 搜索字符串为空则返回null } try { - searchString = String.format("%%%s%%", searchString); + searchString = String.format("%%%s%%", searchString); // 添加通配符进行模糊匹配 c = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY, new String[] { searchString }); } catch (IllegalStateException ex) { @@ -141,21 +162,28 @@ public class NotesProvider extends ContentProvider { default: throw new IllegalArgumentException("Unknown URI " + uri); } + // 设置通知URI,以便在数据变化时能够通知观察者 if (c != null) { c.setNotificationUri(getContext().getContentResolver(), uri); } return c; } + /** + * 插入数据方法 + * @param uri 请求的URI + * @param values 要插入的数据 + * @return 插入数据的新URI + */ @Override public Uri insert(Uri uri, ContentValues values) { SQLiteDatabase db = mHelper.getWritableDatabase(); long dataId = 0, noteId = 0, insertedId = 0; switch (mMatcher.match(uri)) { - case URI_NOTE: + case URI_NOTE: // 插入note表记录 insertedId = noteId = db.insert(TABLE.NOTE, null, values); break; - case URI_DATA: + case URI_DATA: // 插入data表记录 if (values.containsKey(DataColumns.NOTE_ID)) { noteId = values.getAsLong(DataColumns.NOTE_ID); } else { @@ -166,50 +194,57 @@ public class NotesProvider extends ContentProvider { default: throw new IllegalArgumentException("Unknown URI " + uri); } - // Notify the note uri + // 通知note URI的数据变化 if (noteId > 0) { getContext().getContentResolver().notifyChange( ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null); } - // Notify the data uri + // 通知data URI的数据变化 if (dataId > 0) { getContext().getContentResolver().notifyChange( ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), null); } + // 返回新插入数据的URI return ContentUris.withAppendedId(uri, insertedId); } + /** + * 删除数据方法 + * @param uri 请求的URI + * @param selection 删除条件 + * @param selectionArgs 删除参数 + * @return 删除的记录数 + */ @Override public int delete(Uri uri, String selection, String[] selectionArgs) { int count = 0; String id = null; SQLiteDatabase db = mHelper.getWritableDatabase(); - boolean deleteData = false; + boolean deleteData = false; // 标记是否删除data表数据 switch (mMatcher.match(uri)) { - case URI_NOTE: + case URI_NOTE: // 删除note表记录(系统文件夹不允许删除) selection = "(" + selection + ") AND " + NoteColumns.ID + ">0 "; count = db.delete(TABLE.NOTE, selection, selectionArgs); break; - case URI_NOTE_ITEM: + case URI_NOTE_ITEM: // 删除note表单条记录 id = uri.getPathSegments().get(1); /** - * ID that smaller than 0 is system folder which is not allowed to - * trash + * ID小于0的是系统文件夹,不允许删除 */ long noteId = Long.valueOf(id); if (noteId <= 0) { - break; + break; // 系统文件夹不删除 } count = db.delete(TABLE.NOTE, NoteColumns.ID + "=" + id + parseSelection(selection), selectionArgs); break; - case URI_DATA: + case URI_DATA: // 删除data表记录 count = db.delete(TABLE.DATA, selection, selectionArgs); deleteData = true; break; - case URI_DATA_ITEM: + case URI_DATA_ITEM: // 删除data表单条记录 id = uri.getPathSegments().get(1); count = db.delete(TABLE.DATA, DataColumns.ID + "=" + id + parseSelection(selection), selectionArgs); @@ -218,8 +253,10 @@ public class NotesProvider extends ContentProvider { default: throw new IllegalArgumentException("Unknown URI " + uri); } + // 如果删除了记录,通知数据变化 if (count > 0) { if (deleteData) { + // 删除data表数据时需要通知note URI getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null); } getContext().getContentResolver().notifyChange(uri, null); @@ -227,28 +264,36 @@ public class NotesProvider extends ContentProvider { return count; } + /** + * 更新数据方法 + * @param uri 请求的URI + * @param values 更新的数据 + * @param selection 更新条件 + * @param selectionArgs 更新参数 + * @return 更新的记录数 + */ @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { int count = 0; String id = null; SQLiteDatabase db = mHelper.getWritableDatabase(); - boolean updateData = false; + boolean updateData = false; // 标记是否更新data表数据 switch (mMatcher.match(uri)) { - case URI_NOTE: - increaseNoteVersion(-1, selection, selectionArgs); + case URI_NOTE: // 更新note表记录 + increaseNoteVersion(-1, selection, selectionArgs); // 增加版本号 count = db.update(TABLE.NOTE, values, selection, selectionArgs); break; - case URI_NOTE_ITEM: + case URI_NOTE_ITEM: // 更新note表单条记录 id = uri.getPathSegments().get(1); - increaseNoteVersion(Long.valueOf(id), selection, selectionArgs); + increaseNoteVersion(Long.valueOf(id), selection, selectionArgs); // 增加版本号 count = db.update(TABLE.NOTE, values, NoteColumns.ID + "=" + id + parseSelection(selection), selectionArgs); break; - case URI_DATA: + case URI_DATA: // 更新data表记录 count = db.update(TABLE.DATA, values, selection, selectionArgs); updateData = true; break; - case URI_DATA_ITEM: + case URI_DATA_ITEM: // 更新data表单条记录 id = uri.getPathSegments().get(1); count = db.update(TABLE.DATA, values, DataColumns.ID + "=" + id + parseSelection(selection), selectionArgs); @@ -258,8 +303,10 @@ public class NotesProvider extends ContentProvider { throw new IllegalArgumentException("Unknown URI " + uri); } + // 如果更新了记录,通知数据变化 if (count > 0) { if (updateData) { + // 更新data表数据时需要通知note URI getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null); } getContext().getContentResolver().notifyChange(uri, null); @@ -267,18 +314,30 @@ public class NotesProvider extends ContentProvider { return count; } + /** + * 解析选择条件,将传入的条件转换为AND连接的条件 + * @param selection 原始选择条件 + * @return 解析后的选择条件字符串 + */ private String parseSelection(String selection) { return (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : ""); } + /** + * 增加便签版本号 + * @param id 便签ID(大于0)或-1表示更新所有匹配的记录 + * @param selection 更新条件 + * @param selectionArgs 更新参数 + */ private void increaseNoteVersion(long id, String selection, String[] selectionArgs) { StringBuilder sql = new StringBuilder(120); sql.append("UPDATE "); sql.append(TABLE.NOTE); sql.append(" SET "); sql.append(NoteColumns.VERSION); - sql.append("=" + NoteColumns.VERSION + "+1 "); + sql.append("=" + NoteColumns.VERSION + "+1 "); // 版本号加1 + // 构建WHERE子句 if (id > 0 || !TextUtils.isEmpty(selection)) { sql.append(" WHERE "); } @@ -287,19 +346,25 @@ public class NotesProvider extends ContentProvider { } if (!TextUtils.isEmpty(selection)) { String selectString = id > 0 ? parseSelection(selection) : selection; + // 替换参数占位符 for (String args : selectionArgs) { selectString = selectString.replaceFirst("\\?", args); } sql.append(selectString); } + // 执行SQL语句 mHelper.getWritableDatabase().execSQL(sql.toString()); } + /** + * 获取URI对应的MIME类型(暂未实现) + * @param uri 请求的URI + * @return MIME类型字符串 + */ @Override public String getType(Uri uri) { // TODO Auto-generated method stub return null; } - -} +} \ No newline at end of file diff --git a/src/net/micode/notes/gtask/data/MetaData.java b/src/net/micode/notes/gtask/data/MetaData.java new file mode 100644 index 0000000..e30d451 --- /dev/null +++ b/src/net/micode/notes/gtask/data/MetaData.java @@ -0,0 +1,124 @@ +/* + * 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.util.Log; + +import net.micode.notes.tool.GTaskStringUtils; + +import org.json.JSONException; +import org.json.JSONObject; + +/** + * MetaData 类 - 元数据实体类 + * + * 功能:用于存储和管理与Google Tasks同步相关的元数据信息 + * 继承自Task类,专门处理便签与Google Tasks的关联关系 + * 设计为不可直接调用的辅助类,主要存储关联的Google Task ID + */ +public class MetaData extends Task { + private final static String TAG = MetaData.class.getSimpleName(); // 日志标签 + + private String mRelatedGid = null; // 关联的Google Task ID + + /** + * 设置元数据 + * @param gid Google Task的ID + * @param metaInfo 包含元信息的JSONObject对象 + */ + public void setMeta(String gid, JSONObject metaInfo) { + try { + // 将Google Task ID添加到元信息中 + metaInfo.put(GTaskStringUtils.META_HEAD_GTASK_ID, gid); + } catch (JSONException e) { + Log.e(TAG, "failed to put related gid"); // 记录错误日志 + } + setNotes(metaInfo.toString()); // 将JSON字符串设置为便签内容 + setName(GTaskStringUtils.META_NOTE_NAME); // 设置元数据便签的名称 + } + + /** + * 获取关联的Google Task ID + * @return 关联的Google Task ID,如果不存在则返回null + */ + public String getRelatedGid() { + return mRelatedGid; + } + + /** + * 判断是否值得保存(是否有有效的便签内容) + * @return 如果有便签内容则返回true,否则返回false + */ + @Override + public boolean isWorthSaving() { + return getNotes() != null; + } + + /** + * 从远程JSON数据设置内容(从Google Tasks服务器获取) + * @param js 包含远程数据的JSONObject对象 + */ + @Override + public void setContentByRemoteJSON(JSONObject js) { + super.setContentByRemoteJSON(js); // 调用父类方法设置基础内容 + if (getNotes() != null) { + try { + // 解析便签内容为JSON,提取关联的Google Task ID + JSONObject metaInfo = new JSONObject(getNotes().trim()); + mRelatedGid = metaInfo.getString(GTaskStringUtils.META_HEAD_GTASK_ID); + } catch (JSONException e) { + Log.w(TAG, "failed to get related gid"); // 记录警告日志 + mRelatedGid = null; // 解析失败时设为null + } + } + } + + /** + * 从本地JSON数据设置内容(禁止调用) + * @param js 本地JSON数据 + * @throws IllegalAccessError 始终抛出此异常,表示此方法不应被调用 + */ + @Override + public void setContentByLocalJSON(JSONObject js) { + // 此方法不应被调用,MetaData不从本地JSON设置内容 + throw new IllegalAccessError("MetaData:setContentByLocalJSON should not be called"); + } + + /** + * 从内容获取本地JSON数据(禁止调用) + * @return 本地JSON数据 + * @throws IllegalAccessError 始终抛出此异常,表示此方法不应被调用 + */ + @Override + public JSONObject getLocalJSONFromContent() { + // 此方法不应被调用,MetaData不生成本地JSON + throw new IllegalAccessError("MetaData:getLocalJSONFromContent should not be called"); + } + + /** + * 获取同步操作(禁止调用) + * @param c 数据库游标 + * @return 同步操作类型 + * @throws IllegalAccessError 始终抛出此异常,表示此方法不应被调用 + */ + @Override + public int getSyncAction(Cursor c) { + // 此方法不应被调用,MetaData不处理同步操作 + throw new IllegalAccessError("MetaData:getSyncAction should not be called"); + } +} \ No newline at end of file diff --git a/src/net/micode/notes/gtask/data/Node.java b/src/net/micode/notes/gtask/data/Node.java new file mode 100644 index 0000000..344be2e --- /dev/null +++ b/src/net/micode/notes/gtask/data/Node.java @@ -0,0 +1,165 @@ +/* + * 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 org.json.JSONObject; + +/** + * Node 类 - 数据节点的抽象基类 + * + * 功能:定义与Google Tasks同步的数据节点的通用属性和行为 + * 这是一个抽象类,为便签同步提供基础框架,包含同步操作状态码和基本数据字段 + * 采用模板方法模式,具体的数据节点(如Task、MetaData等)需要实现抽象方法 + */ +public abstract class Node { + // 同步操作状态码定义 + public static final int SYNC_ACTION_NONE = 0; // 无需同步 + public static final int SYNC_ACTION_ADD_REMOTE = 1; // 添加数据到远程 + public static final int SYNC_ACTION_ADD_LOCAL = 2; // 添加数据到本地 + public static final int SYNC_ACTION_DEL_REMOTE = 3; // 删除远程数据 + public static final int SYNC_ACTION_DEL_LOCAL = 4; // 删除本地数据 + public static final int SYNC_ACTION_UPDATE_REMOTE = 5; // 更新远程数据 + public static final int SYNC_ACTION_UPDATE_LOCAL = 6; // 更新本地数据 + public static final int SYNC_ACTION_UPDATE_CONFLICT = 7;// 更新冲突 + public static final int SYNC_ACTION_ERROR = 8; // 同步错误 + + // 节点属性字段 + private String mGid; // Google Task ID(全局唯一标识) + private String mName; // 节点名称 + private long mLastModified; // 最后修改时间戳 + private boolean mDeleted; // 删除标记 + + /** + * 默认构造函数,初始化节点属性 + */ + public Node() { + mGid = null; + mName = ""; + mLastModified = 0; + mDeleted = false; + } + + // =============== 抽象方法定义 =============== + // 这些方法需要子类根据具体的数据类型来实现 + + /** + * 获取创建操作的JSON数据 + * @param actionId 操作ID + * @return 包含创建操作数据的JSONObject + */ + public abstract JSONObject getCreateAction(int actionId); + + /** + * 获取更新操作的JSON数据 + * @param actionId 操作ID + * @return 包含更新操作数据的JSONObject + */ + public abstract JSONObject getUpdateAction(int actionId); + + /** + * 从远程JSON数据设置节点内容(从Google Tasks服务器获取) + * @param js 包含远程数据的JSONObject + */ + public abstract void setContentByRemoteJSON(JSONObject js); + + /** + * 从本地JSON数据设置节点内容(从本地数据库获取) + * @param js 包含本地数据的JSONObject + */ + public abstract void setContentByLocalJSON(JSONObject js); + + /** + * 从节点内容生成本地JSON数据(用于本地存储) + * @return 包含本地数据的JSONObject + */ + public abstract JSONObject getLocalJSONFromContent(); + + /** + * 根据数据库游标确定同步操作类型 + * @param c 数据库查询结果的Cursor + * @return 同步操作状态码(SYNC_ACTION_*) + */ + public abstract int getSyncAction(Cursor c); + + // =============== Getter和Setter方法 =============== + + /** + * 设置Google Task ID + * @param gid Google Task的全局唯一标识 + */ + public void setGid(String gid) { + this.mGid = gid; + } + + /** + * 设置节点名称 + * @param name 节点名称 + */ + public void setName(String name) { + this.mName = name; + } + + /** + * 设置最后修改时间戳 + * @param lastModified 最后修改时间戳(毫秒) + */ + public void setLastModified(long lastModified) { + this.mLastModified = lastModified; + } + + /** + * 设置删除标记 + * @param deleted true表示节点已删除,false表示节点有效 + */ + public void setDeleted(boolean deleted) { + this.mDeleted = deleted; + } + + /** + * 获取Google Task ID + * @return Google Task的全局唯一标识 + */ + public String getGid() { + return this.mGid; + } + + /** + * 获取节点名称 + * @return 节点名称 + */ + public String getName() { + return this.mName; + } + + /** + * 获取最后修改时间戳 + * @return 最后修改时间戳(毫秒) + */ + public long getLastModified() { + return this.mLastModified; + } + + /** + * 获取删除标记 + * @return true表示节点已删除,false表示节点有效 + */ + public boolean getDeleted() { + return this.mDeleted; + } +} \ No newline at end of file diff --git a/src/gtask/data/SqlData.java b/src/net/micode/notes/gtask/data/SqlData.java similarity index 59% rename from src/gtask/data/SqlData.java rename to src/net/micode/notes/gtask/data/SqlData.java index d3ec3be..77a98c4 100644 --- a/src/gtask/data/SqlData.java +++ b/src/net/micode/notes/gtask/data/SqlData.java @@ -34,61 +34,71 @@ import net.micode.notes.gtask.exception.ActionFailureException; import org.json.JSONException; import org.json.JSONObject; - +/** + * SqlData 类 - 数据库数据操作类 + * + * 功能:封装对便签数据表(data表)的CRUD操作,提供JSON数据与数据库记录的转换 + * 用于在本地数据库和Google Tasks同步之间进行数据映射和转换 + * 实现数据的差异更新和版本控制,提高同步效率 + */ public class SqlData { - private static final String TAG = SqlData.class.getSimpleName(); + private static final String TAG = SqlData.class.getSimpleName(); // 日志标签 - private static final int INVALID_ID = -99999; + private static final int INVALID_ID = -99999; // 无效ID标识 + // 数据表查询字段投影 public static final String[] PROJECTION_DATA = new String[] { DataColumns.ID, DataColumns.MIME_TYPE, DataColumns.CONTENT, DataColumns.DATA1, DataColumns.DATA3 }; - public static final int DATA_ID_COLUMN = 0; - - public static final int DATA_MIME_TYPE_COLUMN = 1; - - public static final int DATA_CONTENT_COLUMN = 2; - - public static final int DATA_CONTENT_DATA_1_COLUMN = 3; - - public static final int DATA_CONTENT_DATA_3_COLUMN = 4; - - private ContentResolver mContentResolver; - - private boolean mIsCreate; - - private long mDataId; - - private String mDataMimeType; - - private String mDataContent; - - private long mDataContentData1; - - private String mDataContentData3; - - private ContentValues mDiffDataValues; - + // 字段索引常量 + public static final int DATA_ID_COLUMN = 0; // ID字段索引 + public static final int DATA_MIME_TYPE_COLUMN = 1; // MIME类型字段索引 + public static final int DATA_CONTENT_COLUMN = 2; // 内容字段索引 + public static final int DATA_CONTENT_DATA_1_COLUMN = 3;// DATA1字段索引 + public static final int DATA_CONTENT_DATA_3_COLUMN = 4;// DATA3字段索引 + + private ContentResolver mContentResolver; // 内容解析器,用于访问ContentProvider + private boolean mIsCreate; // 创建标记:true表示新数据,false表示已存在数据 + private long mDataId; // 数据ID + private String mDataMimeType; // 数据MIME类型 + private String mDataContent; // 数据内容 + private long mDataContentData1; // 数据DATA1字段值 + private String mDataContentData3; // 数据DATA3字段值 + private ContentValues mDiffDataValues; // 差异数据值,用于记录需要更新的字段 + + /** + * 构造函数:创建新的SqlData对象(用于创建新数据) + * @param context Android上下文 + */ public SqlData(Context context) { mContentResolver = context.getContentResolver(); - mIsCreate = true; - mDataId = INVALID_ID; - mDataMimeType = DataConstants.NOTE; - mDataContent = ""; - mDataContentData1 = 0; - mDataContentData3 = ""; - mDiffDataValues = new ContentValues(); + mIsCreate = true; // 标记为新数据 + mDataId = INVALID_ID; // 初始化ID为无效值 + mDataMimeType = DataConstants.NOTE; // 默认MIME类型为普通便签 + mDataContent = ""; // 初始化内容为空 + mDataContentData1 = 0; // 初始化DATA1为0 + mDataContentData3 = ""; // 初始化DATA3为空 + mDiffDataValues = new ContentValues(); // 创建差异值容器 } + /** + * 构造函数:从数据库游标创建SqlData对象(用于加载已有数据) + * @param context Android上下文 + * @param c 数据库查询结果的Cursor + */ public SqlData(Context context, Cursor c) { mContentResolver = context.getContentResolver(); - mIsCreate = false; - loadFromCursor(c); - mDiffDataValues = new ContentValues(); + mIsCreate = false; // 标记为已有数据 + loadFromCursor(c); // 从游标加载数据 + mDiffDataValues = new ContentValues(); // 创建差异值容器 } + /** + * 从数据库游标加载数据到内存字段 + * @param c 数据库查询结果的Cursor + */ private void loadFromCursor(Cursor c) { mDataId = c.getLong(DATA_ID_COLUMN); mDataMimeType = c.getString(DATA_MIME_TYPE_COLUMN); @@ -97,13 +107,20 @@ public class SqlData { mDataContentData3 = c.getString(DATA_CONTENT_DATA_3_COLUMN); } + /** + * 从JSON对象设置数据内容,并记录变更的字段 + * @param js 包含数据的JSONObject + * @throws JSONException JSON解析异常 + */ public void setContent(JSONObject js) throws JSONException { + // 处理数据ID long dataId = js.has(DataColumns.ID) ? js.getLong(DataColumns.ID) : INVALID_ID; if (mIsCreate || mDataId != dataId) { mDiffDataValues.put(DataColumns.ID, dataId); } mDataId = dataId; + // 处理MIME类型 String dataMimeType = js.has(DataColumns.MIME_TYPE) ? js.getString(DataColumns.MIME_TYPE) : DataConstants.NOTE; if (mIsCreate || !mDataMimeType.equals(dataMimeType)) { @@ -111,18 +128,21 @@ public class SqlData { } mDataMimeType = dataMimeType; + // 处理内容 String dataContent = js.has(DataColumns.CONTENT) ? js.getString(DataColumns.CONTENT) : ""; if (mIsCreate || !mDataContent.equals(dataContent)) { mDiffDataValues.put(DataColumns.CONTENT, dataContent); } mDataContent = dataContent; + // 处理DATA1字段 long dataContentData1 = js.has(DataColumns.DATA1) ? js.getLong(DataColumns.DATA1) : 0; if (mIsCreate || mDataContentData1 != dataContentData1) { mDiffDataValues.put(DataColumns.DATA1, dataContentData1); } mDataContentData1 = dataContentData1; + // 处理DATA3字段 String dataContentData3 = js.has(DataColumns.DATA3) ? js.getString(DataColumns.DATA3) : ""; if (mIsCreate || !mDataContentData3.equals(dataContentData3)) { mDiffDataValues.put(DataColumns.DATA3, dataContentData3); @@ -130,11 +150,17 @@ public class SqlData { mDataContentData3 = dataContentData3; } + /** + * 获取当前数据的JSON表示 + * @return 包含数据的JSONObject,如果是新数据则返回null + * @throws JSONException JSON创建异常 + */ public JSONObject getContent() throws JSONException { if (mIsCreate) { Log.e(TAG, "it seems that we haven't created this in database yet"); return null; } + // 创建JSON对象并添加所有字段 JSONObject js = new JSONObject(); js.put(DataColumns.ID, mDataId); js.put(DataColumns.MIME_TYPE, mDataMimeType); @@ -144,30 +170,41 @@ public class SqlData { return js; } + /** + * 提交数据到数据库 + * @param noteId 关联的便签ID + * @param validateVersion 是否验证版本号 + * @param version 版本号(仅在validateVersion为true时使用) + * @throws ActionFailureException 数据提交失败异常 + */ public void commit(long noteId, boolean validateVersion, long version) { - if (mIsCreate) { + // 创建新数据记录 if (mDataId == INVALID_ID && mDiffDataValues.containsKey(DataColumns.ID)) { mDiffDataValues.remove(DataColumns.ID); } - mDiffDataValues.put(DataColumns.NOTE_ID, noteId); + mDiffDataValues.put(DataColumns.NOTE_ID, noteId); // 设置关联的便签ID Uri uri = mContentResolver.insert(Notes.CONTENT_DATA_URI, mDiffDataValues); try { + // 从返回的URI中提取新创建的数据ID mDataId = Long.valueOf(uri.getPathSegments().get(1)); } catch (NumberFormatException e) { Log.e(TAG, "Get note id error :" + e.toString()); throw new ActionFailureException("create note failed"); } } else { + // 更新已有数据记录 if (mDiffDataValues.size() > 0) { int result = 0; if (!validateVersion) { + // 不验证版本号,直接更新 result = mContentResolver.update(ContentUris.withAppendedId( Notes.CONTENT_DATA_URI, mDataId), mDiffDataValues, null, null); } else { + // 验证版本号,确保数据未被其他操作修改 result = mContentResolver.update(ContentUris.withAppendedId( - Notes.CONTENT_DATA_URI, mDataId), mDiffDataValues, + Notes.CONTENT_DATA_URI, mDataId), mDiffDataValues, " ? in (SELECT " + NoteColumns.ID + " FROM " + TABLE.NOTE + " WHERE " + NoteColumns.VERSION + "=?)", new String[] { String.valueOf(noteId), String.valueOf(version) @@ -179,11 +216,16 @@ public class SqlData { } } + // 重置状态 mDiffDataValues.clear(); mIsCreate = false; } + /** + * 获取数据ID + * @return 数据ID + */ public long getId() { return mDataId; } -} +} \ No newline at end of file diff --git a/src/net/micode/notes/gtask/data/SqlNote.java b/src/net/micode/notes/gtask/data/SqlNote.java new file mode 100644 index 0000000..137f89c --- /dev/null +++ b/src/net/micode/notes/gtask/data/SqlNote.java @@ -0,0 +1,572 @@ +/* + * 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.appwidget.AppWidgetManager; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.util.Log; + +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.exception.ActionFailureException; +import net.micode.notes.tool.GTaskStringUtils; +import net.micode.notes.tool.ResourceParser; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; + +/** + * 数据库笔记类(用于与Google Task同步) + * 负责在SQLite数据库和JSON格式之间转换笔记数据 + */ +public class SqlNote { + private static final String TAG = SqlNote.class.getSimpleName(); // 日志标签 + + private static final int INVALID_ID = -99999; // 无效ID常量 + + // 数据库查询的投影字段(需要查询的列) + public static final String[] PROJECTION_NOTE = new String[] { + NoteColumns.ID, NoteColumns.ALERTED_DATE, NoteColumns.BG_COLOR_ID, + NoteColumns.CREATED_DATE, NoteColumns.HAS_ATTACHMENT, NoteColumns.MODIFIED_DATE, + NoteColumns.NOTES_COUNT, NoteColumns.PARENT_ID, NoteColumns.SNIPPET, NoteColumns.TYPE, + NoteColumns.WIDGET_ID, NoteColumns.WIDGET_TYPE, NoteColumns.SYNC_ID, + NoteColumns.LOCAL_MODIFIED, NoteColumns.ORIGIN_PARENT_ID, NoteColumns.GTASK_ID, + NoteColumns.VERSION + }; + + // 投影字段对应的索引号 + public static final int ID_COLUMN = 0; // ID列索引 + public static final int ALERTED_DATE_COLUMN = 1; // 提醒日期列索引 + public static final int BG_COLOR_ID_COLUMN = 2; // 背景颜色ID列索引 + public static final int CREATED_DATE_COLUMN = 3; // 创建日期列索引 + public static final int HAS_ATTACHMENT_COLUMN = 4; // 是否有附件列索引 + public static final int MODIFIED_DATE_COLUMN = 5; // 修改日期列索引 + public static final int NOTES_COUNT_COLUMN = 6; // 笔记数量列索引 + public static final int PARENT_ID_COLUMN = 7; // 父ID列索引 + public static final int SNIPPET_COLUMN = 8; // 摘要内容列索引 + public static final int TYPE_COLUMN = 9; // 类型列索引 + public static final int WIDGET_ID_COLUMN = 10; // 小部件ID列索引 + public static final int WIDGET_TYPE_COLUMN = 11; // 小部件类型列索引 + public static final int SYNC_ID_COLUMN = 12; // 同步ID列索引 + public static final int LOCAL_MODIFIED_COLUMN = 13; // 本地修改标记列索引 + public static final int ORIGIN_PARENT_ID_COLUMN = 14; // 原始父ID列索引 + public static final int GTASK_ID_COLUMN = 15; // Google Task ID列索引 + public static final int VERSION_COLUMN = 16; // 版本号列索引 + + private Context mContext; // 上下文对象 + private ContentResolver mContentResolver; // 内容解析器,用于数据库操作 + private boolean mIsCreate; // 标记是否为新建笔记(true表示新建,false表示从数据库加载) + private long mId; // 笔记ID + private long mAlertDate; // 提醒日期 + private int mBgColorId; // 背景颜色ID + private long mCreatedDate; // 创建日期 + private int mHasAttachment; // 是否有附件(0表示无,1表示有) + private long mModifiedDate; // 修改日期 + private long mParentId; // 父文件夹ID + private String mSnippet; // 笔记内容摘要 + private int mType; // 笔记类型(笔记、文件夹、系统文件夹等) + private int mWidgetId; // 关联的小部件ID + private int mWidgetType; // 小部件类型 + private long mOriginParent; // 原始父文件夹ID(用于恢复操作) + private long mVersion; // 版本号(用于乐观锁控制) + private ContentValues mDiffNoteValues; // 存储需要更新的字段值(用于数据库更新) + private ArrayList mDataList; // 笔记内容数据列表(用于普通笔记类型) + + /** + * 构造函数 - 创建新笔记对象 + * 初始化所有字段为默认值,标记为创建状态 + * @param context 上下文 + */ + public SqlNote(Context context) { + mContext = context; + mContentResolver = context.getContentResolver(); + mIsCreate = true; // 标记为新建笔记 + mId = INVALID_ID; // 使用无效ID + mAlertDate = 0; // 默认无提醒 + mBgColorId = ResourceParser.getDefaultBgId(context); // 获取默认背景颜色ID + mCreatedDate = System.currentTimeMillis(); // 使用当前时间作为创建时间 + mHasAttachment = 0; // 默认无附件 + mModifiedDate = System.currentTimeMillis(); // 使用当前时间作为修改时间 + mParentId = 0; // 默认父ID为0 + mSnippet = ""; // 空摘要 + mType = Notes.TYPE_NOTE; // 默认为普通笔记类型 + mWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID; // 无效的小部件ID + mWidgetType = Notes.TYPE_WIDGET_INVALIDE; // 无效的小部件类型 + mOriginParent = 0; // 默认原始父ID为0 + mVersion = 0; // 初始版本为0 + mDiffNoteValues = new ContentValues(); // 初始化更新字段容器 + mDataList = new ArrayList(); // 初始化数据列表 + } + + /** + * 构造函数 - 从数据库游标加载笔记对象 + * 从数据库查询结果中加载笔记数据,并加载关联的笔记内容 + * @param context 上下文 + * @param c 数据库游标,包含笔记数据 + */ + public SqlNote(Context context, Cursor c) { + mContext = context; + mContentResolver = context.getContentResolver(); + mIsCreate = false; // 标记为从数据库加载 + loadFromCursor(c); // 从游标加载数据 + mDataList = new ArrayList(); // 初始化数据列表 + if (mType == Notes.TYPE_NOTE) + loadDataContent(); // 如果是普通笔记类型,加载笔记内容数据 + mDiffNoteValues = new ContentValues(); // 初始化更新字段容器 + } + + /** + * 构造函数 - 通过笔记ID加载笔记对象 + * 根据笔记ID查询数据库并加载笔记数据 + * @param context 上下文 + * @param id 笔记ID + */ + public SqlNote(Context context, long id) { + mContext = context; + mContentResolver = context.getContentResolver(); + mIsCreate = false; // 标记为从数据库加载 + loadFromCursor(id); // 通过ID从数据库加载数据 + mDataList = new ArrayList(); // 初始化数据列表 + if (mType == Notes.TYPE_NOTE) + loadDataContent(); // 如果是普通笔记类型,加载笔记内容数据 + mDiffNoteValues = new ContentValues(); // 初始化更新字段容器 + } + + /** + * 通过笔记ID从数据库加载笔记数据 + * 根据ID查询数据库并将结果加载到对象字段中 + * @param id 笔记ID + */ + private void loadFromCursor(long id) { + Cursor c = null; + try { + // 查询指定ID的笔记 + c = mContentResolver.query(Notes.CONTENT_NOTE_URI, PROJECTION_NOTE, "(_id=?)", + new String[] { + String.valueOf(id) + }, null); + if (c != null) { + c.moveToNext(); // 移动到查询结果的第一行 + loadFromCursor(c); // 从游标加载数据 + } else { + Log.w(TAG, "loadFromCursor: cursor = null"); // 日志记录游标为空 + } + } finally { + if (c != null) + c.close(); // 关闭游标 + } + } + + /** + * 从游标加载数据到对象字段 + * 将数据库游标中的数据赋值给对象的各个字段 + * @param c 数据库游标 + */ + private void loadFromCursor(Cursor c) { + mId = c.getLong(ID_COLUMN); // 获取笔记ID + mAlertDate = c.getLong(ALERTED_DATE_COLUMN); // 获取提醒日期 + mBgColorId = c.getInt(BG_COLOR_ID_COLUMN); // 获取背景颜色ID + mCreatedDate = c.getLong(CREATED_DATE_COLUMN); // 获取创建日期 + mHasAttachment = c.getInt(HAS_ATTACHMENT_COLUMN); // 获取是否有附件 + mModifiedDate = c.getLong(MODIFIED_DATE_COLUMN); // 获取修改日期 + mParentId = c.getLong(PARENT_ID_COLUMN); // 获取父文件夹ID + mSnippet = c.getString(SNIPPET_COLUMN); // 获取笔记摘要 + mType = c.getInt(TYPE_COLUMN); // 获取笔记类型 + mWidgetId = c.getInt(WIDGET_ID_COLUMN); // 获取小部件ID + mWidgetType = c.getInt(WIDGET_TYPE_COLUMN); // 获取小部件类型 + mVersion = c.getLong(VERSION_COLUMN); // 获取版本号 + } + + /** + * 加载笔记的内容数据 + * 查询笔记关联的内容数据(如文本内容)并加载到数据列表 + */ + private void loadDataContent() { + Cursor c = null; + mDataList.clear(); // 清空数据列表 + try { + // 查询笔记的内容数据 + c = mContentResolver.query(Notes.CONTENT_DATA_URI, SqlData.PROJECTION_DATA, + "(note_id=?)", new String[] { + String.valueOf(mId) + }, null); + if (c != null) { + if (c.getCount() == 0) { + Log.w(TAG, "it seems that the note has not data"); // 日志记录笔记无内容数据 + return; + } + while (c.moveToNext()) { + SqlData data = new SqlData(mContext, c); // 创建SqlData对象 + mDataList.add(data); // 添加到数据列表 + } + } else { + Log.w(TAG, "loadDataContent: cursor = null"); // 日志记录游标为空 + } + } finally { + if (c != null) + c.close(); // 关闭游标 + } + } + + /** + * 从JSON对象设置笔记内容 + * 解析JSON数据并更新笔记对象的各个字段 + * @param js 包含笔记数据的JSON对象 + * @return 设置成功返回true,失败返回false + */ + public boolean setContent(JSONObject js) { + try { + JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); // 获取笔记JSON对象 + if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_SYSTEM) { + Log.w(TAG, "cannot set system folder"); // 系统文件夹不能设置 + } else if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_FOLDER) { + // 对于文件夹类型,只能更新摘要和类型 + String snippet = note.has(NoteColumns.SNIPPET) ? note + .getString(NoteColumns.SNIPPET) : ""; // 获取摘要,如果不存在则为空字符串 + if (mIsCreate || !mSnippet.equals(snippet)) { + mDiffNoteValues.put(NoteColumns.SNIPPET, snippet); // 如果摘要发生变化,记录更新 + } + mSnippet = snippet; // 更新摘要字段 + + int type = note.has(NoteColumns.TYPE) ? note.getInt(NoteColumns.TYPE) + : Notes.TYPE_NOTE; // 获取类型,如果不存在则默认为笔记类型 + if (mIsCreate || mType != type) { + mDiffNoteValues.put(NoteColumns.TYPE, type); // 如果类型发生变化,记录更新 + } + mType = type; // 更新类型字段 + } else if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_NOTE) { + // 对于普通笔记类型,需要设置所有字段 + JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA); // 获取笔记内容数据数组 + long id = note.has(NoteColumns.ID) ? note.getLong(NoteColumns.ID) : INVALID_ID; // 获取笔记ID + if (mIsCreate || mId != id) { + mDiffNoteValues.put(NoteColumns.ID, id); // 如果ID发生变化,记录更新 + } + mId = id; // 更新ID字段 + + long alertDate = note.has(NoteColumns.ALERTED_DATE) ? note + .getLong(NoteColumns.ALERTED_DATE) : 0; // 获取提醒日期 + if (mIsCreate || mAlertDate != alertDate) { + mDiffNoteValues.put(NoteColumns.ALERTED_DATE, alertDate); // 如果提醒日期变化,记录更新 + } + mAlertDate = alertDate; // 更新提醒日期字段 + + int bgColorId = note.has(NoteColumns.BG_COLOR_ID) ? note + .getInt(NoteColumns.BG_COLOR_ID) : ResourceParser.getDefaultBgId(mContext); // 获取背景颜色ID + if (mIsCreate || mBgColorId != bgColorId) { + mDiffNoteValues.put(NoteColumns.BG_COLOR_ID, bgColorId); // 如果背景颜色变化,记录更新 + } + mBgColorId = bgColorId; // 更新背景颜色ID字段 + + long createDate = note.has(NoteColumns.CREATED_DATE) ? note + .getLong(NoteColumns.CREATED_DATE) : System.currentTimeMillis(); // 获取创建日期 + if (mIsCreate || mCreatedDate != createDate) { + mDiffNoteValues.put(NoteColumns.CREATED_DATE, createDate); // 如果创建日期变化,记录更新 + } + mCreatedDate = createDate; // 更新创建日期字段 + + int hasAttachment = note.has(NoteColumns.HAS_ATTACHMENT) ? note + .getInt(NoteColumns.HAS_ATTACHMENT) : 0; // 获取是否有附件 + if (mIsCreate || mHasAttachment != hasAttachment) { + mDiffNoteValues.put(NoteColumns.HAS_ATTACHMENT, hasAttachment); // 如果附件状态变化,记录更新 + } + mHasAttachment = hasAttachment; // 更新附件字段 + + long modifiedDate = note.has(NoteColumns.MODIFIED_DATE) ? note + .getLong(NoteColumns.MODIFIED_DATE) : System.currentTimeMillis(); // 获取修改日期 + if (mIsCreate || mModifiedDate != modifiedDate) { + mDiffNoteValues.put(NoteColumns.MODIFIED_DATE, modifiedDate); // 如果修改日期变化,记录更新 + } + mModifiedDate = modifiedDate; // 更新修改日期字段 + + long parentId = note.has(NoteColumns.PARENT_ID) ? note + .getLong(NoteColumns.PARENT_ID) : 0; // 获取父文件夹ID + if (mIsCreate || mParentId != parentId) { + mDiffNoteValues.put(NoteColumns.PARENT_ID, parentId); // 如果父ID变化,记录更新 + } + mParentId = parentId; // 更新父ID字段 + + String snippet = note.has(NoteColumns.SNIPPET) ? note + .getString(NoteColumns.SNIPPET) : ""; // 获取摘要 + if (mIsCreate || !mSnippet.equals(snippet)) { + mDiffNoteValues.put(NoteColumns.SNIPPET, snippet); // 如果摘要变化,记录更新 + } + mSnippet = snippet; // 更新摘要字段 + + int type = note.has(NoteColumns.TYPE) ? note.getInt(NoteColumns.TYPE) + : Notes.TYPE_NOTE; // 获取笔记类型 + if (mIsCreate || mType != type) { + mDiffNoteValues.put(NoteColumns.TYPE, type); // 如果类型变化,记录更新 + } + mType = type; // 更新类型字段 + + int widgetId = note.has(NoteColumns.WIDGET_ID) ? note.getInt(NoteColumns.WIDGET_ID) + : AppWidgetManager.INVALID_APPWIDGET_ID; // 获取小部件ID + if (mIsCreate || mWidgetId != widgetId) { + mDiffNoteValues.put(NoteColumns.WIDGET_ID, widgetId); // 如果小部件ID变化,记录更新 + } + mWidgetId = widgetId; // 更新小部件ID字段 + + int widgetType = note.has(NoteColumns.WIDGET_TYPE) ? note + .getInt(NoteColumns.WIDGET_TYPE) : Notes.TYPE_WIDGET_INVALIDE; // 获取小部件类型 + if (mIsCreate || mWidgetType != widgetType) { + mDiffNoteValues.put(NoteColumns.WIDGET_TYPE, widgetType); // 如果小部件类型变化,记录更新 + } + mWidgetType = widgetType; // 更新小部件类型字段 + + long originParent = note.has(NoteColumns.ORIGIN_PARENT_ID) ? note + .getLong(NoteColumns.ORIGIN_PARENT_ID) : 0; // 获取原始父文件夹ID + if (mIsCreate || mOriginParent != originParent) { + mDiffNoteValues.put(NoteColumns.ORIGIN_PARENT_ID, originParent); // 如果原始父ID变化,记录更新 + } + mOriginParent = originParent; // 更新原始父ID字段 + + // 处理笔记内容数据 + for (int i = 0; i < dataArray.length(); i++) { + JSONObject data = dataArray.getJSONObject(i); // 获取每条内容数据 + SqlData sqlData = null; + if (data.has(DataColumns.ID)) { + long dataId = data.getLong(DataColumns.ID); // 获取内容数据ID + // 在现有数据列表中查找匹配的内容数据 + for (SqlData temp : mDataList) { + if (dataId == temp.getId()) { + sqlData = temp; // 找到已存在的数据 + } + } + } + + if (sqlData == null) { + sqlData = new SqlData(mContext); // 创建新的内容数据对象 + mDataList.add(sqlData); // 添加到数据列表 + } + + sqlData.setContent(data); // 设置内容数据 + } + } + } catch (JSONException e) { + Log.e(TAG, e.toString()); // 记录JSON解析异常 + e.printStackTrace(); + return false; // 返回失败 + } + return true; // 返回成功 + } + + /** + * 获取笔记的JSON表示 + * 将笔记对象转换为JSON格式,用于同步或传输 + * @return 包含笔记数据的JSON对象,失败返回null + */ + public JSONObject getContent() { + try { + JSONObject js = new JSONObject(); // 创建JSON对象 + + if (mIsCreate) { + Log.e(TAG, "it seems that we haven't created this in database yet"); // 日志记录笔记尚未创建 + return null; // 返回null + } + + JSONObject note = new JSONObject(); // 创建笔记JSON对象 + if (mType == Notes.TYPE_NOTE) { + // 普通笔记类型,包含所有字段 + note.put(NoteColumns.ID, mId); // 添加ID + note.put(NoteColumns.ALERTED_DATE, mAlertDate); // 添加提醒日期 + note.put(NoteColumns.BG_COLOR_ID, mBgColorId); // 添加背景颜色ID + note.put(NoteColumns.CREATED_DATE, mCreatedDate); // 添加创建日期 + note.put(NoteColumns.HAS_ATTACHMENT, mHasAttachment); // 添加附件状态 + note.put(NoteColumns.MODIFIED_DATE, mModifiedDate); // 添加修改日期 + note.put(NoteColumns.PARENT_ID, mParentId); // 添加父文件夹ID + note.put(NoteColumns.SNIPPET, mSnippet); // 添加摘要 + note.put(NoteColumns.TYPE, mType); // 添加类型 + note.put(NoteColumns.WIDGET_ID, mWidgetId); // 添加小部件ID + note.put(NoteColumns.WIDGET_TYPE, mWidgetType); // 添加小部件类型 + note.put(NoteColumns.ORIGIN_PARENT_ID, mOriginParent); // 添加原始父文件夹ID + js.put(GTaskStringUtils.META_HEAD_NOTE, note); // 将笔记对象添加到JSON + + // 添加笔记内容数据 + JSONArray dataArray = new JSONArray(); // 创建内容数据数组 + for (SqlData sqlData : mDataList) { + JSONObject data = sqlData.getContent(); // 获取内容数据的JSON + if (data != null) { + dataArray.put(data); // 添加到数组 + } + } + js.put(GTaskStringUtils.META_HEAD_DATA, dataArray); // 将内容数据数组添加到JSON + } else if (mType == Notes.TYPE_FOLDER || mType == Notes.TYPE_SYSTEM) { + // 文件夹或系统文件夹类型,只包含必要字段 + note.put(NoteColumns.ID, mId); // 添加ID + note.put(NoteColumns.TYPE, mType); // 添加类型 + note.put(NoteColumns.SNIPPET, mSnippet); // 添加摘要 + js.put(GTaskStringUtils.META_HEAD_NOTE, note); // 将笔记对象添加到JSON + } + + return js; // 返回JSON对象 + } catch (JSONException e) { + Log.e(TAG, e.toString()); // 记录JSON异常 + e.printStackTrace(); + } + return null; // 返回null + } + + /** + * 设置父文件夹ID + * 更新笔记的父文件夹ID,并记录需要更新的字段 + * @param id 父文件夹ID + */ + public void setParentId(long id) { + mParentId = id; // 更新父文件夹ID字段 + mDiffNoteValues.put(NoteColumns.PARENT_ID, id); // 记录需要更新的字段 + } + + /** + * 设置Google Task ID + * 更新笔记的Google Task ID,用于同步 + * @param gid Google Task ID + */ + public void setGtaskId(String gid) { + mDiffNoteValues.put(NoteColumns.GTASK_ID, gid); // 记录需要更新的字段 + } + + /** + * 设置同步ID + * 更新笔记的同步ID,用于同步控制 + * @param syncId 同步ID + */ + public void setSyncId(long syncId) { + mDiffNoteValues.put(NoteColumns.SYNC_ID, syncId); // 记录需要更新的字段 + } + + /** + * 重置本地修改标记 + * 将本地修改标记设为0,表示笔记已同步 + */ + public void resetLocalModified() { + mDiffNoteValues.put(NoteColumns.LOCAL_MODIFIED, 0); // 记录需要更新的字段 + } + + /** + * 获取笔记ID + * @return 笔记ID + */ + public long getId() { + return mId; + } + + /** + * 获取父文件夹ID + * @return 父文件夹ID + */ + public long getParentId() { + return mParentId; + } + + /** + * 获取笔记摘要 + * @return 笔记摘要 + */ + public String getSnippet() { + return mSnippet; + } + + /** + * 检查笔记是否为普通笔记类型 + * @return 如果是普通笔记类型返回true,否则返回false + */ + public boolean isNoteType() { + return mType == Notes.TYPE_NOTE; + } + + /** + * 提交笔记更改到数据库 + * 将笔记对象的更改保存到数据库,包括新建或更新操作 + * @param validateVersion 是否验证版本号(用于乐观锁控制) + */ + public void commit(boolean validateVersion) { + if (mIsCreate) { + // 新建笔记操作 + if (mId == INVALID_ID && mDiffNoteValues.containsKey(NoteColumns.ID)) { + mDiffNoteValues.remove(NoteColumns.ID); // 如果ID为无效值且存在于更新字段中,移除它 + } + + Uri uri = mContentResolver.insert(Notes.CONTENT_NOTE_URI, mDiffNoteValues); // 插入新笔记到数据库 + try { + mId = Long.valueOf(uri.getPathSegments().get(1)); // 从返回的URI中获取新笔记的ID + } catch (NumberFormatException e) { + Log.e(TAG, "Get note id error :" + e.toString()); // 记录获取ID异常 + throw new ActionFailureException("create note failed"); // 抛出创建失败异常 + } + if (mId == 0) { + throw new IllegalStateException("Create thread id failed"); // 如果ID为0,抛出异常 + } + + // 如果是普通笔记类型,提交笔记内容数据 + if (mType == Notes.TYPE_NOTE) { + for (SqlData sqlData : mDataList) { + sqlData.commit(mId, false, -1); // 提交内容数据,不验证版本 + } + } + } else { + // 更新笔记操作 + if (mId <= 0 && mId != Notes.ID_ROOT_FOLDER && mId != Notes.ID_CALL_RECORD_FOLDER) { + Log.e(TAG, "No such note"); // 日志记录无效ID + throw new IllegalStateException("Try to update note with invalid id"); // 抛出无效ID异常 + } + if (mDiffNoteValues.size() > 0) { + mVersion ++; // 版本号递增 + int result = 0; + if (!validateVersion) { + // 不验证版本号,直接更新 + result = mContentResolver.update(Notes.CONTENT_NOTE_URI, mDiffNoteValues, "(" + + NoteColumns.ID + "=?)", new String[] { + String.valueOf(mId) + }); + } else { + // 验证版本号,使用乐观锁控制 + result = mContentResolver.update(Notes.CONTENT_NOTE_URI, mDiffNoteValues, "(" + + NoteColumns.ID + "=?) AND (" + NoteColumns.VERSION + "<=?)", + new String[] { + String.valueOf(mId), String.valueOf(mVersion) + }); + } + if (result == 0) { + Log.w(TAG, "there is no update. maybe user updates note when syncing"); // 日志记录无更新 + } + } + + // 如果是普通笔记类型,提交笔记内容数据的更改 + if (mType == Notes.TYPE_NOTE) { + for (SqlData sqlData : mDataList) { + sqlData.commit(mId, validateVersion, mVersion); // 提交内容数据,可能验证版本 + } + } + } + + // 重新从数据库加载笔记数据,确保对象状态与数据库一致 + loadFromCursor(mId); + if (mType == Notes.TYPE_NOTE) + loadDataContent(); // 重新加载笔记内容数据 + + mDiffNoteValues.clear(); // 清空更新字段容器 + mIsCreate = false; // 标记为非新建状态 + } +} \ No newline at end of file diff --git a/src/gtask/data/Task.java b/src/net/micode/notes/gtask/data/Task.java similarity index 65% rename from src/gtask/data/Task.java rename to src/net/micode/notes/gtask/data/Task.java index 6a19454..3589281 100644 --- a/src/gtask/data/Task.java +++ b/src/net/micode/notes/gtask/data/Task.java @@ -31,65 +31,77 @@ import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; - +/** + * Task 类 - 具体任务实体类 + * + * 功能:代表一个具体的便签任务,继承自Node抽象类 + * 实现便签与Google Tasks同步的具体逻辑,包括创建、更新、同步操作 + * 处理本地数据库与远程Google Tasks服务器之间的数据映射和转换 + */ public class Task extends Node { - private static final String TAG = Task.class.getSimpleName(); - - private boolean mCompleted; - - private String mNotes; - - private JSONObject mMetaInfo; - - private Task mPriorSibling; - - private TaskList mParent; - + private static final String TAG = Task.class.getSimpleName(); // 日志标签 + + // 任务特有属性 + private boolean mCompleted; // 完成状态标记 + private String mNotes; // 任务备注/描述 + private JSONObject mMetaInfo; // 元信息(存储本地便签的详细信息) + private Task mPriorSibling; // 前一个兄弟任务(用于排序) + private TaskList mParent; // 父任务列表(TaskList) + + /** + * 默认构造函数,初始化任务属性 + */ public Task() { super(); - mCompleted = false; - mNotes = null; - mPriorSibling = null; - mParent = null; - mMetaInfo = null; + mCompleted = false; // 默认未完成 + mNotes = null; // 默认无备注 + mPriorSibling = null; // 默认无前兄弟任务 + mParent = null; // 默认无父列表 + mMetaInfo = null; // 默认无元信息 } + /** + * 获取创建任务的JSON操作数据(用于发送到Google Tasks服务器) + * @param actionId 操作ID + * @return 包含创建操作数据的JSONObject + * @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 + // 操作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_NAME, getName()); // 任务名称 + entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null"); // 创建者ID entity.put(GTaskStringUtils.GTASK_JSON_ENTITY_TYPE, - GTaskStringUtils.GTASK_JSON_TYPE_TASK); + GTaskStringUtils.GTASK_JSON_TYPE_TASK); // 实体类型为任务 if (getNotes() != null) { - entity.put(GTaskStringUtils.GTASK_JSON_NOTES, getNotes()); + entity.put(GTaskStringUtils.GTASK_JSON_NOTES, getNotes()); // 任务备注 } js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); - // parent_id + // 父列表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 + // 列表ID(与父列表ID相同) js.put(GTaskStringUtils.GTASK_JSON_LIST_ID, mParent.getGid()); - // prior_sibling_id + // 前兄弟任务ID(用于排序) if (mPriorSibling != null) { js.put(GTaskStringUtils.GTASK_JSON_PRIOR_SIBLING_ID, mPriorSibling.getGid()); } @@ -103,27 +115,33 @@ public class Task extends Node { return js; } + /** + * 获取更新任务的JSON操作数据(用于发送到Google Tasks服务器) + * @param actionId 操作ID + * @return 包含更新操作数据的JSONObject + * @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 + // 操作ID js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); - // id + // 任务ID js.put(GTaskStringUtils.GTASK_JSON_ID, getGid()); - // entity_delta + // 实体数据变化 JSONObject entity = new JSONObject(); - entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); + entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); // 任务名称 if (getNotes() != null) { - entity.put(GTaskStringUtils.GTASK_JSON_NOTES, getNotes()); + entity.put(GTaskStringUtils.GTASK_JSON_NOTES, getNotes()); // 任务备注 } - entity.put(GTaskStringUtils.GTASK_JSON_DELETED, getDeleted()); + entity.put(GTaskStringUtils.GTASK_JSON_DELETED, getDeleted()); // 删除状态 js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); } catch (JSONException e) { @@ -135,35 +153,40 @@ public class Task extends Node { return js; } + /** + * 从远程JSON数据设置任务内容(从Google Tasks服务器获取) + * @param js 包含远程任务数据的JSONObject + * @throws ActionFailureException 解析JSON失败时抛出 + */ public void setContentByRemoteJSON(JSONObject js) { if (js != null) { try { - // id + // 任务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)); } @@ -175,6 +198,10 @@ public class Task extends Node { } } + /** + * 从本地JSON数据设置任务内容(从本地数据库获取) + * @param js 包含本地便签数据的JSONObject + */ public void setContentByLocalJSON(JSONObject js) { if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE) || !js.has(GTaskStringUtils.META_HEAD_DATA)) { @@ -182,18 +209,21 @@ public class Task extends Node { } 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)); + setName(data.getString(DataColumns.CONTENT)); // 设置任务名称为便签内容 break; } } @@ -204,31 +234,37 @@ public class Task extends Node { } } + /** + * 从任务内容生成本地JSON数据(用于本地存储) + * @return 包含本地便签数据的JSONObject,如果任务无内容则返回null + */ public JSONObject getLocalJSONFromContent() { String name = getName(); try { if (mMetaInfo == null) { - // new task created from web + // 从Web新创建的任务(没有元信息) if (name == null) { Log.w(TAG, "the note seems to be an empty one"); return null; } + // 构建新的JSON结构 JSONObject js = new JSONObject(); JSONObject note = new JSONObject(); JSONArray dataArray = new JSONArray(); JSONObject data = new JSONObject(); - data.put(DataColumns.CONTENT, name); + data.put(DataColumns.CONTENT, name); // 便签内容 dataArray.put(data); js.put(GTaskStringUtils.META_HEAD_DATA, dataArray); - note.put(NoteColumns.TYPE, Notes.TYPE_NOTE); + 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)) { @@ -238,7 +274,7 @@ public class Task extends Node { } note.put(NoteColumns.TYPE, Notes.TYPE_NOTE); - return mMetaInfo; + return mMetaInfo; // 返回更新后的元信息 } } catch (JSONException e) { Log.e(TAG, e.toString()); @@ -247,6 +283,10 @@ public class Task extends Node { } } + /** + * 设置元信息(从MetaData对象) + * @param metaData 元数据对象 + */ public void setMetaInfo(MetaData metaData) { if (metaData != null && metaData.getNotes() != null) { try { @@ -258,48 +298,57 @@ public class Task extends Node { } } + /** + * 根据数据库游标确定同步操作类型 + * @param c 数据库查询结果的Cursor + * @return 同步操作状态码(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; + 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; + return SYNC_ACTION_UPDATE_LOCAL; // 本地更新远程 } - // validate the note id now + // 验证便签ID是否匹配 if (c.getLong(SqlNote.ID_COLUMN) != noteInfo.getLong(NoteColumns.ID)) { Log.w(TAG, "note id doesn't match"); - return SYNC_ACTION_UPDATE_LOCAL; + return SYNC_ACTION_UPDATE_LOCAL; // ID不匹配,用本地更新远程 } + // 检查本地修改标记 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 + // 本地有修改 + // 验证Google Task ID是否匹配 if (!c.getString(SqlNote.GTASK_ID_COLUMN).equals(getGid())) { Log.e(TAG, "gtask id doesn't match"); - return SYNC_ACTION_ERROR; + return SYNC_ACTION_ERROR; // ID不匹配,错误 } if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { - // local modification only + // 仅本地有修改 return SYNC_ACTION_UPDATE_REMOTE; } else { + // 两边都有修改,冲突 return SYNC_ACTION_UPDATE_CONFLICT; } } @@ -308,44 +357,79 @@ public class Task extends Node { e.printStackTrace(); } - return SYNC_ACTION_ERROR; + return SYNC_ACTION_ERROR; // 发生异常,返回错误 } + /** + * 判断任务是否值得保存(是否有有效内容) + * @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 前兄弟任务对象 + */ public Task getPriorSibling() { return this.mPriorSibling; } + /** + * 获取父任务列表 + * @return 父任务列表对象 + */ public TaskList getParent() { return this.mParent; } - -} +} \ No newline at end of file diff --git a/src/gtask/data/TaskList.java b/src/net/micode/notes/gtask/data/TaskList.java similarity index 66% rename from src/gtask/data/TaskList.java rename to src/net/micode/notes/gtask/data/TaskList.java index 4ea21c5..e096dd8 100644 --- a/src/gtask/data/TaskList.java +++ b/src/net/micode/notes/gtask/data/TaskList.java @@ -29,40 +29,54 @@ import org.json.JSONObject; import java.util.ArrayList; - +/** + * TaskList 类 - 任务列表实体类 + * + * 功能:代表Google Tasks中的一个任务列表(对应小米便签中的文件夹) + * 继承自Node抽象类,可以包含多个Task子任务,实现任务列表的层级管理 + * 处理文件夹与Google Tasks列表的同步,支持子任务的增删改查和排序 + */ public class TaskList extends Node { - private static final String TAG = TaskList.class.getSimpleName(); + private static final String TAG = TaskList.class.getSimpleName(); // 日志标签 - private int mIndex; - - private ArrayList mChildren; + private int mIndex; // 任务列表在父容器中的索引位置 + private ArrayList mChildren; // 子任务列表 + /** + * 默认构造函数,初始化任务列表属性 + */ public TaskList() { super(); - mChildren = new ArrayList(); - mIndex = 1; + mChildren = new ArrayList(); // 初始化子任务列表 + mIndex = 1; // 默认索引为1(Google Tasks的索引从1开始) } + /** + * 获取创建任务列表的JSON操作数据(用于发送到Google Tasks服务器) + * @param actionId 操作ID + * @return 包含创建操作数据的JSONObject + * @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 + // 操作ID js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); - // index + // 索引位置 js.put(GTaskStringUtils.GTASK_JSON_INDEX, mIndex); - // 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_NAME, getName()); // 列表名称 + entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null"); // 创建者ID entity.put(GTaskStringUtils.GTASK_JSON_ENTITY_TYPE, - GTaskStringUtils.GTASK_JSON_TYPE_GROUP); + GTaskStringUtils.GTASK_JSON_TYPE_GROUP); // 实体类型为组(列表) js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); } catch (JSONException e) { @@ -74,24 +88,30 @@ public class TaskList extends Node { return js; } + /** + * 获取更新任务列表的JSON操作数据(用于发送到Google Tasks服务器) + * @param actionId 操作ID + * @return 包含更新操作数据的JSONObject + * @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 + // 操作ID js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); - // id + // 列表ID js.put(GTaskStringUtils.GTASK_JSON_ID, getGid()); - // entity_delta + // 实体数据变化 JSONObject entity = new JSONObject(); - entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); - entity.put(GTaskStringUtils.GTASK_JSON_DELETED, getDeleted()); + entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); // 列表名称 + entity.put(GTaskStringUtils.GTASK_JSON_DELETED, getDeleted()); // 删除状态 js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); } catch (JSONException e) { @@ -103,20 +123,25 @@ public class TaskList extends Node { return js; } + /** + * 从远程JSON数据设置任务列表内容(从Google Tasks服务器获取) + * @param js 包含远程列表数据的JSONObject + * @throws ActionFailureException 解析JSON失败时抛出 + */ public void setContentByRemoteJSON(JSONObject js) { if (js != null) { try { - // id + // 列表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)); } @@ -129,6 +154,10 @@ public class TaskList extends Node { } } + /** + * 从本地JSON数据设置任务列表内容(从本地数据库获取) + * @param js 包含本地文件夹数据的JSONObject + */ public void setContentByLocalJSON(JSONObject js) { if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE)) { Log.w(TAG, "setContentByLocalJSON: nothing is avaiable"); @@ -137,10 +166,13 @@ public class TaskList extends Node { try { JSONObject folder = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); + // 根据文件夹类型设置名称 if (folder.getInt(NoteColumns.TYPE) == Notes.TYPE_FOLDER) { + // 普通文件夹:使用片段作为名称,并添加MIUI前缀 String name = folder.getString(NoteColumns.SNIPPET); setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + name); } else if (folder.getInt(NoteColumns.TYPE) == Notes.TYPE_SYSTEM) { + // 系统文件夹:根据ID设置特定的名称 if (folder.getLong(NoteColumns.ID) == Notes.ID_ROOT_FOLDER) setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT); else if (folder.getLong(NoteColumns.ID) == Notes.ID_CALL_RECORD_FOLDER) @@ -157,21 +189,28 @@ public class TaskList extends Node { } } + /** + * 从任务列表内容生成本地JSON数据(用于本地存储) + * @return 包含本地文件夹数据的JSONObject + */ public JSONObject getLocalJSONFromContent() { try { JSONObject js = new JSONObject(); JSONObject folder = new JSONObject(); + // 处理文件夹名称:移除MIUI前缀 String folderName = getName(); if (getName().startsWith(GTaskStringUtils.MIUI_FOLDER_PREFFIX)) folderName = folderName.substring(GTaskStringUtils.MIUI_FOLDER_PREFFIX.length(), folderName.length()); folder.put(NoteColumns.SNIPPET, folderName); + + // 根据文件夹名称判断类型 if (folderName.equals(GTaskStringUtils.FOLDER_DEFAULT) || folderName.equals(GTaskStringUtils.FOLDER_CALL_NOTE)) - folder.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + folder.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); // 默认文件夹和通话记录文件夹为系统类型 else - folder.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); + folder.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); // 其他为普通文件夹 js.put(GTaskStringUtils.META_HEAD_NOTE, folder); @@ -183,28 +222,34 @@ public class TaskList extends Node { } } + /** + * 根据数据库游标确定同步操作类型 + * @param c 数据库查询结果的Cursor + * @return 同步操作状态码(SYNC_ACTION_*) + */ public int getSyncAction(Cursor c) { try { 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 + // 本地有修改 + // 验证Google Task 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 { - // for folder conflicts, just apply local modification + // 文件夹冲突时,优先应用本地修改(不同于任务的处理方式) return SYNC_ACTION_UPDATE_REMOTE; } } @@ -216,16 +261,25 @@ public class TaskList extends Node { return SYNC_ACTION_ERROR; } + /** + * 获取子任务数量 + * @return 子任务数量 + */ public int getChildTaskCount() { return mChildren.size(); } + /** + * 添加子任务到列表末尾 + * @param task 要添加的任务 + * @return 添加成功返回true,失败返回false + */ public boolean addChildTask(Task task) { boolean ret = false; if (task != null && !mChildren.contains(task)) { ret = mChildren.add(task); if (ret) { - // need to set prior sibling and parent + // 设置前兄弟任务和父列表 task.setPriorSibling(mChildren.isEmpty() ? null : mChildren .get(mChildren.size() - 1)); task.setParent(this); @@ -234,6 +288,12 @@ public class TaskList extends Node { return ret; } + /** + * 在指定索引位置添加子任务 + * @param task 要添加的任务 + * @param index 要插入的索引位置 + * @return 添加成功返回true,失败返回false + */ public boolean addChildTask(Task task, int index) { if (index < 0 || index > mChildren.size()) { Log.e(TAG, "add child task: invalid index"); @@ -244,7 +304,7 @@ public class TaskList extends Node { if (task != null && pos == -1) { mChildren.add(index, task); - // update the task list + // 更新任务列表的前后兄弟关系 Task preTask = null; Task afterTask = null; if (index != 0) @@ -260,6 +320,11 @@ public class TaskList extends Node { return true; } + /** + * 从列表中移除子任务 + * @param task 要移除的任务 + * @return 移除成功返回true,失败返回false + */ public boolean removeChildTask(Task task) { boolean ret = false; int index = mChildren.indexOf(task); @@ -267,11 +332,11 @@ public class TaskList extends Node { ret = mChildren.remove(task); if (ret) { - // reset prior sibling and parent + // 重置任务的前兄弟任务和父列表 task.setPriorSibling(null); task.setParent(null); - // update the task list + // 更新任务列表的前兄弟关系 if (index != mChildren.size()) { mChildren.get(index).setPriorSibling( index == 0 ? null : mChildren.get(index - 1)); @@ -281,6 +346,12 @@ public class TaskList extends Node { return ret; } + /** + * 移动子任务到指定索引位置 + * @param task 要移动的任务 + * @param index 目标索引位置 + * @return 移动成功返回true,失败返回false + */ public boolean moveChildTask(Task task, int index) { if (index < 0 || index >= mChildren.size()) { @@ -299,6 +370,11 @@ public class TaskList extends Node { return (removeChildTask(task) && addChildTask(task, index)); } + /** + * 根据Google Task ID查找子任务 + * @param gid Google Task的全局唯一标识 + * @return 找到的任务对象,未找到返回null + */ public Task findChildTaskByGid(String gid) { for (int i = 0; i < mChildren.size(); i++) { Task t = mChildren.get(i); @@ -309,10 +385,20 @@ public class TaskList extends Node { return null; } + /** + * 获取子任务在列表中的索引位置 + * @param task 要查找的任务 + * @return 任务在列表中的索引,不存在返回-1 + */ public int getChildTaskIndex(Task task) { return mChildren.indexOf(task); } + /** + * 根据索引获取子任务 + * @param index 子任务索引 + * @return 指定索引位置的子任务,索引无效返回null + */ public Task getChildTaskByIndex(int index) { if (index < 0 || index >= mChildren.size()) { Log.e(TAG, "getTaskByIndex: invalid index"); @@ -321,6 +407,11 @@ public class TaskList extends Node { return mChildren.get(index); } + /** + * 根据Google Task ID获取子任务(方法名有拼写错误) + * @param gid Google Task的全局唯一标识 + * @return 找到的任务对象,未找到返回null + */ public Task getChilTaskByGid(String gid) { for (Task task : mChildren) { if (task.getGid().equals(gid)) @@ -329,15 +420,27 @@ public class TaskList extends Node { return null; } + /** + * 获取所有子任务的列表 + * @return 子任务列表的ArrayList + */ public ArrayList getChildTaskList() { return this.mChildren; } + /** + * 设置任务列表索引 + * @param index 索引值 + */ public void setIndex(int index) { this.mIndex = index; } + /** + * 获取任务列表索引 + * @return 索引值 + */ public int getIndex() { return this.mIndex; } -} +} \ No newline at end of file diff --git a/src/gtask/exception/ActionFailureException.java b/src/net/micode/notes/gtask/exception/ActionFailureException.java similarity index 100% rename from src/gtask/exception/ActionFailureException.java rename to src/net/micode/notes/gtask/exception/ActionFailureException.java diff --git a/src/gtask/exception/NetworkFailureException.java b/src/net/micode/notes/gtask/exception/NetworkFailureException.java similarity index 100% rename from src/gtask/exception/NetworkFailureException.java rename to src/net/micode/notes/gtask/exception/NetworkFailureException.java diff --git a/src/gtask/remote/GTaskASyncTask.java b/src/net/micode/notes/gtask/remote/GTaskASyncTask.java similarity index 100% rename from src/gtask/remote/GTaskASyncTask.java rename to src/net/micode/notes/gtask/remote/GTaskASyncTask.java diff --git a/src/gtask/remote/GTaskClient.java b/src/net/micode/notes/gtask/remote/GTaskClient.java similarity index 100% rename from src/gtask/remote/GTaskClient.java rename to src/net/micode/notes/gtask/remote/GTaskClient.java diff --git a/src/gtask/remote/GTaskManager.java b/src/net/micode/notes/gtask/remote/GTaskManager.java similarity index 100% rename from src/gtask/remote/GTaskManager.java rename to src/net/micode/notes/gtask/remote/GTaskManager.java diff --git a/src/gtask/remote/GTaskSyncService.java b/src/net/micode/notes/gtask/remote/GTaskSyncService.java similarity index 100% rename from src/gtask/remote/GTaskSyncService.java rename to src/net/micode/notes/gtask/remote/GTaskSyncService.java diff --git a/src/model/Note.java b/src/net/micode/notes/model/Note.java similarity index 100% rename from src/model/Note.java rename to src/net/micode/notes/model/Note.java diff --git a/src/model/WorkingNote.java b/src/net/micode/notes/model/WorkingNote.java similarity index 100% rename from src/model/WorkingNote.java rename to src/net/micode/notes/model/WorkingNote.java diff --git a/src/tool/BackupUtils.java b/src/net/micode/notes/tool/BackupUtils.java similarity index 100% rename from src/tool/BackupUtils.java rename to src/net/micode/notes/tool/BackupUtils.java diff --git a/src/tool/DataUtils.java b/src/net/micode/notes/tool/DataUtils.java similarity index 100% rename from src/tool/DataUtils.java rename to src/net/micode/notes/tool/DataUtils.java diff --git a/src/tool/GTaskStringUtils.java b/src/net/micode/notes/tool/GTaskStringUtils.java similarity index 100% rename from src/tool/GTaskStringUtils.java rename to src/net/micode/notes/tool/GTaskStringUtils.java diff --git a/src/tool/ResourceParser.java b/src/net/micode/notes/tool/ResourceParser.java similarity index 100% rename from src/tool/ResourceParser.java rename to src/net/micode/notes/tool/ResourceParser.java diff --git a/src/ui/AlarmAlertActivity.java b/src/net/micode/notes/ui/AlarmAlertActivity.java similarity index 100% rename from src/ui/AlarmAlertActivity.java rename to src/net/micode/notes/ui/AlarmAlertActivity.java diff --git a/src/ui/AlarmInitReceiver.java b/src/net/micode/notes/ui/AlarmInitReceiver.java similarity index 100% rename from src/ui/AlarmInitReceiver.java rename to src/net/micode/notes/ui/AlarmInitReceiver.java diff --git a/src/ui/AlarmReceiver.java b/src/net/micode/notes/ui/AlarmReceiver.java similarity index 100% rename from src/ui/AlarmReceiver.java rename to src/net/micode/notes/ui/AlarmReceiver.java diff --git a/src/ui/DateTimePicker.java b/src/net/micode/notes/ui/DateTimePicker.java similarity index 100% rename from src/ui/DateTimePicker.java rename to src/net/micode/notes/ui/DateTimePicker.java diff --git a/src/ui/DateTimePickerDialog.java b/src/net/micode/notes/ui/DateTimePickerDialog.java similarity index 100% rename from src/ui/DateTimePickerDialog.java rename to src/net/micode/notes/ui/DateTimePickerDialog.java diff --git a/src/ui/DropdownMenu.java b/src/net/micode/notes/ui/DropdownMenu.java similarity index 100% rename from src/ui/DropdownMenu.java rename to src/net/micode/notes/ui/DropdownMenu.java diff --git a/src/ui/FoldersListAdapter.java b/src/net/micode/notes/ui/FoldersListAdapter.java similarity index 100% rename from src/ui/FoldersListAdapter.java rename to src/net/micode/notes/ui/FoldersListAdapter.java diff --git a/src/ui/NoteEditActivity.java b/src/net/micode/notes/ui/NoteEditActivity.java similarity index 100% rename from src/ui/NoteEditActivity.java rename to src/net/micode/notes/ui/NoteEditActivity.java diff --git a/src/ui/NoteEditText.java b/src/net/micode/notes/ui/NoteEditText.java similarity index 100% rename from src/ui/NoteEditText.java rename to src/net/micode/notes/ui/NoteEditText.java diff --git a/src/ui/NoteItemData.java b/src/net/micode/notes/ui/NoteItemData.java similarity index 100% rename from src/ui/NoteItemData.java rename to src/net/micode/notes/ui/NoteItemData.java diff --git a/src/ui/NotesListActivity.java b/src/net/micode/notes/ui/NotesListActivity.java similarity index 100% rename from src/ui/NotesListActivity.java rename to src/net/micode/notes/ui/NotesListActivity.java diff --git a/src/ui/NotesListAdapter.java b/src/net/micode/notes/ui/NotesListAdapter.java similarity index 100% rename from src/ui/NotesListAdapter.java rename to src/net/micode/notes/ui/NotesListAdapter.java diff --git a/src/ui/NotesListItem.java b/src/net/micode/notes/ui/NotesListItem.java similarity index 100% rename from src/ui/NotesListItem.java rename to src/net/micode/notes/ui/NotesListItem.java diff --git a/src/ui/NotesPreferenceActivity.java b/src/net/micode/notes/ui/NotesPreferenceActivity.java similarity index 100% rename from src/ui/NotesPreferenceActivity.java rename to src/net/micode/notes/ui/NotesPreferenceActivity.java diff --git a/src/widget/NoteWidgetProvider.java b/src/net/micode/notes/widget/NoteWidgetProvider.java similarity index 100% rename from src/widget/NoteWidgetProvider.java rename to src/net/micode/notes/widget/NoteWidgetProvider.java diff --git a/src/widget/NoteWidgetProvider_2x.java b/src/net/micode/notes/widget/NoteWidgetProvider_2x.java similarity index 100% rename from src/widget/NoteWidgetProvider_2x.java rename to src/net/micode/notes/widget/NoteWidgetProvider_2x.java diff --git a/src/widget/NoteWidgetProvider_4x.java b/src/net/micode/notes/widget/NoteWidgetProvider_4x.java similarity index 100% rename from src/widget/NoteWidgetProvider_4x.java rename to src/net/micode/notes/widget/NoteWidgetProvider_4x.java