diff --git a/doc/小米便签泛读、标注和维护报告.docx b/doc/小米便签泛读、标注和维护报告.docx deleted file mode 100644 index 709de28..0000000 Binary files a/doc/小米便签泛读、标注和维护报告.docx and /dev/null differ diff --git a/doc/小米便签泛读报告.docx b/doc/小米便签泛读报告.docx deleted file mode 100644 index e15996d..0000000 Binary files a/doc/小米便签泛读报告.docx and /dev/null differ diff --git a/doc/开源软件的质量分析报告.docx b/doc/开源软件的质量分析报告.docx new file mode 100644 index 0000000..4e8e9fa 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 df62799..0000000 --- a/src/data/Contact.java +++ /dev/null @@ -1,114 +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; // 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/NotesProvider.java b/src/data/NotesProvider.java deleted file mode 100644 index 6e71154..0000000 --- a/src/data/NotesProvider.java +++ /dev/null @@ -1,370 +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.app.SearchManager; -import android.content.ContentProvider; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Intent; -import android.content.UriMatcher; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.net.Uri; -import android.text.TextUtils; -import android.util.Log; - -import net.micode.notes.R; -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; // URI匹配器,用于匹配不同的URI请求 - - private NotesDatabaseHelper mHelper; // 数据库帮助类实例 - - private static final String TAG = "NotesProvider"; // 日志标签 - - // 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); - mMatcher.addURI(Notes.AUTHORITY, "note/#", URI_NOTE_ITEM); - mMatcher.addURI(Notes.AUTHORITY, "data", URI_DATA); - mMatcher.addURI(Notes.AUTHORITY, "data/#", URI_DATA_ITEM); - mMatcher.addURI(Notes.AUTHORITY, "search", URI_SEARCH); - mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, URI_SEARCH_SUGGEST); - mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", URI_SEARCH_SUGGEST); - } - - /** - * 搜索投影字段定义: - * 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; - - // 便签片段搜索查询语句 - 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; // 只搜索普通便签类型 - - /** - * 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) { - Cursor c = null; - SQLiteDatabase db = mHelper.getReadableDatabase(); - String id = null; - switch (mMatcher.match(uri)) { - case URI_NOTE: // 查询note表所有记录 - c = db.query(TABLE.NOTE, projection, selection, selectionArgs, null, null, - sortOrder); - break; - 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: // 查询data表所有记录 - c = db.query(TABLE.DATA, projection, selection, selectionArgs, null, null, - sortOrder); - break; - 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: // 处理搜索和搜索建议请求 - 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"); // 从查询参数获取搜索模式 - } - - if (TextUtils.isEmpty(searchString)) { - return null; // 搜索字符串为空则返回null - } - - try { - searchString = String.format("%%%s%%", searchString); // 添加通配符进行模糊匹配 - c = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY, - new String[] { searchString }); - } catch (IllegalStateException ex) { - Log.e(TAG, "got exception: " + ex.toString()); - } - break; - 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: // 插入note表记录 - insertedId = noteId = db.insert(TABLE.NOTE, null, values); - break; - case URI_DATA: // 插入data表记录 - if (values.containsKey(DataColumns.NOTE_ID)) { - noteId = values.getAsLong(DataColumns.NOTE_ID); - } else { - Log.d(TAG, "Wrong data format without note id:" + values.toString()); - } - insertedId = dataId = db.insert(TABLE.DATA, null, values); - break; - default: - throw new IllegalArgumentException("Unknown URI " + uri); - } - // 通知note URI的数据变化 - if (noteId > 0) { - getContext().getContentResolver().notifyChange( - ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null); - } - - // 通知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; // 标记是否删除data表数据 - switch (mMatcher.match(uri)) { - case URI_NOTE: // 删除note表记录(系统文件夹不允许删除) - selection = "(" + selection + ") AND " + NoteColumns.ID + ">0 "; - count = db.delete(TABLE.NOTE, selection, selectionArgs); - break; - case URI_NOTE_ITEM: // 删除note表单条记录 - id = uri.getPathSegments().get(1); - /** - * ID小于0的是系统文件夹,不允许删除 - */ - long noteId = Long.valueOf(id); - if (noteId <= 0) { - break; // 系统文件夹不删除 - } - count = db.delete(TABLE.NOTE, - NoteColumns.ID + "=" + id + parseSelection(selection), selectionArgs); - break; - case URI_DATA: // 删除data表记录 - count = db.delete(TABLE.DATA, selection, selectionArgs); - deleteData = true; - break; - case URI_DATA_ITEM: // 删除data表单条记录 - id = uri.getPathSegments().get(1); - count = db.delete(TABLE.DATA, - DataColumns.ID + "=" + id + parseSelection(selection), selectionArgs); - deleteData = true; - break; - 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); - } - 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; // 标记是否更新data表数据 - switch (mMatcher.match(uri)) { - case URI_NOTE: // 更新note表记录 - increaseNoteVersion(-1, selection, selectionArgs); // 增加版本号 - count = db.update(TABLE.NOTE, values, selection, selectionArgs); - break; - case URI_NOTE_ITEM: // 更新note表单条记录 - id = uri.getPathSegments().get(1); - increaseNoteVersion(Long.valueOf(id), selection, selectionArgs); // 增加版本号 - count = db.update(TABLE.NOTE, values, NoteColumns.ID + "=" + id - + parseSelection(selection), selectionArgs); - break; - case URI_DATA: // 更新data表记录 - count = db.update(TABLE.DATA, values, selection, selectionArgs); - updateData = true; - break; - case URI_DATA_ITEM: // 更新data表单条记录 - id = uri.getPathSegments().get(1); - count = db.update(TABLE.DATA, values, DataColumns.ID + "=" + id - + parseSelection(selection), selectionArgs); - updateData = true; - break; - default: - 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); - } - 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 "); // 版本号加1 - - // 构建WHERE子句 - if (id > 0 || !TextUtils.isEmpty(selection)) { - sql.append(" WHERE "); - } - if (id > 0) { - sql.append(NoteColumns.ID + "=" + String.valueOf(id)); - } - 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/gtask/data/MetaData.java b/src/gtask/data/MetaData.java deleted file mode 100644 index e30d451..0000000 --- a/src/gtask/data/MetaData.java +++ /dev/null @@ -1,124 +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; - -/** - * 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/gtask/data/Node.java b/src/gtask/data/Node.java deleted file mode 100644 index 344be2e..0000000 --- a/src/gtask/data/Node.java +++ /dev/null @@ -1,165 +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; - -/** - * 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/gtask/data/SqlData.java deleted file mode 100644 index 77a98c4..0000000 --- a/src/gtask/data/SqlData.java +++ /dev/null @@ -1,231 +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.content.ContentResolver; -import android.content.ContentUris; -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.DataConstants; -import net.micode.notes.data.Notes.NoteColumns; -import net.micode.notes.data.NotesDatabaseHelper.TABLE; -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 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; // 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; // 初始化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(); // 创建差异值容器 - } - - /** - * 从数据库游标加载数据到内存字段 - * @param c 数据库查询结果的Cursor - */ - private void loadFromCursor(Cursor c) { - mDataId = c.getLong(DATA_ID_COLUMN); - mDataMimeType = c.getString(DATA_MIME_TYPE_COLUMN); - mDataContent = c.getString(DATA_CONTENT_COLUMN); - mDataContentData1 = c.getLong(DATA_CONTENT_DATA_1_COLUMN); - 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)) { - mDiffDataValues.put(DataColumns.MIME_TYPE, dataMimeType); - } - 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); - } - 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); - js.put(DataColumns.CONTENT, mDataContent); - js.put(DataColumns.DATA1, mDataContentData1); - js.put(DataColumns.DATA3, mDataContentData3); - 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); // 设置关联的便签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, - " ? in (SELECT " + NoteColumns.ID + " FROM " + TABLE.NOTE - + " WHERE " + NoteColumns.VERSION + "=?)", new String[] { - String.valueOf(noteId), String.valueOf(version) - }); - } - if (result == 0) { - Log.w(TAG, "there is no update. maybe user updates note when syncing"); - } - } - } - - // 重置状态 - mDiffDataValues.clear(); - mIsCreate = false; - } - - /** - * 获取数据ID - * @return 数据ID - */ - public long getId() { - return mDataId; - } -} \ No newline at end of file diff --git a/src/gtask/data/SqlNote.java b/src/gtask/data/SqlNote.java deleted file mode 100644 index 137f89c..0000000 --- a/src/gtask/data/SqlNote.java +++ /dev/null @@ -1,572 +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; - -/** - * 数据库笔记类(用于与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/gtask/data/Task.java deleted file mode 100644 index 3589281..0000000 --- a/src/gtask/data/Task.java +++ /dev/null @@ -1,435 +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.text.TextUtils; -import android.util.Log; - -import net.micode.notes.data.Notes; -import net.micode.notes.data.Notes.DataColumns; -import net.micode.notes.data.Notes.DataConstants; -import net.micode.notes.data.Notes.NoteColumns; -import net.micode.notes.gtask.exception.ActionFailureException; -import net.micode.notes.tool.GTaskStringUtils; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -/** - * 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; // 父任务列表(TaskList) - - /** - * 默认构造函数,初始化任务属性 - */ - public Task() { - super(); - 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 { - // 操作类型:创建 - js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, - GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE); - - // 操作ID - js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); - - // 索引位置(在父列表中的位置) - js.put(GTaskStringUtils.GTASK_JSON_INDEX, mParent.getChildTaskIndex(this)); - - // 实体数据变化 - JSONObject entity = new JSONObject(); - 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); // 实体类型为任务 - if (getNotes() != null) { - entity.put(GTaskStringUtils.GTASK_JSON_NOTES, getNotes()); // 任务备注 - } - js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); - - // 父列表ID - js.put(GTaskStringUtils.GTASK_JSON_PARENT_ID, mParent.getGid()); - - // 目标父类型 - js.put(GTaskStringUtils.GTASK_JSON_DEST_PARENT_TYPE, - GTaskStringUtils.GTASK_JSON_TYPE_GROUP); - - // 列表ID(与父列表ID相同) - js.put(GTaskStringUtils.GTASK_JSON_LIST_ID, mParent.getGid()); - - // 前兄弟任务ID(用于排序) - if (mPriorSibling != null) { - js.put(GTaskStringUtils.GTASK_JSON_PRIOR_SIBLING_ID, mPriorSibling.getGid()); - } - - } catch (JSONException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new ActionFailureException("fail to generate task-create jsonobject"); - } - - return js; - } - - /** - * 获取更新任务的JSON操作数据(用于发送到Google Tasks服务器) - * @param actionId 操作ID - * @return 包含更新操作数据的JSONObject - * @throws ActionFailureException 生成JSON失败时抛出 - */ - public JSONObject getUpdateAction(int actionId) { - JSONObject js = new JSONObject(); - - try { - // 操作类型:更新 - js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, - GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE); - - // 操作ID - js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); - - // 任务ID - js.put(GTaskStringUtils.GTASK_JSON_ID, getGid()); - - // 实体数据变化 - JSONObject entity = new JSONObject(); - entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); // 任务名称 - if (getNotes() != null) { - entity.put(GTaskStringUtils.GTASK_JSON_NOTES, getNotes()); // 任务备注 - } - entity.put(GTaskStringUtils.GTASK_JSON_DELETED, getDeleted()); // 删除状态 - js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); - - } catch (JSONException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new ActionFailureException("fail to generate task-update jsonobject"); - } - - return js; - } - - /** - * 从远程JSON数据设置任务内容(从Google Tasks服务器获取) - * @param js 包含远程任务数据的JSONObject - * @throws ActionFailureException 解析JSON失败时抛出 - */ - public void setContentByRemoteJSON(JSONObject js) { - if (js != null) { - try { - // 任务ID - if (js.has(GTaskStringUtils.GTASK_JSON_ID)) { - setGid(js.getString(GTaskStringUtils.GTASK_JSON_ID)); - } - - // 最后修改时间 - if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) { - setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)); - } - - // 任务名称 - if (js.has(GTaskStringUtils.GTASK_JSON_NAME)) { - setName(js.getString(GTaskStringUtils.GTASK_JSON_NAME)); - } - - // 任务备注 - if (js.has(GTaskStringUtils.GTASK_JSON_NOTES)) { - setNotes(js.getString(GTaskStringUtils.GTASK_JSON_NOTES)); - } - - // 删除状态 - if (js.has(GTaskStringUtils.GTASK_JSON_DELETED)) { - setDeleted(js.getBoolean(GTaskStringUtils.GTASK_JSON_DELETED)); - } - - // 完成状态 - if (js.has(GTaskStringUtils.GTASK_JSON_COMPLETED)) { - setCompleted(js.getBoolean(GTaskStringUtils.GTASK_JSON_COMPLETED)); - } - } catch (JSONException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new ActionFailureException("fail to get task content from jsonobject"); - } - } - } - - /** - * 从本地JSON数据设置任务内容(从本地数据库获取) - * @param js 包含本地便签数据的JSONObject - */ - public void setContentByLocalJSON(JSONObject js) { - if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE) - || !js.has(GTaskStringUtils.META_HEAD_DATA)) { - Log.w(TAG, "setContentByLocalJSON: nothing is avaiable"); - } - - try { - // 获取便签信息和数据数组 - JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); - JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA); - - // 验证便签类型 - if (note.getInt(NoteColumns.TYPE) != Notes.TYPE_NOTE) { - Log.e(TAG, "invalid type"); - return; - } - - // 遍历数据数组,查找便签内容 - for (int i = 0; i < dataArray.length(); i++) { - JSONObject data = dataArray.getJSONObject(i); - if (TextUtils.equals(data.getString(DataColumns.MIME_TYPE), DataConstants.NOTE)) { - setName(data.getString(DataColumns.CONTENT)); // 设置任务名称为便签内容 - break; - } - } - - } catch (JSONException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - } - } - - /** - * 从任务内容生成本地JSON数据(用于本地存储) - * @return 包含本地便签数据的JSONObject,如果任务无内容则返回null - */ - public JSONObject getLocalJSONFromContent() { - String name = getName(); - try { - if (mMetaInfo == null) { - // 从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); // 便签内容 - dataArray.put(data); - js.put(GTaskStringUtils.META_HEAD_DATA, dataArray); - note.put(NoteColumns.TYPE, Notes.TYPE_NOTE); // 类型为普通便签 - js.put(GTaskStringUtils.META_HEAD_NOTE, note); - return js; - } else { - // 已同步的任务(有元信息) - JSONObject note = mMetaInfo.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); - JSONArray dataArray = mMetaInfo.getJSONArray(GTaskStringUtils.META_HEAD_DATA); - - // 更新便签内容 - for (int i = 0; i < dataArray.length(); i++) { - JSONObject data = dataArray.getJSONObject(i); - if (TextUtils.equals(data.getString(DataColumns.MIME_TYPE), DataConstants.NOTE)) { - data.put(DataColumns.CONTENT, getName()); - break; - } - } - - note.put(NoteColumns.TYPE, Notes.TYPE_NOTE); - return mMetaInfo; // 返回更新后的元信息 - } - } catch (JSONException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - return null; - } - } - - /** - * 设置元信息(从MetaData对象) - * @param metaData 元数据对象 - */ - public void setMetaInfo(MetaData metaData) { - if (metaData != null && metaData.getNotes() != null) { - try { - mMetaInfo = new JSONObject(metaData.getNotes()); - } catch (JSONException e) { - Log.w(TAG, e.toString()); - mMetaInfo = null; - } - } - } - - /** - * 根据数据库游标确定同步操作类型 - * @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; // 远程更新本地 - } - - if (!noteInfo.has(NoteColumns.ID)) { - Log.w(TAG, "remote note id seems to be deleted"); - return SYNC_ACTION_UPDATE_LOCAL; // 本地更新远程 - } - - // 验证便签ID是否匹配 - if (c.getLong(SqlNote.ID_COLUMN) != noteInfo.getLong(NoteColumns.ID)) { - Log.w(TAG, "note id doesn't match"); - return SYNC_ACTION_UPDATE_LOCAL; // ID不匹配,用本地更新远程 - } - - // 检查本地修改标记 - if (c.getInt(SqlNote.LOCAL_MODIFIED_COLUMN) == 0) { - // 本地无修改 - if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { - // 两边都无更新 - return SYNC_ACTION_NONE; - } else { - // 远程有更新,应用到本地 - return SYNC_ACTION_UPDATE_LOCAL; - } - } else { - // 本地有修改 - // 验证Google Task ID是否匹配 - if (!c.getString(SqlNote.GTASK_ID_COLUMN).equals(getGid())) { - Log.e(TAG, "gtask id doesn't match"); - return SYNC_ACTION_ERROR; // ID不匹配,错误 - } - if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { - // 仅本地有修改 - return SYNC_ACTION_UPDATE_REMOTE; - } else { - // 两边都有修改,冲突 - return SYNC_ACTION_UPDATE_CONFLICT; - } - } - } catch (Exception e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - } - - return SYNC_ACTION_ERROR; // 发生异常,返回错误 - } - - /** - * 判断任务是否值得保存(是否有有效内容) - * @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/gtask/data/TaskList.java deleted file mode 100644 index e096dd8..0000000 --- a/src/gtask/data/TaskList.java +++ /dev/null @@ -1,446 +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.data.Notes; -import net.micode.notes.data.Notes.NoteColumns; -import net.micode.notes.gtask.exception.ActionFailureException; -import net.micode.notes.tool.GTaskStringUtils; - -import org.json.JSONException; -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 int mIndex; // 任务列表在父容器中的索引位置 - private ArrayList mChildren; // 子任务列表 - - /** - * 默认构造函数,初始化任务列表属性 - */ - public TaskList() { - super(); - 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 { - // 操作类型:创建 - js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, - GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE); - - // 操作ID - js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); - - // 索引位置 - js.put(GTaskStringUtils.GTASK_JSON_INDEX, mIndex); - - // 实体数据变化 - JSONObject entity = new JSONObject(); - 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); // 实体类型为组(列表) - js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); - - } catch (JSONException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new ActionFailureException("fail to generate tasklist-create jsonobject"); - } - - return js; - } - - /** - * 获取更新任务列表的JSON操作数据(用于发送到Google Tasks服务器) - * @param actionId 操作ID - * @return 包含更新操作数据的JSONObject - * @throws ActionFailureException 生成JSON失败时抛出 - */ - public JSONObject getUpdateAction(int actionId) { - JSONObject js = new JSONObject(); - - try { - // 操作类型:更新 - js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, - GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE); - - // 操作ID - js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); - - // 列表ID - js.put(GTaskStringUtils.GTASK_JSON_ID, getGid()); - - // 实体数据变化 - JSONObject entity = new JSONObject(); - 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) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new ActionFailureException("fail to generate tasklist-update jsonobject"); - } - - return js; - } - - /** - * 从远程JSON数据设置任务列表内容(从Google Tasks服务器获取) - * @param js 包含远程列表数据的JSONObject - * @throws ActionFailureException 解析JSON失败时抛出 - */ - public void setContentByRemoteJSON(JSONObject js) { - if (js != null) { - try { - // 列表ID - if (js.has(GTaskStringUtils.GTASK_JSON_ID)) { - setGid(js.getString(GTaskStringUtils.GTASK_JSON_ID)); - } - - // 最后修改时间 - if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) { - setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)); - } - - // 列表名称 - if (js.has(GTaskStringUtils.GTASK_JSON_NAME)) { - setName(js.getString(GTaskStringUtils.GTASK_JSON_NAME)); - } - - } catch (JSONException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new ActionFailureException("fail to get tasklist content from jsonobject"); - } - } - } - - /** - * 从本地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"); - } - - 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) - setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX - + GTaskStringUtils.FOLDER_CALL_NOTE); - else - Log.e(TAG, "invalid system folder"); - } else { - Log.e(TAG, "error type"); - } - } catch (JSONException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - } - } - - /** - * 从任务列表内容生成本地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); // 默认文件夹和通话记录文件夹为系统类型 - else - folder.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); // 其他为普通文件夹 - - js.put(GTaskStringUtils.META_HEAD_NOTE, folder); - - return js; - } catch (JSONException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - return null; - } - } - - /** - * 根据数据库游标确定同步操作类型 - * @param c 数据库查询结果的Cursor - * @return 同步操作状态码(SYNC_ACTION_*) - */ - public int getSyncAction(Cursor c) { - try { - if (c.getInt(SqlNote.LOCAL_MODIFIED_COLUMN) == 0) { - // 本地无修改 - if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { - // 两边都无更新 - return SYNC_ACTION_NONE; - } else { - // 远程有更新,应用到本地 - return SYNC_ACTION_UPDATE_LOCAL; - } - } else { - // 本地有修改 - // 验证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()) { - // 仅本地有修改 - return SYNC_ACTION_UPDATE_REMOTE; - } else { - // 文件夹冲突时,优先应用本地修改(不同于任务的处理方式) - return SYNC_ACTION_UPDATE_REMOTE; - } - } - } catch (Exception e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - } - - 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) { - // 设置前兄弟任务和父列表 - task.setPriorSibling(mChildren.isEmpty() ? null : mChildren - .get(mChildren.size() - 1)); - task.setParent(this); - } - } - 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"); - return false; - } - - int pos = mChildren.indexOf(task); - if (task != null && pos == -1) { - mChildren.add(index, task); - - // 更新任务列表的前后兄弟关系 - Task preTask = null; - Task afterTask = null; - if (index != 0) - preTask = mChildren.get(index - 1); - if (index != mChildren.size() - 1) - afterTask = mChildren.get(index + 1); - - task.setPriorSibling(preTask); - if (afterTask != null) - afterTask.setPriorSibling(task); - } - - return true; - } - - /** - * 从列表中移除子任务 - * @param task 要移除的任务 - * @return 移除成功返回true,失败返回false - */ - public boolean removeChildTask(Task task) { - boolean ret = false; - int index = mChildren.indexOf(task); - if (index != -1) { - ret = mChildren.remove(task); - - if (ret) { - // 重置任务的前兄弟任务和父列表 - task.setPriorSibling(null); - task.setParent(null); - - // 更新任务列表的前兄弟关系 - if (index != mChildren.size()) { - mChildren.get(index).setPriorSibling( - index == 0 ? null : mChildren.get(index - 1)); - } - } - } - return ret; - } - - /** - * 移动子任务到指定索引位置 - * @param task 要移动的任务 - * @param index 目标索引位置 - * @return 移动成功返回true,失败返回false - */ - public boolean moveChildTask(Task task, int index) { - - if (index < 0 || index >= mChildren.size()) { - Log.e(TAG, "move child task: invalid index"); - return false; - } - - int pos = mChildren.indexOf(task); - if (pos == -1) { - Log.e(TAG, "move child task: the task should in the list"); - return false; - } - - if (pos == index) - return true; - 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); - if (t.getGid().equals(gid)) { - return t; - } - } - 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"); - return null; - } - 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)) - return task; - } - 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/ui/AlarmAlertActivity.java b/src/ui/AlarmAlertActivity.java deleted file mode 100644 index f96f8ef..0000000 --- a/src/ui/AlarmAlertActivity.java +++ /dev/null @@ -1,216 +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.ui; - -import android.app.Activity; -import android.app.AlertDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.DialogInterface.OnClickListener; -import android.content.DialogInterface.OnDismissListener; -import android.content.Intent; -import android.media.AudioManager; -import android.media.MediaPlayer; -import android.media.RingtoneManager; -import android.net.Uri; -import android.os.Bundle; -import android.os.PowerManager; -import android.provider.Settings; -import android.view.Window; -import android.view.WindowManager; - -import net.micode.notes.R; -import net.micode.notes.data.Notes; -import net.micode.notes.tool.DataUtils; - -import java.io.IOException; - -/** - * 便签闹钟提醒活动 - * - * 负责处理便签提醒时间到达时的提醒功能,包括: - * 1. 播放闹钟提示音(使用系统默认闹钟铃声) - * 2. 在屏幕锁定状态下显示提醒对话框 - * 3. 提供用户操作选项:关闭提醒或进入便签编辑界面 - * - * 设计说明: - * - 使用全屏对话框形式显示提醒 - * - 支持在屏幕锁定时唤醒屏幕并显示提醒 - * - 自动检查便签是否存在,防止已删除便签的错误提醒 - */ -public class AlarmAlertActivity extends Activity implements OnClickListener, OnDismissListener { - private long mNoteId; // 触发提醒的便签ID - private String mSnippet; // 便签内容摘要 - private static final int SNIPPET_PREW_MAX_LEN = 60; // 摘要显示的最大长度 - MediaPlayer mPlayer; // 媒体播放器,用于播放闹钟音 - - /** - * Activity创建时的回调方法 - * 初始化提醒界面和播放闹钟音 - * - * @param savedInstanceState 保存的状态数据 - */ - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - requestWindowFeature(Window.FEATURE_NO_TITLE); // 设置无标题栏 - - final Window win = getWindow(); - win.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); // 在锁屏状态下显示 - - // 如果屏幕未亮起,则添加更多窗口标志以唤醒屏幕 - if (!isScreenOn()) { - win.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON // 保持屏幕常亮 - | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON // 点亮屏幕 - | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON // 允许屏幕点亮时锁定 - | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR); // 允许装饰嵌入布局 - } - - Intent intent = getIntent(); - - try { - // 从Intent中提取便签ID和内容 - mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1)); - mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId); - // 截取便签内容摘要,避免过长显示 - mSnippet = mSnippet.length() > SNIPPET_PREW_MAX_LEN ? mSnippet.substring(0, - SNIPPET_PREW_MAX_LEN) + getResources().getString(R.string.notelist_string_info) - : mSnippet; - } catch (IllegalArgumentException e) { - e.printStackTrace(); - return; // 如果便签ID无效,直接返回 - } - - mPlayer = new MediaPlayer(); // 初始化媒体播放器 - // 检查便签是否在数据库中且可见(不在回收站中) - if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) { - showActionDialog(); // 显示操作对话框 - playAlarmSound(); // 播放闹钟音 - } else { - finish(); // 便签不存在或不可见,直接结束Activity - } - } - - /** - * 检查屏幕是否亮起 - * - * @return true表示屏幕亮起,false表示屏幕关闭 - */ - private boolean isScreenOn() { - PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); - return pm.isScreenOn(); - } - - /** - * 播放闹钟提示音 - * 使用系统默认闹钟铃声,并根据系统静音设置调整音频流类型 - */ - private void playAlarmSound() { - Uri url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM); // 获取默认闹钟铃声 - - // 检查系统静音设置是否影响闹钟音频流 - int silentModeStreams = Settings.System.getInt(getContentResolver(), - Settings.System.MODE_RINGER_STREAMS_AFFECTED, 0); - - // 如果静音设置影响闹钟音频流,则使用该设置;否则使用默认的闹钟音频流 - if ((silentModeStreams & (1 << AudioManager.STREAM_ALARM)) != 0) { - mPlayer.setAudioStreamType(silentModeStreams); - } else { - mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM); - } - - // 设置并播放闹钟音 - try { - mPlayer.setDataSource(this, url); - mPlayer.prepare(); - mPlayer.setLooping(true); // 设置循环播放,直到用户操作 - mPlayer.start(); - } catch (IllegalArgumentException e) { - e.printStackTrace(); - } catch (SecurityException e) { - e.printStackTrace(); - } catch (IllegalStateException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - } - - /** - * 显示提醒操作对话框 - * 对话框显示便签内容摘要,并提供操作按钮 - */ - private void showActionDialog() { - AlertDialog.Builder dialog = new AlertDialog.Builder(this); - dialog.setTitle(R.string.app_name); // 设置应用名称作为标题 - dialog.setMessage(mSnippet); // 设置便签内容摘要 - - // 确定按钮:关闭提醒 - dialog.setPositiveButton(R.string.notealert_ok, this); - - // 如果屏幕亮起,显示"进入编辑"按钮 - if (isScreenOn()) { - dialog.setNegativeButton(R.string.notealert_enter, this); - } - - dialog.show().setOnDismissListener(this); // 显示对话框并设置关闭监听器 - } - - /** - * 对话框按钮点击回调方法 - * - * @param dialog 被点击的对话框 - * @param which 被点击的按钮标识 - */ - public void onClick(DialogInterface dialog, int which) { - switch (which) { - case DialogInterface.BUTTON_NEGATIVE: - // 用户点击"进入编辑"按钮,启动便签编辑界面 - Intent intent = new Intent(this, NoteEditActivity.class); - intent.setAction(Intent.ACTION_VIEW); - intent.putExtra(Intent.EXTRA_UID, mNoteId); // 传递便签ID - startActivity(intent); - break; - default: - // 其他按钮(如确定按钮)无额外操作 - break; - } - } - - /** - * 对话框关闭回调方法 - * 停止闹钟音并结束Activity - * - * @param dialog 被关闭的对话框 - */ - public void onDismiss(DialogInterface dialog) { - stopAlarmSound(); // 停止播放闹钟音 - finish(); // 结束Activity - } - - /** - * 停止闹钟音播放 - * 释放媒体播放器资源 - */ - private void stopAlarmSound() { - if (mPlayer != null) { - mPlayer.stop(); // 停止播放 - mPlayer.release(); // 释放资源 - mPlayer = null; // 清空引用 - } - } -} \ No newline at end of file diff --git a/src/ui/AlarmInitReceiver.java b/src/ui/AlarmInitReceiver.java deleted file mode 100644 index 4d1d7ba..0000000 --- a/src/ui/AlarmInitReceiver.java +++ /dev/null @@ -1,89 +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.ui; - -import android.app.AlarmManager; -import android.app.PendingIntent; -import android.content.BroadcastReceiver; -import android.content.ContentUris; -import android.content.Context; -import android.content.Intent; -import android.database.Cursor; - -import net.micode.notes.data.Notes; -import net.micode.notes.data.Notes.NoteColumns; - -/** - * 闹钟初始化接收器 - * 主要功能:在系统启动或应用启动时,重新设置所有未来的闹钟提醒 - */ -public class AlarmInitReceiver extends BroadcastReceiver { - - // 数据库查询需要的列 - private static final String [] PROJECTION = new String [] { - NoteColumns.ID, // 笔记ID - NoteColumns.ALERTED_DATE // 提醒时间 - }; - - // 列索引常量 - private static final int COLUMN_ID = 0; // ID列索引 - private static final int COLUMN_ALERTED_DATE = 1; // 提醒时间列索引 - - @Override - public void onReceive(Context context, Intent intent) { - // 获取当前系统时间 - long currentDate = System.currentTimeMillis(); - - // 查询所有需要设置闹钟的笔记 - // 条件:提醒时间大于当前时间,且笔记类型为普通笔记(非文件夹等) - Cursor c = context.getContentResolver().query(Notes.CONTENT_NOTE_URI, - PROJECTION, - NoteColumns.ALERTED_DATE + ">? AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE, - new String[] { String.valueOf(currentDate) }, - null); - - if (c != null) { - // 遍历查询结果 - if (c.moveToFirst()) { - do { - // 获取该笔记的提醒时间 - long alertDate = c.getLong(COLUMN_ALERTED_DATE); - - // 创建触发闹钟的Intent,目标为AlarmReceiver - Intent sender = new Intent(context, AlarmReceiver.class); - // 将笔记ID附加到URI中,以便AlarmReceiver知道是哪个笔记的提醒 - sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, - c.getLong(COLUMN_ID))); - - // 创建PendingIntent,用于在指定时间触发广播 - PendingIntent pendingIntent = PendingIntent.getBroadcast(context, - 0, sender, 0); - - // 获取系统闹钟服务 - AlarmManager alermManager = (AlarmManager) context - .getSystemService(Context.ALARM_SERVICE); - - // 设置闹钟 - // 使用RTC_WAKEUP模式:在指定时间唤醒设备并触发PendingIntent - alermManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent); - } while (c.moveToNext()); // 继续处理下一条记录 - } - c.close(); // 关闭游标,释放资源 - } - // 注意:如果c为null,说明查询失败,这里没有错误处理 - } -} \ No newline at end of file diff --git a/src/ui/AlarmReceiver.java b/src/ui/AlarmReceiver.java deleted file mode 100644 index 0a907f0..0000000 --- a/src/ui/AlarmReceiver.java +++ /dev/null @@ -1,57 +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.ui; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; - -/** - * 闹钟触发接收器 - * 主要功能:接收由AlarmManager触发的闹钟广播,并启动提醒界面 - * - * 注意:此类与AlarmInitReceiver配合工作: - * - AlarmInitReceiver: 负责在系统启动时重新设置所有未来的闹钟 - * - AlarmReceiver: 负责在闹钟触发时执行实际的动作(启动提醒界面) - */ -public class AlarmReceiver extends BroadcastReceiver { - - /** - * 接收广播并处理闹钟触发事件 - * 当AlarmManager设置的闹钟时间到达时,系统会调用此方法 - * - * @param context 当前上下文,用于启动新的Activity - * @param intent 包含触发闹钟的笔记信息的Intent, - * 通常通过AlarmInitReceiver设置,包含笔记的URI数据 - */ - @Override - public void onReceive(Context context, Intent intent) { - // 将Intent的目标类设置为AlarmAlertActivity,这是显示提醒的界面 - intent.setClass(context, AlarmAlertActivity.class); - - // 添加FLAG_ACTIVITY_NEW_TASK标志,因为从BroadcastReceiver启动Activity - // 必须在一个新的任务栈中,不能直接在当前任务栈启动 - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - - // 启动闹钟提醒Activity - // AlarmAlertActivity将显示具体的笔记内容提醒用户 - context.startActivity(intent); - - // 注意:此处没有取消或清除闹钟,如果需要一次性闹钟, - // 应在AlarmAlertActivity中处理闹钟的清除 - } -} \ No newline at end of file diff --git a/src/ui/DateTimePicker.java b/src/ui/DateTimePicker.java deleted file mode 100644 index 0354a85..0000000 --- a/src/ui/DateTimePicker.java +++ /dev/null @@ -1,570 +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.ui; - -import java.text.DateFormatSymbols; -import java.util.Calendar; - -import net.micode.notes.R; - -import android.content.Context; -import android.text.format.DateFormat; -import android.view.View; -import android.widget.FrameLayout; -import android.widget.NumberPicker; - -/** - * 日期时间选择器自定义控件 - * 提供年、月、日、时、分的选择功能,支持12小时制和24小时制显示 - * 使用NumberPicker实现滚动选择,具有日期跨天自动调整功能 - */ -public class DateTimePicker extends FrameLayout { - - // 默认启用状态 - private static final boolean DEFAULT_ENABLE_STATE = true; - - // 时间相关常量 - private static final int HOURS_IN_HALF_DAY = 12; // 半天的小时数 - private static final int HOURS_IN_ALL_DAY = 24; // 全天的小时数 - private static final int DAYS_IN_ALL_WEEK = 7; // 一周的天数(用于显示最近一周的日期) - - // NumberPicker范围常量 - private static final int DATE_SPINNER_MIN_VAL = 0; // 日期选择器最小值 - private static final int DATE_SPINNER_MAX_VAL = DAYS_IN_ALL_WEEK - 1; // 日期选择器最大值 - private static final int HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW = 0; // 24小时制小时最小值 - private static final int HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW = 23; // 24小时制小时最大值 - private static final int HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW = 1; // 12小时制小时最小值 - private static final int HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW = 12; // 12小时制小时最大值 - private static final int MINUT_SPINNER_MIN_VAL = 0; // 分钟选择器最小值 - private static final int MINUT_SPINNER_MAX_VAL = 59; // 分钟选择器最大值 - private static final int AMPM_SPINNER_MIN_VAL = 0; // AM/PM选择器最小值 - private static final int AMPM_SPINNER_MAX_VAL = 1; // AM/PM选择器最大值 - - // UI控件引用 - private final NumberPicker mDateSpinner; // 日期选择器 - private final NumberPicker mHourSpinner; // 小时选择器 - private final NumberPicker mMinuteSpinner; // 分钟选择器 - private final NumberPicker mAmPmSpinner; // 上午/下午选择器 - - // 数据模型 - private Calendar mDate; // 当前选择的日期时间 - private String[] mDateDisplayValues = new String[DAYS_IN_ALL_WEEK]; // 日期显示值(一周) - private boolean mIsAm; // 是否为上午 - private boolean mIs24HourView; // 是否使用24小时制 - private boolean mIsEnabled = DEFAULT_ENABLE_STATE; // 控件是否启用 - private boolean mInitialising; // 是否正在初始化标志,防止初始化期间触发事件 - - // 日期时间变化监听器 - private OnDateTimeChangedListener mOnDateTimeChangedListener; - - /** - * 日期变化监听器 - * 当日期选择器值变化时,调整Calendar对象并更新显示 - */ - private NumberPicker.OnValueChangeListener mOnDateChangedListener = new NumberPicker.OnValueChangeListener() { - @Override - public void onValueChange(NumberPicker picker, int oldVal, int newVal) { - // 根据新旧值差异调整天数 - mDate.add(Calendar.DAY_OF_YEAR, newVal - oldVal); - updateDateControl(); // 更新日期显示 - onDateTimeChanged(); // 通知监听器 - } - }; - - /** - * 小时变化监听器 - * 处理小时变化,包括12/24小时制转换和跨天边界处理 - */ - private NumberPicker.OnValueChangeListener mOnHourChangedListener = new NumberPicker.OnValueChangeListener() { - @Override - public void onValueChange(NumberPicker picker, int oldVal, int newVal) { - boolean isDateChanged = false; - Calendar cal = Calendar.getInstance(); - - // 12小时制下的特殊处理 - if (!mIs24HourView) { - // 处理PM到AM的跨天转换(12小时制) - if (!mIsAm && oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) { - cal.setTimeInMillis(mDate.getTimeInMillis()); - cal.add(Calendar.DAY_OF_YEAR, 1); - isDateChanged = true; - } - // 处理AM到PM的跨天转换(12小时制) - else if (mIsAm && oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) { - cal.setTimeInMillis(mDate.getTimeInMillis()); - cal.add(Calendar.DAY_OF_YEAR, -1); - isDateChanged = true; - } - - // 处理12点切换时AM/PM状态变化 - if (oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY || - oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) { - mIsAm = !mIsAm; - updateAmPmControl(); - } - } - // 24小时制下的跨天处理 - else { - // 23->0 跨到第二天 - if (oldVal == HOURS_IN_ALL_DAY - 1 && newVal == 0) { - cal.setTimeInMillis(mDate.getTimeInMillis()); - cal.add(Calendar.DAY_OF_YEAR, 1); - isDateChanged = true; - } - // 0->23 跨到前一天 - else if (oldVal == 0 && newVal == HOURS_IN_ALL_DAY - 1) { - cal.setTimeInMillis(mDate.getTimeInMillis()); - cal.add(Calendar.DAY_OF_YEAR, -1); - isDateChanged = true; - } - } - - // 设置新的小时值 - int newHour = mHourSpinner.getValue() % HOURS_IN_HALF_DAY + (mIsAm ? 0 : HOURS_IN_HALF_DAY); - mDate.set(Calendar.HOUR_OF_DAY, newHour); - onDateTimeChanged(); // 通知监听器 - - // 如果日期已变化,更新年、月、日 - if (isDateChanged) { - setCurrentYear(cal.get(Calendar.YEAR)); - setCurrentMonth(cal.get(Calendar.MONTH)); - setCurrentDay(cal.get(Calendar.DAY_OF_MONTH)); - } - } - }; - - /** - * 分钟变化监听器 - * 处理分钟滚动时的跨小时边界调整 - */ - private NumberPicker.OnValueChangeListener mOnMinuteChangedListener = new NumberPicker.OnValueChangeListener() { - @Override - public void onValueChange(NumberPicker picker, int oldVal, int newVal) { - int minValue = mMinuteSpinner.getMinValue(); - int maxValue = mMinuteSpinner.getMaxValue(); - int offset = 0; - - // 检测分钟是否从最大值滚动到最小值(59->0,增加1小时) - if (oldVal == maxValue && newVal == minValue) { - offset += 1; - } - // 检测分钟是否从最小值滚动到最大值(0->59,减少1小时) - else if (oldVal == minValue && newVal == maxValue) { - offset -= 1; - } - - // 如果有小时偏移,调整时间 - if (offset != 0) { - mDate.add(Calendar.HOUR_OF_DAY, offset); - mHourSpinner.setValue(getCurrentHour()); - updateDateControl(); // 更新日期显示 - - // 更新AM/PM状态 - int newHour = getCurrentHourOfDay(); - if (newHour >= HOURS_IN_HALF_DAY) { - mIsAm = false; - updateAmPmControl(); - } else { - mIsAm = true; - updateAmPmControl(); - } - } - - // 设置新的分钟值 - mDate.set(Calendar.MINUTE, newVal); - onDateTimeChanged(); // 通知监听器 - } - }; - - /** - * AM/PM变化监听器 - * 处理上午/下午切换,调整12小时 - */ - private NumberPicker.OnValueChangeListener mOnAmPmChangedListener = new NumberPicker.OnValueChangeListener() { - @Override - public void onValueChange(NumberPicker picker, int oldVal, int newVal) { - mIsAm = !mIsAm; - // AM/PM切换时调整12小时 - if (mIsAm) { - mDate.add(Calendar.HOUR_OF_DAY, -HOURS_IN_HALF_DAY); - } else { - mDate.add(Calendar.HOUR_OF_DAY, HOURS_IN_HALF_DAY); - } - updateAmPmControl(); // 更新AM/PM显示 - onDateTimeChanged(); // 通知监听器 - } - }; - - /** - * 日期时间变化监听器接口 - */ - public interface OnDateTimeChangedListener { - void onDateTimeChanged(DateTimePicker view, int year, int month, - int dayOfMonth, int hourOfDay, int minute); - } - - /** - * 构造函数 - 使用当前时间初始化 - */ - public DateTimePicker(Context context) { - this(context, System.currentTimeMillis()); - } - - /** - * 构造函数 - 使用指定时间戳初始化 - */ - public DateTimePicker(Context context, long date) { - this(context, date, DateFormat.is24HourFormat(context)); - } - - /** - * 构造函数 - 完整初始化 - * @param context 上下文 - * @param date 初始时间戳 - * @param is24HourView 是否使用24小时制 - */ - public DateTimePicker(Context context, long date, boolean is24HourView) { - super(context); - mDate = Calendar.getInstance(); - mInitialising = true; // 设置初始化标志 - // 根据当前小时判断是上午还是下午 - mIsAm = getCurrentHourOfDay() < HOURS_IN_HALF_DAY; - - // 加载布局 - inflate(context, R.layout.datetime_picker, this); - - // 初始化日期选择器 - mDateSpinner = (NumberPicker) findViewById(R.id.date); - mDateSpinner.setMinValue(DATE_SPINNER_MIN_VAL); - mDateSpinner.setMaxValue(DATE_SPINNER_MAX_VAL); - mDateSpinner.setOnValueChangedListener(mOnDateChangedListener); - - // 初始化小时选择器 - mHourSpinner = (NumberPicker) findViewById(R.id.hour); - mHourSpinner.setOnValueChangedListener(mOnHourChangedListener); - - // 初始化分钟选择器 - mMinuteSpinner = (NumberPicker) findViewById(R.id.minute); - mMinuteSpinner.setMinValue(MINUT_SPINNER_MIN_VAL); - mMinuteSpinner.setMaxValue(MINUT_SPINNER_MAX_VAL); - mMinuteSpinner.setOnLongPressUpdateInterval(100); // 设置长按更新间隔 - mMinuteSpinner.setOnValueChangedListener(mOnMinuteChangedListener); - - // 初始化AM/PM选择器 - String[] stringsForAmPm = new DateFormatSymbols().getAmPmStrings(); // 获取本地化的AM/PM字符串 - mAmPmSpinner = (NumberPicker) findViewById(R.id.amPm); - mAmPmSpinner.setMinValue(AMPM_SPINNER_MIN_VAL); - mAmPmSpinner.setMaxValue(AMPM_SPINNER_MAX_VAL); - mAmPmSpinner.setDisplayedValues(stringsForAmPm); - mAmPmSpinner.setOnValueChangedListener(mOnAmPmChangedListener); - - // 更新控件初始状态 - updateDateControl(); - updateHourControl(); - updateAmPmControl(); - - set24HourView(is24HourView); // 设置12/24小时制显示 - setCurrentDate(date); // 设置当前时间 - setEnabled(isEnabled()); // 设置启用状态 - - mInitialising = false; // 初始化完成 - } - - /** - * 设置控件启用状态 - */ - @Override - public void setEnabled(boolean enabled) { - if (mIsEnabled == enabled) { - return; - } - super.setEnabled(enabled); - // 设置所有子控件的启用状态 - mDateSpinner.setEnabled(enabled); - mMinuteSpinner.setEnabled(enabled); - mHourSpinner.setEnabled(enabled); - mAmPmSpinner.setEnabled(enabled); - mIsEnabled = enabled; - } - - /** - * 获取控件启用状态 - */ - @Override - public boolean isEnabled() { - return mIsEnabled; - } - - /** - * 获取当前时间的时间戳 - */ - public long getCurrentDateInTimeMillis() { - return mDate.getTimeInMillis(); - } - - /** - * 通过时间戳设置当前时间 - */ - public void setCurrentDate(long date) { - Calendar cal = Calendar.getInstance(); - cal.setTimeInMillis(date); - setCurrentDate(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH), - cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE)); - } - - /** - * 通过各时间分量设置当前时间 - */ - public void setCurrentDate(int year, int month, - int dayOfMonth, int hourOfDay, int minute) { - setCurrentYear(year); - setCurrentMonth(month); - setCurrentDay(dayOfMonth); - setCurrentHour(hourOfDay); - setCurrentMinute(minute); - } - - /** - * 获取当前年份 - */ - public int getCurrentYear() { - return mDate.get(Calendar.YEAR); - } - - /** - * 设置当前年份 - */ - public void setCurrentYear(int year) { - // 防止初始化期间重复触发事件 - if (!mInitialising && year == getCurrentYear()) { - return; - } - mDate.set(Calendar.YEAR, year); - updateDateControl(); - onDateTimeChanged(); - } - - /** - * 获取当前月份(0-11) - */ - public int getCurrentMonth() { - return mDate.get(Calendar.MONTH); - } - - /** - * 设置当前月份 - */ - public void setCurrentMonth(int month) { - if (!mInitialising && month == getCurrentMonth()) { - return; - } - mDate.set(Calendar.MONTH, month); - updateDateControl(); - onDateTimeChanged(); - } - - /** - * 获取当前日(月中的第几天) - */ - public int getCurrentDay() { - return mDate.get(Calendar.DAY_OF_MONTH); - } - - /** - * 设置当前日 - */ - public void setCurrentDay(int dayOfMonth) { - if (!mInitialising && dayOfMonth == getCurrentDay()) { - return; - } - mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); - updateDateControl(); - onDateTimeChanged(); - } - - /** - * 获取24小时制下的小时(0-23) - */ - public int getCurrentHourOfDay() { - return mDate.get(Calendar.HOUR_OF_DAY); - } - - /** - * 获取当前小时(根据12/24小时制返回对应值) - */ - private int getCurrentHour() { - if (mIs24HourView){ - return getCurrentHourOfDay(); // 24小时制直接返回 - } else { - // 12小时制转换:0点显示为12,13点显示为1 - int hour = getCurrentHourOfDay(); - if (hour >= HOURS_IN_HALF_DAY) { - return hour - HOURS_IN_HALF_DAY; - } else { - return hour == 0 ? HOURS_IN_HALF_DAY : hour; - } - } - } - - /** - * 设置当前小时(24小时制) - */ - public void setCurrentHour(int hourOfDay) { - if (!mInitialising && hourOfDay == getCurrentHourOfDay()) { - return; - } - mDate.set(Calendar.HOUR_OF_DAY, hourOfDay); - - // 12小时制下需要同步AM/PM状态 - if (!mIs24HourView) { - if (hourOfDay >= HOURS_IN_HALF_DAY) { - mIsAm = false; - if (hourOfDay > HOURS_IN_HALF_DAY) { - hourOfDay -= HOURS_IN_HALF_DAY; - } - } else { - mIsAm = true; - if (hourOfDay == 0) { - hourOfDay = HOURS_IN_HALF_DAY; - } - } - updateAmPmControl(); - } - mHourSpinner.setValue(hourOfDay); - onDateTimeChanged(); - } - - /** - * 获取当前分钟 - */ - public int getCurrentMinute() { - return mDate.get(Calendar.MINUTE); - } - - /** - * 设置当前分钟 - */ - public void setCurrentMinute(int minute) { - if (!mInitialising && minute == getCurrentMinute()) { - return; - } - mMinuteSpinner.setValue(minute); - mDate.set(Calendar.MINUTE, minute); - onDateTimeChanged(); - } - - /** - * 是否使用24小时制 - */ - public boolean is24HourView () { - return mIs24HourView; - } - - /** - * 设置12/24小时制显示 - */ - public void set24HourView(boolean is24HourView) { - if (mIs24HourView == is24HourView) { - return; - } - mIs24HourView = is24HourView; - - // 根据12/24小时制显示或隐藏AM/PM选择器 - mAmPmSpinner.setVisibility(is24HourView ? View.GONE : View.VISIBLE); - - // 更新小时选择器范围 - updateHourControl(); - - // 保持当前小时,但根据新的显示模式调整 - int hour = getCurrentHourOfDay(); - setCurrentHour(hour); - - updateAmPmControl(); - } - - /** - * 更新日期选择器显示 - * 显示以当前日期为中心的前后三天(共一周) - */ - private void updateDateControl() { - Calendar cal = Calendar.getInstance(); - cal.setTimeInMillis(mDate.getTimeInMillis()); - - // 计算显示范围:从当前日期前3天开始 - cal.add(Calendar.DAY_OF_YEAR, -DAYS_IN_ALL_WEEK / 2 - 1); - - mDateSpinner.setDisplayedValues(null); - - // 生成一周的日期显示字符串 - for (int i = 0; i < DAYS_IN_ALL_WEEK; ++i) { - cal.add(Calendar.DAY_OF_YEAR, 1); - // 格式:月份.日期 星期几(如:01.15 星期一) - mDateDisplayValues[i] = (String) DateFormat.format("MM.dd EEEE", cal); - } - - mDateSpinner.setDisplayedValues(mDateDisplayValues); - mDateSpinner.setValue(DAYS_IN_ALL_WEEK / 2); // 当前日期在中间位置 - mDateSpinner.invalidate(); // 刷新显示 - } - - /** - * 更新AM/PM选择器显示 - */ - private void updateAmPmControl() { - if (mIs24HourView) { - // 24小时制隐藏AM/PM选择器 - mAmPmSpinner.setVisibility(View.GONE); - } else { - // 12小时制显示AM/PM选择器 - int index = mIsAm ? Calendar.AM : Calendar.PM; - mAmPmSpinner.setValue(index); - mAmPmSpinner.setVisibility(View.VISIBLE); - } - } - - /** - * 更新小时选择器范围 - * 根据12/24小时制设置不同的取值范围 - */ - private void updateHourControl() { - if (mIs24HourView) { - mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW); - mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW); - } else { - mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW); - mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW); - } - } - - /** - * 设置日期时间变化监听器 - */ - public void setOnDateTimeChangedListener(OnDateTimeChangedListener callback) { - mOnDateTimeChangedListener = callback; - } - - /** - * 触发日期时间变化事件 - * 通知监听器当前选择的时间 - */ - private void onDateTimeChanged() { - if (mOnDateTimeChangedListener != null) { - mOnDateTimeChangedListener.onDateTimeChanged(this, getCurrentYear(), - getCurrentMonth(), getCurrentDay(), getCurrentHourOfDay(), getCurrentMinute()); - } - } -} \ No newline at end of file diff --git a/src/ui/DateTimePickerDialog.java b/src/ui/DateTimePickerDialog.java deleted file mode 100644 index 6021daf..0000000 --- a/src/ui/DateTimePickerDialog.java +++ /dev/null @@ -1,142 +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.ui; - -import java.util.Calendar; - -import net.micode.notes.R; -import net.micode.notes.ui.DateTimePicker; -import net.micode.notes.ui.DateTimePicker.OnDateTimeChangedListener; - -import android.app.AlertDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.DialogInterface.OnClickListener; -import android.text.format.DateFormat; -import android.text.format.DateUtils; - -/** - * 日期时间选择对话框 - * 将DateTimePicker控件封装到对话框中,提供完整的日期时间选择界面 - * 用户可以通过此对话框设置笔记的提醒时间 - */ -public class DateTimePickerDialog extends AlertDialog implements OnClickListener { - - private Calendar mDate = Calendar.getInstance(); // 当前选择的日期时间 - private boolean mIs24HourView; // 是否使用24小时制 - private OnDateTimeSetListener mOnDateTimeSetListener; // 日期时间设置完成监听器 - private DateTimePicker mDateTimePicker; // 日期时间选择器控件 - - /** - * 日期时间设置完成监听器接口 - * 当用户点击"确定"按钮时回调 - */ - public interface OnDateTimeSetListener { - void OnDateTimeSet(AlertDialog dialog, long date); - } - - /** - * 构造函数 - * @param context 上下文 - * @param date 初始日期时间(时间戳) - */ - public DateTimePickerDialog(Context context, long date) { - super(context); - - // 创建DateTimePicker控件 - mDateTimePicker = new DateTimePicker(context); - setView(mDateTimePicker); // 将DateTimePicker设置为对话框的内容视图 - - // 设置DateTimePicker的日期时间变化监听器 - mDateTimePicker.setOnDateTimeChangedListener(new OnDateTimeChangedListener() { - public void onDateTimeChanged(DateTimePicker view, int year, int month, - int dayOfMonth, int hourOfDay, int minute) { - // 当DateTimePicker中的时间变化时,更新内部Calendar对象 - mDate.set(Calendar.YEAR, year); - mDate.set(Calendar.MONTH, month); - mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); - mDate.set(Calendar.HOUR_OF_DAY, hourOfDay); - mDate.set(Calendar.MINUTE, minute); - // 更新对话框标题显示新的时间 - updateTitle(mDate.getTimeInMillis()); - } - }); - - // 设置初始时间 - mDate.setTimeInMillis(date); - mDate.set(Calendar.SECOND, 0); // 将秒数设为0,只精确到分钟 - mDateTimePicker.setCurrentDate(mDate.getTimeInMillis()); - - // 设置对话框按钮 - setButton(DialogInterface.BUTTON_POSITIVE, context.getString(R.string.datetime_dialog_ok), this); - setButton(DialogInterface.BUTTON_NEGATIVE, context.getString(R.string.datetime_dialog_cancel), (OnClickListener)null); - - // 根据系统设置决定是否使用24小时制 - set24HourView(DateFormat.is24HourFormat(this.getContext())); - - // 初始化对话框标题 - updateTitle(mDate.getTimeInMillis()); - } - - /** - * 设置是否使用24小时制显示 - * @param is24HourView true表示24小时制,false表示12小时制 - */ - public void set24HourView(boolean is24HourView) { - mIs24HourView = is24HourView; - } - - /** - * 设置日期时间设置完成的监听器 - * @param callBack 监听器对象 - */ - public void setOnDateTimeSetListener(OnDateTimeSetListener callBack) { - mOnDateTimeSetListener = callBack; - } - - /** - * 更新对话框标题 - * 根据当前选择的日期时间格式化显示为标题 - * @param date 当前选择的时间戳 - */ - private void updateTitle(long date) { - // 设置日期时间显示格式 - int flag = - DateUtils.FORMAT_SHOW_YEAR | // 显示年份 - DateUtils.FORMAT_SHOW_DATE | // 显示日期 - DateUtils.FORMAT_SHOW_TIME; // 显示时间 - - // 根据24小时制设置选择时间格式 - flag |= mIs24HourView ? DateUtils.FORMAT_24HOUR : 0; - - // 格式化日期时间并设置为对话框标题 - setTitle(DateUtils.formatDateTime(this.getContext(), date, flag)); - } - - /** - * 对话框按钮点击事件处理 - * 当用户点击"确定"按钮时调用 - * @param arg0 对话框对象 - * @param arg1 按钮ID(这里只处理确定按钮) - */ - public void onClick(DialogInterface arg0, int arg1) { - // 如果设置了监听器,回调选择的日期时间 - if (mOnDateTimeSetListener != null) { - mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis()); - } - } -} \ No newline at end of file diff --git a/src/ui/DropdownMenu.java b/src/ui/DropdownMenu.java deleted file mode 100644 index c7ce675..0000000 --- a/src/ui/DropdownMenu.java +++ /dev/null @@ -1,92 +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.ui; - -import android.content.Context; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.view.View.OnClickListener; -import android.widget.Button; -import android.widget.PopupMenu; -import android.widget.PopupMenu.OnMenuItemClickListener; - -import net.micode.notes.R; - -/** - * 下拉菜单封装类 - * 将Button和PopupMenu组合封装,提供便捷的下拉菜单功能 - * 主要用于笔记列表界面的排序、筛选等操作菜单 - */ -public class DropdownMenu { - private Button mButton; // 显示下拉菜单的按钮 - private PopupMenu mPopupMenu; // Android原生弹出菜单 - private Menu mMenu; // 菜单项容器 - - /** - * 构造函数 - * @param context 上下文环境 - * @param button 触发下拉菜单的按钮 - * @param menuId 菜单资源ID(R.menu.xxx) - */ - public DropdownMenu(Context context, Button button, int menuId) { - mButton = button; - // 设置按钮背景为下拉箭头图标 - mButton.setBackgroundResource(R.drawable.dropdown_icon); - - // 创建PopupMenu,绑定到指定按钮 - mPopupMenu = new PopupMenu(context, mButton); - mMenu = mPopupMenu.getMenu(); - - // 从菜单资源文件加载菜单项 - mPopupMenu.getMenuInflater().inflate(menuId, mMenu); - - // 设置按钮点击事件:显示下拉菜单 - mButton.setOnClickListener(new OnClickListener() { - public void onClick(View v) { - mPopupMenu.show(); // 显示下拉菜单 - } - }); - } - - /** - * 设置菜单项点击监听器 - * @param listener 菜单项点击监听器 - */ - public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) { - if (mPopupMenu != null) { - mPopupMenu.setOnMenuItemClickListener(listener); - } - } - - /** - * 根据ID查找菜单项 - * @param id 菜单项资源ID - * @return 找到的菜单项,未找到返回null - */ - public MenuItem findItem(int id) { - return mMenu.findItem(id); - } - - /** - * 设置按钮显示的标题文本 - * @param title 要显示的文本 - */ - public void setTitle(CharSequence title) { - mButton.setText(title); - } -} \ No newline at end of file diff --git a/src/ui/FoldersListAdapter.java b/src/ui/FoldersListAdapter.java deleted file mode 100644 index 1eba158..0000000 --- a/src/ui/FoldersListAdapter.java +++ /dev/null @@ -1,138 +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.ui; - -import android.content.Context; -import android.database.Cursor; -import android.view.View; -import android.view.ViewGroup; -import android.widget.CursorAdapter; -import android.widget.LinearLayout; -import android.widget.TextView; - -import net.micode.notes.R; -import net.micode.notes.data.Notes; -import net.micode.notes.data.Notes.NoteColumns; - -/** - * 文件夹列表适配器 - * 用于在移动笔记等场景中显示文件夹选择列表 - * 继承自CursorAdapter,将数据库中的文件夹数据绑定到列表视图 - */ -public class FoldersListAdapter extends CursorAdapter { - - // 数据库查询的列投影,只查询需要的字段 - public static final String [] PROJECTION = { - NoteColumns.ID, // 文件夹ID - NoteColumns.SNIPPET // 文件夹名称(在数据库中可能存储在snippet字段) - }; - - // 列索引常量,便于访问Cursor中的数据 - public static final int ID_COLUMN = 0; // ID列索引 - public static final int NAME_COLUMN = 1; // 名称列索引 - - /** - * 构造函数 - * @param context 上下文 - * @param c 包含文件夹数据的游标 - */ - public FoldersListAdapter(Context context, Cursor c) { - super(context, c); - // TODO Auto-generated constructor stub - } - - /** - * 创建新的列表项视图 - * @param context 上下文 - * @param cursor 当前数据游标 - * @param parent 父视图组 - * @return 新创建的视图 - */ - @Override - public View newView(Context context, Cursor cursor, ViewGroup parent) { - // 创建FolderListItem作为列表项视图 - return new FolderListItem(context); - } - - /** - * 将数据绑定到已有的视图 - * @param view 要绑定的视图 - * @param context 上下文 - * @param cursor 当前数据游标 - */ - @Override - public void bindView(View view, Context context, Cursor cursor) { - // 确保视图是FolderListItem类型 - if (view instanceof FolderListItem) { - // 获取文件夹名称 - String folderName; - // 如果是根文件夹(ID为Notes.ID_ROOT_FOLDER),显示特殊名称 - if (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) { - folderName = context.getString(R.string.menu_move_parent_folder); - } else { - folderName = cursor.getString(NAME_COLUMN); - } - // 绑定文件夹名称到视图 - ((FolderListItem) view).bind(folderName); - } - } - - /** - * 获取指定位置的文件夹名称 - * @param context 上下文,用于获取本地化字符串 - * @param position 列表位置 - * @return 文件夹名称字符串 - */ - public String getFolderName(Context context, int position) { - // 获取指定位置的Cursor - Cursor cursor = (Cursor) getItem(position); - // 根据是否为根文件夹返回相应的名称 - if (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) { - return context.getString(R.string.menu_move_parent_folder); - } else { - return cursor.getString(NAME_COLUMN); - } - } - - /** - * 文件夹列表项内部类 - * 表示单个文件夹在列表中的显示视图 - */ - private class FolderListItem extends LinearLayout { - private TextView mName; // 显示文件夹名称的TextView - - /** - * 构造函数 - * @param context 上下文 - */ - public FolderListItem(Context context) { - super(context); - // 从布局文件加载视图 - inflate(context, R.layout.folder_list_item, this); - // 初始化TextView - mName = (TextView) findViewById(R.id.tv_folder_name); - } - - /** - * 绑定数据到视图 - * @param name 文件夹名称 - */ - public void bind(String name) { - mName.setText(name); // 设置文件夹名称 - } - } -} \ No newline at end of file diff --git a/src/ui/NoteEditText.java b/src/ui/NoteEditText.java deleted file mode 100644 index 1f38316..0000000 --- a/src/ui/NoteEditText.java +++ /dev/null @@ -1,284 +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.ui; - -import android.content.Context; -import android.graphics.Rect; -import android.text.Layout; -import android.text.Selection; -import android.text.Spanned; -import android.text.TextUtils; -import android.text.style.URLSpan; -import android.util.AttributeSet; -import android.util.Log; -import android.view.ContextMenu; -import android.view.KeyEvent; -import android.view.MenuItem; -import android.view.MenuItem.OnMenuItemClickListener; -import android.view.MotionEvent; -import android.widget.EditText; - -import net.micode.notes.R; - -import java.util.HashMap; -import java.util.Map; - -/** - * 自定义笔记编辑文本框 - * 扩展自EditText,用于在NoteEditActivity中提供特殊的文本编辑功能 - * 主要功能:处理清单模式下的键盘事件、文本变化监听和链接识别 - */ -public class NoteEditText extends EditText { - private static final String TAG = "NoteEditText"; // 日志标签 - private int mIndex; // 当前编辑框在列表中的索引 - private int mSelectionStartBeforeDelete; // 删除操作前的光标起始位置 - - // URL协议常量 - private static final String SCHEME_TEL = "tel:" ; // 电话协议 - private static final String SCHEME_HTTP = "http:" ; // HTTP协议 - private static final String SCHEME_EMAIL = "mailto:"; // 邮件协议 - - // URL协议与对应菜单资源ID的映射 - private static final Map sSchemaActionResMap = new HashMap(); - static { - sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel); // 电话链接 - sSchemaActionResMap.put(SCHEME_HTTP, R.string.note_link_web); // 网页链接 - sSchemaActionResMap.put(SCHEME_EMAIL, R.string.note_link_email);// 邮件链接 - } - - /** - * 文本视图变化监听器接口 - * 由NoteEditActivity实现,用于处理编辑框的变化事件 - */ - public interface OnTextViewChangeListener { - /** - * 当按下删除键且文本为空时,删除当前编辑框 - * @param index 编辑框索引 - * @param text 编辑框内容 - */ - void onEditTextDelete(int index, String text); - - /** - * 当按下回车键时,在当前编辑框后添加新的编辑框 - * @param index 当前编辑框索引 - * @param text 当前光标后的文本(将被移动到新编辑框) - */ - void onEditTextEnter(int index, String text); - - /** - * 当文本变化时,显示或隐藏项目选项 - * @param index 编辑框索引 - * @param hasText 是否有文本 - */ - void onTextChange(int index, boolean hasText); - } - - private OnTextViewChangeListener mOnTextViewChangeListener; // 文本变化监听器 - - /** - * 构造函数 - * @param context 上下文 - */ - public NoteEditText(Context context) { - super(context, null); - mIndex = 0; - } - - /** - * 设置编辑框索引 - * @param index 索引值 - */ - public void setIndex(int index) { - mIndex = index; - } - - /** - * 设置文本变化监听器 - * @param listener 监听器对象 - */ - public void setOnTextViewChangeListener(OnTextViewChangeListener listener) { - mOnTextViewChangeListener = listener; - } - - /** - * 构造函数 - * @param context 上下文 - * @param attrs 属性集 - */ - public NoteEditText(Context context, AttributeSet attrs) { - super(context, attrs, android.R.attr.editTextStyle); - } - - /** - * 构造函数 - * @param context 上下文 - * @param attrs 属性集 - * @param defStyle 默认样式 - */ - public NoteEditText(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - // TODO Auto-generated constructor stub - } - - /** - * 处理触摸事件 - * 重写此方法以支持在触摸位置设置光标 - */ - @Override - public boolean onTouchEvent(MotionEvent event) { - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - // 计算触摸位置对应的字符偏移量 - int x = (int) event.getX(); - int y = (int) event.getY(); - x -= getTotalPaddingLeft(); // 减去内边距 - y -= getTotalPaddingTop(); - x += getScrollX(); // 加上滚动偏移 - y += getScrollY(); - - Layout layout = getLayout(); - int line = layout.getLineForVertical(y); // 获取垂直方向行号 - int off = layout.getOffsetForHorizontal(line, x); // 获取水平方向字符偏移 - Selection.setSelection(getText(), off); // 设置光标位置 - break; - } - - return super.onTouchEvent(event); - } - - /** - * 按键按下事件处理 - * 主要处理回车键和删除键的预处理 - */ - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - switch (keyCode) { - case KeyEvent.KEYCODE_ENTER: // 回车键 - if (mOnTextViewChangeListener != null) { - // 如果设置了监听器,返回false让onKeyUp处理 - return false; - } - break; - case KeyEvent.KEYCODE_DEL: // 删除键 - // 记录删除前的光标位置 - mSelectionStartBeforeDelete = getSelectionStart(); - break; - default: - break; - } - return super.onKeyDown(keyCode, event); - } - - /** - * 按键抬起事件处理 - * 主要处理回车键和删除键的具体逻辑 - */ - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - switch(keyCode) { - case KeyEvent.KEYCODE_DEL: // 删除键 - if (mOnTextViewChangeListener != null) { - // 如果光标在开头且不是第一个编辑框,则触发删除事件 - if (0 == mSelectionStartBeforeDelete && mIndex != 0) { - mOnTextViewChangeListener.onEditTextDelete(mIndex, getText().toString()); - return true; - } - } else { - Log.d(TAG, "OnTextViewChangeListener was not seted"); - } - break; - case KeyEvent.KEYCODE_ENTER: // 回车键 - if (mOnTextViewChangeListener != null) { - // 将当前光标后的文本移到新编辑框 - int selectionStart = getSelectionStart(); - String text = getText().subSequence(selectionStart, length()).toString(); - setText(getText().subSequence(0, selectionStart)); // 保留光标前的文本 - mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, text); - } else { - Log.d(TAG, "OnTextViewChangeListener was not seted"); - } - break; - default: - break; - } - return super.onKeyUp(keyCode, event); - } - - /** - * 焦点变化事件处理 - * 当焦点变化时,通知监听器文本存在状态 - */ - @Override - protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { - if (mOnTextViewChangeListener != null) { - // 失去焦点时检查是否有文本 - if (!focused && TextUtils.isEmpty(getText())) { - mOnTextViewChangeListener.onTextChange(mIndex, false); - } else { - mOnTextViewChangeListener.onTextChange(mIndex, true); - } - } - super.onFocusChanged(focused, direction, previouslyFocusedRect); - } - - /** - * 创建上下文菜单 - * 识别文本中的链接,并提供相应的操作菜单 - */ - @Override - protected void onCreateContextMenu(ContextMenu menu) { - // 检查文本是否包含Span(如链接) - if (getText() instanceof Spanned) { - int selStart = getSelectionStart(); - int selEnd = getSelectionEnd(); - - // 获取选中区域的最小和最大位置 - int min = Math.min(selStart, selEnd); - int max = Math.max(selStart, selEnd); - - // 获取选中区域内的URLSpan - final URLSpan[] urls = ((Spanned) getText()).getSpans(min, max, URLSpan.class); - // 如果只有一个链接,则添加上下文菜单 - if (urls.length == 1) { - int defaultResId = 0; - // 根据URL协议确定菜单文本资源 - for(String schema: sSchemaActionResMap.keySet()) { - if(urls[0].getURL().indexOf(schema) >= 0) { - defaultResId = sSchemaActionResMap.get(schema); - break; - } - } - - // 如果未匹配到已知协议,使用"其他链接" - if (defaultResId == 0) { - defaultResId = R.string.note_link_other; - } - - // 添加上下文菜单项 - menu.add(0, 0, 0, defaultResId).setOnMenuItemClickListener( - new OnMenuItemClickListener() { - public boolean onMenuItemClick(MenuItem item) { - // 触发链接点击事件 - urls[0].onClick(NoteEditText.this); - return true; - } - }); - } - } - super.onCreateContextMenu(menu); - } -} \ No newline at end of file