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.
XiaoMiNotes/src/net/micode/notes/tool/DataUtils.java

574 lines
21 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.tool;
import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.OperationApplicationException;
import android.database.Cursor;
import android.os.RemoteException;
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.NoteColumns;
import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 数据操作工具类
*
* 提供便签数据的批量操作、查询和验证功能,包括:
* 1. 批量删除便签
* 2. 批量移动便签到指定文件夹
* 3. 查询文件夹和便签信息
* 4. 验证数据存在性和唯一性
*
* 设计说明:
* - 所有方法均为静态方法,无需实例化
* - 使用批量操作提高数据库操作效率
* - 提供详细的错误日志输出
*/
public class DataUtils {
public static final String TAG = "DataUtils"; // 日志标签
/**
* 批量删除便签
*
* 使用批量操作applyBatch高效删除多个便签支持以下特性
* 1. 自动跳过系统文件夹(如根文件夹)的保护
* 2. 支持空集合和空参数的检查
* 3. 使用事务保证数据一致性
*
* @param resolver 内容解析器用于访问ContentProvider
* @param ids 要删除的便签ID集合为null或空集合时直接返回成功
* @return true表示删除成功false表示删除失败
*/
public static boolean batchDeleteNotes(ContentResolver resolver, HashSet<Long> ids) {
if (ids == null) {
Log.d(TAG, "the ids is null");
return true; // 空参数视为成功,无需操作
}
if (ids.size() == 0) {
Log.d(TAG, "no id is in the hashset");
return true; // 空集合视为成功,无需操作
}
ArrayList<ContentProviderOperation> operationList = new ArrayList<ContentProviderOperation>();
for (long id : ids) {
if(id == Notes.ID_ROOT_FOLDER) {
Log.e(TAG, "Don't delete system folder root");
continue; // 跳过系统根文件夹,防止误删系统数据
}
ContentProviderOperation.Builder builder = ContentProviderOperation
.newDelete(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id));
operationList.add(builder.build());
}
try {
ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operationList);
if (results == null || results.length == 0 || results[0] == null) {
Log.d(TAG, "delete notes failed, ids:" + ids.toString());
return false; // 批量操作返回结果异常
}
return true; // 删除成功
} catch (RemoteException e) {
Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage()));
} catch (OperationApplicationException e) {
Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage()));
}
return false; // 异常情况
}
/**
* 字数统计相关方法
* 用于统计文本中的中文字数、数字和英文字符
* 统计规则:
* 1. 中文字符每个汉字算1个字数
* 2. 数字每个数字算1个字数
* 3. 英文字符每个英文字母算1个字数
* 4. 不统计标点符号与空格
*/
// 中文字符正则表达式
private static final Pattern CHINESE_CHAR_PATTERN = Pattern.compile("[\\u4e00-\\u9fa5]");
// 数字正则表达式
private static final Pattern NUMBER_PATTERN = Pattern.compile("[0-9]");
// 英文字符正则表达式
private static final Pattern ENGLISH_CHAR_PATTERN = Pattern.compile("[a-zA-Z]");
/**
* 统计文本总字数
* @param text 要统计的文本
* @return 总字数
*/
public static int getTotalWordCount(String text) {
if (TextUtils.isEmpty(text)) {
return 0;
}
int chineseCount = countChineseCharacters(text);
int numberCount = countNumbers(text);
int englishCount = countEnglishCharacters(text);
return chineseCount + numberCount + englishCount;
}
/**
* 统计中文字符数
* @param text 要统计的文本
* @return 中文字符数
*/
private static int countChineseCharacters(String text) {
Matcher matcher = CHINESE_CHAR_PATTERN.matcher(text);
int count = 0;
while (matcher.find()) {
count++;
}
return count;
}
/**
* 统计数字字符数
* @param text 要统计的文本
* @return 数字字符数
*/
private static int countNumbers(String text) {
Matcher matcher = NUMBER_PATTERN.matcher(text);
int count = 0;
while (matcher.find()) {
count++;
}
return count;
}
/**
* 统计英文字符数
* @param text 要统计的文本
* @return 英文字符数
*/
private static int countEnglishCharacters(String text) {
Matcher matcher = ENGLISH_CHAR_PATTERN.matcher(text);
int count = 0;
while (matcher.find()) {
count++;
}
return count;
}
/**
* 移动单个便签到指定文件夹
*
* 更新便签的父文件夹ID并记录原始父文件夹ID以便撤销操作
*
* @param resolver 内容解析器
* @param id 要移动的便签ID
* @param srcFolderId 原始文件夹ID
* @param desFolderId 目标文件夹ID
*/
public static void moveNoteToFoler(ContentResolver resolver, long id, long srcFolderId, long desFolderId) {
ContentValues values = new ContentValues();
values.put(NoteColumns.PARENT_ID, desFolderId); // 设置新父文件夹
// 只有当移动到回收站时才记录原始父文件夹ID
// 从回收站恢复时保留原始的ORIGIN_PARENT_ID以便递归恢复子项
if (desFolderId == Notes.ID_TRASH_FOLER) {
values.put(NoteColumns.ORIGIN_PARENT_ID, srcFolderId); // 记录原始父文件夹,用于撤销
}
values.put(NoteColumns.LOCAL_MODIFIED, 1); // 标记为本地已修改,需要同步
resolver.update(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id), values, null, null);
}
/**
* 批量移动便签到指定文件夹
*
* 使用批量操作高效移动多个便签,支持以下特性:
* 1. 支持空集合的检查
* 2. 使用事务保证数据一致性
* 3. 自动标记为本地修改,触发同步
* 4. 移动到回收站时记录原始父文件夹ID
*
* @param resolver 内容解析器
* @param ids 要移动的便签ID集合
* @param folderId 目标文件夹ID
* @return true表示移动成功false表示移动失败
*/
public static boolean batchMoveToFolder(ContentResolver resolver, HashSet<Long> ids,
long folderId) {
if (ids == null) {
Log.d(TAG, "the ids is null");
return true; // 空参数视为成功
}
ArrayList<ContentProviderOperation> operationList = new ArrayList<ContentProviderOperation>();
for (long id : ids) {
ContentProviderOperation.Builder builder = ContentProviderOperation
.newUpdate(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id));
// 如果是移动到回收站记录原始父文件夹ID
if (folderId == Notes.ID_TRASH_FOLER) {
// 首先查询当前父文件夹ID
Cursor cursor = resolver.query(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id),
new String[]{NoteColumns.PARENT_ID},
null,
null,
null);
if (cursor != null) {
if (cursor.moveToFirst()) {
long currentParentId = cursor.getLong(0);
// 保存原始父文件夹ID
builder.withValue(NoteColumns.ORIGIN_PARENT_ID, currentParentId);
}
cursor.close();
}
}
builder.withValue(NoteColumns.PARENT_ID, folderId); // 设置新父文件夹
builder.withValue(NoteColumns.LOCAL_MODIFIED, 1); // 标记为本地修改
operationList.add(builder.build());
}
try {
ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operationList);
if (results == null || results.length == 0 || results[0] == null) {
Log.d(TAG, "delete notes failed, ids:" + ids.toString());
return false;
}
return true;
} catch (RemoteException e) {
Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage()));
} catch (OperationApplicationException e) {
Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage()));
}
return false;
}
/**
* 获取指定文件夹下的所有直接子项(包括便签和子文件夹)
*
* @param resolver 内容解析器
* @param folderId 文件夹ID
* @return 子项ID集合没有子项时返回空集合
*/
public static HashSet<Long> getFolderItems(ContentResolver resolver, long folderId) {
HashSet<Long> items = new HashSet<Long>();
Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI,
new String[]{NoteColumns.ID},
NoteColumns.PARENT_ID + "=?",
new String[]{String.valueOf(folderId)},
null);
if (cursor != null) {
if (cursor.moveToFirst()) {
do {
long id = cursor.getLong(0);
items.add(id);
} while (cursor.moveToNext());
}
cursor.close();
}
return items;
}
/**
* 获取用户文件夹数量(排除系统文件夹)
*
* 统计用户创建的自定义文件夹数量,不包括系统文件夹和回收站
*
* @param resolver 内容解析器
* @return 用户文件夹数量查询失败时返回0
*/
public static int getUserFolderCount(ContentResolver resolver) {
Cursor cursor =resolver.query(Notes.CONTENT_NOTE_URI,
new String[] { "COUNT(*)" },
NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>?",
new String[] { String.valueOf(Notes.TYPE_FOLDER), String.valueOf(Notes.ID_TRASH_FOLER)},
null);
int count = 0;
if(cursor != null) {
if(cursor.moveToFirst()) {
try {
count = cursor.getInt(0);
} catch (IndexOutOfBoundsException e) {
Log.e(TAG, "get folder count failed:" + e.toString());
} finally {
cursor.close();
}
}
}
return count;
}
/**
* 检查指定类型便签在数据库中是否可见(不在回收站中)
*
* 用于验证便签是否存在于数据库中且未被放入回收站
*
* @param resolver 内容解析器
* @param noteId 要检查的便签ID
* @param type 便签类型Notes.TYPE_NOTE或Notes.TYPE_FOLDER
* @return true表示便签存在且不在回收站中false表示不存在或在回收站中
*/
public static boolean visibleInNoteDatabase(ContentResolver resolver, long noteId, int type) {
// 根文件夹永远可见
if (noteId == Notes.ID_ROOT_FOLDER) {
return true;
}
Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId),
null,
NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER,
new String [] {String.valueOf(type)},
null);
boolean exist = false;
if (cursor != null) {
if (cursor.getCount() > 0) {
exist = true;
}
cursor.close();
}
return exist;
}
/**
* 检查便签是否存在于数据库中
*
* 不检查便签是否在回收站中只验证ID是否存在
*
* @param resolver 内容解析器
* @param noteId 要检查的便签ID
* @return true表示便签ID存在false表示不存在
*/
public static boolean existInNoteDatabase(ContentResolver resolver, long noteId) {
Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId),
null, null, null, null);
boolean exist = false;
if (cursor != null) {
if (cursor.getCount() > 0) {
exist = true;
}
cursor.close();
}
return exist;
}
/**
* 检查数据记录是否存在于数据表中
*
* 用于验证数据ID是否可用避免ID冲突
*
* @param resolver 内容解析器
* @param dataId 要检查的数据记录ID
* @return true表示数据ID存在false表示不存在
*/
public static boolean existInDataDatabase(ContentResolver resolver, long dataId) {
Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId),
null, null, null, null);
boolean exist = false;
if (cursor != null) {
if (cursor.getCount() > 0) {
exist = true;
}
cursor.close();
}
return exist;
}
/**
* 检查指定文件夹名称是否已存在(在可见文件夹中)
*
* 用于创建新文件夹时的名称重复性检查,不检查回收站中的文件夹
*
* @param resolver 内容解析器
* @param name 要检查的文件夹名称
* @return true表示同名文件夹已存在false表示不存在
*/
public static boolean checkVisibleFolderName(ContentResolver resolver, String name) {
Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI, null,
NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER +
" AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER +
" AND " + NoteColumns.SNIPPET + "=?",
new String[] { name }, null);
boolean exist = false;
if(cursor != null) {
if(cursor.getCount() > 0) {
exist = true;
}
cursor.close();
}
return exist;
}
/**
* 获取指定文件夹下所有便签关联的小部件属性
*
* 用于文件夹删除或移动时,更新关联的小部件显示
*
* @param resolver 内容解析器
* @param folderId 文件夹ID
* @return 小部件属性集合没有关联小部件时返回null
*/
public static HashSet<AppWidgetAttribute> getFolderNoteWidget(ContentResolver resolver, long folderId) {
Cursor c = resolver.query(Notes.CONTENT_NOTE_URI,
new String[] { NoteColumns.WIDGET_ID, NoteColumns.WIDGET_TYPE },
NoteColumns.PARENT_ID + "=?",
new String[] { String.valueOf(folderId) },
null);
HashSet<AppWidgetAttribute> set = null;
if (c != null) {
if (c.moveToFirst()) {
set = new HashSet<AppWidgetAttribute>();
do {
try {
AppWidgetAttribute widget = new AppWidgetAttribute();
widget.widgetId = c.getInt(0);
widget.widgetType = c.getInt(1);
set.add(widget);
} catch (IndexOutOfBoundsException e) {
Log.e(TAG, e.toString());
}
} while (c.moveToNext());
}
c.close();
}
return set;
}
/**
* 根据便签ID获取关联的通话记录电话号码
*
* 用于通话记录便签的显示和查询
*
* @param resolver 内容解析器
* @param noteId 便签ID
* @return 电话号码字符串,未找到时返回空字符串
*/
public static String getCallNumberByNoteId(ContentResolver resolver, long noteId) {
Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI,
new String [] { CallNote.PHONE_NUMBER },
CallNote.NOTE_ID + "=? AND " + CallNote.MIME_TYPE + "=?",
new String [] { String.valueOf(noteId), CallNote.CONTENT_ITEM_TYPE },
null);
if (cursor != null && cursor.moveToFirst()) {
try {
return cursor.getString(0);
} catch (IndexOutOfBoundsException e) {
Log.e(TAG, "Get call number fails " + e.toString());
} finally {
cursor.close();
}
}
return ""; // 未找到通话记录或查询失败
}
/**
* 根据电话号码和通话时间获取便签ID
*
* 用于检查是否存在相同通话记录,避免重复创建
* 使用自定义SQL函数PHONE_NUMBERS_EQUAL进行电话号码匹配
*
* @param resolver 内容解析器
* @param phoneNumber 电话号码
* @param callDate 通话时间戳
* @return 便签ID未找到时返回0
*/
public static long getNoteIdByPhoneNumberAndCallDate(ContentResolver resolver, String phoneNumber, long callDate) {
Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI,
new String [] { CallNote.NOTE_ID },
CallNote.CALL_DATE + "=? AND " + CallNote.MIME_TYPE + "=? AND PHONE_NUMBERS_EQUAL("
+ CallNote.PHONE_NUMBER + ",?)",
new String [] { String.valueOf(callDate), CallNote.CONTENT_ITEM_TYPE, phoneNumber },
null);
if (cursor != null) {
if (cursor.moveToFirst()) {
try {
return cursor.getLong(0);
} catch (IndexOutOfBoundsException e) {
Log.e(TAG, "Get call note id fails " + e.toString());
}
}
cursor.close();
}
return 0; // 未找到匹配的通话记录
}
/**
* 根据便签ID获取便签片段snippet
*
* 便签片段通常用于列表显示,是便签内容的摘要
*
* @param resolver 内容解析器
* @param noteId 便签ID
* @return 便签片段字符串
* @throws IllegalArgumentException 当便签ID不存在时抛出
*/
public static String getSnippetById(ContentResolver resolver, long noteId) {
Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI,
new String [] { NoteColumns.SNIPPET },
NoteColumns.ID + "=?",
new String [] { String.valueOf(noteId)},
null);
if (cursor != null) {
String snippet = "";
if (cursor.moveToFirst()) {
snippet = cursor.getString(0);
}
cursor.close();
return snippet;
}
throw new IllegalArgumentException("Note is not found with id: " + noteId);
}
/**
* 格式化便签片段
*
* 处理便签片段使其适合在UI中显示
* 1. 去除首尾空格
* 2. 截取到第一个换行符处(如果存在)
*
* @param snippet 原始便签片段
* @return 格式化后的便签片段
*/
public static String getFormattedSnippet(String snippet) {
if (snippet != null) {
snippet = snippet.trim(); // 去除首尾空格
int index = snippet.indexOf('\n');
if (index != -1) {
snippet = snippet.substring(0, index); // 截取到第一个换行符
}
}
return snippet;
}
}