From 51aba8b0e3963d0fd6cd53a63780273938d28399 Mon Sep 17 00:00:00 2001 From: hjc <3053383330@qq.com> Date: Thu, 15 May 2025 22:27:41 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B3=A8=E9=87=8A=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/任佳瑶-model/Note.java | 337 ++++++++++++ src/任佳瑶-model/WorkingNote.java | 496 ++++++++++++++++++ src/王军慧-widget/NoteWidgetProvider.java | 188 +++++++ .../NoteWidgetProvider_2x.java | 71 +++ .../NoteWidgetProvider_4x.java | 70 +++ src/贺俊程-data/Contact.java | 97 ++++ src/贺俊程-data/Notes.java | 364 +++++++++++++ src/贺俊程-data/NotesDatabaseHelper.java | 488 +++++++++++++++++ src/贺俊程-data/NotesProvider.java | 417 +++++++++++++++ 9 files changed, 2528 insertions(+) create mode 100644 src/任佳瑶-model/Note.java create mode 100644 src/任佳瑶-model/WorkingNote.java create mode 100644 src/王军慧-widget/NoteWidgetProvider.java create mode 100644 src/王军慧-widget/NoteWidgetProvider_2x.java create mode 100644 src/王军慧-widget/NoteWidgetProvider_4x.java create mode 100644 src/贺俊程-data/Contact.java create mode 100644 src/贺俊程-data/Notes.java create mode 100644 src/贺俊程-data/NotesDatabaseHelper.java create mode 100644 src/贺俊程-data/NotesProvider.java diff --git a/src/任佳瑶-model/Note.java b/src/任佳瑶-model/Note.java new file mode 100644 index 0000000..0487bdf --- /dev/null +++ b/src/任佳瑶-model/Note.java @@ -0,0 +1,337 @@ +/* + * 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.model; + +import android.content.ContentProviderOperation; // 用于批量操作内容提供者(插入、更新、删除) +import android.content.ContentProviderResult; // 批量操作的结果 +import android.content.ContentUris; // 用于操作URI中的ID +import android.content.ContentValues; // 存储键值对数据 +import android.content.Context; // 应用上下文 +import android.content.OperationApplicationException; // 批量操作异常 +import android.net.Uri; // 统一资源标识符 +import android.os.RemoteException; // 远程过程调用异常 +import android.util.Log; // 日志工具 + +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.CallNote; // 通话便签类型 +import net.micode.notes.data.Notes.DataColumns; // 数据列定义 +import net.micode.notes.data.Notes.NoteColumns; // 便签列定义 +import net.micode.notes.data.Notes.TextNote; // 文本便签类型 + +import java.util.ArrayList; + +/** + * Note 类表示一个便签对象,负责便签的创建、修改和同步操作 + */ +public class Note { + private ContentValues mNoteDiffValues; // 存储便签属性的修改值 + private NoteData mNoteData; // 存储便签数据(文本或通话记录) + private static final String TAG = "Note"; // 日志标签 + + /** + * 创建新便签ID并插入数据库 + * @param context 应用上下文 + * @param folderId 父文件夹ID + * @return 新创建的便签ID + */ + public static synchronized long getNewNoteId(Context context, long folderId) { + // 创建新便签并设置初始值 + ContentValues values = new ContentValues(); + long createdTime = System.currentTimeMillis(); + values.put(NoteColumns.CREATED_DATE, createdTime); // 创建时间 + values.put(NoteColumns.MODIFIED_DATE, createdTime); // 修改时间 + values.put(NoteColumns.TYPE, Notes.TYPE_NOTE); // 便签类型 + values.put(NoteColumns.LOCAL_MODIFIED, 1); // 标记为本地修改 + values.put(NoteColumns.PARENT_ID, folderId); // 父文件夹ID + + // 插入数据库 + Uri uri = context.getContentResolver().insert(Notes.CONTENT_NOTE_URI, values); + + long noteId = 0; + try { + // 从URI中提取新创建的便签ID + noteId = Long.valueOf(uri.getPathSegments().get(1)); + } catch (NumberFormatException e) { + Log.e(TAG, "Get note id error :" + e.toString()); + noteId = 0; + } + if (noteId == -1) { + throw new IllegalStateException("Wrong note id:" + noteId); + } + return noteId; + } + + /** + * 构造函数 + */ + public Note() { + mNoteDiffValues = new ContentValues(); // 初始化便签属性修改值 + mNoteData = new NoteData(); // 初始化便签数据 + } + + /** + * 设置便签属性值 + * @param key 属性键 + * @param value 属性值 + */ + public void setNoteValue(String key, String value) { + mNoteDiffValues.put(key, value); + mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); // 标记为本地修改 + mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); // 更新修改时间 + } + + /** + * 设置文本数据 + * @param key 数据键 + * @param value 数据值 + */ + public void setTextData(String key, String value) { + mNoteData.setTextData(key, value); + } + + /** + * 设置文本数据ID + * @param id 数据ID + */ + public void setTextDataId(long id) { + mNoteData.setTextDataId(id); + } + + /** + * 获取文本数据ID + * @return 文本数据ID + */ + public long getTextDataId() { + return mNoteData.mTextDataId; + } + + /** + * 设置通话数据ID + * @param id 数据ID + */ + public void setCallDataId(long id) { + mNoteData.setCallDataId(id); + } + + /** + * 设置通话数据 + * @param key 数据键 + * @param value 数据值 + */ + public void setCallData(String key, String value) { + mNoteData.setCallData(key, value); + } + + /** + * 检查是否有本地修改 + * @return 是否有修改 + */ + public boolean isLocalModified() { + return mNoteDiffValues.size() > 0 || mNoteData.isLocalModified(); + } + + /** + * 同步便签到数据库 + * @param context 应用上下文 + * @param noteId 便签ID + * @return 是否同步成功 + */ + public boolean syncNote(Context context, long noteId) { + if (noteId <= 0) { + throw new IllegalArgumentException("Wrong note id:" + noteId); + } + + // 如果没有本地修改,直接返回成功 + if (!isLocalModified()) { + return true; + } + + /** + * 更新便签基本信息到数据库 + * 即使更新失败,也会继续更新便签数据(数据安全考虑) + */ + if (context.getContentResolver().update( + ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), mNoteDiffValues, null, + null) == 0) { + Log.e(TAG, "Update note error, should not happen"); + // 不返回,继续执行 + } + mNoteDiffValues.clear(); // 清空修改值 + + // 更新便签数据到数据库 + if (mNoteData.isLocalModified() + && (mNoteData.pushIntoContentResolver(context, noteId) == null)) { + return false; + } + + return true; + } + + /** + * NoteData 内部类,处理便签数据(文本或通话记录) + */ + private class NoteData { + private long mTextDataId; // 文本数据ID + private ContentValues mTextDataValues; // 文本数据值 + private long mCallDataId; // 通话数据ID + private ContentValues mCallDataValues; // 通话数据值 + private static final String TAG = "NoteData"; // 日志标签 + + /** + * 构造函数 + */ + public NoteData() { + mTextDataValues = new ContentValues(); + mCallDataValues = new ContentValues(); + mTextDataId = 0; + mCallDataId = 0; + } + + /** + * 检查是否有本地修改 + * @return 是否有修改 + */ + boolean isLocalModified() { + return mTextDataValues.size() > 0 || mCallDataValues.size() > 0; + } + + /** + * 设置文本数据ID + * @param id 数据ID + */ + void setTextDataId(long id) { + if(id <= 0) { + throw new IllegalArgumentException("Text data id should larger than 0"); + } + mTextDataId = id; + } + + /** + * 设置通话数据ID + * @param id 数据ID + */ + void setCallDataId(long id) { + if (id <= 0) { + throw new IllegalArgumentException("Call data id should larger than 0"); + } + mCallDataId = id; + } + + /** + * 设置通话数据 + * @param key 数据键 + * @param value 数据值 + */ + void setCallData(String key, String value) { + mCallDataValues.put(key, value); + mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); + mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); + } + + /** + * 设置文本数据 + * @param key 数据键 + * @param value 数据值 + */ + void setTextData(String key, String value) { + mTextDataValues.put(key, value); + mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); + mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); + } + + /** + * 将数据提交到内容提供者 + * @param context 应用上下文 + * @param noteId 便签ID + * @return 操作结果的URI + */ + Uri pushIntoContentResolver(Context context, long noteId) { + // 检查便签ID有效性 + if (noteId <= 0) { + throw new IllegalArgumentException("Wrong note id:" + noteId); + } + + ArrayList operationList = new ArrayList(); + ContentProviderOperation.Builder builder = null; + + // 处理文本数据 + if(mTextDataValues.size() > 0) { + mTextDataValues.put(DataColumns.NOTE_ID, noteId); // 设置便签ID + if (mTextDataId == 0) { // 新数据 + mTextDataValues.put(DataColumns.MIME_TYPE, TextNote.CONTENT_ITEM_TYPE); // 设置MIME类型 + // 插入新数据 + Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI, + mTextDataValues); + try { + setTextDataId(Long.valueOf(uri.getPathSegments().get(1))); // 从URI提取ID + } catch (NumberFormatException e) { + Log.e(TAG, "Insert new text data fail with noteId" + noteId); + mTextDataValues.clear(); + return null; + } + } else { // 更新现有数据 + builder = ContentProviderOperation.newUpdate(ContentUris.withAppendedId( + Notes.CONTENT_DATA_URI, mTextDataId)); + builder.withValues(mTextDataValues); + operationList.add(builder.build()); + } + mTextDataValues.clear(); // 清空数据 + } + + // 处理通话数据 + if(mCallDataValues.size() > 0) { + mCallDataValues.put(DataColumns.NOTE_ID, noteId); // 设置便签ID + if (mCallDataId == 0) { // 新数据 + mCallDataValues.put(DataColumns.MIME_TYPE, CallNote.CONTENT_ITEM_TYPE); // 设置MIME类型 + // 插入新数据 + Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI, + mCallDataValues); + try { + setCallDataId(Long.valueOf(uri.getPathSegments().get(1))); // 从URI提取ID + } catch (NumberFormatException e) { + Log.e(TAG, "Insert new call data fail with noteId" + noteId); + mCallDataValues.clear(); + return null; + } + } else { // 更新现有数据 + builder = ContentProviderOperation.newUpdate(ContentUris.withAppendedId( + Notes.CONTENT_DATA_URI, mCallDataId)); + builder.withValues(mCallDataValues); + operationList.add(builder.build()); + } + mCallDataValues.clear(); // 清空数据 + } + + // 执行批量操作 + if (operationList.size() > 0) { + try { + ContentProviderResult[] results = context.getContentResolver().applyBatch( + Notes.AUTHORITY, operationList); + return (results == null || results.length == 0 || results[0] == null) ? null + : ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId); + } catch (RemoteException e) { + Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); + return null; + } catch (OperationApplicationException e) { + Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); + return null; + } + } + return null; + } + } +} \ No newline at end of file diff --git a/src/任佳瑶-model/WorkingNote.java b/src/任佳瑶-model/WorkingNote.java new file mode 100644 index 0000000..b21cd98 --- /dev/null +++ b/src/任佳瑶-model/WorkingNote.java @@ -0,0 +1,496 @@ +/* + * 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.model; + +import android.appwidget.AppWidgetManager; +import android.content.ContentUris; +import android.content.Context; +import android.database.Cursor; +import android.text.TextUtils; +import android.util.Log; + +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.CallNote; +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.Notes.TextNote; +import net.micode.notes.tool.ResourceParser.NoteBgResources; + +/** + * WorkingNote 类表示一个正在处理的便签对象,封装了便签的创建、加载、保存和修改等操作。 + * 它作为便签数据模型和业务逻辑的中间层,提供了对便签内容的统一管理。 + */ +public class WorkingNote { + // 便签对象 + private Note mNote; + // 便签ID + private long mNoteId; + // 便签内容 + private String mContent; + // 便签模式(普通/待办清单) + private int mMode; + // 提醒日期 + private long mAlertDate; + // 修改日期 + private long mModifiedDate; + // 背景颜色资源ID + private int mBgColorId; + // 小部件ID + private int mWidgetId; + // 小部件类型 + private int mWidgetType; + // 所属文件夹ID + private long mFolderId; + // 上下文对象 + private Context mContext; + // 日志标签 + private static final String TAG = "WorkingNote"; + // 是否已删除标志 + private boolean mIsDeleted; + // 便签设置变更监听器 + private NoteSettingChangedListener mNoteSettingStatusListener; + + // 数据投影列定义 + public static final String[] DATA_PROJECTION = new String[] { + DataColumns.ID, + DataColumns.CONTENT, + DataColumns.MIME_TYPE, + DataColumns.DATA1, + DataColumns.DATA2, + DataColumns.DATA3, + DataColumns.DATA4, + }; + + // 便签投影列定义 + public static final String[] NOTE_PROJECTION = new String[] { + NoteColumns.PARENT_ID, + NoteColumns.ALERTED_DATE, + NoteColumns.BG_COLOR_ID, + NoteColumns.WIDGET_ID, + NoteColumns.WIDGET_TYPE, + NoteColumns.MODIFIED_DATE + }; + + // 列索引常量 + private static final int DATA_ID_COLUMN = 0; + private static final int DATA_CONTENT_COLUMN = 1; + private static final int DATA_MIME_TYPE_COLUMN = 2; + private static final int DATA_MODE_COLUMN = 3; + private static final int NOTE_PARENT_ID_COLUMN = 0; + private static final int NOTE_ALERTED_DATE_COLUMN = 1; + private static final int NOTE_BG_COLOR_ID_COLUMN = 2; + private static final int NOTE_WIDGET_ID_COLUMN = 3; + private static final int NOTE_WIDGET_TYPE_COLUMN = 4; + private static final int NOTE_MODIFIED_DATE_COLUMN = 5; + + /** + * 构造函数 - 创建新便签 + * @param context 上下文对象 + * @param folderId 所属文件夹ID + */ + private WorkingNote(Context context, long folderId) { + mContext = context; + mAlertDate = 0; + mModifiedDate = System.currentTimeMillis(); + mFolderId = folderId; + mNote = new Note(); + mNoteId = 0; + mIsDeleted = false; + mMode = 0; + mWidgetType = Notes.TYPE_WIDGET_INVALIDE; + } + + /** + * 构造函数 - 加载已有便签 + * @param context 上下文对象 + * @param noteId 便签ID + * @param folderId 文件夹ID + */ + private WorkingNote(Context context, long noteId, long folderId) { + mContext = context; + mNoteId = noteId; + mFolderId = folderId; + mIsDeleted = false; + mNote = new Note(); + loadNote(); + } + + /** + * 加载便签数据 + */ + private void loadNote() { + Cursor cursor = mContext.getContentResolver().query( + ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mNoteId), NOTE_PROJECTION, null, + null, null); + + if (cursor != null) { + if (cursor.moveToFirst()) { + mFolderId = cursor.getLong(NOTE_PARENT_ID_COLUMN); + mBgColorId = cursor.getInt(NOTE_BG_COLOR_ID_COLUMN); + mWidgetId = cursor.getInt(NOTE_WIDGET_ID_COLUMN); + mWidgetType = cursor.getInt(NOTE_WIDGET_TYPE_COLUMN); + mAlertDate = cursor.getLong(NOTE_ALERTED_DATE_COLUMN); + mModifiedDate = cursor.getLong(NOTE_MODIFIED_DATE_COLUMN); + } + cursor.close(); + } else { + Log.e(TAG, "No note with id:" + mNoteId); + throw new IllegalArgumentException("Unable to find note with id " + mNoteId); + } + loadNoteData(); + } + + /** + * 加载便签内容数据 + */ + private void loadNoteData() { + Cursor cursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI, DATA_PROJECTION, + DataColumns.NOTE_ID + "=?", new String[] { + String.valueOf(mNoteId) + }, null); + + if (cursor != null) { + if (cursor.moveToFirst()) { + do { + String type = cursor.getString(DATA_MIME_TYPE_COLUMN); + if (DataConstants.NOTE.equals(type)) { + mContent = cursor.getString(DATA_CONTENT_COLUMN); + mMode = cursor.getInt(DATA_MODE_COLUMN); + mNote.setTextDataId(cursor.getLong(DATA_ID_COLUMN)); + } else if (DataConstants.CALL_NOTE.equals(type)) { + mNote.setCallDataId(cursor.getLong(DATA_ID_COLUMN)); + } else { + Log.d(TAG, "Wrong note type with type:" + type); + } + } while (cursor.moveToNext()); + } + cursor.close(); + } else { + Log.e(TAG, "No data with id:" + mNoteId); + throw new IllegalArgumentException("Unable to find note's data with id " + mNoteId); + } + } + + /** + * 创建空便签 + * @param context 上下文对象 + * @param folderId 文件夹ID + * @param widgetId 小部件ID + * @param widgetType 小部件类型 + * @param defaultBgColorId 默认背景颜色ID + * @return 新创建的WorkingNote对象 + */ + public static WorkingNote createEmptyNote(Context context, long folderId, int widgetId, + int widgetType, int defaultBgColorId) { + WorkingNote note = new WorkingNote(context, folderId); + note.setBgColorId(defaultBgColorId); + note.setWidgetId(widgetId); + note.setWidgetType(widgetType); + return note; + } + + /** + * 加载已有便签 + * @param context 上下文对象 + * @param id 便签ID + * @return 加载的WorkingNote对象 + */ + public static WorkingNote load(Context context, long id) { + return new WorkingNote(context, id, 0); + } + + /** + * 保存便签 + * @return 保存是否成功 + */ + public synchronized boolean saveNote() { + if (isWorthSaving()) { + if (!existInDatabase()) { + if ((mNoteId = Note.getNewNoteId(mContext, mFolderId)) == 0) { + Log.e(TAG, "Create new note fail with id:" + mNoteId); + return false; + } + } + + mNote.syncNote(mContext, mNoteId); + + // 更新小部件内容 + if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID + && mWidgetType != Notes.TYPE_WIDGET_INVALIDE + && mNoteSettingStatusListener != null) { + mNoteSettingStatusListener.onWidgetChanged(); + } + return true; + } else { + return false; + } + } + + /** + * 检查便签是否存在于数据库中 + * @return 是否存在 + */ + public boolean existInDatabase() { + return mNoteId > 0; + } + + /** + * 检查便签是否值得保存 + * @return 是否值得保存 + */ + private boolean isWorthSaving() { + if (mIsDeleted || (!existInDatabase() && TextUtils.isEmpty(mContent)) + || (existInDatabase() && !mNote.isLocalModified())) { + return false; + } else { + return true; + } + } + + /** + * 设置设置变更监听器 + * @param l 监听器对象 + */ + public void setOnSettingStatusChangedListener(NoteSettingChangedListener l) { + mNoteSettingStatusListener = l; + } + + /** + * 设置提醒日期 + * @param date 提醒日期 + * @param set 是否设置 + */ + public void setAlertDate(long date, boolean set) { + if (date != mAlertDate) { + mAlertDate = date; + mNote.setNoteValue(NoteColumns.ALERTED_DATE, String.valueOf(mAlertDate)); + } + if (mNoteSettingStatusListener != null) { + mNoteSettingStatusListener.onClockAlertChanged(date, set); + } + } + + /** + * 标记便签为已删除 + * @param mark 是否标记 + */ + public void markDeleted(boolean mark) { + mIsDeleted = mark; + if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID + && mWidgetType != Notes.TYPE_WIDGET_INVALIDE && mNoteSettingStatusListener != null) { + mNoteSettingStatusListener.onWidgetChanged(); + } + } + + /** + * 设置背景颜色ID + * @param id 背景颜色ID + */ + public void setBgColorId(int id) { + if (id != mBgColorId) { + mBgColorId = id; + if (mNoteSettingStatusListener != null) { + mNoteSettingStatusListener.onBackgroundColorChanged(); + } + mNote.setNoteValue(NoteColumns.BG_COLOR_ID, String.valueOf(id)); + } + } + + /** + * 设置待办清单模式 + * @param mode 模式值 + */ + public void setCheckListMode(int mode) { + if (mMode != mode) { + if (mNoteSettingStatusListener != null) { + mNoteSettingStatusListener.onCheckListModeChanged(mMode, mode); + } + mMode = mode; + mNote.setTextData(TextNote.MODE, String.valueOf(mMode)); + } + } + + /** + * 设置小部件类型 + * @param type 小部件类型 + */ + public void setWidgetType(int type) { + if (type != mWidgetType) { + mWidgetType = type; + mNote.setNoteValue(NoteColumns.WIDGET_TYPE, String.valueOf(mWidgetType)); + } + } + + /** + * 设置小部件ID + * @param id 小部件ID + */ + public void setWidgetId(int id) { + if (id != mWidgetId) { + mWidgetId = id; + mNote.setNoteValue(NoteColumns.WIDGET_ID, String.valueOf(mWidgetId)); + } + } + + /** + * 设置便签文本内容 + * @param text 文本内容 + */ + public void setWorkingText(String text) { + if (!TextUtils.equals(mContent, text)) { + mContent = text; + mNote.setTextData(DataColumns.CONTENT, mContent); + } + } + + /** + * 转换为通话记录便签 + * @param phoneNumber 电话号码 + * @param callDate 通话日期 + */ + public void convertToCallNote(String phoneNumber, long callDate) { + mNote.setCallData(CallNote.CALL_DATE, String.valueOf(callDate)); + mNote.setCallData(CallNote.PHONE_NUMBER, phoneNumber); + mNote.setNoteValue(NoteColumns.PARENT_ID, String.valueOf(Notes.ID_CALL_RECORD_FOLDER)); + } + + /** + * 检查是否有提醒 + * @return 是否有提醒 + */ + public boolean hasClockAlert() { + return (mAlertDate > 0 ? true : false); + } + + /** + * 获取便签内容 + * @return 便签内容 + */ + public String getContent() { + return mContent; + } + + /** + * 获取提醒日期 + * @return 提醒日期 + */ + public long getAlertDate() { + return mAlertDate; + } + + /** + * 获取修改日期 + * @return 修改日期 + */ + public long getModifiedDate() { + return mModifiedDate; + } + + /** + * 获取背景颜色资源ID + * @return 背景颜色资源ID + */ + public int getBgColorResId() { + return NoteBgResources.getNoteBgResource(mBgColorId); + } + + /** + * 获取背景颜色ID + * @return 背景颜色ID + */ + public int getBgColorId() { + return mBgColorId; + } + + /** + * 获取标题背景资源ID + * @return 标题背景资源ID + */ + public int getTitleBgResId() { + return NoteBgResources.getNoteTitleBgResource(mBgColorId); + } + + /** + * 获取待办清单模式 + * @return 待办清单模式 + */ + public int getCheckListMode() { + return mMode; + } + + /** + * 获取便签ID + * @return 便签ID + */ + public long getNoteId() { + return mNoteId; + } + + /** + * 获取文件夹ID + * @return 文件夹ID + */ + public long getFolderId() { + return mFolderId; + } + + /** + * 获取小部件ID + * @return 小部件ID + */ + public int getWidgetId() { + return mWidgetId; + } + + /** + * 获取小部件类型 + * @return 小部件类型 + */ + public int getWidgetType() { + return mWidgetType; + } + + /** + * 便签设置变更监听器接口 + */ + public interface NoteSettingChangedListener { + /** + * 背景颜色变更回调 + */ + void onBackgroundColorChanged(); + + /** + * 提醒设置变更回调 + * @param date 提醒日期 + * @param set 是否设置 + */ + void onClockAlertChanged(long date, boolean set); + + /** + * 小部件变更回调 + */ + void onWidgetChanged(); + + /** + * 待办清单模式变更回调 + * @param oldMode 旧模式 + * @param newMode 新模式 + */ + void onCheckListModeChanged(int oldMode, int newMode); + } +} \ No newline at end of file diff --git a/src/王军慧-widget/NoteWidgetProvider.java b/src/王军慧-widget/NoteWidgetProvider.java new file mode 100644 index 0000000..fa2315f --- /dev/null +++ b/src/王军慧-widget/NoteWidgetProvider.java @@ -0,0 +1,188 @@ +/* + * 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.widget; + +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.util.Log; +import android.widget.RemoteViews; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.tool.ResourceParser; +import net.micode.notes.ui.NoteEditActivity; +import net.micode.notes.ui.NotesListActivity; + +/** + * 小米便签小部件的基类实现 + * 继承自Android框架的 {@link AppWidgetProvider} + * 提供小部件的核心功能,包括数据更新、点击事件处理等 + */ +public abstract class NoteWidgetProvider extends AppWidgetProvider { + + // 数据库查询投影字段 + public static final String[] PROJECTION = new String[] { + NoteColumns.ID, // 便签ID + NoteColumns.BG_COLOR_ID, // 背景颜色ID + NoteColumns.SNIPPET // 便签摘要 + }; + + // 投影字段的索引常量 + public static final int COLUMN_ID = 0; + public static final int COLUMN_BG_COLOR_ID = 1; + public static final int COLUMN_SNIPPET = 2; + + private static final String TAG = "NoteWidgetProvider"; // 日志标签 + + /** + * 当小部件被删除时调用 + * @param context 上下文对象 + * @param appWidgetIds 被删除的小部件ID数组 + */ + @Override + public void onDeleted(Context context, int[] appWidgetIds) { + ContentValues values = new ContentValues(); + values.put(NoteColumns.WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); + // 更新数据库,将关联的便签的widget_id设为无效值 + for (int i = 0; i < appWidgetIds.length; i++) { + context.getContentResolver().update(Notes.CONTENT_NOTE_URI, + values, + NoteColumns.WIDGET_ID + "=?", + new String[] { String.valueOf(appWidgetIds[i])}); + } + } + + /** + * 获取与小部件关联的便签信息 + * @param context 上下文对象 + * @param widgetId 小部件ID + * @return 返回查询到的游标对象 + */ + private Cursor getNoteWidgetInfo(Context context, int widgetId) { + return context.getContentResolver().query(Notes.CONTENT_NOTE_URI, + PROJECTION, + NoteColumns.WIDGET_ID + "=? AND " + NoteColumns.PARENT_ID + "<>?", + new String[] { String.valueOf(widgetId), String.valueOf(Notes.ID_TRASH_FOLER) }, + null); + } + + /** + * 更新小部件显示 + * @param context 上下文对象 + * @param appWidgetManager 小部件管理器 + * @param appWidgetIds 需要更新的小部件ID数组 + */ + protected void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + update(context, appWidgetManager, appWidgetIds, false); + } + + /** + * 更新小部件显示(支持隐私模式) + * @param context 上下文对象 + * @param appWidgetManager 小部件管理器 + * @param appWidgetIds 需要更新的小部件ID数组 + * @param privacyMode 是否处于隐私模式 + */ + private void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds, + boolean privacyMode) { + for (int i = 0; i < appWidgetIds.length; i++) { + if (appWidgetIds[i] != AppWidgetManager.INVALID_APPWIDGET_ID) { + // 初始化默认背景ID和摘要内容 + int bgId = ResourceParser.getDefaultBgId(context); + String snippet = ""; + + // 准备点击跳转的Intent + Intent intent = new Intent(context, NoteEditActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + intent.putExtra(Notes.INTENT_EXTRA_WIDGET_ID, appWidgetIds[i]); + intent.putExtra(Notes.INTENT_EXTRA_WIDGET_TYPE, getWidgetType()); + + // 查询关联的便签数据 + Cursor c = getNoteWidgetInfo(context, appWidgetIds[i]); + if (c != null && c.moveToFirst()) { + // 处理查询结果 + if (c.getCount() > 1) { + Log.e(TAG, "Multiple message with same widget id:" + appWidgetIds[i]); + c.close(); + return; + } + snippet = c.getString(COLUMN_SNIPPET); + bgId = c.getInt(COLUMN_BG_COLOR_ID); + intent.putExtra(Intent.EXTRA_UID, c.getLong(COLUMN_ID)); + intent.setAction(Intent.ACTION_VIEW); + } else { + // 没有关联便签时显示默认文本 + snippet = context.getResources().getString(R.string.widget_havenot_content); + intent.setAction(Intent.ACTION_INSERT_OR_EDIT); + } + + if (c != null) { + c.close(); + } + + // 创建远程视图并设置内容 + RemoteViews rv = new RemoteViews(context.getPackageName(), getLayoutId()); + rv.setImageViewResource(R.id.widget_bg_image, getBgResourceId(bgId)); + intent.putExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, bgId); + + // 设置点击事件 + PendingIntent pendingIntent = null; + if (privacyMode) { + // 隐私模式下的显示和处理 + rv.setTextViewText(R.id.widget_text, + context.getString(R.string.widget_under_visit_mode)); + pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], new Intent( + context, NotesListActivity.class), PendingIntent.FLAG_UPDATE_CURRENT); + } else { + // 正常模式下的显示和处理 + rv.setTextViewText(R.id.widget_text, snippet); + pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], intent, + PendingIntent.FLAG_UPDATE_CURRENT); + } + + rv.setOnClickPendingIntent(R.id.widget_text, pendingIntent); + // 更新小部件显示 + appWidgetManager.updateAppWidget(appWidgetIds[i], rv); + } + } + } + + /** + * 获取背景资源ID(由子类实现) + * @param bgId 背景ID + * @return 返回对应的背景资源ID + */ + protected abstract int getBgResourceId(int bgId); + + /** + * 获取布局资源ID(由子类实现) + * @return 返回布局资源ID + */ + protected abstract int getLayoutId(); + + /** + * 获取小部件类型(由子类实现) + * @return 返回小部件类型标识 + */ + protected abstract int getWidgetType(); +} \ No newline at end of file diff --git a/src/王军慧-widget/NoteWidgetProvider_2x.java b/src/王军慧-widget/NoteWidgetProvider_2x.java new file mode 100644 index 0000000..0ac1491 --- /dev/null +++ b/src/王军慧-widget/NoteWidgetProvider_2x.java @@ -0,0 +1,71 @@ +/* + * 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.widget; + +import android.appwidget.AppWidgetManager; +import android.content.Context; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.tool.ResourceParser; + +/** + * 小米便签的2x尺寸桌面小部件实现类 + * 继承自基础小部件类 {@link NoteWidgetProvider} + * 提供2x尺寸小部件的特定布局和资源管理 + */ +public class NoteWidgetProvider_2x extends NoteWidgetProvider { + + /** + * 当小部件需要更新时调用 + * @param context 上下文对象 + * @param appWidgetManager 小部件管理器 + * @param appWidgetIds 需要更新的小部件ID数组 + */ + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + super.update(context, appWidgetManager, appWidgetIds); // 调用父类的更新方法 + } + + /** + * 获取小部件使用的布局资源ID + * @return 返回2x尺寸小部件的布局资源ID + */ + @Override + protected int getLayoutId() { + return R.layout.widget_2x; // 指向res/layout/widget_2x.xml布局文件 + } + + /** + * 根据背景ID获取对应的背景资源ID + * @param bgId 背景ID + * @return 返回2x尺寸小部件对应的背景资源ID + */ + @Override + protected int getBgResourceId(int bgId) { + return ResourceParser.WidgetBgResources.getWidget2xBgResource(bgId); // 从资源解析器获取2x尺寸的背景资源 + } + + /** + * 获取小部件的类型标识 + * @return 返回2x尺寸小部件的类型常量 {@link Notes#TYPE_WIDGET_2X} + */ + @Override + protected int getWidgetType() { + return Notes.TYPE_WIDGET_2X; // 标识这是2x尺寸的小部件类型 + } +} \ No newline at end of file diff --git a/src/王军慧-widget/NoteWidgetProvider_4x.java b/src/王军慧-widget/NoteWidgetProvider_4x.java new file mode 100644 index 0000000..b61df2c --- /dev/null +++ b/src/王军慧-widget/NoteWidgetProvider_4x.java @@ -0,0 +1,70 @@ +/* + * 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.widget; + +import android.appwidget.AppWidgetManager; +import android.content.Context; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.tool.ResourceParser; + +/** + * 小米便签的4x尺寸桌面小部件实现类 + * 继承自基础小部件类 {@link NoteWidgetProvider} + * 提供4x尺寸小部件的特定布局和资源管理 + */ +public class NoteWidgetProvider_4x extends NoteWidgetProvider { + + /** + * 当小部件需要更新时调用 + * @param context 上下文对象 + * @param appWidgetManager 小部件管理器 + * @param appWidgetIds 需要更新的小部件ID数组 + */ + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + super.update(context, appWidgetManager, appWidgetIds); // 调用父类的更新方法 + } + + /** + * 获取小部件使用的布局资源ID + * @return 返回4x尺寸小部件的布局资源ID + */ + protected int getLayoutId() { + return R.layout.widget_4x; // 指向res/layout/widget_4x.xml布局文件 + } + + /** + * 根据背景ID获取对应的背景资源ID + * @param bgId 背景ID + * @return 返回4x尺寸小部件对应的背景资源ID + */ + @Override + protected int getBgResourceId(int bgId) { + return ResourceParser.WidgetBgResources.getWidget4xBgResource(bgId); // 从资源解析器获取4x尺寸的背景资源 + } + + /** + * 获取小部件的类型标识 + * @return 返回4x尺寸小部件的类型常量 {@link Notes#TYPE_WIDGET_4X} + */ + @Override + protected int getWidgetType() { + return Notes.TYPE_WIDGET_4X; // 标识这是4x尺寸的小部件类型 + } +} \ No newline at end of file diff --git a/src/贺俊程-data/Contact.java b/src/贺俊程-data/Contact.java new file mode 100644 index 0000000..90de20e --- /dev/null +++ b/src/贺俊程-data/Contact.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.data; + +import android.content.Context; +import android.database.Cursor; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.Data; +import android.telephony.PhoneNumberUtils; +import android.util.Log; + +import java.util.HashMap; +/** + * 联系人工具类,用于通过电话号码查询联系人姓名,并支持缓存机制 + */ +public class Contact { + // 联系人缓存(电话号码 -> 联系人姓名) + + private static HashMap sContactCache; + private static final String TAG = "Contact"; // 日志标签 + + // 查询条件模板,用于匹配电话号码对应的联系人 + private static final String CALLER_ID_SELECTION = "PHONE_NUMBERS_EQUAL(" + Phone.NUMBER// 电话号码匹配函数 + + + ",?) AND " + Data.MIMETYPE + "='" + Phone.CONTENT_ITEM_TYPE + "'" // 限制为电话类型数据 + + + " AND " + Data.RAW_CONTACT_ID + " IN " + + "(SELECT raw_contact_id " // 原始联系人ID筛选 + + + " FROM phone_lookup" + + " WHERE min_match = '+')"; // 匹配最小匹配规则 + + + + + /** + * 通过电话号码获取联系人姓名(带缓存) + * @param context 上下文对象 + * @param phoneNumber 要查询的电话号码 + * @return 对应的联系人姓名,未找到返回null + */ + public static String getContact(Context context, String phoneNumber) { + // 初始化缓存(首次调用时) + if(sContactCache == null) { + sContactCache = new HashMap(); + } + // 缓存命中直接返回 + if(sContactCache.containsKey(phoneNumber)) { + return sContactCache.get(phoneNumber); + } + // 动态构建查询条件(替换模板中的占位符) + String selection = CALLER_ID_SELECTION.replace("+", + PhoneNumberUtils.toCallerIDMinMatch(phoneNumber)); + // 执行内容提供者查询 + Cursor cursor = context.getContentResolver().query( + Data.CONTENT_URI, // 查询URI(联系人数据表) + new String [] { Phone.DISPLAY_NAME },// 查询字段(显示名称) + selection,// 筛选条件 + new String[] { phoneNumber },// 参数占位符值 + + null); // 排序方式 + // 处理查询结果 + if (cursor != null && cursor.moveToFirst()) { + try { + // 获取第一条结果的显示名称 + String name = cursor.getString(0); + sContactCache.put(phoneNumber, name);// 存入缓存 + return name; + } catch (IndexOutOfBoundsException e) { + // 异常处理(列索引越界) + Log.e(TAG, " Cursor get string error " + e.toString()); + return null; + } finally { + // 确保关闭游标释放资源 + cursor.close(); + } + } else { + // 未找到匹配联系人 + Log.d(TAG, "No contact matched with number:" + phoneNumber); + return null; + } + } +} diff --git a/src/贺俊程-data/Notes.java b/src/贺俊程-data/Notes.java new file mode 100644 index 0000000..08de8cf --- /dev/null +++ b/src/贺俊程-data/Notes.java @@ -0,0 +1,364 @@ +/* + * 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.net.Uri; + + +/** + * 笔记系统核心数据模型类,提供: + * 1. 内容提供者URI定义 + * 2. 数据表结构常量 + * 3. 笔记类型常量 + * 4. 系统文件夹ID定义 + * 5. Intent传输常量 + */ + +public class Notes { + // 内容提供者授权标识 + public static final String AUTHORITY = "micode_notes"; + public static final String TAG = "Notes";// 日志标签 + // 笔记类型常量 + public static final int TYPE_NOTE = 0;// 普通笔记 + + public static final int TYPE_FOLDER = 1;// 文件夹 + public static final int TYPE_SYSTEM = 2;// 系统笔记 + + /** + * Following IDs are system folders' identifiers + * {@link Notes#ID_ROOT_FOLDER } is default folder + * {@link Notes#ID_TEMPARAY_FOLDER } is for notes belonging no folder + * {@link Notes#ID_CALL_RECORD_FOLDER} is to store call records + */ + + + /** + * 系统文件夹ID定义: + * {@link #ID_ROOT_FOLDER} 根文件夹(默认) + * {@link #ID_TEMPARAY_FOLDER} 临时文件夹(未分类笔记) + * {@link #ID_CALL_RECORD_FOLDER} 通话记录文件夹 + * {@link #ID_TRASH_FOLER} 回收站文件夹 + */ + + public static final int ID_ROOT_FOLDER = 0; + public static final int ID_TEMPARAY_FOLDER = -1; + public static final int ID_CALL_RECORD_FOLDER = -2; + public static final int ID_TRASH_FOLER = -3; + // Intent传输数据常量 + public static final String INTENT_EXTRA_ALERT_DATE = "net.micode.notes.alert_date"; // 提醒日期 + + public static final String INTENT_EXTRA_BACKGROUND_ID = "net.micode.notes.background_color_id";// 背景颜色ID + public static final String INTENT_EXTRA_WIDGET_ID = "net.micode.notes.widget_id";// 小部件ID + + public static final String INTENT_EXTRA_WIDGET_TYPE = "net.micode.notes.widget_type";// 小部件类型 + + public static final String INTENT_EXTRA_FOLDER_ID = "net.micode.notes.folder_id"; // 文件夹ID + + public static final String INTENT_EXTRA_CALL_DATE = "net.micode.notes.call_date"; // 通话日期 + + + public static final int TYPE_WIDGET_INVALIDE = -1;// 无效类型 + + public static final int TYPE_WIDGET_2X = 0; // 2x尺寸 + public static final int TYPE_WIDGET_4X = 1;// 4x尺寸 + + /** + * 数据类型常量映射(用于内容提供者查询) + */ + + public static class DataConstants { + public static final String NOTE = TextNote.CONTENT_ITEM_TYPE;// 文本笔记类型 + + public static final String CALL_NOTE = CallNote.CONTENT_ITEM_TYPE; // 通话记录类型 + + } + + /** + * Uri to query all notes and folders + */ + + // 内容URI定义 + public static final Uri CONTENT_NOTE_URI = Uri.parse("content://" + AUTHORITY + "/note"); // 笔记主数据URI + + + /** + * Uri to query data + */ + + public static final Uri CONTENT_DATA_URI = Uri.parse("content://" + AUTHORITY + "/data"); // 详细数据URI + /** + * 笔记表结构接口(主表) + */ + + public interface NoteColumns { + /** + * The unique ID for a row + *

Type: INTEGER (long)

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

Type: INTEGER (long)

+ */ + public static final String PARENT_ID = "parent_id"; // 父级ID(用于文件夹层级) + + /** + * Created data for note or folder + *

Type: INTEGER (long)

+ */ + public static final String CREATED_DATE = "created_date";// 创建时间戳 + + + /** + * Latest modified date + *

Type: INTEGER (long)

+ */ + public static final String MODIFIED_DATE = "modified_date"; // 修改时间戳 + + + + /** + * Alert date + *

Type: INTEGER (long)

+ */ + public static final String ALERTED_DATE = "alert_date"; // 提醒时间戳 + + + /** + * Folder's name or text content of note + *

Type: TEXT

+ */ + public static final String SNIPPET = "snippet";// 摘要内容(文件夹名/笔记预览) + + /** + * Note's widget id + *

Type: INTEGER (long)

+ */ + public static final String WIDGET_ID = "widget_id"; // 关联的小部件ID + + + /** + * Note's widget type + *

Type: INTEGER (long)

+ */ + public static final String WIDGET_TYPE = "widget_type"; // 小部件类型 + + + /** + * Note's background color's id + *

Type: INTEGER (long)

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

Type: INTEGER

+ */ + public static final String HAS_ATTACHMENT = "has_attachment"; // 是否有附件标记 + + + /** + * Folder's count of notes + *

Type: INTEGER (long)

+ */ + public static final String NOTES_COUNT = "notes_count";// 文件夹内笔记数量 + + + /** + * The file type: folder or note + *

Type: INTEGER

+ */ + public static final String TYPE = "type";// 文件类型(笔记/文件夹) + + + /** + * The last sync id + *

Type: INTEGER (long)

+ */ + public static final String SYNC_ID = "sync_id";// 同步ID + + + /** + * Sign to indicate local modified or not + *

Type: INTEGER

+ */ + public static final String LOCAL_MODIFIED = "local_modified"; // 本地修改标记 + + + /** + * Original parent id before moving into temporary folder + *

Type : INTEGER

+ */ + public static final String ORIGIN_PARENT_ID = "origin_parent_id";// 原始父级ID(用于移动恢复) + + + /** + * The gtask id + *

Type : TEXT

+ */ + public static final String GTASK_ID = "gtask_id";// Google任务ID + + + /** + * The version code + *

Type : INTEGER (long)

+ */ + public static final String VERSION = "version";// 数据版本 + } + + + /** + * 详细数据表结构接口(子表) + */ + public interface DataColumns { + /** + * The unique ID for a row + *

Type: INTEGER (long)

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

Type: Text

+ */ + public static final String MIME_TYPE = "mime_type";// MIME类型(区分数据类型) + + /** + * The reference id to note that this data belongs to + *

Type: INTEGER (long)

+ */ + public static final String NOTE_ID = "note_id"; // 关联的笔记ID + + /** + * Created data for note or folder + *

Type: INTEGER (long)

+ */ + public static final String CREATED_DATE = "created_date"; // 创建时间戳 + + + /** + * Latest modified date + *

Type: INTEGER (long)

+ */ + public static final String MODIFIED_DATE = "modified_date";// 修改时间戳 + + + /** + * Data's content + *

Type: TEXT

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

Type: INTEGER

+ */ + public static final String DATA1 = "data1"; // 通用数据字段1(整数类型) + + + /** + * Generic data column, the meaning is {@link #MIMETYPE} specific, used for + * integer data type + *

Type: INTEGER

+ */ + public static final String DATA2 = "data2";// 通用数据字段2(整数类型) + + + /** + * Generic data column, the meaning is {@link #MIMETYPE} specific, used for + * TEXT data type + *

Type: TEXT

+ */ + public static final String DATA3 = "data3"; // 通用数据字段3(文本类型) + + + /** + * Generic data column, the meaning is {@link #MIMETYPE} specific, used for + * TEXT data type + *

Type: TEXT

+ */ + public static final String DATA4 = "data4"; // 通用数据字段4(文本类型) + + + /** + * Generic data column, the meaning is {@link #MIMETYPE} specific, used for + * TEXT data type + *

Type: TEXT

+ */ + public static final String DATA5 = "data5";// 通用数据字段5(文本类型) + + } + /** + * 文本笔记数据模型 + */ + + public static final class TextNote implements DataColumns { + /** + * Mode to indicate the text in check list mode or not + *

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

+ */ + public static final String MODE = DATA1; // 模式标识(1=复选列表模式) + + + public static final int MODE_CHECK_LIST = 1;// 复选列表模式常量 + + + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/text_note"; // 内容类型(目录) + + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/text_note";// 内容类型(单项) + + + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/text_note");// 内容URI + + } + + /** + * 通话记录数据模型 + */ + + public static final class CallNote implements DataColumns { + /** + * Call date for this record + *

Type: INTEGER (long)

+ */ + public static final String CALL_DATE = DATA1; // 通话日期(时间戳) + + + /** + * Phone number for this record + *

Type: TEXT

+ */ + public static final String PHONE_NUMBER = DATA3;// 电话号码 + + + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/call_note"; // 内容类型(目录) + + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/call_note";// 内容类型(单项) + + + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/call_note"); // 内容URI + + } +} diff --git a/src/贺俊程-data/NotesDatabaseHelper.java b/src/贺俊程-data/NotesDatabaseHelper.java new file mode 100644 index 0000000..812d4fb --- /dev/null +++ b/src/贺俊程-data/NotesDatabaseHelper.java @@ -0,0 +1,488 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.data; + +import android.content.ContentValues; +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.util.Log; + +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.DataConstants; +import net.micode.notes.data.Notes.NoteColumns; + +/** + * 笔记系统数据库管理类,负责: + * 1. 数据库创建与升级 + * 2. 表结构定义 + * 3. 触发器管理 + * 4. 系统文件夹初始化 + * + * 采用单例模式保证数据库连接唯一性 + */ +public class NotesDatabaseHelper extends SQLiteOpenHelper { + private static final String DB_NAME = "note.db"; // 数据库文件名 + + + private static final int DB_VERSION = 4;// 数据库版本号 + + public interface TABLE { + public static final String NOTE = "note";// 主表(存储笔记元数据) + + + public static final String DATA = "data"; // 子表(存储具体内容数据) + } + + private static final String TAG = "NotesDatabaseHelper"; + + private static NotesDatabaseHelper mInstance; + // 核心表结构定义 + + private static final String CREATE_NOTE_TABLE_SQL = + "CREATE TABLE " + TABLE.NOTE + "(" + + NoteColumns.ID + " INTEGER PRIMARY KEY," + // 唯一主键 + + NoteColumns.PARENT_ID + " INTEGER NOT NULL DEFAULT 0," +// 父级ID(文件夹层级) + + NoteColumns.ALERTED_DATE + " INTEGER NOT NULL DEFAULT 0," +// 提醒时间 + + NoteColumns.BG_COLOR_ID + " INTEGER NOT NULL DEFAULT 0," + // 背景色ID + + NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +// 创建时间 + + NoteColumns.HAS_ATTACHMENT + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +// 修改时间 + NoteColumns.NOTES_COUNT + " INTEGER NOT NULL DEFAULT 0," +// 文件夹笔记数量 + + NoteColumns.SNIPPET + " TEXT NOT NULL DEFAULT ''," +// 摘要内容 + + NoteColumns.TYPE + " INTEGER NOT NULL DEFAULT 0," +// 类型标识 + NoteColumns.WIDGET_ID + " INTEGER NOT NULL DEFAULT 0," +// 小部件ID + NoteColumns.WIDGET_TYPE + " INTEGER NOT NULL DEFAULT -1," +// 小部件类型 + NoteColumns.SYNC_ID + " INTEGER NOT NULL DEFAULT 0," +// 同步ID + + NoteColumns.LOCAL_MODIFIED + " INTEGER NOT NULL DEFAULT 0," +// 本地修改标记 + + NoteColumns.ORIGIN_PARENT_ID + " INTEGER NOT NULL DEFAULT 0," +// 原始父级ID + + NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''," + // Google任务ID + + NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0" + // 数据版本 + + ")"; + + private static final String CREATE_DATA_TABLE_SQL = + "CREATE TABLE " + TABLE.DATA + "(" + + DataColumns.ID + " INTEGER PRIMARY KEY," +// 唯一主键 + + DataColumns.MIME_TYPE + " TEXT NOT NULL," +// 数据类型标识 + + DataColumns.NOTE_ID + " INTEGER NOT NULL DEFAULT 0," + // 关联笔记ID + + NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +// 创建时间 + NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + // 修改时间 + + DataColumns.CONTENT + " TEXT NOT NULL DEFAULT ''," + // 具体内容 + + DataColumns.DATA1 + " INTEGER," + // 通用数据字段1 + + DataColumns.DATA2 + " INTEGER," + // 通用数据字段2 + + DataColumns.DATA3 + " TEXT NOT NULL DEFAULT ''," + // 通用数据字段3 + + DataColumns.DATA4 + " TEXT NOT NULL DEFAULT ''," + // 通用数据字段4 + + DataColumns.DATA5 + " TEXT NOT NULL DEFAULT ''" + // 通用数据字段5 + + ")"; + // 数据库触发器定义(用于维护数据一致性 + private static final String CREATE_DATA_NOTE_ID_INDEX_SQL = + "CREATE INDEX IF NOT EXISTS note_id_index ON " + + TABLE.DATA + "(" + DataColumns.NOTE_ID + ");";// 笔记ID索引 + + + /** + * Increase folder's note count when move note to the folder + */ + + /** + * 文件夹计数维护触发器组: + * 1. 更新时增加目标文件夹计数 + * 2. 更新时减少原文件夹计数 + * 3. 插入时增加目标文件夹计数 + * 4. 删除时减少原文件夹计数 + */ + + + /** + * 数据库触发器定义模块,实现复杂业务逻辑的自动化维护: + * + * 一、文件夹计数维护触发器组(Folder Counter Triggers) + * 目标:自动维护文件夹的笔记数量统计(NOTES_COUNT字段) + * + * 1. 更新时增加目标文件夹计数 + * 触发场景:将笔记移动到新文件夹(UPDATE操作) + * 逻辑:当PARENT_ID变更时,新父文件夹的计数+1 + * 特点:使用new.PARENT_ID获取变更后的目标ID + */ + private static final String NOTE_INCREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER = + "CREATE TRIGGER increase_folder_count_on_update "+ + " AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE + + " BEGIN " + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" + + " WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" + + " END"; + + /** + * 2. 更新时减少原文件夹计数 + * 触发场景:将笔记移出原文件夹(UPDATE操作) + * 逻辑:当PARENT_ID变更时,原父文件夹的计数-1(需>0时才执行) + * 特点:使用old.PARENT_ID获取变更前的原ID,防止计数负值 + */ + private static final String NOTE_DECREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER = + "CREATE TRIGGER decrease_folder_count_on_update " + + " AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE + + " BEGIN " + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" + + " WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID + + " AND " + NoteColumns.NOTES_COUNT + ">0" + ";" + + " END"; + + /** + * 3. 插入时增加目标文件夹计数 + * 触发场景:新建笔记到文件夹(INSERT操作) + * 逻辑:根据新笔记的PARENT_ID增加对应文件夹计数 + * 特点:使用new.PARENT_ID直接获取目标ID + */ + private static final String NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER = + "CREATE TRIGGER increase_folder_count_on_insert " + + " AFTER INSERT ON " + TABLE.NOTE + + " BEGIN " + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" + + " WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" + + " END"; + + /** + * 4. 删除时减少原文件夹计数 + * 触发场景:从文件夹删除笔记(DELETE操作) + * 逻辑:根据被删笔记的原PARENT_ID减少对应文件夹计数 + * 特点:使用old.PARENT_ID获取原ID,自动级联处理 + */ + private static final String NOTE_DECREASE_FOLDER_COUNT_ON_DELETE_TRIGGER = + "CREATE TRIGGER decrease_folder_count_on_delete " + + " AFTER DELETE ON " + TABLE.NOTE + + " BEGIN " + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" + + " WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID + + " AND " + NoteColumns.NOTES_COUNT + ">0;" + + " END"; + + /** + * 二、笔记内容同步触发器组(Content Sync Triggers) + * 目标:保持DATA表与NOTE表的SNIPPET字段同步 + * + * 1. 插入时更新笔记内容 + * 触发场景:新增NOTE类型数据(如文本内容) + * 逻辑:将DATA.CONTENT同步到关联笔记的SNIPPET + * 特点:使用WHEN子句过滤MIME_TYPE,通过new.NOTE_ID关联 + */ + private static final String DATA_UPDATE_NOTE_CONTENT_ON_INSERT_TRIGGER = + "CREATE TRIGGER update_note_content_on_insert " + + " AFTER INSERT ON " + TABLE.DATA + + " WHEN new." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" + + " BEGIN" + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT + + " WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" + + " END"; + + /** + * 2. 更新时同步内容变更 + * 触发场景:修改NOTE类型数据内容 + * 逻辑:实时更新关联笔记的SNIPPET + * 特点:使用old.MIME_TYPE确保只处理NOTE类型数据 + */ + private static final String DATA_UPDATE_NOTE_CONTENT_ON_UPDATE_TRIGGER = + "CREATE TRIGGER update_note_content_on_update " + + " AFTER UPDATE ON " + TABLE.DATA + + " WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" + + " BEGIN" + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT + + " WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" + + " END"; + + /** + * 3. 删除时清空笔记内容 + * 触发场景:删除NOTE类型数据 + * 逻辑:将关联笔记的SNIPPET置为空字符串 + * 特点:使用old前缀访问被删除数据的内容 + */ + private static final String DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER = + "CREATE TRIGGER update_note_content_on_delete " + + " AFTER delete ON " + TABLE.DATA + + " WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" + + " BEGIN" + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.SNIPPET + "=''" + + " WHERE " + NoteColumns.ID + "=old." + DataColumns.NOTE_ID + ";" + + " END"; + + /** + * 三、级联操作触发器组(Cascade Operations Triggers) + * + * 1. 笔记删除时级联删除关联数据 + * 触发场景:删除笔记记录 + * 逻辑:自动删除该笔记在DATA表中的所有关联数据 + * 特点:通过old.ID匹配DATA.NOTE_ID实现级联删除 + */ + private static final String NOTE_DELETE_DATA_ON_DELETE_TRIGGER = + "CREATE TRIGGER delete_data_on_delete " + + " AFTER DELETE ON " + TABLE.NOTE + + " BEGIN" + + " DELETE FROM " + TABLE.DATA + + " WHERE " + DataColumns.NOTE_ID + "=old." + NoteColumns.ID + ";" + + " END"; + /** + * 2. 文件夹删除时级联删除子笔记 + * 触发场景:删除文件夹记录 + * 逻辑:自动删除该文件夹下的所有笔记(包括子文件夹) + * 特点:通过old.ID匹配NOTE.PARENT_ID实现递归删除 + */ + private static final String FOLDER_DELETE_NOTES_ON_DELETE_TRIGGER = + "CREATE TRIGGER folder_delete_notes_on_delete " + + " AFTER DELETE ON " + TABLE.NOTE + + " BEGIN" + + " DELETE FROM " + TABLE.NOTE + + " WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" + + " END"; + + /** + * 3. 文件夹移入回收站时级联移动子笔记 + * 触发场景:文件夹被移动到回收站(PARENT_ID更新) + * 逻辑:自动将该文件夹下的所有笔记移入回收站 + * 特点:通过new.PARENT_ID判断目标位置,使用old.ID匹配原父级 + */ + private static final String FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER = + "CREATE TRIGGER folder_move_notes_on_trash " + + " AFTER UPDATE ON " + TABLE.NOTE + + " WHEN new." + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER + + " BEGIN" + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER + + " WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" + + " END"; +/** + * 触发器设计特点: + * 1. 事务安全:所有操作在数据库事务中自动完成 + * 2. 数据一致性:通过计数维护保证统计准确率 + * 3. 级联处理:自动处理关联数据的增删改 + * 4. 性能优化:避免在应用层维护复杂状态 + * 5. 防御性编程:通过WHERE条件防止无效操作(如计数<0) + */ + + + + + /** + * 构造函数(私有化保证单例) + * @param context 上下文对象 + */ + public NotesDatabaseHelper(Context context) { + super(context, DB_NAME, null, DB_VERSION); + } + /** + * 创建笔记主表并初始化触发器 + * @param db 数据库操作对象 + */ + public void createNoteTable(SQLiteDatabase db) { + db.execSQL(CREATE_NOTE_TABLE_SQL); + reCreateNoteTableTriggers(db);// 重建触发器 + + createSystemFolder(db);// 创建系统文件夹 + + Log.d(TAG, "note table has been created"); + } + /** + * 重建笔记表相关触发器(用于版本升级) + * @param db 数据库操作对象 + */ + private void reCreateNoteTableTriggers(SQLiteDatabase db) { + // 删除旧触发器... + + db.execSQL("DROP TRIGGER IF EXISTS increase_folder_count_on_update"); + db.execSQL("DROP TRIGGER IF EXISTS decrease_folder_count_on_update"); + db.execSQL("DROP TRIGGER IF EXISTS decrease_folder_count_on_delete"); + db.execSQL("DROP TRIGGER IF EXISTS delete_data_on_delete"); + db.execSQL("DROP TRIGGER IF EXISTS increase_folder_count_on_insert"); + db.execSQL("DROP TRIGGER IF EXISTS folder_delete_notes_on_delete"); + db.execSQL("DROP TRIGGER IF EXISTS folder_move_notes_on_trash"); + // 创建新触发器(包含文件夹计数维护、数据删除级联等逻辑)... + db.execSQL(NOTE_INCREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER); + db.execSQL(NOTE_DECREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER); + db.execSQL(NOTE_DECREASE_FOLDER_COUNT_ON_DELETE_TRIGGER); + db.execSQL(NOTE_DELETE_DATA_ON_DELETE_TRIGGER); + db.execSQL(NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER); + db.execSQL(FOLDER_DELETE_NOTES_ON_DELETE_TRIGGER); + db.execSQL(FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER); + } + /** + * 创建系统预置文件夹: + * 1. 通话记录文件夹 + * 2. 根文件夹 + * 3. 临时文件夹 + * 4. 回收站文件夹 + */ + private void createSystemFolder(SQLiteDatabase db) { + ContentValues values = new ContentValues(); + + /** + * call record foler for call notes + */ + values.put(NoteColumns.ID, Notes.ID_CALL_RECORD_FOLDER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + + /** + * root folder which is default folder + */ + values.clear(); + values.put(NoteColumns.ID, Notes.ID_ROOT_FOLDER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + + /** + * temporary folder which is used for moving note + */ + values.clear(); + values.put(NoteColumns.ID, Notes.ID_TEMPARAY_FOLDER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + + /** + * create trash folder + */ + values.clear(); + values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + } + // 使用ContentValues插入系统文件夹记录... + public void createDataTable(SQLiteDatabase db) { + db.execSQL(CREATE_DATA_TABLE_SQL); + reCreateDataTableTriggers(db); + db.execSQL(CREATE_DATA_NOTE_ID_INDEX_SQL); + Log.d(TAG, "data table has been created"); + } + + private void reCreateDataTableTriggers(SQLiteDatabase db) { + db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_insert"); + db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_update"); + db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_delete"); + + db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_INSERT_TRIGGER); + db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_UPDATE_TRIGGER); + db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER); + } + /** + * 获取数据库实例(单例模式) + * @param context 上下文对象 + * @return 数据库帮助类实例 + */ + static synchronized NotesDatabaseHelper getInstance(Context context) { + if (mInstance == null) { + mInstance = new NotesDatabaseHelper(context); + } + return mInstance; + } + + @Override + public void onCreate(SQLiteDatabase db) { + createNoteTable(db); + createDataTable(db); + } + /** + * 数据库升级处理逻辑 + * @param db 数据库对象 + * @param oldVersion 旧版本号 + * @param newVersion 新版本号 + */ + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + boolean reCreateTriggers = false; + boolean skipV2 = false; + + if (oldVersion == 1) { // 1.0 → 2.0(重建表结构) + upgradeToV2(db); + skipV2 = true; // this upgrade including the upgrade from v2 to v3 + oldVersion++; + } + + if (oldVersion == 2 && !skipV2) { // 2.0 → 3.0(添加GTASK_ID字段和回收站) + upgradeToV3(db); + reCreateTriggers = true; + oldVersion++; + } + + if (oldVersion == 3) { // 3.0 → 4.0(添加版本字段) + upgradeToV4(db); + oldVersion++; + } + + if (reCreateTriggers) { + reCreateNoteTableTriggers(db); + reCreateDataTableTriggers(db); + } + + if (oldVersion != newVersion) { + throw new IllegalStateException("Upgrade notes database to version " + newVersion + + "fails"); + } + } + + private void upgradeToV2(SQLiteDatabase db) { + db.execSQL("DROP TABLE IF EXISTS " + TABLE.NOTE); + db.execSQL("DROP TABLE IF EXISTS " + TABLE.DATA); + createNoteTable(db); + createDataTable(db); + } + + private void upgradeToV3(SQLiteDatabase db) { + // drop unused triggers + db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_insert"); + db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_delete"); + db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_update"); + // add a column for gtask id + db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.GTASK_ID + + " TEXT NOT NULL DEFAULT ''"); + // add a trash system folder + ContentValues values = new ContentValues(); + values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + } + + private void upgradeToV4(SQLiteDatabase db) { + db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.VERSION + + " INTEGER NOT NULL DEFAULT 0"); + } +} diff --git a/src/贺俊程-data/NotesProvider.java b/src/贺俊程-data/NotesProvider.java new file mode 100644 index 0000000..4848949 --- /dev/null +++ b/src/贺俊程-data/NotesProvider.java @@ -0,0 +1,417 @@ +/* + * 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; + +/** + * 笔记系统内容提供者,实现: + * 1. 统一数据访问接口(CRUD操作) + * 2. URI路由分发 + * 3. 搜索建议支持 + * 4. 数据变更通知 + * 5. 版本控制机制 + */ +public class NotesProvider extends ContentProvider { + + private static final UriMatcher mMatcher;// URI匹配器(单例模式) + + private NotesDatabaseHelper mHelper;// 数据库帮助类 + + // URI类型常量定义 + private static final String TAG = "NotesProvider"; + + private static final int URI_NOTE = 1;// 笔记集合 + + private static final int URI_NOTE_ITEM = 2;// 单个笔记 + + private static final int URI_DATA = 3;// 数据集合 + + private static final int URI_DATA_ITEM = 4; // 单条数据 + + + private static final int URI_SEARCH = 5; // 搜索接口 + + private static final int URI_SEARCH_SUGGEST = 6;// 搜索建议接口 + + + // 静态初始化URI匹配规则 + static { + mMatcher = new UriMatcher(UriMatcher.NO_MATCH); + // 笔记相关URI + mMatcher.addURI(Notes.AUTHORITY, "note", URI_NOTE); + mMatcher.addURI(Notes.AUTHORITY, "note/#", URI_NOTE_ITEM); + // 数据相关URI + mMatcher.addURI(Notes.AUTHORITY, "data", URI_DATA); + mMatcher.addURI(Notes.AUTHORITY, "data/#", URI_DATA_ITEM); + // 搜索相关URI + 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' represents the '\n' character in sqlite. For title and content in the search result, + * we will trim '\n' and white space in order to show more information. + */ + + /** + * 搜索结果投影定义: + * 1. 基础字段映射 + * 2. 文本处理(去除换行符和空格) + * 3. 搜索建议专用字段(图标、意图动作等) + */ + private static final String NOTES_SEARCH_PROJECTION = NoteColumns.ID + "," // 笔记ID + + + NoteColumns.ID + " AS " + SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA + ","// 附加数据 + + + "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " // 清理后的摘要 + + SearchManager.SUGGEST_COLUMN_TEXT_1 + ","// 建议文本1 + + "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_2 + "," // 建议文本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; + + + /** + * 搜索查询SQL模板: + * 1. 排除回收站文件夹 + * 2. 限制笔记类型 + * 3. 使用LIKE进行模糊匹配 + */ + 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; + + @Override + public boolean onCreate() { + mHelper = NotesDatabaseHelper.getInstance(getContext());// 获取数据库实例 + + return true; + } + /** + * 统一数据查询入口,支持多种URI类型的查询操作: + * 1. 笔记集合查询(URI_NOTE) + * 2. 单条笔记查询(URI_NOTE_ITEM) + * 3. 数据集合查询(URI_DATA) + * 4. 单条数据查询(URI_DATA_ITEM) + * 5. 全局搜索(URI_SEARCH) + * 6. 搜索建议(URI_SEARCH_SUGGEST) + */ + + @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: // 查询笔记集合(支持排序和条件过滤) + + c = db.query(TABLE.NOTE, projection, selection, selectionArgs, null, null, + sortOrder); + break; + case URI_NOTE_ITEM:// 查询单条笔记(优先使用URI中的ID) + id = uri.getPathSegments().get(1); + c = db.query(TABLE.NOTE, projection, NoteColumns.ID + "=" + id + + parseSelection(selection), selectionArgs, null, null, sortOrder); + break; + case URI_DATA: // 查询数据集合(支持复杂条件) + + c = db.query(TABLE.DATA, projection, selection, selectionArgs, null, null, + sortOrder); + break; + case URI_DATA_ITEM: // 查询单条数据(优先使用URI中的ID) + + 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; + 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; + } + + 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); + } + if (c != null) { // 设置观察者(数据变更时通知客户端) + + c.setNotificationUri(getContext().getContentResolver(), uri); + + } + return c; + } + /** + * 数据插入中心: + * 1. 笔记/数据表智能路由 + * 2. 自动关联note_id处理 + * 3. 数据变更通知 + */ + @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: // 插入笔记到主表(自动生成ID) + + insertedId = noteId = db.insert(TABLE.NOTE, null, values);// 插入笔记 + + break; + case URI_DATA: // 插入数据到子表(强制要求关联note_id) + + + 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); + } + // Notify the note uri + // 数据变更通知机制: + // 1. 当插入笔记时,通知对应笔记URI的观察者 + // 2. 当插入数据时,通知对应数据URI的观察者 + // 3. 使用ContentUris构建标准返回URI + + if (noteId > 0) { + getContext().getContentResolver().notifyChange( + ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null); + } + + // Notify the data uri + if (dataId > 0) { + getContext().getContentResolver().notifyChange( + ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), null); + } + // 触发观察者更新 + return ContentUris.withAppendedId(uri, insertedId); + } + /** + * 数据删除中心: + * 1. 系统文件夹保护(ID<=0禁止删除) + * 2. 级联删除处理(通过触发器实现) + * 3. 安全删除条件构建 + */ + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + int count = 0; + String id = null; + SQLiteDatabase db = mHelper.getWritableDatabase(); + boolean deleteData = false;// 数据删除标记(用于级联通知) + switch (mMatcher.match(uri)) { + case URI_NOTE: // 删除笔记集合(自动追加ID>0条件,防止系统文件夹误删) + + selection = "(" + selection + ") AND " + NoteColumns.ID + ">0 "; + count = db.delete(TABLE.NOTE, selection, selectionArgs); + break; + case URI_NOTE_ITEM:// 删除单条笔记(URI中的ID优先) + + id = uri.getPathSegments().get(1); + /** + * ID that smaller than 0 is system folder which is not allowed to + * trash + */ + long noteId = Long.valueOf(id); + if (noteId <= 0) {// 系统文件夹保护(ID≤0禁止删除) + + break; + } + count = db.delete(TABLE.NOTE, + NoteColumns.ID + "=" + id + parseSelection(selection), selectionArgs); + break; + case URI_DATA:// 删除数据集合(标记数据删除) + count = db.delete(TABLE.DATA, selection, selectionArgs); + deleteData = true; + break; + case URI_DATA_ITEM: // 删除单条数据(URI中的ID优先) + 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); + } + // 数据变更通知机制: + // 1. 精准通知当前操作的URI + // 2. 当删除数据时,级联通知笔记URI(数据删除可能影响笔记内容) + + if (count > 0) { + if (deleteData) { + getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null); + } + getContext().getContentResolver().notifyChange(uri, null); + } + return count; + } + /** + * 数据更新中心: + * 1. 自动版本递增机制 + * 2. 精准更新条件构建 + * 3. 变更通知机制 + */ + @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;// 数据变更标记(用于级联通知) + + + switch (mMatcher.match(uri)) { + case URI_NOTE: // 更新笔记集合(自动版本递增,基于selection条件) + + increaseNoteVersion(-1, selection, selectionArgs); // 版本递增 + + count = db.update(TABLE.NOTE, values, selection, selectionArgs); + break; + case URI_NOTE_ITEM: // 更新单条笔记(优先使用URI中的ID) + + 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:// 更新数据集合(标记数据变更) + count = db.update(TABLE.DATA, values, selection, selectionArgs); + updateData = true; + break; + case URI_DATA_ITEM: // 更新单条数据(优先使用URI中的ID) + + 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); + } + // 数据变更通知机制: + // 1. 精准通知当前操作的URI + // 2. 当更新数据表时,级联通知笔记URI(数据变更可能影响笔记内容) + + if (count > 0) { + if (updateData) { + getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null); + } + getContext().getContentResolver().notifyChange(uri, null); + } + return count; + } + /** + * 辅助方法: + * 1. 条件解析(处理SQL注入防护) + * 2. 版本递增实现(用于数据同步) + * 3. 变更通知封装 + */ + private String parseSelection(String selection) { + return (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : ""); + } + + private void increaseNoteVersion(long id, String selection, String[] selectionArgs) { + // 构建基础UPDATE语句 + StringBuilder sql = new StringBuilder(120); + sql.append("UPDATE "); + sql.append(TABLE.NOTE); + sql.append(" SET "); + sql.append(NoteColumns.VERSION); + sql.append("=" + NoteColumns.VERSION + "+1 ");// 版本号递增 + + // 构建WHERE条件(ID优先) + if (id > 0 || !TextUtils.isEmpty(selection)) { + sql.append(" WHERE "); + } + if (id > 0) { + sql.append(NoteColumns.ID + "=" + String.valueOf(id)); // 优先使用ID条件 + + } + // 处理复合条件(当同时存在ID和selection时自动添加AND) + + 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()); + } + + @Override + public String getType(Uri uri) { + // TODO Auto-generated method stub + return null; + } + +}