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.text/src/data/NotesProvider.java

662 lines
25 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.data.Notes.DataColumns;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.NotesDatabaseHelper.TABLE;
/**
* 便签应用的Content Provider负责处理便签数据的CRUD操作和搜索功能
* <p>
* 核心职责:
* <ul>
* <li>提供便签数据的Content Provider访问接口</li>
* <li>处理便签和便签详细数据的查询、插入、更新、删除操作</li>
* <li>支持便签搜索和搜索建议功能</li>
* <li>管理便签数据的版本控制</li>
* <li>在数据变更时通知相关组件</li>
* </ul>
* <p>
* 设计意图:
* <ul>
* <li>遵循Android Content Provider设计规范提供统一的数据访问接口</li>
* <li>使用UriMatcher处理不同类型的URI请求</li>
* <li>实现数据的封装和隔离,保护数据安全</li>
* <li>支持跨应用数据访问(如果需要)</li>
* <li>集成Android搜索框架支持全局搜索</li>
* </ul>
* <p>
* 继承关系:
* <ul>
* <li>直接继承自{@link android.content.ContentProvider}实现标准的Content Provider接口</li>
* </ul>
* <p>
* 与其他类的关键关联:
* <ul>
* <li>依赖{@link NotesDatabaseHelper}获取数据库实例,进行底层数据操作</li>
* <li>使用{@link Notes}类中定义的常量和URI作为数据访问的标准</li>
* <li>实现{@link SearchManager}接口支持Android全局搜索功能</li>
* <li>被应用的UI组件和Widget组件使用作为数据访问的主要入口</li>
* </ul>
* <p>
* URI匹配规则
* <ul>
* <li>content://micode_notes/note - 所有便签和文件夹</li>
* <li>content://micode_notes/note/# - 单个便签或文件夹</li>
* <li>content://micode_notes/data - 所有便签详细数据</li>
* <li>content://micode_notes/data/# - 单个便签详细数据</li>
* <li>content://micode_notes/search - 搜索便签</li>
* <li>content://micode_notes/search_suggest_query - 搜索建议</li>
* </ul>
*
*/
public class NotesProvider extends ContentProvider {
/**
* UriMatcher实例用于匹配不同类型的URI请求
* <p>
* 用途根据URI路径和格式将请求分发到不同的处理逻辑
* <p>
* 初始化在静态代码块中初始化添加所有支持的URI模式
*/
private static final UriMatcher mMatcher;
/**
* 数据库辅助类实例用于获取SQLiteDatabase对象
* <p>
* 用途:提供底层数据库操作支持,包括创建、打开和升级数据库
* <p>
* 初始化在onCreate方法中通过单例模式获取实例
*/
private NotesDatabaseHelper mHelper;
/**
* 日志标签,用于调试和日志记录
*/
private static final String TAG = "NotesProvider";
/**
* URI类型常量表示所有便签和文件夹的集合
*/
private static final int URI_NOTE = 1;
/**
* URI类型常量表示单个便签或文件夹
*/
private static final int URI_NOTE_ITEM = 2;
/**
* URI类型常量表示所有便签详细数据的集合
*/
private static final int URI_DATA = 3;
/**
* URI类型常量表示单个便签详细数据
*/
private static final int URI_DATA_ITEM = 4;
/**
* URI类型常量表示便签搜索请求
*/
private static final int URI_SEARCH = 5;
/**
* URI类型常量表示搜索建议请求
*/
private static final int URI_SEARCH_SUGGEST = 6;
/**
* 静态代码块初始化UriMatcher实例添加所有支持的URI模式
* <p>
* 注册的URI模式包括
* <ul>
* <li>note - 所有便签和文件夹</li>
* <li>note/# - 单个便签或文件夹</li>
* <li>data - 所有便签详细数据</li>
* <li>data/# - 单个便签详细数据</li>
* <li>search - 便签搜索</li>
* <li>search_suggest_query - 搜索建议</li>
* <li>search_suggest_query/* - 带查询参数的搜索建议</li>
* </ul>
*/
static {
mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
mMatcher.addURI(Notes.AUTHORITY, "note", URI_NOTE);
mMatcher.addURI(Notes.AUTHORITY, "note/#", URI_NOTE_ITEM);
mMatcher.addURI(Notes.AUTHORITY, "data", URI_DATA);
mMatcher.addURI(Notes.AUTHORITY, "data/#", URI_DATA_ITEM);
mMatcher.addURI(Notes.AUTHORITY, "search", URI_SEARCH);
mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, URI_SEARCH_SUGGEST);
mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", URI_SEARCH_SUGGEST);
}
/**
* 便签搜索的投影列定义,用于构建搜索结果
* <p>
* 包含以下字段:
* <ul>
* <li>便签ID</li>
* <li>搜索建议的额外数据便签ID</li>
* <li>搜索建议的文本1便签内容去除换行符和空格</li>
* <li>搜索建议的文本2便签内容去除换行符和空格</li>
* <li>搜索结果图标</li>
* <li>搜索结果的意图动作(查看)</li>
* <li>搜索结果的意图数据类型</li>
* </ul>
* <p>
* 特殊处理:
* <ul>
* <li>使用SQLite函数TRIM和REPLACE去除便签内容中的换行符x'0A')和空格</li>
* <li>使用固定的搜索结果图标</li>
* </ul>
*/
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查询语句用于执行实际的搜索操作
* <p>
* 查询条件:
* <ul>
* <li>便签内容包含搜索关键字LIKE ?</li>
* <li>便签不在回收站中PARENT_ID <> TRASH_FOLDER</li>
* <li>便签类型为普通便签TYPE = TYPE_NOTE</li>
* </ul>
* <p>
* 用途在URI_SEARCH和URI_SEARCH_SUGGEST类型的请求中执行搜索操作
*/
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;
/**
* 初始化Content Provider
* <p>
* 业务逻辑:
* <ul>
* <li>获取NotesDatabaseHelper的单例实例</li>
* <li>初始化数据库连接</li>
* </ul>
* <p>
* 与其他类的交互:
* <ul>
* <li>调用{@link NotesDatabaseHelper#getInstance(android.content.Context)}获取数据库辅助类实例</li>
* </ul>
*
* @return true 初始化成功
* @see android.content.ContentProvider#onCreate()
*/
@Override
public boolean onCreate() {
mHelper = NotesDatabaseHelper.getInstance(getContext());
return true;
}
/**
* 处理查询请求根据URI类型返回相应的Cursor对象
* <p>
* 业务逻辑:
* <ul>
* <li>根据URI类型分发到不同的查询逻辑</li>
* <li>获取可读数据库实例</li>
* <li>执行查询操作返回Cursor对象</li>
* <li>为Cursor设置通知URI以便数据变更时接收通知</li>
* </ul>
* <p>
* URI类型处理
* <ul>
* <li>{@link #URI_NOTE} - 查询所有便签和文件夹</li>
* <li>{@link #URI_NOTE_ITEM} - 查询单个便签或文件夹</li>
* <li>{@link #URI_DATA} - 查询所有便签详细数据</li>
* <li>{@link #URI_DATA_ITEM} - 查询单个便签详细数据</li>
* <li>{@link #URI_SEARCH} - 执行便签搜索</li>
* <li>{@link #URI_SEARCH_SUGGEST} - 提供搜索建议</li>
* </ul>
* <p>
* 与其他类的交互:
* <ul>
* <li>使用{@link NotesDatabaseHelper#getReadableDatabase()}获取可读数据库</li>
* <li>使用{@link SQLiteDatabase}执行查询操作</li>
* <li>使用{@link SearchManager}处理搜索相关请求</li>
* </ul>
* <p>
* 特殊处理:
* <ul>
* <li>搜索请求不允许指定sortOrder和projection参数</li>
* <li>搜索结果会去除换行符和空格,以便显示更多信息</li>
* <li>搜索范围不包括回收站中的便签</li>
* </ul>
*
* @param uri 请求的URI
* @param projection 返回的列名数组
* @param selection 查询条件
* @param selectionArgs 查询条件参数
* @param sortOrder 排序方式
* @return Cursor 查询结果的Cursor对象或null如果查询失败
* @throws IllegalArgumentException 如果URI无效或参数不符合要求
* @see android.content.ContentProvider#query(android.net.Uri, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String)
*/
@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:
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:
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;
}
/**
* 处理插入请求根据URI类型插入数据并返回新的URI
* <p>
* 业务逻辑:
* <ul>
* <li>根据URI类型分发到不同的插入逻辑</li>
* <li>获取可写数据库实例</li>
* <li>执行插入操作返回插入的行ID</li>
* <li>通知相关URI的数据变更</li>
* <li>返回包含新ID的URI</li>
* </ul>
* <p>
* URI类型处理
* <ul>
* <li>{@link #URI_NOTE} - 插入便签或文件夹</li>
* <li>{@link #URI_DATA} - 插入便签详细数据</li>
* </ul>
* <p>
* 与其他类的交互:
* <ul>
* <li>使用{@link NotesDatabaseHelper#getWritableDatabase()}获取可写数据库</li>
* <li>使用{@link SQLiteDatabase}执行插入操作</li>
* <li>通过{@link android.content.ContentResolver#notifyChange(android.net.Uri, android.database.ContentObserver)}通知数据变更</li>
* </ul>
* <p>
* 特殊处理:
* <ul>
* <li>插入数据时记录插入的noteId和dataId</li>
* <li>插入成功后通知相关URI的数据变更</li>
* </ul>
*
* @param uri 请求的URI
* @param values 要插入的数据
* @return Uri 包含新插入行ID的URI
* @throws IllegalArgumentException 如果URI无效
* @see android.content.ContentProvider#insert(android.net.Uri, android.content.ContentValues)
*/
@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:
insertedId = noteId = db.insert(TABLE.NOTE, null, values);
break;
case URI_DATA:
if (values.containsKey(DataColumns.NOTE_ID)) {
noteId = values.getAsLong(DataColumns.NOTE_ID);
} else {
Log.d(TAG, "Wrong data format without note id:" + values.toString());
}
insertedId = dataId = db.insert(TABLE.DATA, null, values);
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
// Notify the note 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);
}
/**
* 处理删除请求根据URI类型删除数据并返回删除的行数
* <p>
* 业务逻辑:
* <ul>
* <li>根据URI类型分发到不同的删除逻辑</li>
* <li>获取可写数据库实例</li>
* <li>执行删除操作,返回删除的行数</li>
* <li>通知相关URI的数据变更</li>
* </ul>
* <p>
* URI类型处理
* <ul>
* <li>{@link #URI_NOTE} - 删除便签或文件夹</li>
* <li>{@link #URI_NOTE_ITEM} - 删除单个便签或文件夹</li>
* <li>{@link #URI_DATA} - 删除便签详细数据</li>
* <li>{@link #URI_DATA_ITEM} - 删除单个便签详细数据</li>
* </ul>
* <p>
* 与其他类的交互:
* <ul>
* <li>使用{@link NotesDatabaseHelper#getWritableDatabase()}获取可写数据库</li>
* <li>使用{@link SQLiteDatabase}执行删除操作</li>
* <li>通过{@link android.content.ContentResolver#notifyChange(android.net.Uri, android.database.ContentObserver)}通知数据变更</li>
* </ul>
* <p>
* 特殊处理:
* <ul>
* <li>删除便签时跳过ID小于等于0的系统文件夹</li>
* <li>删除数据时同时通知CONTENT_NOTE_URI的数据变更</li>
* </ul>
*
* @param uri 请求的URI
* @param selection 删除条件
* @param selectionArgs 删除条件参数
* @return int 删除的行数
* @throws IllegalArgumentException 如果URI无效
* @see android.content.ContentProvider#delete(android.net.Uri, java.lang.String, java.lang.String[])
*/
@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:
selection = "(" + selection + ") AND " + NoteColumns.ID + ">0 ";
count = db.delete(TABLE.NOTE, selection, selectionArgs);
break;
case URI_NOTE_ITEM:
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) {
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:
id = uri.getPathSegments().get(1);
count = db.delete(TABLE.DATA,
DataColumns.ID + "=" + id + parseSelection(selection), selectionArgs);
deleteData = true;
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
if (count > 0) {
if (deleteData) {
getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null);
}
getContext().getContentResolver().notifyChange(uri, null);
}
return count;
}
/**
* 处理更新请求根据URI类型更新数据并返回更新的行数
* <p>
* 业务逻辑:
* <ul>
* <li>根据URI类型分发到不同的更新逻辑</li>
* <li>获取可写数据库实例</li>
* <li>对于便签更新,先增加版本号</li>
* <li>执行更新操作,返回更新的行数</li>
* <li>通知相关URI的数据变更</li>
* </ul>
* <p>
* URI类型处理
* <ul>
* <li>{@link #URI_NOTE} - 更新便签或文件夹</li>
* <li>{@link #URI_NOTE_ITEM} - 更新单个便签或文件夹</li>
* <li>{@link #URI_DATA} - 更新便签详细数据</li>
* <li>{@link #URI_DATA_ITEM} - 更新单个便签详细数据</li>
* </ul>
* <p>
* 与其他类的交互:
* <ul>
* <li>使用{@link NotesDatabaseHelper#getWritableDatabase()}获取可写数据库</li>
* <li>使用{@link SQLiteDatabase}执行更新操作</li>
* <li>调用{@link #increaseNoteVersion(long, String, String[])}增加便签版本号</li>
* <li>通过{@link android.content.ContentResolver#notifyChange(android.net.Uri, android.database.ContentObserver)}通知数据变更</li>
* </ul>
* <p>
* 特殊处理:
* <ul>
* <li>更新便签时,自动增加版本号</li>
* <li>更新数据时同时通知CONTENT_NOTE_URI的数据变更</li>
* </ul>
*
* @param uri 请求的URI
* @param values 要更新的数据
* @param selection 更新条件
* @param selectionArgs 更新条件参数
* @return int 更新的行数
* @throws IllegalArgumentException 如果URI无效
* @see android.content.ContentProvider#update(android.net.Uri, android.content.ContentValues, java.lang.String, java.lang.String[])
*/
@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:
increaseNoteVersion(-1, selection, selectionArgs);
count = db.update(TABLE.NOTE, values, selection, selectionArgs);
break;
case URI_NOTE_ITEM:
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:
id = uri.getPathSegments().get(1);
count = db.update(TABLE.DATA, values, DataColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs);
updateData = true;
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
if (count > 0) {
if (updateData) {
getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null);
}
getContext().getContentResolver().notifyChange(uri, null);
}
return count;
}
/**
* 辅助方法,处理查询条件,将额外条件添加到现有条件中
* <p>
* 业务逻辑:
* <ul>
* <li>如果selection不为空将其包装为" AND (selection)"格式</li>
* <li>否则返回空字符串</li>
* </ul>
*
* @param selection 原始查询条件
* @return String 处理后的查询条件
*/
private String parseSelection(String selection) {
return (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : "");
}
/**
* 辅助方法,增加便签的版本号
* <p>
* 业务逻辑:
* <ul>
* <li>构建UPDATE语句将版本号加1</li>
* <li>根据id和selection参数构建WHERE子句</li>
* <li>执行SQL语句更新版本号</li>
* </ul>
* <p>
* 与其他类的交互:
* <ul>
* <li>使用{@link NotesDatabaseHelper#getWritableDatabase()}获取可写数据库</li>
* <li>使用{@link SQLiteDatabase#execSQL(String)}执行SQL语句</li>
* </ul>
* <p>
* 特殊处理:
* <ul>
* <li>直接使用字符串拼接构建SQL语句存在SQL注入风险</li>
* <li>使用replaceFirst替换占位符仅适用于简单的条件</li>
* <li>TODO: 考虑使用PreparedStatement或参数化查询提高安全性</li>
* </ul>
*
* @param id 便签ID-1表示更新多个便签
* @param selection 更新条件
* @param selectionArgs 更新条件参数
*/
private void increaseNoteVersion(long id, String selection, String[] selectionArgs) {
StringBuilder sql = new StringBuilder(120);
sql.append("UPDATE ");
sql.append(TABLE.NOTE);
sql.append(" SET ");
sql.append(NoteColumns.VERSION);
sql.append("=" + NoteColumns.VERSION + "+1 ");
if (id > 0 || !TextUtils.isEmpty(selection)) {
sql.append(" WHERE ");
}
if (id > 0) {
sql.append(NoteColumns.ID + "=" + String.valueOf(id));
}
if (!TextUtils.isEmpty(selection)) {
String selectString = id > 0 ? parseSelection(selection) : selection;
for (String args : selectionArgs) {
selectString = selectString.replaceFirst("\\?", args);
}
sql.append(selectString);
}
mHelper.getWritableDatabase().execSQL(sql.toString());
}
/**
* 获取URI对应的MIME类型
* <p>
* 注意该方法目前未实现返回null
* <p>
* TODO: 实现该方法返回正确的MIME类型
*
* @param uri 请求的URI
* @return String URI对应的MIME类型目前返回null
* @see android.content.ContentProvider#getType(android.net.Uri)
*/
@Override
public String getType(Uri uri) {
// TODO Auto-generated method stub
return null;
}
}