/* * 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.tool.SearchHistoryManager; import net.micode.notes.data.Notes.DataColumns; import net.micode.notes.data.Notes.NoteColumns; import net.micode.notes.data.NotesDatabaseHelper.TABLE; /** * 笔记应用的ContentProvider实现类,继承自Android的ContentProvider * 核心功能: * 1. 作为应用数据的对外统一访问接口,封装SQLite数据库的CRUD(增删改查)操作; * 2. 通过UriMatcher匹配不同的Uri,处理note表、data表的单条/多条数据操作; * 3. 支持安卓系统的搜索框架,提供笔记搜索和搜索建议功能; * 4. 数据变更时发送通知(notifyChange),让UI组件同步更新数据; * 5. 更新笔记时自动增加版本号,用于数据同步的版本控制。 * * ContentProvider是安卓四大组件之一,用于跨应用/进程的数据共享,本类也是应用内部数据访问的统一入口。 * * @author MiCode Open Source Community * @date 2010-2011 */ public class NotesProvider extends ContentProvider { /** * Uri匹配器,用于将外部传入的Uri匹配为对应的操作类型(如查询所有笔记、查询单条数据、搜索等) * 静态常量,类加载时初始化,全局唯一 */ private static final UriMatcher mMatcher; /** * 数据库帮助类实例,用于获取SQLiteDatabase对象执行数据库操作 */ private NotesDatabaseHelper mHelper; /** * 日志标签,用于Logcat输出时标识当前类,方便调试定位问题 */ private static final String TAG = "NotesProvider"; // ====================== Uri匹配类型常量 ====================== /** * Uri匹配类型:查询/操作note表的所有数据 */ private static final int URI_NOTE = 1; /** * Uri匹配类型:查询/操作note表的单条数据(通过ID,如note/1) */ private static final int URI_NOTE_ITEM = 2; /** * Uri匹配类型:查询/操作data表的所有数据 */ private static final int URI_DATA = 3; /** * Uri匹配类型:查询/操作data表的单条数据(通过ID,如data/1) */ private static final int URI_DATA_ITEM = 4; /** * Uri匹配类型:执行笔记搜索操作 */ private static final int URI_SEARCH = 5; /** * Uri匹配类型:提供搜索建议(适配安卓SearchManager) */ private static final int URI_SEARCH_SUGGEST = 6; /** * 静态代码块:初始化UriMatcher,添加Uri匹配规则 * 规则格式:authority(授权名) + path(路径) -> 匹配类型常量 */ static { // 初始化UriMatcher,默认匹配失败返回UriMatcher.NO_MATCH mMatcher = new UriMatcher(UriMatcher.NO_MATCH); // 匹配note表所有数据:content://micode_notes/note -> URI_NOTE mMatcher.addURI(Notes.AUTHORITY, "note", URI_NOTE); // 匹配note表单条数据:content://micode_notes/note/#(#表示数字ID) -> URI_NOTE_ITEM mMatcher.addURI(Notes.AUTHORITY, "note/#", URI_NOTE_ITEM); // 匹配data表所有数据:content://micode_notes/data -> URI_DATA mMatcher.addURI(Notes.AUTHORITY, "data", URI_DATA); // 匹配data表单条数据:content://micode_notes/data/# -> URI_DATA_ITEM mMatcher.addURI(Notes.AUTHORITY, "data/#", URI_DATA_ITEM); // 匹配搜索操作:content://micode_notes/search -> URI_SEARCH mMatcher.addURI(Notes.AUTHORITY, "search", URI_SEARCH); // 匹配搜索建议(空查询):content://micode_notes/suggestions/query -> URI_SEARCH_SUGGEST mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, URI_SEARCH_SUGGEST); // 匹配搜索建议(带关键词):content://micode_notes/suggestions/query/关键词 -> URI_SEARCH_SUGGEST mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", URI_SEARCH_SUGGEST); } /** * 笔记搜索的投影(Projection)字符串,定义了搜索结果需要返回的列 * 适配安卓SearchManager的搜索建议列名,核心处理逻辑: * 1. x'0A'是SQLite中的换行符\n,替换并修剪空格,让搜索结果显示更整洁; * 2. 映射SearchManager的标准列名(如SUGGEST_COLUMN_TEXT_1、SUGGEST_COLUMN_ICON_1),供搜索框架使用; * 3. 包含笔记ID、内容、图标、意图动作等信息,用于搜索建议的展示和跳转。 */ 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; /** * 笔记搜索的SQL查询语句,逻辑: * 1. 选择NOTES_SEARCH_PROJECTION定义的列; * 2. 从note表查询; * 3. 条件:摘要包含搜索关键词(LIKE ?)、不在回收站(PARENT_ID != 回收站ID)、类型为普通笔记(TYPE=NOTE)。 */ 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表示ContentProvider创建成功,false表示失败 */ @Override public boolean onCreate() { // 获取NotesDatabaseHelper的单例实例,上下文使用ContentProvider的上下文 mHelper = NotesDatabaseHelper.getInstance(getContext()); return true; } /** * 处理数据查询请求,是ContentProvider的核心方法之一 * 根据Uri匹配的类型,执行不同的数据库查询逻辑,返回Cursor对象 * * @param uri 要查询的Uri * @param projection 要返回的列(投影),null表示返回所有列 * @param selection 查询条件(WHERE子句),?作为占位符 * @param selectionArgs 查询条件的参数,替换selection中的? * @param sortOrder 排序规则(ORDER BY子句) * @return 查询结果的Cursor对象,若查询失败返回null */ @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { Cursor c = null; // 获取只读的SQLiteDatabase对象(查询操作使用只读数据库,提升性能) SQLiteDatabase db = mHelper.getReadableDatabase(); String id = null; // 存储Uri中的ID(如note/1中的1) // 根据Uri匹配的类型执行不同的查询逻辑 switch (mMatcher.match(uri)) { case URI_NOTE: // 查询note表的所有数据 c = db.query(TABLE.NOTE, projection, selection, selectionArgs, null, null, sortOrder); break; case URI_NOTE_ITEM: // 获取Uri中的ID(路径的第二个分段,如note/1的路径分段是["note", "1"]) id = uri.getPathSegments().get(1); // 查询note表的单条数据,条件:ID=id + 传入的selection 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: // 获取Uri中的ID id = uri.getPathSegments().get(1); // 查询data表的单条数据,条件:ID=id + 传入的selection c = db.query(TABLE.DATA, projection, DataColumns.ID + "=" + id + parseSelection(selection), selectionArgs, null, null, sortOrder); break; case URI_SEARCH: case URI_SEARCH_SUGGEST: // 搜索操作不允许指定sortOrder和projection,否则抛出异常 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) { // 搜索建议:路径分段大于1时,第二个分段是关键词(如query/笔记) if (uri.getPathSegments().size() > 1) { searchString = uri.getPathSegments().get(1); } } else { // 普通搜索:从查询参数"pattern"中获取关键词 searchString = uri.getQueryParameter("pattern"); } // 如果是搜索建议类型,且搜索关键词不为空,返回合并结果 if (mMatcher.match(uri) == URI_SEARCH_SUGGEST && !TextUtils.isEmpty(searchString)) { try { // 1. 获取搜索历史记录 SearchHistoryManager historyManager = SearchHistoryManager.getInstance(getContext()); java.util.List historyList = historyManager.getSearchHistoryList(); // 2. 获取便签搜索结果 String likeSearchString = String.format("%%%s%%", searchString); Cursor noteCursor = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY, new String[] { likeSearchString }); // 3. 创建矩阵游标,用于合并结果 String[] columns = { NoteColumns.ID, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA, SearchManager.SUGGEST_COLUMN_TEXT_1, SearchManager.SUGGEST_COLUMN_TEXT_2, SearchManager.SUGGEST_COLUMN_ICON_1, SearchManager.SUGGEST_COLUMN_INTENT_ACTION, SearchManager.SUGGEST_COLUMN_INTENT_DATA }; android.database.MatrixCursor matrixCursor = new android.database.MatrixCursor(columns); // 4. 添加搜索历史记录(只添加匹配的历史) for (String history : historyList) { if (history.toLowerCase().contains(searchString.toLowerCase())) { matrixCursor.addRow(new Object[] { -1, // ID为-1表示是历史记录 history, // 历史记录作为Intent Extra数据 history, // 显示的文本1 getContext().getString(R.string.search_history), // 显示的文本2 R.drawable.search_result, // 图标 Intent.ACTION_SEARCH, // Intent动作 Notes.TextNote.CONTENT_TYPE // Intent数据类型 }); } } // 5. 添加便签搜索结果 if (noteCursor != null && noteCursor.moveToFirst()) { do { // 从便签搜索结果中获取列数据 long noteId = noteCursor.getLong(noteCursor.getColumnIndexOrThrow(NoteColumns.ID)); String extraData = noteCursor.getString(noteCursor.getColumnIndexOrThrow(SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA)); String text1 = noteCursor.getString(noteCursor.getColumnIndexOrThrow(SearchManager.SUGGEST_COLUMN_TEXT_1)); String text2 = noteCursor.getString(noteCursor.getColumnIndexOrThrow(SearchManager.SUGGEST_COLUMN_TEXT_2)); int icon = noteCursor.getInt(noteCursor.getColumnIndexOrThrow(SearchManager.SUGGEST_COLUMN_ICON_1)); String action = noteCursor.getString(noteCursor.getColumnIndexOrThrow(SearchManager.SUGGEST_COLUMN_INTENT_ACTION)); String data = noteCursor.getString(noteCursor.getColumnIndexOrThrow(SearchManager.SUGGEST_COLUMN_INTENT_DATA)); matrixCursor.addRow(new Object[] { noteId, extraData, text1, text2, icon, action, data }); } while (noteCursor.moveToNext()); } // 6. 关闭便签搜索结果游标 if (noteCursor != null) { noteCursor.close(); } // 7. 设置矩阵游标为结果 c = matrixCursor; } catch (IllegalStateException ex) { // 捕获异常,输出错误日志 Log.e(TAG, "got exception: " + ex.toString()); } } else if (!TextUtils.isEmpty(searchString)) { // 普通搜索或搜索建议但关键词为空,只返回便签搜索结果 try { // 拼接SQL的LIKE关键词(%表示任意字符,如%笔记%) searchString = String.format("%%%s%%", searchString); // 执行原生SQL查询,获取搜索结果 c = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY, new String[] { searchString }); } catch (IllegalStateException ex) { // 捕获异常,输出错误日志 Log.e(TAG, "got exception: " + ex.toString()); } } break; default: // 未知Uri,抛出异常 throw new IllegalArgumentException("Unknown URI " + uri); } // 为Cursor设置通知Uri,当数据变更时,Cursor会收到通知并更新 if (c != null) { c.setNotificationUri(getContext().getContentResolver(), uri); } return c; } /** * 处理数据插入请求,向note表或data表插入数据 * * @param uri 要插入的Uri * @param values 要插入的数据(ContentValues键值对) * @return 插入数据后的新Uri(包含插入的ID,如content://micode_notes/note/1) */ @Override public Uri insert(Uri uri, ContentValues values) { // 获取可写的SQLiteDatabase对象(插入操作需要写权限) SQLiteDatabase db = mHelper.getWritableDatabase(); long dataId = 0, noteId = 0, insertedId = 0; // 存储插入的ID // 根据Uri匹配的类型执行插入逻辑 switch (mMatcher.match(uri)) { case URI_NOTE: // 插入note表,获取插入的ID insertedId = noteId = db.insert(TABLE.NOTE, null, values); break; case URI_DATA: // 插入data表时,先获取关联的noteId(必须包含NOTE_ID列) if (values.containsKey(DataColumns.NOTE_ID)) { noteId = values.getAsLong(DataColumns.NOTE_ID); } else { // 无NOTE_ID时输出调试日志 Log.d(TAG, "Wrong data format without note id:" + values.toString()); } // 插入data表,获取插入的ID insertedId = dataId = db.insert(TABLE.DATA, null, values); break; default: // 未知Uri,抛出异常 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); } // 返回包含插入ID的新Uri return ContentUris.withAppendedId(uri, insertedId); } /** * 处理数据删除请求,从note表或data表删除数据 * * @param uri 要删除的Uri * @param selection 删除条件(WHERE子句) * @param selectionArgs 删除条件的参数 * @return 删除的行数 */ @Override public int delete(Uri uri, String selection, String[] selectionArgs) { int count = 0; // 存储删除的行数 String id = null; // 存储Uri中的ID // 获取可写的SQLiteDatabase对象 SQLiteDatabase db = mHelper.getWritableDatabase(); boolean deleteData = false; // 标记是否删除的是data表数据 long noteId = 0; // 用于存储便签ID,以便发送通知 // 根据Uri匹配的类型执行删除逻辑 switch (mMatcher.match(uri)) { case URI_NOTE: // 直接删除便签,条件:传入的selection + 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小于等于0的是系统文件夹,不允许删除 */ noteId = Long.valueOf(id); if (noteId <= 0) { break; } // 直接删除便签,条件:ID=id + 传入的selection 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: // 获取Uri中的ID,删除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); } // 数据删除成功时,发送通知更新UI if (count > 0) { // 删除data表数据时,同时通知note表的Uri(因为data表变更会影响note表的摘要) if (deleteData) { getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null); } // 如果是便签相关操作,通知对应的便签Uri if (noteId > 0) { getContext().getContentResolver().notifyChange( ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null); } // 通知当前Uri的数据变更 getContext().getContentResolver().notifyChange(uri, null); } return count; } /** * 处理数据更新请求,更新note表或data表的数据 * * @param uri 要更新的Uri * @param values 要更新的数据(ContentValues键值对) * @param selection 更新条件(WHERE子句) * @param selectionArgs 更新条件的参数 * @return 更新的行数 */ @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { int count = 0; // 存储更新的行数 String id = null; // 存储Uri中的ID // 获取可写的SQLiteDatabase对象 SQLiteDatabase db = mHelper.getWritableDatabase(); boolean updateData = false; // 标记是否更新的是data表数据 long noteId = 0; // 用于存储便签ID,以便发送通知 // 根据Uri匹配的类型执行更新逻辑 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: // 获取Uri中的ID id = uri.getPathSegments().get(1); // 更新note表单条数据前,增加版本号 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: // 获取Uri中的ID,更新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); } // 数据更新成功时,发送通知更新UI if (count > 0) { // 更新data表数据时,同时通知note表的Uri if (updateData) { getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null); } // 如果是便签相关操作,通知对应的便签Uri if (noteId > 0) { getContext().getContentResolver().notifyChange( ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null); } // 通知当前Uri的数据变更 getContext().getContentResolver().notifyChange(uri, null); } return count; } /** * 辅助方法:处理选择条件,拼接AND和括号 * 若selection不为空,返回" AND (selection)";否则返回空字符串 * 用于将Uri的ID条件和传入的selection条件拼接,避免SQL语法错误 * * @param selection 传入的选择条件 * @return 处理后的选择条件字符串 */ private String parseSelection(String selection) { return (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : ""); } /** * 辅助方法:增加笔记的版本号(VERSION列) * 执行SQL语句将VERSION列加1,用于数据同步的版本控制,标记数据已修改 * * @param id 笔记的ID(-1表示更新多条数据) * @param selection 更新条件 * @param selectionArgs 更新条件的参数 */ private void increaseNoteVersion(long id, String selection, String[] selectionArgs) { StringBuilder sql = new StringBuilder(120); // 构建SQL语句的字符串构建器 // 拼接UPDATE语句:UPDATE note SET VERSION=VERSION+1 [WHERE 条件] sql.append("UPDATE "); sql.append(TABLE.NOTE); sql.append(" SET "); sql.append(NoteColumns.VERSION); sql.append("=" + NoteColumns.VERSION + "+1 "); // 添加WHERE子句(ID>0或selection不为空时) if (id > 0 || !TextUtils.isEmpty(selection)) { sql.append(" WHERE "); } // 添加ID条件(id>0时) if (id > 0) { sql.append(NoteColumns.ID + "=" + String.valueOf(id)); } // 添加传入的selection条件(替换占位符?为实际参数) if (!TextUtils.isEmpty(selection)) { // 处理selection:已有ID时拼接,否则直接使用 String selectString = id > 0 ? parseSelection(selection) : selection; // 替换selection中的?为selectionArgs的参数(简单替换,适用于基础场景) for (String args : selectionArgs) { selectString = selectString.replaceFirst("\\?", args); } sql.append(selectString); } // 执行SQL语句 mHelper.getWritableDatabase().execSQL(sql.toString()); } /** * 获取Uri对应的MIME类型,适配ContentProvider的规范 * 本类暂未实现该方法,返回null * * @param uri 要查询的Uri * @return Uri对应的MIME类型 */ @Override public String getType(Uri uri) { // TODO Auto-generated method stub return null; } }