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

571 lines
27 KiB

This file contains ambiguous Unicode characters!

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

/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.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<String> 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;
}
}