Compare commits

...

5 Commits

@ -0,0 +1,259 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
AndroidManifest.xml - Android应用清单文件
此文件定义了应用的基本信息、组件、权限等核心配置
version="1.0" - XML版本
encoding="utf-8" - 文件编码格式
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!--
manifest - 根元素,定义应用的包名、版本等元数据
xmlns:android - Android命名空间用于访问Android系统属性
xmlns:tools - 工具命名空间,用于开发工具的特殊处理
-->
<!-- ======================= 权限声明区域 ======================= -->
<!-- 这一部分列出了应用需要的所有系统权限,安装时会向用户请求 -->
<!-- 写入外部存储权限 - 允许应用保存文件到设备存储 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- 安装快捷方式权限 - 允许应用在桌面创建快捷方式 -->
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
<!-- 网络访问权限 - 允许应用访问互联网(用于同步等功能) -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- 读取联系人权限 - 允许应用访问设备联系人 -->
<uses-permission android:name="android.permission.READ_CONTACTS" />
<!-- 管理账户权限 - 允许应用管理账户(添加/删除账户等) -->
<!-- tools:ignore="Deprecated" - 忽略此权限已被弃用的警告 -->
<uses-permission
android:name="android.permission.MANAGE_ACCOUNTS"
tools:ignore="Deprecated" />
<!-- 验证账户权限 - 允许应用验证账户凭据 -->
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
<!-- 获取账户权限 - 允许应用读取设备上的账户列表 -->
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<!-- 使用凭据权限 - 允许应用使用账户凭据 -->
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
<!-- 开机启动完成广播权限 - 允许应用接收系统启动完成的广播 -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!-- ======================= 应用配置区域 ======================= -->
<!-- application - 应用全局配置 -->
<!--
android:icon - 应用图标,显示在桌面和应用列表中,支持不同分辨率需提供多套资源
android:label - 应用名称,引用字符串资源
android:allowBackup - 是否允许系统备份应用数据
android:supportsRtl - 是否支持从右到左的布局(如阿拉伯语)
android:theme - 应用主题样式
tools:replace - 指定要替换的属性(用于解决与其他应用的冲突)
-->
<application
android:icon="@drawable/icon_app"
android:label="@string/app_name"
android:allowBackup="true"
android:supportsRtl="true"
android:theme="@style/NoteTheme"
tools:replace="android:icon,android:theme">
<!-- ======================= Activity组件 ======================= -->
<!-- 主Activity - 便签列表界面 -->
<!--
android:name - Activity类名.表示相对路径)
android:configChanges - 指定配置变化时由应用自己处理
android:label - Activity标题
android:launchMode - 启动模式singleTop栈顶复用
android:uiOptions - 界面选项splitActionBarWhenNarrow窄屏时拆分操作栏
android:windowSoftInputMode - 软键盘显示模式adjustPan调整面板
android:exported - 是否允许外部应用调用true表示允许
-->
<activity
android:name=".ui.NotesListActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:label="@string/app_name"
android:launchMode="singleTop"
android:theme="@style/NoteTheme"
android:uiOptions="splitActionBarWhenNarrow"
android:windowSoftInputMode="adjustPan"
android:exported="true">
<!-- intent-filter - 意图过滤器定义Activity能响应的操作 -->
<intent-filter>
<!-- 主入口点应用启动时第一个显示的Activity -->
<action android:name="android.intent.action.MAIN" />
<!-- 启动器类别,会在应用列表中显示 -->
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- 编辑Activity - 便签编辑界面 -->
<activity
android:name=".ui.NoteEditActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:launchMode="singleTop"
android:theme="@style/NoteTheme"
android:exported="true">
<!-- 第一个intent-filter查看便签 -->
<intent-filter>
<!-- 查看操作 -->
<action android:name="android.intent.action.VIEW" />
<!-- 默认类别 -->
<category android:name="android.intent.category.DEFAULT" />
<!-- 支持的数据类型:文本便签和通话便签 -->
<data android:mimeType="vnd.android.cursor.item/text_note" />
<data android:mimeType="vnd.android.cursor.item/call_note" />
</intent-filter>
<!-- 第二个intent-filter插入或编辑便签 -->
<intent-filter>
<action android:name="android.intent.action.INSERT_OR_EDIT" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="vnd.android.cursor.item/text_note" />
<data android:mimeType="vnd.android.cursor.item/call_note" />
</intent-filter>
<!-- 第三个intent-filter搜索功能 -->
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<!-- 搜索配置元数据 -->
<meta-data
android:name="android.app.searchable"
android:resource="@xml/searchable" />
</activity>
<!-- ======================= Content Provider组件 ======================= -->
<!-- 数据提供者 - 管理便签数据 -->
<!--
android:name - Provider类名
android:authorities - 内容URI的授权标识
android:multiprocess - 是否支持多进程
android:exported - 是否允许外部应用访问false表示不允许
tools:replace - 替换authorities属性
-->
<provider
android:name="net.micode.notes.data.NotesProvider"
android:authorities="micode_notes"
android:multiprocess="true"
android:exported="false"
tools:replace="android:authorities" />
<!-- ======================= Broadcast Receiver组件 ======================= -->
<!-- 2x2桌面小部件接收器 -->
<!--
android:name - Receiver类名
android:label - 小部件名称
android:exported - 是否允许外部应用调用
-->
<receiver
android:name=".widget.NoteWidgetProvider_2x"
android:label="@string/app_widget2x2"
android:exported="true"
tools:replace="android:label">
<!-- 小部件相关广播过滤器 -->
<intent-filter>
<!-- 小部件更新 -->
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<!-- 小部件删除 -->
<action android:name="android.appwidget.action.APPWIDGET_DELETED" />
<!-- 隐私模式变更 -->
<action android:name="android.intent.action.PRIVACY_MODE_CHANGED" />
</intent-filter>
<!-- 小部件配置元数据 -->
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_2x_info" />
</receiver>
<!-- 4x4桌面小部件接收器 -->
<receiver
android:name=".widget.NoteWidgetProvider_4x"
android:label="@string/app_widget4x4"
android:exported="true"
tools:replace="android:label">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="android.appwidget.action.APPWIDGET_DELETED" />
<action android:name="android.intent.action.PRIVACY_MODE_CHANGED" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_4x_info" />
</receiver>
<!-- 闹钟初始化接收器 - 接收开机启动完成广播 -->
<receiver
android:name=".ui.AlarmInitReceiver"
android:exported="true">
<intent-filter>
<!-- 系统启动完成广播 -->
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<!-- 闹钟接收器 - 处理闹钟提醒 -->
<!--
android:process=":remote" - 在独立进程中运行
避免主进程被杀死时无法接收闹钟
-->
<receiver
android:name="net.micode.notes.ui.AlarmReceiver"
android:process=":remote" />
<!-- ======================= 其他Activity组件 ======================= -->
<!-- 闹钟提醒Activity - 显示闹钟提醒界面 -->
<!--
android:launchMode="singleInstance" - 独立任务栈启动
android:theme - 使用系统主题(无标题栏壁纸主题)
-->
<activity
android:name=".ui.AlarmAlertActivity"
android:label="@string/app_name"
android:launchMode="singleInstance"
android:theme="@android:style/Theme.Holo.Wallpaper.NoTitleBar"
tools:replace="android:theme" />
<!-- 设置Activity - 应用偏好设置界面 -->
<activity
android:name="net.micode.notes.ui.NotesPreferenceActivity"
android:label="@string/preferences_title"
android:launchMode="singleTop"
android:theme="@style/NoteTheme"
tools:replace="android:theme" />
<!-- ======================= Service组件 ======================= -->
<!-- 同步服务 - 处理Google任务同步 -->
<!--
android:exported="false" - 不允许外部应用绑定此服务
这是应用内部使用的同步服务
-->
<service
android:name="net.micode.notes.gtask.remote.GTaskSyncService"
android:exported="false" />
<!-- ======================= 应用级元数据 ======================= -->
<!-- 默认搜索Activity配置 -->
<!--
指定应用默认的搜索Activity
当用户执行搜索操作时会启动此Activity
-->
<meta-data
android:name="android.app.default_searchable"
android:value=".ui.NoteEditActivity" />
</application>
</manifest>

@ -59,7 +59,6 @@ public class Notes {
* {@link Notes#ID_ROOT_FOLDER }
* {@link Notes#ID_TEMPARAY_FOLDER }
* {@link Notes#ID_CALL_RECORD_FOLDER}
* {@link Notes#ID_TRASH_FOLER}/
*/
// 根文件夹ID默认文件夹所有无指定文件夹的笔记默认归属此文件夹
public static final int ID_ROOT_FOLDER = 0;
@ -67,7 +66,7 @@ public class Notes {
public static final int ID_TEMPARAY_FOLDER = -1;
// 通话记录文件夹ID专门存储通话记录类型的笔记
public static final int ID_CALL_RECORD_FOLDER = -2;
// 回收站文件夹ID存放被用户删除的笔记或文件夹
// 回收站文件夹ID已废弃,用于兼容旧代码
public static final int ID_TRASH_FOLER = -3;
/**
@ -207,6 +206,12 @@ public class Notes {
* <P> : INTEGER </P>
*/
public static final String TYPE = "type";
/**
* 01
* <P> : INTEGER </P>
*/
public static final String PINNED = "pinned";
/**
* IDGTask
@ -220,6 +225,8 @@ public class Notes {
*/
public static final String LOCAL_MODIFIED = "local_modified";
/**
* ID
* <P> : INTEGER (long) </P>
@ -381,4 +388,6 @@ public class Notes {
*/
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/call_note");
}
}

@ -45,12 +45,12 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
private static final String DB_NAME = "note.db";
/**
* 4
* 6
*/
private static final int DB_VERSION = 4;
private static final int DB_VERSION = 6;
/**
* notedata
* notedata便-
*/
public interface TABLE {
// 笔记/文件夹表名称
@ -90,6 +90,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
NoteColumns.NOTES_COUNT + " INTEGER NOT NULL DEFAULT 0," + // 文件夹下的笔记数量
NoteColumns.SNIPPET + " TEXT NOT NULL DEFAULT ''," + // 文件夹名称/笔记摘要
NoteColumns.TYPE + " INTEGER NOT NULL DEFAULT 0," + // 类型(笔记/文件夹/系统)
NoteColumns.PINNED + " INTEGER NOT NULL DEFAULT 0," + // 是否置顶0不置顶1置顶
NoteColumns.WIDGET_ID + " INTEGER NOT NULL DEFAULT 0," + // 关联的Widget ID
NoteColumns.WIDGET_TYPE + " INTEGER NOT NULL DEFAULT -1," + // 关联的Widget类型
NoteColumns.SYNC_ID + " INTEGER NOT NULL DEFAULT 0," + // 同步IDGTask
@ -122,10 +123,12 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
* dataNOTE_IDSQL
* NOTE_IDdata
*/
private static final String CREATE_DATA_NOTE_ID_INDEX_SQL =
"CREATE INDEX IF NOT EXISTS note_id_index ON " +
private static final String CREATE_DATA_NOTE_ID_INDEX_SQL =
"CREATE INDEX IF NOT EXISTS note_id_index ON " +
TABLE.DATA + "(" + DataColumns.NOTE_ID + ");";
// ====================== 数据库触发器SQL语句note表 ======================
/**
* ID
@ -255,20 +258,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
" WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" +
" END";
/**
*
* notePARENT_IDID
* PARENT_IDID
*/
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";
/**
* SQLiteOpenHelper
@ -308,7 +298,6 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
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);
@ -317,7 +306,6 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
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);
}
/**
@ -352,13 +340,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
/**
* 4.
*/
values.clear();
values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
}
/**
@ -376,6 +358,8 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
Log.d(TAG, "data table has been created");
}
/**
* data
*
@ -454,6 +438,16 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
upgradeToV4(db);
oldVersion++;
}
// 从版本4升级到版本5
if (oldVersion == 4) {
// 为note表添加pinned字段用于置顶功能
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.PINNED
+ " INTEGER NOT NULL DEFAULT 0");
oldVersion++;
}
// 如果需要,重建触发器
if (reCreateTriggers) {
@ -468,6 +462,8 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
}
}
/**
* 12
*
@ -486,8 +482,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
/**
* 23
* 1.
* 2. noteGTASK_ID
* 3.
* 2. noteGTASK_ID
*
* @param db SQLiteDatabase
*/
@ -499,11 +494,6 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
// 为note表添加GTASK_ID列用于GTask同步
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.GTASK_ID
+ " TEXT NOT NULL DEFAULT ''");
// 新增回收站系统文件夹
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);
}
/**

@ -30,6 +30,7 @@ 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;
@ -91,6 +92,7 @@ public class NotesProvider extends ContentProvider {
*/
private static final int URI_SEARCH_SUGGEST = 6;
/**
* UriMatcherUri
* authority + path ->
@ -112,6 +114,7 @@ public class NotesProvider extends ContentProvider {
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);
}
/**
@ -217,22 +220,81 @@ public class NotesProvider extends ContentProvider {
searchString = uri.getQueryParameter("pattern");
}
// 关键词为空时返回null
if (TextUtils.isEmpty(searchString)) {
return null;
}
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());
// 如果是搜索建议类型,且搜索关键词不为空,返回合并结果
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);
@ -275,6 +337,7 @@ public class NotesProvider extends ContentProvider {
// 插入data表获取插入的ID
insertedId = dataId = db.insert(TABLE.DATA, null, values);
break;
default:
// 未知Uri抛出异常
throw new IllegalArgumentException("Unknown URI " + uri);
@ -292,6 +355,8 @@ public class NotesProvider extends ContentProvider {
ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), null);
}
// 返回包含插入ID的新Uri
return ContentUris.withAppendedId(uri, insertedId);
}
@ -311,11 +376,12 @@ public class NotesProvider extends ContentProvider {
// 获取可写的SQLiteDatabase对象
SQLiteDatabase db = mHelper.getWritableDatabase();
boolean deleteData = false; // 标记是否删除的是data表数据
long noteId = 0; // 用于存储便签ID以便发送通知
// 根据Uri匹配的类型执行删除逻辑
switch (mMatcher.match(uri)) {
case URI_NOTE:
// 删除note表数据条件传入的selection + ID>0排除系统文件夹
// 直接删除便签条件传入的selection + ID>0排除系统文件夹
selection = "(" + selection + ") AND " + NoteColumns.ID + ">0 ";
count = db.delete(TABLE.NOTE, selection, selectionArgs);
break;
@ -325,13 +391,12 @@ public class NotesProvider extends ContentProvider {
/**
* ID0
*/
long noteId = Long.valueOf(id);
noteId = Long.valueOf(id);
if (noteId <= 0) {
break;
}
// 删除note表单条数据条件ID=id + 传入的selection
count = db.delete(TABLE.NOTE,
NoteColumns.ID + "=" + id + parseSelection(selection), selectionArgs);
// 直接删除便签条件ID=id + 传入的selection
count = db.delete(TABLE.NOTE, NoteColumns.ID + "=" + id + parseSelection(selection), selectionArgs);
break;
case URI_DATA:
// 删除data表数据
@ -345,6 +410,9 @@ public class NotesProvider extends ContentProvider {
DataColumns.ID + "=" + id + parseSelection(selection), selectionArgs);
deleteData = true;
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
@ -355,6 +423,11 @@ public class NotesProvider extends ContentProvider {
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);
}
@ -377,6 +450,7 @@ public class NotesProvider extends ContentProvider {
// 获取可写的SQLiteDatabase对象
SQLiteDatabase db = mHelper.getWritableDatabase();
boolean updateData = false; // 标记是否更新的是data表数据
long noteId = 0; // 用于存储便签ID以便发送通知
// 根据Uri匹配的类型执行更新逻辑
switch (mMatcher.match(uri)) {
@ -405,6 +479,9 @@ public class NotesProvider extends ContentProvider {
+ parseSelection(selection), selectionArgs);
updateData = true;
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
@ -415,6 +492,11 @@ public class NotesProvider extends ContentProvider {
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);
}

@ -37,33 +37,12 @@ import org.json.JSONObject;
import java.util.ArrayList;
/**
* <code>note</code><code>data</code>
* GTask/
* 1. CursornoteID<code>note</code><code>data</code>
* 2. <code>mDiffNoteValues</code>
* 3. JSON/JSONGTask
* 4. <code>data</code>
* 5. TYPE_NOTETYPE_FOLDERTYPE_SYSTEM
*
* @author MiCode Open Source Community
* @date 2010-2011
*/
public class SqlNote {
/**
* 使便
*/
private static final String TAG = SqlNote.class.getSimpleName();
/**
* IDnoteIDID
*/
private static final int INVALID_ID = -99999;
/**
* <code>note</code>Projection
* note
*/
public static final String[] PROJECTION_NOTE = new String[] {
NoteColumns.ID, NoteColumns.ALERTED_DATE, NoteColumns.BG_COLOR_ID,
NoteColumns.CREATED_DATE, NoteColumns.HAS_ATTACHMENT, NoteColumns.MODIFIED_DATE,
@ -73,193 +52,139 @@ public class SqlNote {
NoteColumns.VERSION
};
// ====================== PROJECTION_NOTE的列索引常量 ======================
/** PROJECTION_NOTE中ID列的索引对应NoteColumns.ID */
public static final int ID_COLUMN = 0;
/** PROJECTION_NOTE中提醒时间列的索引对应NoteColumns.ALERTED_DATE */
public static final int ALERTED_DATE_COLUMN = 1;
/** PROJECTION_NOTE中背景颜色ID列的索引对应NoteColumns.BG_COLOR_ID */
public static final int BG_COLOR_ID_COLUMN = 2;
/** PROJECTION_NOTE中创建时间列的索引对应NoteColumns.CREATED_DATE */
public static final int CREATED_DATE_COLUMN = 3;
/** PROJECTION_NOTE中是否有附件列的索引对应NoteColumns.HAS_ATTACHMENT */
public static final int HAS_ATTACHMENT_COLUMN = 4;
/** PROJECTION_NOTE中修改时间列的索引对应NoteColumns.MODIFIED_DATE */
public static final int MODIFIED_DATE_COLUMN = 5;
/** PROJECTION_NOTE中文件夹笔记数量列的索引对应NoteColumns.NOTES_COUNT */
public static final int NOTES_COUNT_COLUMN = 6;
/** PROJECTION_NOTE中父级ID列的索引对应NoteColumns.PARENT_ID */
public static final int PARENT_ID_COLUMN = 7;
/** PROJECTION_NOTE中摘要/名称列的索引对应NoteColumns.SNIPPET */
public static final int SNIPPET_COLUMN = 8;
/** PROJECTION_NOTE中类型列的索引对应NoteColumns.TYPE */
public static final int TYPE_COLUMN = 9;
/** PROJECTION_NOTE中小组件ID列的索引对应NoteColumns.WIDGET_ID */
public static final int WIDGET_ID_COLUMN = 10;
/** PROJECTION_NOTE中小组件类型列的索引对应NoteColumns.WIDGET_TYPE */
public static final int WIDGET_TYPE_COLUMN = 11;
/** PROJECTION_NOTE中同步ID列的索引对应NoteColumns.SYNC_ID */
public static final int SYNC_ID_COLUMN = 12;
/** PROJECTION_NOTE中本地修改标记列的索引对应NoteColumns.LOCAL_MODIFIED */
public static final int LOCAL_MODIFIED_COLUMN = 13;
/** PROJECTION_NOTE中原始父级ID列的索引对应NoteColumns.ORIGIN_PARENT_ID */
public static final int ORIGIN_PARENT_ID_COLUMN = 14;
/** PROJECTION_NOTE中GTask ID列的索引对应NoteColumns.GTASK_ID */
public static final int GTASK_ID_COLUMN = 15;
/** PROJECTION_NOTE中版本号列的索引对应NoteColumns.VERSION */
public static final int VERSION_COLUMN = 16;
// ====================== 成员变量 ======================
/** 上下文对象用于获取资源、ContentResolver等 */
private Context mContext;
/** Android内容解析器用于访问ContentProvider进行<code>note</code>/<code>data</code>表的增删改查 */
private ContentResolver mContentResolver;
/** 数据创建标记true表示新数据需插入数据库false表示已有数据需更新数据库 */
private boolean mIsCreate;
/** <code>note</code>表的主键ID关联到具体的行数据 */
private long mId;
/** 笔记的提醒时间戳(毫秒) */
private long mAlertDate;
/** 笔记/文件夹的背景颜色ID对应资源文件中的颜色配置 */
private int mBgColorId;
/** 数据创建时间戳(毫秒) */
private long mCreatedDate;
/** 是否有附件0表示无1表示有整型标记 */
private int mHasAttachment;
/** 数据最后修改时间戳(毫秒) */
private long mModifiedDate;
/** 父级ID关联到文件夹的note ID根文件夹为0 */
private long mParentId;
/** 摘要/名称:笔记的内容摘要、文件夹的名称 */
private String mSnippet;
/** 数据类型:{@link Notes#TYPE_NOTE}(笔记)、{TYPE_FOLDER}(文件夹)、{TYPE_SYSTEM}(系统文件夹) */
private int mType;
/** 关联的小组件ID无效时为{@link AppWidgetManager#INVALID_APPWIDGET_ID} */
private int mWidgetId;
/** 关联的小组件类型(无效时为{@link Notes#TYPE_WIDGET_INVALIDE} */
private int mWidgetType;
/** 原始父级ID用于记录文件夹移动前的原始父级支撑同步回滚 */
private long mOriginParent;
/** 版本号:用于同步时的版本验证,防止并发修改冲突 */
private long mVersion;
/** <code>note</code>表的差异数据容器,仅存储有变化的字段,用于提交到数据库 */
private ContentValues mDiffNoteValues;
/** 关联的<code>data</code>表数据列表(存储笔记的具体内容,如文本、通话记录等) */
private ArrayList<SqlData> mDataList;
// ====================== 构造方法 ======================
/**
* SqlNote/
*
*
* @param context
*/
public SqlNote(Context context) {
mContext = context;
mContentResolver = context.getContentResolver();
mIsCreate = true;
mId = INVALID_ID;
mAlertDate = 0;
mBgColorId = ResourceParser.getDefaultBgId(context); // 默认背景颜色
mCreatedDate = System.currentTimeMillis(); // 当前时间为创建时间
mBgColorId = ResourceParser.getDefaultBgId(context);
mCreatedDate = System.currentTimeMillis();
mHasAttachment = 0;
mModifiedDate = System.currentTimeMillis(); // 当前时间为修改时间
mModifiedDate = System.currentTimeMillis();
mParentId = 0;
mSnippet = "";
mType = Notes.TYPE_NOTE; // 默认类型为普通笔记
mWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID; // 无效小组件ID
mWidgetType = Notes.TYPE_WIDGET_INVALIDE; // 无效小组件类型
mType = Notes.TYPE_NOTE;
mWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
mWidgetType = Notes.TYPE_WIDGET_INVALIDE;
mOriginParent = 0;
mVersion = 0;
mDiffNoteValues = new ContentValues(); // 初始化差异容器
mDataList = new ArrayList<SqlData>(); // 初始化data数据列表
mDiffNoteValues = new ContentValues();
mDataList = new ArrayList<SqlData>();
}
/**
* Cursor<code>note</code>SqlNote
* <code>data</code>
*
* @param context
* @param c <code>note</code>Cursor使PROJECTION_NOTE
*/
public SqlNote(Context context, Cursor c) {
mContext = context;
mContentResolver = context.getContentResolver();
mIsCreate = false; // 标记为已有数据
loadFromCursor(c); // 从Cursor加载note表数据
mIsCreate = false;
loadFromCursor(c);
mDataList = new ArrayList<SqlData>();
if (mType == Notes.TYPE_NOTE) // 仅笔记类型加载关联的data表数据
if (mType == Notes.TYPE_NOTE)
loadDataContent();
mDiffNoteValues = new ContentValues(); // 初始化差异容器
mDiffNoteValues = new ContentValues();
}
/**
* <code>note</code>IDSqlNote
* IDCursordata
*
* @param context
* @param id <code>note</code>ID
*/
public SqlNote(Context context, long id) {
mContext = context;
mContentResolver = context.getContentResolver();
mIsCreate = false; // 标记为已有数据
loadFromCursor(id); // 根据ID加载note表数据
mIsCreate = false;
loadFromCursor(id);
mDataList = new ArrayList<SqlData>();
if (mType == Notes.TYPE_NOTE) // 仅笔记类型加载关联的data表数据
if (mType == Notes.TYPE_NOTE)
loadDataContent();
mDiffNoteValues = new ContentValues(); // 初始化差异容器
mDiffNoteValues = new ContentValues();
}
// ====================== 私有辅助方法 ======================
/**
* <code>note</code>IDCursor
* Cursor
*
* @param id <code>note</code>ID
*/
private void loadFromCursor(long id) {
Cursor c = null;
try {
// 查询note表根据ID获取单条数据
c = mContentResolver.query(Notes.CONTENT_NOTE_URI, PROJECTION_NOTE, "(_id=?)",
new String[] { String.valueOf(id) }, null);
new String[] {
String.valueOf(id)
}, null);
if (c != null) {
c.moveToNext(); // 移动到第一条数据(唯一结果)
loadFromCursor(c); // 加载数据到成员变量
c.moveToNext();
loadFromCursor(c);
} else {
Log.w(TAG, "loadFromCursor: cursor = null");
}
} finally {
// 最终关闭Cursor释放资源
if (c != null)
c.close();
}
}
/**
* Cursor<code>note</code>
* Cursor使PROJECTION_NOTE
*
* @param c <code>note</code>Cursor
*/
private void loadFromCursor(Cursor c) {
mId = c.getLong(ID_COLUMN);
mAlertDate = c.getLong(ALERTED_DATE_COLUMN);
@ -275,23 +200,19 @@ public class SqlNote {
mVersion = c.getLong(VERSION_COLUMN);
}
/**
* note<code>data</code>mDataList
* note IDdataSqlData
*/
private void loadDataContent() {
Cursor c = null;
mDataList.clear(); // 清空原有数据
mDataList.clear();
try {
// 查询data表根据note ID获取关联的所有数据
c = mContentResolver.query(Notes.CONTENT_DATA_URI, SqlData.PROJECTION_DATA,
"(note_id=?)", new String[] { String.valueOf(mId) }, null);
"(note_id=?)", new String[] {
String.valueOf(mId)
}, null);
if (c != null) {
if (c.getCount() == 0) {
Log.w(TAG, "it seems that the note has not data");
return;
}
// 遍历Cursor创建SqlData对象并添加到列表
while (c.moveToNext()) {
SqlData data = new SqlData(mContext, c);
mDataList.add(data);
@ -300,154 +221,137 @@ public class SqlNote {
Log.w(TAG, "loadDataContent: cursor = null");
}
} finally {
// 最终关闭Cursor释放资源
if (c != null)
c.close();
}
}
// ====================== 公共核心方法 ======================
/**
* JSON/
*
* -
* -
* - data
*
* @param js /JSON
* @return truefalseJSON
*/
public boolean setContent(JSONObject js) {
try {
// 获取JSON中的note核心数据GTask约定的字段名
JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
// 系统文件夹:不允许修改,输出警告
if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_SYSTEM) {
Log.w(TAG, "cannot set system folder");
}
// 普通文件夹:仅更新名称和类型
else if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_FOLDER) {
// 更新摘要/名称
String snippet = note.has(NoteColumns.SNIPPET) ? note.getString(NoteColumns.SNIPPET) : "";
} else if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_FOLDER) {
// for folder we can only update the snnipet and type
String snippet = note.has(NoteColumns.SNIPPET) ? note
.getString(NoteColumns.SNIPPET) : "";
if (mIsCreate || !mSnippet.equals(snippet)) {
mDiffNoteValues.put(NoteColumns.SNIPPET, snippet);
}
mSnippet = snippet;
// 更新类型
int type = note.has(NoteColumns.TYPE) ? note.getInt(NoteColumns.TYPE) : Notes.TYPE_NOTE;
int type = note.has(NoteColumns.TYPE) ? note.getInt(NoteColumns.TYPE)
: Notes.TYPE_NOTE;
if (mIsCreate || mType != type) {
mDiffNoteValues.put(NoteColumns.TYPE, type);
}
mType = type;
}
// 普通笔记更新所有字段并处理关联的data表数据
else if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_NOTE) {
// 获取JSON中的data数组笔记的具体内容
} else if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_NOTE) {
JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA);
// 1. 处理note表的各个字段记录差异
long id = note.has(NoteColumns.ID) ? note.getLong(NoteColumns.ID) : INVALID_ID;
if (mIsCreate || mId != id) {
mDiffNoteValues.put(NoteColumns.ID, id);
}
mId = id;
long alertDate = note.has(NoteColumns.ALERTED_DATE) ? note.getLong(NoteColumns.ALERTED_DATE) : 0;
long alertDate = note.has(NoteColumns.ALERTED_DATE) ? note
.getLong(NoteColumns.ALERTED_DATE) : 0;
if (mIsCreate || mAlertDate != alertDate) {
mDiffNoteValues.put(NoteColumns.ALERTED_DATE, alertDate);
}
mAlertDate = alertDate;
int bgColorId = note.has(NoteColumns.BG_COLOR_ID) ? note.getInt(NoteColumns.BG_COLOR_ID) : ResourceParser.getDefaultBgId(mContext);
int bgColorId = note.has(NoteColumns.BG_COLOR_ID) ? note
.getInt(NoteColumns.BG_COLOR_ID) : ResourceParser.getDefaultBgId(mContext);
if (mIsCreate || mBgColorId != bgColorId) {
mDiffNoteValues.put(NoteColumns.BG_COLOR_ID, bgColorId);
}
mBgColorId = bgColorId;
long createDate = note.has(NoteColumns.CREATED_DATE) ? note.getLong(NoteColumns.CREATED_DATE) : System.currentTimeMillis();
long createDate = note.has(NoteColumns.CREATED_DATE) ? note
.getLong(NoteColumns.CREATED_DATE) : System.currentTimeMillis();
if (mIsCreate || mCreatedDate != createDate) {
mDiffNoteValues.put(NoteColumns.CREATED_DATE, createDate);
}
mCreatedDate = createDate;
int hasAttachment = note.has(NoteColumns.HAS_ATTACHMENT) ? note.getInt(NoteColumns.HAS_ATTACHMENT) : 0;
int hasAttachment = note.has(NoteColumns.HAS_ATTACHMENT) ? note
.getInt(NoteColumns.HAS_ATTACHMENT) : 0;
if (mIsCreate || mHasAttachment != hasAttachment) {
mDiffNoteValues.put(NoteColumns.HAS_ATTACHMENT, hasAttachment);
}
mHasAttachment = hasAttachment;
long modifiedDate = note.has(NoteColumns.MODIFIED_DATE) ? note.getLong(NoteColumns.MODIFIED_DATE) : System.currentTimeMillis();
long modifiedDate = note.has(NoteColumns.MODIFIED_DATE) ? note
.getLong(NoteColumns.MODIFIED_DATE) : System.currentTimeMillis();
if (mIsCreate || mModifiedDate != modifiedDate) {
mDiffNoteValues.put(NoteColumns.MODIFIED_DATE, modifiedDate);
}
mModifiedDate = modifiedDate;
long parentId = note.has(NoteColumns.PARENT_ID) ? note.getLong(NoteColumns.PARENT_ID) : 0;
long parentId = note.has(NoteColumns.PARENT_ID) ? note
.getLong(NoteColumns.PARENT_ID) : 0;
if (mIsCreate || mParentId != parentId) {
mDiffNoteValues.put(NoteColumns.PARENT_ID, parentId);
}
mParentId = parentId;
String snippet = note.has(NoteColumns.SNIPPET) ? note.getString(NoteColumns.SNIPPET) : "";
String snippet = note.has(NoteColumns.SNIPPET) ? note
.getString(NoteColumns.SNIPPET) : "";
if (mIsCreate || !mSnippet.equals(snippet)) {
mDiffNoteValues.put(NoteColumns.SNIPPET, snippet);
}
mSnippet = snippet;
int type = note.has(NoteColumns.TYPE) ? note.getInt(NoteColumns.TYPE) : Notes.TYPE_NOTE;
int type = note.has(NoteColumns.TYPE) ? note.getInt(NoteColumns.TYPE)
: Notes.TYPE_NOTE;
if (mIsCreate || mType != type) {
mDiffNoteValues.put(NoteColumns.TYPE, type);
}
mType = type;
int widgetId = note.has(NoteColumns.WIDGET_ID) ? note.getInt(NoteColumns.WIDGET_ID) : AppWidgetManager.INVALID_APPWIDGET_ID;
int widgetId = note.has(NoteColumns.WIDGET_ID) ? note.getInt(NoteColumns.WIDGET_ID)
: AppWidgetManager.INVALID_APPWIDGET_ID;
if (mIsCreate || mWidgetId != widgetId) {
mDiffNoteValues.put(NoteColumns.WIDGET_ID, widgetId);
}
mWidgetId = widgetId;
int widgetType = note.has(NoteColumns.WIDGET_TYPE) ? note.getInt(NoteColumns.WIDGET_TYPE) : Notes.TYPE_WIDGET_INVALIDE;
int widgetType = note.has(NoteColumns.WIDGET_TYPE) ? note
.getInt(NoteColumns.WIDGET_TYPE) : Notes.TYPE_WIDGET_INVALIDE;
if (mIsCreate || mWidgetType != widgetType) {
mDiffNoteValues.put(NoteColumns.WIDGET_TYPE, widgetType);
}
mWidgetType = widgetType;
long originParent = note.has(NoteColumns.ORIGIN_PARENT_ID) ? note.getLong(NoteColumns.ORIGIN_PARENT_ID) : 0;
long originParent = note.has(NoteColumns.ORIGIN_PARENT_ID) ? note
.getLong(NoteColumns.ORIGIN_PARENT_ID) : 0;
if (mIsCreate || mOriginParent != originParent) {
mDiffNoteValues.put(NoteColumns.ORIGIN_PARENT_ID, originParent);
}
mOriginParent = originParent;
// 2. 处理关联的data表数据
for (int i = 0; i < dataArray.length(); i++) {
JSONObject data = dataArray.getJSONObject(i);
SqlData sqlData = null;
// 根据data ID查找已有SqlData对象更新场景
if (data.has(DataColumns.ID)) {
long dataId = data.getLong(DataColumns.ID);
for (SqlData temp : mDataList) {
if (dataId == temp.getId()) {
sqlData = temp;
break;
}
}
}
// 未找到则创建新SqlData对象新增场景
if (sqlData == null) {
sqlData = new SqlData(mContext);
mDataList.add(sqlData);
}
// 设置data内容并记录差异
sqlData.setContent(data);
}
}
} catch (JSONException e) {
// JSON解析失败输出错误日志并返回false
Log.e(TAG, e.toString());
e.printStackTrace();
return false;
@ -455,26 +359,16 @@ public class SqlNote {
return true;
}
/**
* /dataJSON
*
* - note + data
* - /ID
*
* @return JSONnull
*/
public JSONObject getContent() {
try {
JSONObject js = new JSONObject();
// 新数据尚未持久化输出错误日志并返回null
if (mIsCreate) {
Log.e(TAG, "it seems that we haven't created this in database yet");
return null;
}
JSONObject note = new JSONObject();
// 普通笔记:序列化所有字段 + 关联的data数组
if (mType == Notes.TYPE_NOTE) {
note.put(NoteColumns.ID, mId);
note.put(NoteColumns.ALERTED_DATE, mAlertDate);
@ -490,7 +384,6 @@ public class SqlNote {
note.put(NoteColumns.ORIGIN_PARENT_ID, mOriginParent);
js.put(GTaskStringUtils.META_HEAD_NOTE, note);
// 序列化关联的data表数据为JSON数组
JSONArray dataArray = new JSONArray();
for (SqlData sqlData : mDataList) {
JSONObject data = sqlData.getContent();
@ -499,9 +392,7 @@ public class SqlNote {
}
}
js.put(GTaskStringUtils.META_HEAD_DATA, dataArray);
}
// 文件夹/系统文件夹:仅序列化核心字段
else if (mType == Notes.TYPE_FOLDER || mType == Notes.TYPE_SYSTEM) {
} else if (mType == Notes.TYPE_FOLDER || mType == Notes.TYPE_SYSTEM) {
note.put(NoteColumns.ID, mId);
note.put(NoteColumns.TYPE, mType);
note.put(NoteColumns.SNIPPET, mSnippet);
@ -510,145 +401,92 @@ public class SqlNote {
return js;
} catch (JSONException e) {
// JSON序列化失败输出错误日志
Log.e(TAG, e.toString());
e.printStackTrace();
}
return null;
}
// ====================== 字段设置方法(记录差异) ======================
/**
* ID
* @param id IDnote ID
*/
public void setParentId(long id) {
mParentId = id;
mDiffNoteValues.put(NoteColumns.PARENT_ID, id);
}
/**
* GTask ID
* @param gid GTaskID
*/
public void setGtaskId(String gid) {
mDiffNoteValues.put(NoteColumns.GTASK_ID, gid);
}
/**
* ID
* @param syncId ID
*/
public void setSyncId(long syncId) {
mDiffNoteValues.put(NoteColumns.SYNC_ID, syncId);
}
/**
* 0
*
*/
public void resetLocalModified() {
mDiffNoteValues.put(NoteColumns.LOCAL_MODIFIED, 0);
}
// ====================== 字段获取方法 ======================
/**
* <code>note</code>ID
* @return IDINVALID_ID
*/
public long getId() {
return mId;
}
/**
* ID
* @return note ID
*/
public long getParentId() {
return mParentId;
}
/**
* /
* @return
*/
public String getSnippet() {
return mSnippet;
}
/**
*
* @return trueTYPE_NOTEfalse
*/
public boolean isNoteType() {
return mType == Notes.TYPE_NOTE;
}
/**
* /<code>data</code>
*
*
* @param validateVersion truefalse
*/
public void commit(boolean validateVersion) {
// 新数据:执行插入操作
if (mIsCreate) {
// 若ID为无效值且差异容器中包含ID移除该ID数据库自增ID无需手动设置
if (mId == INVALID_ID && mDiffNoteValues.containsKey(NoteColumns.ID)) {
mDiffNoteValues.remove(NoteColumns.ID);
}
// 插入note表数据获取返回的Uri包含新数据的ID
Uri uri = mContentResolver.insert(Notes.CONTENT_NOTE_URI, mDiffNoteValues);
try {
// 从Uri中解析出新数据的IDUri路径分段的第二个元素如note/123中的123
mId = Long.valueOf(uri.getPathSegments().get(1));
} catch (NumberFormatException e) {
// 解析ID失败输出错误日志并抛出同步失败异常
Log.e(TAG, "Get note id error :" + e.toString());
throw new ActionFailureException("create note failed");
}
// ID为0表示创建失败抛出异常
if (mId == 0) {
throw new IllegalStateException("Create thread id failed");
}
// 笔记类型联动提交关联的data表数据
if (mType == Notes.TYPE_NOTE) {
for (SqlData sqlData : mDataList) {
sqlData.commit(mId, false, -1); // 无需版本验证
sqlData.commit(mId, false, -1);
}
}
}
// 已有数据:执行更新操作
else {
// 验证ID有效性排除系统文件夹的有效ID
} else {
if (mId <= 0 && mId != Notes.ID_ROOT_FOLDER && mId != Notes.ID_CALL_RECORD_FOLDER) {
Log.e(TAG, "No such note");
throw new IllegalStateException("Try to update note with invalid id");
}
// 仅当存在差异时执行更新操作
if (mDiffNoteValues.size() > 0) {
mVersion ++; // 版本号自增
mVersion ++;
int result = 0;
if (!validateVersion) {
// 不验证版本直接更新数据根据note ID
result = mContentResolver.update(Notes.CONTENT_NOTE_URI, mDiffNoteValues, "("
+ NoteColumns.ID + "=?)", new String[] { String.valueOf(mId) });
+ NoteColumns.ID + "=?)", new String[] {
String.valueOf(mId)
});
} else {
// 验证版本仅当note表的版本号小于等于当前版本时才更新
result = mContentResolver.update(Notes.CONTENT_NOTE_URI, mDiffNoteValues, "("
+ NoteColumns.ID + "=?) AND (" + NoteColumns.VERSION + "<=?)",
new String[] { String.valueOf(mId), String.valueOf(mVersion) });
+ NoteColumns.ID + "=?) AND (" + NoteColumns.VERSION + "<=?)",
new String[] {
String.valueOf(mId), String.valueOf(mVersion)
});
}
// 更新结果为0说明数据未更新可能同步时用户修改了数据输出警告日志
if (result == 0) {
Log.w(TAG, "there is no update. maybe user updates note when syncing");
}
}
// 笔记类型联动提交关联的data表数据支持版本验证
if (mType == Notes.TYPE_NOTE) {
for (SqlData sqlData : mDataList) {
sqlData.commit(mId, validateVersion, mVersion);
@ -656,13 +494,12 @@ public class SqlNote {
}
}
// 提交后刷新本地数据重新加载note和关联的data表数据
// refresh local info
loadFromCursor(mId);
if (mType == Notes.TYPE_NOTE)
loadDataContent();
// 清空差异容器,标记为已有数据
mDiffNoteValues.clear();
mIsCreate = false;
}
}
}

@ -0,0 +1,351 @@
/*
* 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.gtask.data;
import android.database.Cursor;
import android.text.TextUtils;
import android.util.Log;
import net.micode.notes.data.Notes;
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.gtask.exception.ActionFailureException;
import net.micode.notes.tool.GTaskStringUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
public class Task extends Node {
private static final String TAG = Task.class.getSimpleName();
private boolean mCompleted;
private String mNotes;
private JSONObject mMetaInfo;
private Task mPriorSibling;
private TaskList mParent;
public Task() {
super();
mCompleted = false;
mNotes = null;
mPriorSibling = null;
mParent = null;
mMetaInfo = null;
}
public JSONObject getCreateAction(int actionId) {
JSONObject js = new JSONObject();
try {
// action_type
js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE);
// action_id
js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId);
// index
js.put(GTaskStringUtils.GTASK_JSON_INDEX, mParent.getChildTaskIndex(this));
// entity_delta
JSONObject entity = new JSONObject();
entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName());
entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null");
entity.put(GTaskStringUtils.GTASK_JSON_ENTITY_TYPE,
GTaskStringUtils.GTASK_JSON_TYPE_TASK);
if (getNotes() != null) {
entity.put(GTaskStringUtils.GTASK_JSON_NOTES, getNotes());
}
js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity);
// parent_id
js.put(GTaskStringUtils.GTASK_JSON_PARENT_ID, mParent.getGid());
// dest_parent_type
js.put(GTaskStringUtils.GTASK_JSON_DEST_PARENT_TYPE,
GTaskStringUtils.GTASK_JSON_TYPE_GROUP);
// list_id
js.put(GTaskStringUtils.GTASK_JSON_LIST_ID, mParent.getGid());
// prior_sibling_id
if (mPriorSibling != null) {
js.put(GTaskStringUtils.GTASK_JSON_PRIOR_SIBLING_ID, mPriorSibling.getGid());
}
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to generate task-create jsonobject");
}
return js;
}
public JSONObject getUpdateAction(int actionId) {
JSONObject js = new JSONObject();
try {
// action_type
js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE);
// action_id
js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId);
// id
js.put(GTaskStringUtils.GTASK_JSON_ID, getGid());
// entity_delta
JSONObject entity = new JSONObject();
entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName());
if (getNotes() != null) {
entity.put(GTaskStringUtils.GTASK_JSON_NOTES, getNotes());
}
entity.put(GTaskStringUtils.GTASK_JSON_DELETED, getDeleted());
js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity);
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to generate task-update jsonobject");
}
return js;
}
public void setContentByRemoteJSON(JSONObject js) {
if (js != null) {
try {
// id
if (js.has(GTaskStringUtils.GTASK_JSON_ID)) {
setGid(js.getString(GTaskStringUtils.GTASK_JSON_ID));
}
// last_modified
if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) {
setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED));
}
// name
if (js.has(GTaskStringUtils.GTASK_JSON_NAME)) {
setName(js.getString(GTaskStringUtils.GTASK_JSON_NAME));
}
// notes
if (js.has(GTaskStringUtils.GTASK_JSON_NOTES)) {
setNotes(js.getString(GTaskStringUtils.GTASK_JSON_NOTES));
}
// deleted
if (js.has(GTaskStringUtils.GTASK_JSON_DELETED)) {
setDeleted(js.getBoolean(GTaskStringUtils.GTASK_JSON_DELETED));
}
// completed
if (js.has(GTaskStringUtils.GTASK_JSON_COMPLETED)) {
setCompleted(js.getBoolean(GTaskStringUtils.GTASK_JSON_COMPLETED));
}
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to get task content from jsonobject");
}
}
}
public void setContentByLocalJSON(JSONObject js) {
if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE)
|| !js.has(GTaskStringUtils.META_HEAD_DATA)) {
Log.w(TAG, "setContentByLocalJSON: nothing is avaiable");
}
try {
JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA);
if (note.getInt(NoteColumns.TYPE) != Notes.TYPE_NOTE) {
Log.e(TAG, "invalid type");
return;
}
for (int i = 0; i < dataArray.length(); i++) {
JSONObject data = dataArray.getJSONObject(i);
if (TextUtils.equals(data.getString(DataColumns.MIME_TYPE), DataConstants.NOTE)) {
setName(data.getString(DataColumns.CONTENT));
break;
}
}
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
}
}
public JSONObject getLocalJSONFromContent() {
String name = getName();
try {
if (mMetaInfo == null) {
// new task created from web
if (name == null) {
Log.w(TAG, "the note seems to be an empty one");
return null;
}
JSONObject js = new JSONObject();
JSONObject note = new JSONObject();
JSONArray dataArray = new JSONArray();
JSONObject data = new JSONObject();
data.put(DataColumns.CONTENT, name);
dataArray.put(data);
js.put(GTaskStringUtils.META_HEAD_DATA, dataArray);
note.put(NoteColumns.TYPE, Notes.TYPE_NOTE);
js.put(GTaskStringUtils.META_HEAD_NOTE, note);
return js;
} else {
// synced task
JSONObject note = mMetaInfo.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
JSONArray dataArray = mMetaInfo.getJSONArray(GTaskStringUtils.META_HEAD_DATA);
for (int i = 0; i < dataArray.length(); i++) {
JSONObject data = dataArray.getJSONObject(i);
if (TextUtils.equals(data.getString(DataColumns.MIME_TYPE), DataConstants.NOTE)) {
data.put(DataColumns.CONTENT, getName());
break;
}
}
note.put(NoteColumns.TYPE, Notes.TYPE_NOTE);
return mMetaInfo;
}
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
return null;
}
}
public void setMetaInfo(MetaData metaData) {
if (metaData != null && metaData.getNotes() != null) {
try {
mMetaInfo = new JSONObject(metaData.getNotes());
} catch (JSONException e) {
Log.w(TAG, e.toString());
mMetaInfo = null;
}
}
}
public int getSyncAction(Cursor c) {
try {
JSONObject noteInfo = null;
if (mMetaInfo != null && mMetaInfo.has(GTaskStringUtils.META_HEAD_NOTE)) {
noteInfo = mMetaInfo.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
}
if (noteInfo == null) {
Log.w(TAG, "it seems that note meta has been deleted");
return SYNC_ACTION_UPDATE_REMOTE;
}
if (!noteInfo.has(NoteColumns.ID)) {
Log.w(TAG, "remote note id seems to be deleted");
return SYNC_ACTION_UPDATE_LOCAL;
}
// validate the note id now
if (c.getLong(SqlNote.ID_COLUMN) != noteInfo.getLong(NoteColumns.ID)) {
Log.w(TAG, "note id doesn't match");
return SYNC_ACTION_UPDATE_LOCAL;
}
if (c.getInt(SqlNote.LOCAL_MODIFIED_COLUMN) == 0) {
// there is no local update
if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) {
// no update both side
return SYNC_ACTION_NONE;
} else {
// apply remote to local
return SYNC_ACTION_UPDATE_LOCAL;
}
} else {
// validate gtask id
if (!c.getString(SqlNote.GTASK_ID_COLUMN).equals(getGid())) {
Log.e(TAG, "gtask id doesn't match");
return SYNC_ACTION_ERROR;
}
if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) {
// local modification only
return SYNC_ACTION_UPDATE_REMOTE;
} else {
return SYNC_ACTION_UPDATE_CONFLICT;
}
}
} catch (Exception e) {
Log.e(TAG, e.toString());
e.printStackTrace();
}
return SYNC_ACTION_ERROR;
}
public boolean isWorthSaving() {
return mMetaInfo != null || (getName() != null && getName().trim().length() > 0)
|| (getNotes() != null && getNotes().trim().length() > 0);
}
public void setCompleted(boolean completed) {
this.mCompleted = completed;
}
public void setNotes(String notes) {
this.mNotes = notes;
}
public void setPriorSibling(Task priorSibling) {
this.mPriorSibling = priorSibling;
}
public void setParent(TaskList parent) {
this.mParent = parent;
}
public boolean getCompleted() {
return this.mCompleted;
}
public String getNotes() {
return this.mNotes;
}
public Task getPriorSibling() {
return this.mPriorSibling;
}
public TaskList getParent() {
return this.mParent;
}
}

@ -0,0 +1,343 @@
/*
* 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.gtask.data;
import android.database.Cursor;
import android.util.Log;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.gtask.exception.ActionFailureException;
import net.micode.notes.tool.GTaskStringUtils;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
public class TaskList extends Node {
private static final String TAG = TaskList.class.getSimpleName();
private int mIndex;
private ArrayList<Task> mChildren;
public TaskList() {
super();
mChildren = new ArrayList<Task>();
mIndex = 1;
}
public JSONObject getCreateAction(int actionId) {
JSONObject js = new JSONObject();
try {
// action_type
js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE);
// action_id
js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId);
// index
js.put(GTaskStringUtils.GTASK_JSON_INDEX, mIndex);
// entity_delta
JSONObject entity = new JSONObject();
entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName());
entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null");
entity.put(GTaskStringUtils.GTASK_JSON_ENTITY_TYPE,
GTaskStringUtils.GTASK_JSON_TYPE_GROUP);
js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity);
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to generate tasklist-create jsonobject");
}
return js;
}
public JSONObject getUpdateAction(int actionId) {
JSONObject js = new JSONObject();
try {
// action_type
js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE);
// action_id
js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId);
// id
js.put(GTaskStringUtils.GTASK_JSON_ID, getGid());
// entity_delta
JSONObject entity = new JSONObject();
entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName());
entity.put(GTaskStringUtils.GTASK_JSON_DELETED, getDeleted());
js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity);
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to generate tasklist-update jsonobject");
}
return js;
}
public void setContentByRemoteJSON(JSONObject js) {
if (js != null) {
try {
// id
if (js.has(GTaskStringUtils.GTASK_JSON_ID)) {
setGid(js.getString(GTaskStringUtils.GTASK_JSON_ID));
}
// last_modified
if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) {
setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED));
}
// name
if (js.has(GTaskStringUtils.GTASK_JSON_NAME)) {
setName(js.getString(GTaskStringUtils.GTASK_JSON_NAME));
}
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to get tasklist content from jsonobject");
}
}
}
public void setContentByLocalJSON(JSONObject js) {
if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE)) {
Log.w(TAG, "setContentByLocalJSON: nothing is avaiable");
}
try {
JSONObject folder = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
if (folder.getInt(NoteColumns.TYPE) == Notes.TYPE_FOLDER) {
String name = folder.getString(NoteColumns.SNIPPET);
setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + name);
} else if (folder.getInt(NoteColumns.TYPE) == Notes.TYPE_SYSTEM) {
if (folder.getLong(NoteColumns.ID) == Notes.ID_ROOT_FOLDER)
setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT);
else if (folder.getLong(NoteColumns.ID) == Notes.ID_CALL_RECORD_FOLDER)
setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX
+ GTaskStringUtils.FOLDER_CALL_NOTE);
else
Log.e(TAG, "invalid system folder");
} else {
Log.e(TAG, "error type");
}
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
}
}
public JSONObject getLocalJSONFromContent() {
try {
JSONObject js = new JSONObject();
JSONObject folder = new JSONObject();
String folderName = getName();
if (getName().startsWith(GTaskStringUtils.MIUI_FOLDER_PREFFIX))
folderName = folderName.substring(GTaskStringUtils.MIUI_FOLDER_PREFFIX.length(),
folderName.length());
folder.put(NoteColumns.SNIPPET, folderName);
if (folderName.equals(GTaskStringUtils.FOLDER_DEFAULT)
|| folderName.equals(GTaskStringUtils.FOLDER_CALL_NOTE))
folder.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
else
folder.put(NoteColumns.TYPE, Notes.TYPE_FOLDER);
js.put(GTaskStringUtils.META_HEAD_NOTE, folder);
return js;
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
return null;
}
}
public int getSyncAction(Cursor c) {
try {
if (c.getInt(SqlNote.LOCAL_MODIFIED_COLUMN) == 0) {
// there is no local update
if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) {
// no update both side
return SYNC_ACTION_NONE;
} else {
// apply remote to local
return SYNC_ACTION_UPDATE_LOCAL;
}
} else {
// validate gtask id
if (!c.getString(SqlNote.GTASK_ID_COLUMN).equals(getGid())) {
Log.e(TAG, "gtask id doesn't match");
return SYNC_ACTION_ERROR;
}
if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) {
// local modification only
return SYNC_ACTION_UPDATE_REMOTE;
} else {
// for folder conflicts, just apply local modification
return SYNC_ACTION_UPDATE_REMOTE;
}
}
} catch (Exception e) {
Log.e(TAG, e.toString());
e.printStackTrace();
}
return SYNC_ACTION_ERROR;
}
public int getChildTaskCount() {
return mChildren.size();
}
public boolean addChildTask(Task task) {
boolean ret = false;
if (task != null && !mChildren.contains(task)) {
ret = mChildren.add(task);
if (ret) {
// need to set prior sibling and parent
task.setPriorSibling(mChildren.isEmpty() ? null : mChildren
.get(mChildren.size() - 1));
task.setParent(this);
}
}
return ret;
}
public boolean addChildTask(Task task, int index) {
if (index < 0 || index > mChildren.size()) {
Log.e(TAG, "add child task: invalid index");
return false;
}
int pos = mChildren.indexOf(task);
if (task != null && pos == -1) {
mChildren.add(index, task);
// update the task list
Task preTask = null;
Task afterTask = null;
if (index != 0)
preTask = mChildren.get(index - 1);
if (index != mChildren.size() - 1)
afterTask = mChildren.get(index + 1);
task.setPriorSibling(preTask);
if (afterTask != null)
afterTask.setPriorSibling(task);
}
return true;
}
public boolean removeChildTask(Task task) {
boolean ret = false;
int index = mChildren.indexOf(task);
if (index != -1) {
ret = mChildren.remove(task);
if (ret) {
// reset prior sibling and parent
task.setPriorSibling(null);
task.setParent(null);
// update the task list
if (index != mChildren.size()) {
mChildren.get(index).setPriorSibling(
index == 0 ? null : mChildren.get(index - 1));
}
}
}
return ret;
}
public boolean moveChildTask(Task task, int index) {
if (index < 0 || index >= mChildren.size()) {
Log.e(TAG, "move child task: invalid index");
return false;
}
int pos = mChildren.indexOf(task);
if (pos == -1) {
Log.e(TAG, "move child task: the task should in the list");
return false;
}
if (pos == index)
return true;
return (removeChildTask(task) && addChildTask(task, index));
}
public Task findChildTaskByGid(String gid) {
for (int i = 0; i < mChildren.size(); i++) {
Task t = mChildren.get(i);
if (t.getGid().equals(gid)) {
return t;
}
}
return null;
}
public int getChildTaskIndex(Task task) {
return mChildren.indexOf(task);
}
public Task getChildTaskByIndex(int index) {
if (index < 0 || index >= mChildren.size()) {
Log.e(TAG, "getTaskByIndex: invalid index");
return null;
}
return mChildren.get(index);
}
public Task getChilTaskByGid(String gid) {
for (Task task : mChildren) {
if (task.getGid().equals(gid))
return task;
}
return null;
}
public ArrayList<Task> getChildTaskList() {
return this.mChildren;
}
public void setIndex(int index) {
this.mIndex = index;
}
public int getIndex() {
return this.mIndex;
}
}

@ -0,0 +1,33 @@
/*
* 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.gtask.exception;
public class ActionFailureException extends RuntimeException {
private static final long serialVersionUID = 4425249765923293627L;
public ActionFailureException() {
super();
}
public ActionFailureException(String paramString) {
super(paramString);
}
public ActionFailureException(String paramString, Throwable paramThrowable) {
super(paramString, paramThrowable);
}
}

@ -0,0 +1,33 @@
/*
* 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.gtask.exception;
public class NetworkFailureException extends Exception {
private static final long serialVersionUID = 2107610287180234136L;
public NetworkFailureException() {
super();
}
public NetworkFailureException(String paramString) {
super(paramString);
}
public NetworkFailureException(String paramString, Throwable paramThrowable) {
super(paramString, paramThrowable);
}
}

@ -0,0 +1,151 @@
/*
* 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.gtask.remote;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Build;
import androidx.core.app.NotificationCompat;
import net.micode.notes.R;
import net.micode.notes.ui.NotesListActivity;
import net.micode.notes.ui.NotesPreferenceActivity;
public class GTaskASyncTask extends AsyncTask<Void, String, Integer> {
private static int GTASK_SYNC_NOTIFICATION_ID = 5234235;
private static final String CHANNEL_ID = "gtask_sync_channel"; // 新增通知渠道ID
public interface OnCompleteListener {
void onComplete();
}
private Context mContext;
private NotificationManager mNotifiManager;
private GTaskManager mTaskManager;
private OnCompleteListener mOnCompleteListener;
public GTaskASyncTask(Context context, OnCompleteListener listener) {
mContext = context;
mOnCompleteListener = listener;
mNotifiManager = (NotificationManager) mContext
.getSystemService(Context.NOTIFICATION_SERVICE);
mTaskManager = GTaskManager.getInstance();
// 初始化通知渠道仅Android 8.0+需要)
createNotificationChannel();
}
// 新增:创建通知渠道
private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
CharSequence name = mContext.getString(R.string.app_name); // 渠道名称
String description = "GTask同步通知"; // 渠道描述
int importance = NotificationManager.IMPORTANCE_DEFAULT;
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance);
channel.setDescription(description);
// 向系统注册渠道
if (mNotifiManager != null) {
mNotifiManager.createNotificationChannel(channel);
}
}
}
public void cancelSync() {
mTaskManager.cancelSync();
}
public void publishProgess(String message) {
publishProgress(new String[] {
message
});
}
private void showNotification(int tickerId, String content) {
// 替换为NotificationCompat.Builder构建通知
NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext, CHANNEL_ID)
.setSmallIcon(R.drawable.notification) // 保持原图标
.setContentTitle(mContext.getString(R.string.app_name)) // 原标题
.setContentText(content) // 原内容
.setTicker(mContext.getString(tickerId)) // 原滚动提示文字
.setWhen(System.currentTimeMillis()) // 原时间戳
.setDefaults(Notification.DEFAULT_LIGHTS) // 保持原灯光效果
.setAutoCancel(true); // 保持点击自动取消
// 设置跳转意图(保持原逻辑)
PendingIntent pendingIntent;
if (tickerId != R.string.ticker_success) {
pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext,
NotesPreferenceActivity.class), PendingIntent.FLAG_IMMUTABLE); // 适配高版本
} else {
pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext,
NotesListActivity.class), PendingIntent.FLAG_IMMUTABLE); // 适配高版本
}
builder.setContentIntent(pendingIntent);
// 发送通知保持原ID
mNotifiManager.notify(GTASK_SYNC_NOTIFICATION_ID, builder.build());
}
@Override
protected Integer doInBackground(Void... unused) {
publishProgess(mContext.getString(R.string.sync_progress_login, NotesPreferenceActivity
.getSyncAccountName(mContext)));
return mTaskManager.sync(mContext, this);
}
@Override
protected void onProgressUpdate(String... progress) {
showNotification(R.string.ticker_syncing, progress[0]);
if (mContext instanceof GTaskSyncService) {
((GTaskSyncService) mContext).sendBroadcast(progress[0]);
}
}
@Override
protected void onPostExecute(Integer result) {
if (result == GTaskManager.STATE_SUCCESS) {
showNotification(R.string.ticker_success, mContext.getString(
R.string.success_sync_account, mTaskManager.getSyncAccount()));
NotesPreferenceActivity.setLastSyncTime(mContext, System.currentTimeMillis());
} else if (result == GTaskManager.STATE_NETWORK_ERROR) {
showNotification(R.string.ticker_fail, mContext.getString(R.string.error_sync_network));
} else if (result == GTaskManager.STATE_INTERNAL_ERROR) {
showNotification(R.string.ticker_fail, mContext.getString(R.string.error_sync_internal));
} else if (result == GTaskManager.STATE_SYNC_CANCELLED) {
showNotification(R.string.ticker_cancel, mContext
.getString(R.string.error_sync_cancelled));
}
if (mOnCompleteListener != null) {
new Thread(new Runnable() {
public void run() {
mOnCompleteListener.onComplete();
}
}).start();
}
}
}

@ -60,117 +60,48 @@ import java.util.zip.GZIPInputStream;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
/**
* GTask****Google TasksGTask
* GTask
* 1. GoogleGoogleAuthTokenToken
* 2. GTaskAuthTokenGTaskCookieclient_version
* 3. HTTPGET/POSTGzip/DeflateJSON
* 4. GTaskTask/TaskList
* 5. 10
*
* Apache HttpClientAndroid
* {@link NetworkFailureException}{@link ActionFailureException}
*
* @author MiCode Open Source Community
* @date 2010-2011
*/
public class GTaskClient {
/**
* 使便
*/
private static final String TAG = GTaskClient.class.getSimpleName();
// ====================== GTask服务URL常量 ======================
/**
* GTaskURL
*/
private static final String GTASK_URL = "https://mail.google.com/tasks/";
/**
* GTaskGETURL
*/
private static final String GTASK_GET_URL = "https://mail.google.com/tasks/ig";
/**
* GTaskPOSTURL
*/
private static final String GTASK_POST_URL = "https://mail.google.com/tasks/r/ig";
// ====================== 单例模式相关 ======================
/**
* GTaskClient{@link #getInstance()}
*/
private static GTaskClient mInstance = null;
// ====================== 网络请求相关成员变量 ======================
/**
* Apache HttpClientHTTPCookie
*/
private DefaultHttpClient mHttpClient;
/**
* GTask GETURL
*/
private String mGetUrl;
/**
* GTask POSTURL
*/
private String mPostUrl;
// ====================== GTask服务认证/版本相关 ======================
/**
* GTaskGTaskJSON
*/
private long mClientVersion;
/**
* trueGTaskfalse/
*/
private boolean mLoggedin;
/**
* 5
*/
private long mLastLoginTime;
/**
* IDGTask
*/
private int mActionId;
/**
* Google
*/
private Account mAccount;
// ====================== 批量更新相关 ======================
/**
* Task/TaskListupdateAction
*/
private JSONArray mUpdateArray;
/**
* GTaskClient
* URL
*/
private GTaskClient() {
mHttpClient = null;
mGetUrl = GTASK_GET_URL; // 默认使用官方GET URL
mPostUrl = GTASK_POST_URL; // 默认使用官方POST URL
mClientVersion = -1; // 初始化为无效版本号
mLoggedin = false; // 初始未登录
mLastLoginTime = 0; // 初始无登录时间
mActionId = 1; // 动作ID从1开始自增
mAccount = null; // 初始无账户
mUpdateArray = null; // 初始无批量更新动作
mGetUrl = GTASK_GET_URL;
mPostUrl = GTASK_POST_URL;
mClientVersion = -1;
mLoggedin = false;
mLastLoginTime = 0;
mActionId = 1;
mAccount = null;
mUpdateArray = null;
}
/**
* GTaskClient线
* @return GTaskClient
*/
public static synchronized GTaskClient getInstance() {
if (mInstance == null) {
mInstance = new GTaskClient();
@ -178,59 +109,49 @@ public class GTaskClient {
return mInstance;
}
/**
* GTask
*
* 1. 5
* 2. GoogleAuthTokenAccountManagerToken
* 3. GTaskURLGmail使URL
* @param activity ActivityAccountManagerToken
* @return truefalse
*/
public boolean login(Activity activity) {
// 步骤1判断登录有效期5分钟超时则标记为未登录
final long interval = 1000 * 60 * 5; // 5分钟毫秒数
// we suppose that the cookie would expire after 5 minutes
// then we need to re-login
final long interval = 1000 * 60 * 5;
if (mLastLoginTime + interval < System.currentTimeMillis()) {
mLoggedin = false;
}
// 步骤2判断账户是否切换切换则标记为未登录
if (mLoggedin && !TextUtils.equals(getSyncAccount().name, NotesPreferenceActivity.getSyncAccountName(activity))) {
// need to re-login after account switch
if (mLoggedin
&& !TextUtils.equals(getSyncAccount().name, NotesPreferenceActivity
.getSyncAccountName(activity))) {
mLoggedin = false;
}
// 步骤3已登录则直接返回成功
if (mLoggedin) {
Log.d(TAG, "already logged in");
return true;
}
// 步骤4记录本次登录时间开始新的登录流程
mLastLoginTime = System.currentTimeMillis();
// 获取Google账户的AuthToken
String authToken = loginGoogleAccount(activity, false);
if (authToken == null) {
Log.e(TAG, "login google account failed");
return false;
}
// 步骤5处理非Gmail/GoogleMail账户自定义域名如企业邮箱
if (!(mAccount.name.toLowerCase().endsWith("gmail.com") || mAccount.name.toLowerCase().endsWith("googlemail.com"))) {
// 构建自定义域名的GTask URL
// login with custom domain if necessary
if (!(mAccount.name.toLowerCase().endsWith("gmail.com") || mAccount.name.toLowerCase()
.endsWith("googlemail.com"))) {
StringBuilder url = new StringBuilder(GTASK_URL).append("a/");
int index = mAccount.name.indexOf('@') + 1; // 截取@后的域名部分
int index = mAccount.name.indexOf('@') + 1;
String suffix = mAccount.name.substring(index);
url.append(suffix + "/");
mGetUrl = url.toString() + "ig"; // 自定义GET URL
mPostUrl = url.toString() + "r/ig"; // 自定义POST URL
mGetUrl = url.toString() + "ig";
mPostUrl = url.toString() + "r/ig";
// 尝试使用自定义URL登录GTask
if (tryToLoginGtask(activity, authToken)) {
mLoggedin = true;
}
}
// 步骤6自定义URL登录失败/是Gmail账户使用官方URL登录
// try to login with google official url
if (!mLoggedin) {
mGetUrl = GTASK_GET_URL;
mPostUrl = GTASK_POST_URL;
@ -239,47 +160,28 @@ public class GTaskClient {
}
}
// 登录成功,标记状态
mLoggedin = true;
return true;
}
/**
* GoogleAuthToken
*
* 1. Google
* 2.
* 3. AccountManagerAuthTokengoanna_mobile
* 4. invalidateTokentrueToken
* @param activity Activity
* @param invalidateToken AuthTokenToken
* @return GoogleAuthTokennull
*/
private String loginGoogleAccount(Activity activity, boolean invalidateToken) {
String authToken;
// 获取AccountManager服务
AccountManager accountManager = AccountManager.get(activity);
// 获取所有Google类型的账户type为com.google
Account[] accounts = accountManager.getAccountsByType("com.google");
// 无Google账户返回null
if (accounts.length == 0) {
Log.e(TAG, "there is no available google account");
return null;
}
// 获取设置中的同步账户名称
String accountName = NotesPreferenceActivity.getSyncAccountName(activity);
Account account = null;
// 匹配账户名称
for (Account a : accounts) {
if (a.name.equals(accountName)) {
account = a;
break;
}
}
// 匹配成功,存储账户实例
if (account != null) {
mAccount = account;
} else {
@ -287,24 +189,15 @@ public class GTaskClient {
return null;
}
// 获取AuthToken
AccountManagerFuture<Bundle> accountManagerFuture = accountManager.getAuthToken(
account,
"goanna_mobile", // GTask服务的Token类型
null,
activity,
null,
null
);
// get the token now
AccountManagerFuture<Bundle> accountManagerFuture = accountManager.getAuthToken(account,
"goanna_mobile", null, activity, null, null);
try {
// 获取Token结果
Bundle authTokenBundle = accountManagerFuture.getResult();
authToken = authTokenBundle.getString(AccountManager.KEY_AUTHTOKEN);
// 失效旧Token并重新获取递归调用
if (invalidateToken) {
accountManager.invalidateAuthToken("com.google", authToken);
authToken = loginGoogleAccount(activity, false);
loginGoogleAccount(activity, false);
}
} catch (Exception e) {
Log.e(TAG, "get auth token failed");
@ -314,26 +207,16 @@ public class GTaskClient {
return authToken;
}
/**
* GTaskToken
*
* 1. 使AuthTokenGTask
* 2. Token
* 3. falsetrue
* @param activity Activity
* @param authToken GoogleAuthToken
* @return truefalse
*/
private boolean tryToLoginGtask(Activity activity, String authToken) {
if (!loginGtask(authToken)) {
// Token过期失效并重新获取
// maybe the auth token is out of date, now let's invalidate the
// token and try again
authToken = loginGoogleAccount(activity, true);
if (authToken == null) {
Log.e(TAG, "login google account failed");
return false;
}
// 再次尝试登录
if (!loginGtask(authToken)) {
Log.e(TAG, "login gtask failed");
return false;
@ -342,39 +225,25 @@ public class GTaskClient {
return true;
}
/**
* GTask
*
* 1. HttpClientSocketCookieStore
* 2. GETAuthToken访GTaskGET URL
* 3. CookieGTLCookie
* 4. _setup()JSONclient_version
* @param authToken GoogleAuthToken
* @return truefalse
*/
private boolean loginGtask(String authToken) {
// 配置HTTP参数连接超时10秒Socket超时15秒
int timeoutConnection = 10000;
int timeoutSocket = 15000;
HttpParams httpParameters = new BasicHttpParams();
HttpConnectionParams.setConnectionTimeout(httpParameters, timeoutConnection);
HttpConnectionParams.setSoTimeout(httpParameters, timeoutSocket);
// 初始化HttpClient配置CookieStore
mHttpClient = new DefaultHttpClient(httpParameters);
BasicCookieStore localBasicCookieStore = new BasicCookieStore();
mHttpClient.setCookieStore(localBasicCookieStore);
// 禁用Expect-Continue头避免部分服务器不兼容
HttpProtocolParams.setUseExpectContinue(mHttpClient.getParams(), false);
// 执行GTask登录
// login gtask
try {
// 构建登录URL携带AuthToken
String loginUrl = mGetUrl + "?auth=" + authToken;
HttpGet httpGet = new HttpGet(loginUrl);
HttpResponse response = mHttpClient.execute(httpGet);
HttpResponse response = null;
response = mHttpClient.execute(httpGet);
// 检查认证CookieGTL开头的Cookie为GTask认证Cookie
// get the cookie now
List<Cookie> cookies = mHttpClient.getCookieStore().getCookies();
boolean hasAuthCookie = false;
for (Cookie cookie : cookies) {
@ -386,18 +255,16 @@ public class GTaskClient {
Log.w(TAG, "it seems that there is no auth cookie");
}
// 解析响应内容获取客户端版本号client_version
// get the client version
String resString = getResponseContent(response.getEntity());
String jsBegin = "_setup("; // JSON数据的起始标记
String jsEnd = ")}</script>"; // JSON数据的结束标记
String jsBegin = "_setup(";
String jsEnd = ")}</script>";
int begin = resString.indexOf(jsBegin);
int end = resString.lastIndexOf(jsEnd);
String jsString = null;
// 截取_setup()方法内的JSON字符串
if (begin != -1 && end != -1 && begin < end) {
jsString = resString.substring(begin + jsBegin.length(), end);
}
// 解析JSON获取client_version
JSONObject js = new JSONObject(jsString);
mClientVersion = js.getLong("v");
} catch (JSONException e) {
@ -405,7 +272,7 @@ public class GTaskClient {
e.printStackTrace();
return false;
} catch (Exception e) {
// 捕获所有异常HTTP请求、IO、解析等
// simply catch all exceptions
Log.e(TAG, "httpget gtask_url failed");
return false;
}
@ -413,89 +280,50 @@ public class GTaskClient {
return true;
}
/**
* IDID+1
* GTaskID
* @return ID
*/
private int getActionId() {
return mActionId++;
}
/**
* HTTP POSTPOST
* Content-Typeform-urlencodedAT1GTask
* @return HttpPost
*/
private HttpPost createHttpPost() {
HttpPost httpPost = new HttpPost(mPostUrl);
// 设置内容类型表单编码UTF-8字符集
httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8");
// GTask服务要求的AT头固定为1
httpPost.setHeader("AT", "1");
return httpPost;
}
/**
* HTTPGzip/Deflate
*
* 1. Content-Encoding
* 2. GzipInputStream/InflaterInputStream
* 3.
* @param entity HTTPHttpEntity
* @return
* @throws IOException IO
*/
private String getResponseContent(HttpEntity entity) throws IOException {
String contentEncoding = null;
// 获取响应的编码类型gzip/deflate/null
if (entity.getContentEncoding() != null) {
contentEncoding = entity.getContentEncoding().getValue();
Log.d(TAG, "encoding: " + contentEncoding);
}
// 根据编码类型创建输入流
InputStream input = entity.getContent();
if (contentEncoding != null && contentEncoding.equalsIgnoreCase("gzip")) {
// Gzip压缩使用GZIPInputStream解压缩
input = new GZIPInputStream(entity.getContent());
} else if (contentEncoding != null && contentEncoding.equalsIgnoreCase("deflate")) {
// Deflate压缩使用InflaterInputStream解压缩启用nowrap模式
Inflater inflater = new Inflater(true);
input = new InflaterInputStream(entity.getContent(), inflater);
}
// 读取输入流内容
try {
InputStreamReader isr = new InputStreamReader(input);
BufferedReader br = new BufferedReader(isr);
StringBuilder sb = new StringBuilder();
String buff;
while ((buff = br.readLine()) != null) {
sb.append(buff);
while (true) {
String buff = br.readLine();
if (buff == null) {
return sb.toString();
}
sb = sb.append(buff);
}
return sb.toString();
} finally {
// 确保输入流关闭,释放资源
input.close();
}
}
/**
* POSTGTask
*
* 1.
* 2. POSTJSONkeyr
* 3. POSTJSON
* 4.
* @param js JSON
* @return GTaskJSON
* @throws NetworkFailureException IO
* @throws ActionFailureException JSON
*/
private JSONObject postRequest(JSONObject js) throws NetworkFailureException {
// 检查登录状态
if (!mLoggedin) {
Log.e(TAG, "please login first");
throw new ActionFailureException("not logged in");
@ -503,15 +331,13 @@ public class GTaskClient {
HttpPost httpPost = createHttpPost();
try {
// 封装JSON参数为表单参数key为r值为JSON字符串
LinkedList<BasicNameValuePair> list = new LinkedList<BasicNameValuePair>();
list.add(new BasicNameValuePair("r", js.toString()));
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(list, "UTF-8");
httpPost.setEntity(entity);
// 执行POST请求
// execute the post
HttpResponse response = mHttpClient.execute(httpPost);
// 解析响应内容为JSON对象
String jsString = getResponseContent(response.getEntity());
return new JSONObject(jsString);
@ -534,33 +360,23 @@ public class GTaskClient {
}
}
/**
* TaskGTask
*
* 1.
* 2. JSON
* 3. POSTGIDTask
* @param task Task
* @throws NetworkFailureException
* @throws ActionFailureException JSON/
*/
public void createTask(Task task) throws NetworkFailureException {
commitUpdate();
try {
JSONObject jsPost = new JSONObject();
JSONArray actionList = new JSONArray();
// 添加创建任务的动作getCreateAction返回创建动作的JSON
// action_list
actionList.put(task.getCreateAction(getActionId()));
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
// 添加客户端版本号
// client_version
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
// 发送POST请求
// post
JSONObject jsResponse = postRequest(jsPost);
// 解析响应中的新任务GIDNEW_ID字段
JSONObject jsResult = (JSONObject) jsResponse.getJSONArray(GTaskStringUtils.GTASK_JSON_RESULTS).get(0);
JSONObject jsResult = (JSONObject) jsResponse.getJSONArray(
GTaskStringUtils.GTASK_JSON_RESULTS).get(0);
task.setGid(jsResult.getString(GTaskStringUtils.GTASK_JSON_NEW_ID));
} catch (JSONException e) {
@ -570,30 +386,23 @@ public class GTaskClient {
}
}
/**
* TaskListGTask
* 使TaskList
* @param tasklist TaskList
* @throws NetworkFailureException
* @throws ActionFailureException JSON/
*/
public void createTaskList(TaskList tasklist) throws NetworkFailureException {
commitUpdate();
try {
JSONObject jsPost = new JSONObject();
JSONArray actionList = new JSONArray();
// 添加创建任务列表的动作
// action_list
actionList.put(tasklist.getCreateAction(getActionId()));
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
// 添加客户端版本号
// client version
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
// 发送POST请求
// post
JSONObject jsResponse = postRequest(jsPost);
// 解析响应中的新列表GID
JSONObject jsResult = (JSONObject) jsResponse.getJSONArray(GTaskStringUtils.GTASK_JSON_RESULTS).get(0);
JSONObject jsResult = (JSONObject) jsResponse.getJSONArray(
GTaskStringUtils.GTASK_JSON_RESULTS).get(0);
tasklist.setGid(jsResult.getString(GTaskStringUtils.GTASK_JSON_NEW_ID));
} catch (JSONException e) {
@ -603,26 +412,18 @@ public class GTaskClient {
}
}
/**
* mUpdateArrayGTask
* POST
* @throws NetworkFailureException
* @throws ActionFailureException JSON/
*/
public void commitUpdate() throws NetworkFailureException {
if (mUpdateArray != null) {
try {
JSONObject jsPost = new JSONObject();
// 添加更新动作列表
// action_list
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, mUpdateArray);
// 添加客户端版本号
// client_version
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
// 发送POST请求
postRequest(jsPost);
// 清空更新数组
mUpdateArray = null;
} catch (JSONException e) {
Log.e(TAG, e.toString());
@ -632,73 +433,48 @@ public class GTaskClient {
}
}
/**
*
*
* 1. 10
* 2.
* 3.
* @param node Task/TaskList
* @throws NetworkFailureException
*/
public void addUpdateNode(Node node) throws NetworkFailureException {
if (node != null) {
// 限制单次批量更新的最大数量为10条避免请求失败
// too many update items may result in an error
// set max to 10 items
if (mUpdateArray != null && mUpdateArray.length() > 10) {
commitUpdate();
}
// 初始化更新数组
if (mUpdateArray == null) {
if (mUpdateArray == null)
mUpdateArray = new JSONArray();
}
// 添加节点的更新动作
mUpdateArray.put(node.getUpdateAction(getActionId()));
}
}
/**
* /
*
* 1.
* 2. JSON/
* 3. POST
* @param task
* @param preParent
* @param curParent
* @throws NetworkFailureException
* @throws ActionFailureException JSON/
*/
public void moveTask(Task task, TaskList preParent, TaskList curParent) throws NetworkFailureException {
public void moveTask(Task task, TaskList preParent, TaskList curParent)
throws NetworkFailureException {
commitUpdate();
try {
JSONObject jsPost = new JSONObject();
JSONArray actionList = new JSONArray();
JSONObject action = new JSONObject();
// 配置移动动作的参数
action.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, GTaskStringUtils.GTASK_JSON_ACTION_TYPE_MOVE);
// action_list
action.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_MOVE);
action.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, getActionId());
action.put(GTaskStringUtils.GTASK_JSON_ID, task.getGid()); // 要移动的任务ID
// 同列表移动且任务非第一个添加前序兄弟ID用于排序
action.put(GTaskStringUtils.GTASK_JSON_ID, task.getGid());
if (preParent == curParent && task.getPriorSibling() != null) {
// put prioring_sibing_id only if moving within the tasklist and
// it is not the first one
action.put(GTaskStringUtils.GTASK_JSON_PRIOR_SIBLING_ID, task.getPriorSibling());
}
// 原列表ID
action.put(GTaskStringUtils.GTASK_JSON_SOURCE_LIST, preParent.getGid());
// 新父列表ID
action.put(GTaskStringUtils.GTASK_JSON_DEST_PARENT, curParent.getGid());
// 不同列表移动添加目标列表ID
if (preParent != curParent) {
// put the dest_list only if moving between tasklists
action.put(GTaskStringUtils.GTASK_JSON_DEST_LIST, curParent.getGid());
}
// 添加动作到列表,发送请求
actionList.put(action);
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
// client_version
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
postRequest(jsPost);
@ -710,31 +486,20 @@ public class GTaskClient {
}
}
/**
* Task/TaskList
*
* 1.
* 2. deletedtrue
* 3. JSON
* 4.
* @param node Task/TaskList
* @throws NetworkFailureException
* @throws ActionFailureException JSON/
*/
public void deleteNode(Node node) throws NetworkFailureException {
commitUpdate();
try {
JSONObject jsPost = new JSONObject();
JSONArray actionList = new JSONArray();
// 标记节点为已删除
// action_list
node.setDeleted(true);
// 添加删除动作更新动作包含deleted标记
actionList.put(node.getUpdateAction(getActionId()));
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
// client_version
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
// 发送请求,清空更新数组
postRequest(jsPost);
mUpdateArray = null;
} catch (JSONException e) {
@ -744,16 +509,6 @@ public class GTaskClient {
}
}
/**
* GTask
*
* 1.
* 2. GETGTaskGET URL
* 3. JSONlists
* @return JSON
* @throws NetworkFailureException
* @throws ActionFailureException JSON/
*/
public JSONArray getTaskLists() throws NetworkFailureException {
if (!mLoggedin) {
Log.e(TAG, "please login first");
@ -762,9 +517,10 @@ public class GTaskClient {
try {
HttpGet httpGet = new HttpGet(mGetUrl);
HttpResponse response = mHttpClient.execute(httpGet);
HttpResponse response = null;
response = mHttpClient.execute(httpGet);
// 解析响应内容,获取任务列表数组
// get the task list
String resString = getResponseContent(response.getEntity());
String jsBegin = "_setup(";
String jsEnd = ")}</script>";
@ -775,7 +531,6 @@ public class GTaskClient {
jsString = resString.substring(begin + jsBegin.length(), end);
}
JSONObject js = new JSONObject(jsString);
// 返回lists字段的JSON数组
return js.getJSONObject("t").getJSONArray(GTaskStringUtils.GTASK_JSON_LISTS);
} catch (ClientProtocolException e) {
Log.e(TAG, e.toString());
@ -792,17 +547,6 @@ public class GTaskClient {
}
}
/**
* GTask
*
* 1.
* 2. GETALLJSONID
* 3. POST
* @param listGid GID
* @return JSON
* @throws NetworkFailureException
* @throws ActionFailureException JSON/
*/
public JSONArray getTaskList(String listGid) throws NetworkFailureException {
commitUpdate();
try {
@ -810,18 +554,18 @@ public class GTaskClient {
JSONArray actionList = new JSONArray();
JSONObject action = new JSONObject();
// 配置GETALL动作获取列表下的所有任务
action.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, GTaskStringUtils.GTASK_JSON_ACTION_TYPE_GETALL);
// action_list
action.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_GETALL);
action.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, getActionId());
action.put(GTaskStringUtils.GTASK_JSON_LIST_ID, listGid); // 指定列表ID
action.put(GTaskStringUtils.GTASK_JSON_GET_DELETED, false); // 不获取已删除任务
// 添加动作到列表,发送请求
action.put(GTaskStringUtils.GTASK_JSON_LIST_ID, listGid);
action.put(GTaskStringUtils.GTASK_JSON_GET_DELETED, false);
actionList.put(action);
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
// client_version
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
// 解析响应中的任务数组
JSONObject jsResponse = postRequest(jsPost);
return jsResponse.getJSONArray(GTaskStringUtils.GTASK_JSON_TASKS);
} catch (JSONException e) {
@ -831,19 +575,11 @@ public class GTaskClient {
}
}
/**
* Google
* @return Accountnull
*/
public Account getSyncAccount() {
return mAccount;
}
/**
*
*
*/
public void resetUpdateArray() {
mUpdateArray = null;
}
}
}

@ -0,0 +1,128 @@
/*
* 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.gtask.remote;
import android.app.Activity;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.IBinder;
public class GTaskSyncService extends Service {
public final static String ACTION_STRING_NAME = "sync_action_type";
public final static int ACTION_START_SYNC = 0;
public final static int ACTION_CANCEL_SYNC = 1;
public final static int ACTION_INVALID = 2;
public final static String GTASK_SERVICE_BROADCAST_NAME = "net.micode.notes.gtask.remote.gtask_sync_service";
public final static String GTASK_SERVICE_BROADCAST_IS_SYNCING = "isSyncing";
public final static String GTASK_SERVICE_BROADCAST_PROGRESS_MSG = "progressMsg";
private static GTaskASyncTask mSyncTask = null;
private static String mSyncProgress = "";
private void startSync() {
if (mSyncTask == null) {
mSyncTask = new GTaskASyncTask(this, new GTaskASyncTask.OnCompleteListener() {
public void onComplete() {
mSyncTask = null;
sendBroadcast("");
stopSelf();
}
});
sendBroadcast("");
mSyncTask.execute();
}
}
private void cancelSync() {
if (mSyncTask != null) {
mSyncTask.cancelSync();
}
}
@Override
public void onCreate() {
mSyncTask = null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Bundle bundle = intent.getExtras();
if (bundle != null && bundle.containsKey(ACTION_STRING_NAME)) {
switch (bundle.getInt(ACTION_STRING_NAME, ACTION_INVALID)) {
case ACTION_START_SYNC:
startSync();
break;
case ACTION_CANCEL_SYNC:
cancelSync();
break;
default:
break;
}
return START_STICKY;
}
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onLowMemory() {
if (mSyncTask != null) {
mSyncTask.cancelSync();
}
}
public IBinder onBind(Intent intent) {
return null;
}
public void sendBroadcast(String msg) {
mSyncProgress = msg;
Intent intent = new Intent(GTASK_SERVICE_BROADCAST_NAME);
intent.putExtra(GTASK_SERVICE_BROADCAST_IS_SYNCING, mSyncTask != null);
intent.putExtra(GTASK_SERVICE_BROADCAST_PROGRESS_MSG, msg);
sendBroadcast(intent);
}
public static void startSync(Activity activity) {
GTaskManager.getInstance().setActivityContext(activity);
Intent intent = new Intent(activity, GTaskSyncService.class);
intent.putExtra(GTaskSyncService.ACTION_STRING_NAME, GTaskSyncService.ACTION_START_SYNC);
activity.startService(intent);
}
public static void cancelSync(Context context) {
Intent intent = new Intent(context, GTaskSyncService.class);
intent.putExtra(GTaskSyncService.ACTION_STRING_NAME, GTaskSyncService.ACTION_CANCEL_SYNC);
context.startService(intent);
}
public static boolean isSyncing() {
return mSyncTask != null;
}
public static String getProgressString() {
return mSyncProgress;
}
}

@ -0,0 +1,253 @@
/*
* 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;
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;
public class Note {
private ContentValues mNoteDiffValues;
private NoteData mNoteData;
private static final String TAG = "Note";
/**
* Create a new note id for adding a new note to databases
*/
public static synchronized long getNewNoteId(Context context, long folderId) {
// Create a new note in the database
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);
Uri uri = context.getContentResolver().insert(Notes.CONTENT_NOTE_URI, values);
long noteId = 0;
try {
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();
}
public void setNoteValue(String key, String value) {
mNoteDiffValues.put(key, value);
mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1);
mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis());
}
public void setTextData(String key, String value) {
mNoteData.setTextData(key, value);
}
public void setTextDataId(long id) {
mNoteData.setTextDataId(id);
}
public long getTextDataId() {
return mNoteData.mTextDataId;
}
public void setCallDataId(long id) {
mNoteData.setCallDataId(id);
}
public void setCallData(String key, String value) {
mNoteData.setCallData(key, value);
}
public boolean isLocalModified() {
return mNoteDiffValues.size() > 0 || mNoteData.isLocalModified();
}
public boolean syncNote(Context context, long noteId) {
if (noteId <= 0) {
throw new IllegalArgumentException("Wrong note id:" + noteId);
}
if (!isLocalModified()) {
return true;
}
/**
* In theory, once data changed, the note should be updated on {@link NoteColumns#LOCAL_MODIFIED} and
* {@link NoteColumns#MODIFIED_DATE}. For data safety, though update note fails, we also update the
* note data info
*/
if (context.getContentResolver().update(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), mNoteDiffValues, null,
null) == 0) {
Log.e(TAG, "Update note error, should not happen");
// Do not return, fall through
}
mNoteDiffValues.clear();
if (mNoteData.isLocalModified()
&& (mNoteData.pushIntoContentResolver(context, noteId) == null)) {
return false;
}
return true;
}
private class NoteData {
private long mTextDataId;
private ContentValues mTextDataValues;
private long mCallDataId;
private ContentValues mCallDataValues;
private static final String TAG = "NoteData";
public NoteData() {
mTextDataValues = new ContentValues();
mCallDataValues = new ContentValues();
mTextDataId = 0;
mCallDataId = 0;
}
boolean isLocalModified() {
return mTextDataValues.size() > 0 || mCallDataValues.size() > 0;
}
void setTextDataId(long id) {
if(id <= 0) {
throw new IllegalArgumentException("Text data id should larger than 0");
}
mTextDataId = id;
}
void setCallDataId(long id) {
if (id <= 0) {
throw new IllegalArgumentException("Call data id should larger than 0");
}
mCallDataId = id;
}
void setCallData(String key, String value) {
mCallDataValues.put(key, value);
mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1);
mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis());
}
void setTextData(String key, String value) {
mTextDataValues.put(key, value);
mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1);
mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis());
}
Uri pushIntoContentResolver(Context context, long noteId) {
/**
* Check for safety
*/
if (noteId <= 0) {
throw new IllegalArgumentException("Wrong note id:" + noteId);
}
ArrayList<ContentProviderOperation> operationList = new ArrayList<ContentProviderOperation>();
ContentProviderOperation.Builder builder = null;
if(mTextDataValues.size() > 0) {
mTextDataValues.put(DataColumns.NOTE_ID, noteId);
if (mTextDataId == 0) {
mTextDataValues.put(DataColumns.MIME_TYPE, TextNote.CONTENT_ITEM_TYPE);
Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI,
mTextDataValues);
try {
setTextDataId(Long.valueOf(uri.getPathSegments().get(1)));
} 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);
if (mCallDataId == 0) {
mCallDataValues.put(DataColumns.MIME_TYPE, CallNote.CONTENT_ITEM_TYPE);
Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI,
mCallDataValues);
try {
setCallDataId(Long.valueOf(uri.getPathSegments().get(1)));
} 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;
}
}
}

@ -0,0 +1,400 @@
/*
* 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;
public class WorkingNote {
// Note for the working note
private Note mNote;
// Note Id
private long mNoteId;
// Note content
private String mContent;
// Note mode
private int mMode;
private long mAlertDate;
private long mModifiedDate;
private int mBgColorId;
private int mWidgetId;
private int mWidgetType;
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;
// New note construct
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;
}
// Existing note construct
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);
}
}
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;
}
public static WorkingNote load(Context context, long id) {
return new WorkingNote(context, id, 0);
}
public synchronized boolean saveNote() {
if (mIsDeleted && existInDatabase()) {
// 如果便签已被标记为删除且存在于数据库中,则执行删除操作
int rows = mContext.getContentResolver().delete(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mNoteId),
null, null);
if (rows > 0) {
Log.d(TAG, "Deleted empty note with id:" + mNoteId);
} else {
Log.e(TAG, "Failed to delete empty note with id:" + mNoteId);
}
return true;
}
if (isWorthSaving()) {
if (!existInDatabase()) {
if ((mNoteId = Note.getNewNoteId(mContext, mFolderId)) == 0) {
Log.e(TAG, "Create new note fail with id:" + mNoteId);
return false;
}
}
// 更新便签数据
mNote.setTextData(DataColumns.CONTENT, mContent);
// 同步到数据库
boolean result = mNote.syncNote(mContext, mNoteId);
/**
* Update widget content if there exist any widget of this note
*/
if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID
&& mWidgetType != Notes.TYPE_WIDGET_INVALIDE
&& mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onWidgetChanged();
}
return result;
} else {
return false;
}
}
public boolean existInDatabase() {
return mNoteId > 0;
}
private boolean isWorthSaving() {
if (mIsDeleted) {
return false;
}
// 如果便签不存在于数据库且内容为空,则不值得保存
if (!existInDatabase() && TextUtils.isEmpty(mContent)) {
return false;
}
// 如果便签已存在于数据库但内容为空,则需要删除它
if (existInDatabase() && TextUtils.isEmpty(mContent)) {
// 标记为需要删除
mIsDeleted = true;
return false;
}
// 如果便签已存在于数据库但未被修改,则不需要保存
if (existInDatabase() && !mNote.isLocalModified()) {
return false;
}
return true;
}
public void setOnSettingStatusChangedListener(NoteSettingChangedListener l) {
mNoteSettingStatusListener = l;
}
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);
}
}
public void markDeleted(boolean mark) {
mIsDeleted = mark;
if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID
&& mWidgetType != Notes.TYPE_WIDGET_INVALIDE && mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onWidgetChanged();
}
}
public void setBgColorId(int id) {
if (id != mBgColorId) {
mBgColorId = id;
if (mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onBackgroundColorChanged();
}
mNote.setNoteValue(NoteColumns.BG_COLOR_ID, String.valueOf(id));
}
}
public void setCheckListMode(int mode) {
if (mMode != mode) {
if (mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onCheckListModeChanged(mMode, mode);
}
mMode = mode;
mNote.setTextData(TextNote.MODE, String.valueOf(mMode));
}
}
public void setWidgetType(int type) {
if (type != mWidgetType) {
mWidgetType = type;
mNote.setNoteValue(NoteColumns.WIDGET_TYPE, String.valueOf(mWidgetType));
}
}
public void setWidgetId(int id) {
if (id != mWidgetId) {
mWidgetId = id;
mNote.setNoteValue(NoteColumns.WIDGET_ID, String.valueOf(mWidgetId));
}
}
public void setWorkingText(String text) {
if (!TextUtils.equals(mContent, text)) {
mContent = text;
mNote.setTextData(DataColumns.CONTENT, mContent);
}
}
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));
}
public boolean hasClockAlert() {
return (mAlertDate > 0 ? true : false);
}
public String getContent() {
return mContent;
}
public long getAlertDate() {
return mAlertDate;
}
public long getModifiedDate() {
return mModifiedDate;
}
public int getBgColorResId() {
return NoteBgResources.getNoteBgResource(mBgColorId);
}
public int getBgColorId() {
return mBgColorId;
}
public int getTitleBgResId() {
return NoteBgResources.getNoteTitleBgResource(mBgColorId);
}
public int getCheckListMode() {
return mMode;
}
public long getNoteId() {
return mNoteId;
}
public long getFolderId() {
return mFolderId;
}
public int getWidgetId() {
return mWidgetId;
}
public int getWidgetType() {
return mWidgetType;
}
public interface NoteSettingChangedListener {
/**
* Called when the background color of current note has just changed
*/
void onBackgroundColorChanged();
/**
* Called when user set clock
*/
void onClockAlertChanged(long date, boolean set);
/**
* Call when user create note from widget
*/
void onWidgetChanged();
/**
* Call when switch between check list mode and normal mode
* @param oldMode is previous mode before change
* @param newMode is new mode
*/
void onCheckListModeChanged(int oldMode, int newMode);
}
}

@ -355,8 +355,7 @@ public class BackupUtils {
Cursor folderCursor = mContext.getContentResolver().query(
Notes.CONTENT_NOTE_URI, // 便签表
NOTE_PROJECTION, // 查询字段
"(" + NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER + " AND "
+ NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER + ") OR "
"(" + NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER + ") OR "
+ NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER,
null, // 查询参数
null // 排序

@ -181,13 +181,12 @@ public class DataUtils {
* @return
*/
public static int getUserFolderCount(ContentResolver resolver) {
// 查询条件:类型为文件夹 且 不在回收站中
// 查询条件:类型为文件夹
Cursor cursor =resolver.query(Notes.CONTENT_NOTE_URI,
new String[] { "COUNT(*)" }, // 只查询数量
NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>?", // 查询条件
NoteColumns.TYPE + "=?", // 查询条件
new String[] {
String.valueOf(Notes.TYPE_FOLDER), // 参数1文件夹类型
String.valueOf(Notes.ID_TRASH_FOLER) // 参数2排除回收站
String.valueOf(Notes.TYPE_FOLDER) // 参数1文件夹类型
},
null);
@ -218,7 +217,7 @@ public class DataUtils {
// 查询指定便签
Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId),
null, // 查询所有列
NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER,
NoteColumns.TYPE + "=?",
new String [] {String.valueOf(type)}, // 类型参数
null);
@ -284,10 +283,9 @@ public class DataUtils {
* @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 + "=?", // SNIPPET字段存储文件夹名称
new String[] { name }, null);

@ -115,7 +115,7 @@ public class ResourceParser {
*/
public static int getDefaultBgId(Context context) {
// 从偏好设置读取是否启用随机背景颜色
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean(
if (context.getSharedPreferences(NotesPreferenceActivity.PREFERENCE_NAME, Context.MODE_PRIVATE).getBoolean(
NotesPreferenceActivity.PREFERENCE_SET_BG_COLOR_KEY, // 偏好设置键名
false)) { // 默认值为false不随机
// 随机选择一种颜色

@ -0,0 +1,143 @@
/*
* 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.Context;
import android.content.SharedPreferences;
import android.text.TextUtils;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* SearchHistoryManager -
* 使SharedPreferences
*/
public class SearchHistoryManager {
private static final String TAG = "SearchHistoryManager";
// SharedPreferences文件名
private static final String PREFERENCE_NAME = "search_history";
// 搜索历史键
private static final String KEY_SEARCH_HISTORY = "search_history";
// 最大历史记录数量
private static final int MAX_HISTORY_COUNT = 10;
// 单例实例
private static SearchHistoryManager sInstance;
// SharedPreferences实例
private SharedPreferences mSharedPreferences;
/**
*
* @param context
*/
private SearchHistoryManager(Context context) {
mSharedPreferences = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
}
/**
*
* @param context
* @return SearchHistoryManager
*/
public static synchronized SearchHistoryManager getInstance(Context context) {
if (sInstance == null) {
sInstance = new SearchHistoryManager(context.getApplicationContext());
}
return sInstance;
}
/**
*
* @param keyword
*/
public void saveSearchKeyword(String keyword) {
if (TextUtils.isEmpty(keyword)) {
return;
}
// 获取现有历史记录
List<String> historyList = getSearchHistoryList();
// 如果已存在,移除旧的位置
if (historyList.contains(keyword)) {
historyList.remove(keyword);
}
// 添加到最前面
historyList.add(0, keyword);
// 限制历史记录数量
if (historyList.size() > MAX_HISTORY_COUNT) {
historyList = historyList.subList(0, MAX_HISTORY_COUNT);
}
// 保存到SharedPreferences
saveHistoryList(historyList);
}
/**
*
* @return
*/
public List<String> getSearchHistoryList() {
List<String> historyList = new ArrayList<>();
try {
String historyJson = mSharedPreferences.getString(KEY_SEARCH_HISTORY, "[]");
JSONArray jsonArray = new JSONArray(historyJson);
for (int i = 0; i < jsonArray.length(); i++) {
historyList.add(jsonArray.getString(i));
}
} catch (JSONException e) {
Log.e(TAG, "Failed to parse search history: " + e.getMessage());
}
return historyList;
}
/**
*
*/
public void clearSearchHistory() {
mSharedPreferences.edit().remove(KEY_SEARCH_HISTORY).apply();
}
/**
* SharedPreferences
* @param historyList
*/
private void saveHistoryList(List<String> historyList) {
try {
JSONArray jsonArray = new JSONArray(historyList);
mSharedPreferences.edit().putString(KEY_SEARCH_HISTORY, jsonArray.toString()).apply();
} catch (Exception e) {
Log.e(TAG, "Failed to save search history: " + e.getMessage());
}
}
}

@ -35,11 +35,15 @@ import android.graphics.Paint; // 画笔,用于文本样式
import android.os.Bundle; // 状态保存
import android.preference.PreferenceManager; // 偏好设置管理器
// Android文本处理
import android.text.Editable; // 可编辑文本
import android.text.Spannable; // 可设置样式的文本
import android.text.SpannableString; // 可设置样式的字符串
import android.text.TextUtils; // 文本工具
import android.text.TextWatcher; // 文本变化监听
import android.text.format.DateUtils; // 日期工具
import android.text.style.BackgroundColorSpan; // 背景色样式
// Android网络
import android.net.Uri; // URI工具
import android.util.Log; // 日志工具
// Android视图
import android.view.LayoutInflater; // 布局加载器
@ -71,6 +75,8 @@ import net.micode.notes.model.WorkingNote.NoteSettingChangedListener; // 便签
import net.micode.notes.tool.DataUtils; // 数据工具
import net.micode.notes.tool.ResourceParser; // 资源解析器
import net.micode.notes.tool.ResourceParser.TextAppearanceResources; // 文本外观资源
import net.micode.notes.tool.SearchHistoryManager; // 搜索历史管理器
// 应用对话框
import net.micode.notes.ui.DateTimePickerDialog.OnDateTimeSetListener; // 日期时间设置监听
// 应用自定义控件
@ -82,6 +88,7 @@ import net.micode.notes.widget.NoteWidgetProvider_4x; // 4x小部件
// Java集合
import java.util.HashMap; // 哈希映射
import java.util.HashSet; // 哈希集合
import java.util.List; // 列表接口
import java.util.Map; // 映射接口
// Java正则表达式
import java.util.regex.Matcher; // 正则匹配器
@ -181,6 +188,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
private EditText mNoteEditor; // 便签编辑器
private View mNoteEditorPanel; // 编辑器面板
private LinearLayout mEditTextList; // 列表模式编辑容器
private TextView mWordCountView; // 字数统计视图
// 业务模型
private WorkingNote mWorkingNote; // 工作便签模型
@ -196,6 +204,9 @@ public class NoteEditActivity extends Activity implements OnClickListener,
// 清单模式常量
public static final String TAG_CHECKED = String.valueOf('\u221A'); // 已勾选符号 √
public static final String TAG_UNCHECKED = String.valueOf('\u25A1'); // 未勾选符号 □
// 请求码常量
private static final int REQUEST_CODE_IMAGE_SELECTION = 1;
// 搜索高亮相关
private String mUserQuery; // 用户搜索关键词
@ -263,6 +274,9 @@ public class NoteEditActivity extends Activity implements OnClickListener,
if (intent.hasExtra(SearchManager.EXTRA_DATA_KEY)) {
noteId = Long.parseLong(intent.getStringExtra(SearchManager.EXTRA_DATA_KEY));
mUserQuery = intent.getStringExtra(SearchManager.USER_QUERY);
// 保存搜索关键词到历史记录
SearchHistoryManager.getInstance(this).saveSearchKeyword(mUserQuery);
}
// 检查便签是否存在
@ -387,6 +401,9 @@ public class NoteEditActivity extends Activity implements OnClickListener,
// 显示提醒信息
showAlertHeader();
// 更新字数统计
updateWordCount();
}
/**
@ -498,6 +515,25 @@ public class NoteEditActivity extends Activity implements OnClickListener,
// 编辑视图
mNoteEditor = (EditText) findViewById(R.id.note_edit_view);
mNoteEditorPanel = findViewById(R.id.sv_note_edit);
// 字数统计视图
mWordCountView = (TextView) findViewById(R.id.tv_word_count);
// 设置文本变化监听
mNoteEditor.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
updateWordCount();
}
@Override
public void afterTextChanged(Editable s) {
}
});
// 背景选择器
mNoteBgColorSelector = findViewById(R.id.note_bg_color_selector);
@ -514,7 +550,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
};
// 偏好设置
mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
mSharedPrefs = getSharedPreferences(NotesPreferenceActivity.PREFERENCE_NAME, Context.MODE_PRIVATE);
mFontSizeId = mSharedPrefs.getInt(PREFERENCE_FONT_SIZE, ResourceParser.BG_DEFAULT_FONT_SIZE);
// 修复字体大小ID可能越界的bug
@ -541,6 +577,25 @@ public class NoteEditActivity extends Activity implements OnClickListener,
clearSettingState();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CODE_IMAGE_SELECTION && resultCode == RESULT_OK) {
if (data != null && data.getData() != null) {
// 获取选中图片的Uri
Uri imageUri = data.getData();
// 将图片Uri插入到当前编辑位置
String imageTag = "[IMAGE:" + imageUri.toString() + "]";
int currentPosition = mNoteEditor.getSelectionStart();
Editable text = mNoteEditor.getText();
text.insert(currentPosition, imageTag);
// 保存便签内容
getWorkingText();
}
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
// ======================= 小部件更新 =======================
/**
@ -732,6 +787,11 @@ public class NoteEditActivity extends Activity implements OnClickListener,
setReminder();
} else if (itemId == R.id.menu_delete_remind) {
mWorkingNote.setAlertDate(0, false);
} else if (itemId == R.id.menu_insert_image) {
// 启动图片选择器
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("image/*");
startActivityForResult(intent, REQUEST_CODE_IMAGE_SELECTION);
} else {
// 默认分支原default
}
@ -781,6 +841,16 @@ public class NoteEditActivity extends Activity implements OnClickListener,
startActivity(intent);
}
/**
* Toast
* @param resId ID
*/
// 已删除重复的showToast方法使用第1383行定义的更灵活的方法
/**
* 便
*/
@ -793,15 +863,9 @@ public class NoteEditActivity extends Activity implements OnClickListener,
} else {
Log.d(TAG, "Wrong note id, should not happen");
}
// 根据同步模式选择删除或移动到回收站
if (!isSyncMode()) {
if (!DataUtils.batchDeleteNotes(getContentResolver(), ids)) {
Log.e(TAG, "Delete Note error");
}
} else {
if (!DataUtils.batchMoveToFolder(getContentResolver(), ids, Notes.ID_TRASH_FOLER)) {
Log.e(TAG, "Move notes to trash folder error, should not happens");
}
// 统一使用永久删除方式
if (!DataUtils.batchDeleteNotes(getContentResolver(), ids)) {
Log.e(TAG, "Delete Note error");
}
}
mWorkingNote.markDeleted(true);
@ -1006,6 +1070,8 @@ public class NoteEditActivity extends Activity implements OnClickListener,
} else {
mEditTextList.getChildAt(index).findViewById(R.id.cb_edit_item).setVisibility(View.GONE);
}
// 更新字数统计
updateWordCount();
}
// ======================= 清单模式变化回调 =======================
@ -1027,6 +1093,8 @@ public class NoteEditActivity extends Activity implements OnClickListener,
mEditTextList.setVisibility(View.GONE);
mNoteEditor.setVisibility(View.VISIBLE);
}
// 更新字数统计
updateWordCount();
}
// ======================= 获取工作文本 =======================
@ -1063,6 +1131,28 @@ public class NoteEditActivity extends Activity implements OnClickListener,
// ======================= 保存便签 =======================
/**
*
*
*/
private void updateWordCount() {
int wordCount = 0;
if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) {
// 清单模式:计算所有列表项的字数
StringBuilder content = new StringBuilder();
for (int i = 0; i < mEditTextList.getChildCount(); i++) {
View view = mEditTextList.getChildAt(i);
NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text);
content.append(edit.getText().toString());
}
wordCount = content.length();
} else {
// 普通模式:直接计算文本长度
wordCount = mNoteEditor.getText().length();
}
mWordCountView.setText(String.valueOf(wordCount));
}
/**
* 便
* @return true: ; false:

@ -21,6 +21,7 @@ package net.micode.notes.ui;
// ======================= 导入区域 =======================
// Android基础
import android.content.Context; // 上下文
import android.content.ContentResolver; // 内容解析器
import android.graphics.Rect; // 矩形区域
// Android文本处理
import android.text.Layout; // 文本布局
@ -36,8 +37,22 @@ import android.view.KeyEvent; // 按键事件
import android.view.MenuItem; // 菜单项
import android.view.MenuItem.OnMenuItemClickListener; // 菜单项点击监听
import android.view.MotionEvent; // 触摸事件
// Android文本处理
import android.text.InputType; // 输入类型
import android.text.Spannable; // 可设置样式的文本
import android.text.SpannableStringBuilder; // 可设置样式的字符串构建器
import android.text.style.ImageSpan; // 图片样式
// Android输入法
import android.view.inputmethod.EditorInfo; // 输入法编辑器信息
// Android控件
import android.widget.EditText; // 编辑文本控件基类
import android.widget.EditText; // 编辑文本控件基类
import android.widget.ImageView; // 图片视图
// Android图形
import android.graphics.Bitmap; // 位图
import android.graphics.BitmapFactory; // 位图工厂
import android.graphics.drawable.Drawable; // 可绘制对象
// Android网络
import android.net.Uri; // URI工具
// 应用内部资源
import net.micode.notes.R; // 资源文件R类
@ -127,7 +142,7 @@ public class NoteEditText extends EditText {
* @param context
*/
public NoteEditText(Context context) {
super(context, null);
this(context, null);
mIndex = 0; // 默认索引为0
}
@ -137,7 +152,7 @@ public class NoteEditText extends EditText {
* @param attrs
*/
public NoteEditText(Context context, AttributeSet attrs) {
super(context, attrs, android.R.attr.editTextStyle);
this(context, attrs, android.R.attr.editTextStyle);
}
/**
@ -148,7 +163,98 @@ public class NoteEditText extends EditText {
*/
public NoteEditText(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// TODO: 可在此处添加额外初始化代码
// 确保输入法支持中文输入
setInputType(getInputType() | InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES);
setImeOptions(EditorInfo.IME_ACTION_NONE);
}
/**
* URI
* @param uri URI
* @return Bitmap
*/
private Bitmap loadImageFromUri(Uri uri) {
try {
ContentResolver resolver = getContext().getContentResolver();
Bitmap bitmap = BitmapFactory.decodeStream(resolver.openInputStream(uri));
if (bitmap != null) {
// 调整图片大小以适应编辑框
int maxWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
if (bitmap.getWidth() > maxWidth) {
float scale = (float) maxWidth / bitmap.getWidth();
int newHeight = (int) (bitmap.getHeight() * scale);
bitmap = Bitmap.createScaledBitmap(bitmap, maxWidth, newHeight, true);
}
}
return bitmap;
} catch (Exception e) {
Log.e(TAG, "Failed to load image from URI: " + uri, e);
return null;
}
}
/**
* [IMAGE:uri]
* @param text
* @return SpannableStringBuilder
*/
private SpannableStringBuilder parseImageTags(CharSequence text) {
SpannableStringBuilder builder = new SpannableStringBuilder(text);
String content = text.toString();
int startIndex = 0;
while (true) {
startIndex = content.indexOf("[IMAGE:", startIndex);
if (startIndex == -1) break;
int endIndex = content.indexOf("]", startIndex);
if (endIndex == -1) break;
String imageTag = content.substring(startIndex, endIndex + 1);
String imageUriStr = imageTag.substring(7, imageTag.length() - 1); // 去掉[IMAGE:和]
try {
Uri imageUri = Uri.parse(imageUriStr);
Bitmap bitmap = loadImageFromUri(imageUri);
if (bitmap != null) {
// 创建一个占位符文本,用于放置图片
String placeholder = "[图片]";
int placeholderLength = placeholder.length();
// 替换图片标签为占位符
builder.replace(startIndex, endIndex + 1, placeholder);
// 创建ImageSpan并添加到占位符位置
ImageSpan imageSpan = new ImageSpan(getContext(), bitmap);
builder.setSpan(imageSpan, startIndex, startIndex + placeholderLength, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
// 更新content和startIndex以继续处理
content = builder.toString();
startIndex += placeholderLength;
} else {
startIndex = endIndex + 1;
}
} catch (Exception e) {
Log.e(TAG, "Failed to parse image URI: " + imageUriStr, e);
startIndex = endIndex + 1;
}
}
return builder;
}
/**
* setText
*/
@Override
public void setText(CharSequence text, BufferType type) {
if (text != null) {
SpannableStringBuilder builder = parseImageTags(text);
super.setText(builder, BufferType.SPANNABLE);
} else {
super.setText(text, type);
}
}
// ======================= 设置方法 =======================

@ -58,6 +58,7 @@ public class NoteItemData {
NoteColumns.TYPE, // 9 - 便签类型
NoteColumns.WIDGET_ID, // 10 - 小部件ID
NoteColumns.WIDGET_TYPE, // 11 - 小部件类型
NoteColumns.PINNED // 12 - 是否置顶
};
// ======================= 字段索引常量 =======================
@ -75,6 +76,7 @@ public class NoteItemData {
private static final int TYPE_COLUMN = 9; // 类型字段索引
private static final int WIDGET_ID_COLUMN = 10; // 小部件ID字段索引
private static final int WIDGET_TYPE_COLUMN = 11; // 小部件类型字段索引
private static final int PINNED_COLUMN = 12; // 是否置顶字段索引
// ======================= 数据成员 =======================
@ -91,6 +93,7 @@ public class NoteItemData {
private int mType; // 便签类型
private int mWidgetId; // 小部件ID
private int mWidgetType; // 小部件类型
private boolean mPinned; // 是否置顶
// 通话记录相关
private String mName; // 联系人姓名
@ -131,6 +134,7 @@ public class NoteItemData {
mType = cursor.getInt(TYPE_COLUMN);
mWidgetId = cursor.getInt(WIDGET_ID_COLUMN);
mWidgetType = cursor.getInt(WIDGET_TYPE_COLUMN);
mPinned = (cursor.getInt(PINNED_COLUMN) > 0) ? true : false;
// 2. 清理摘要中的清单标记
// 移除已勾选(√)和未勾选(□)标记,只显示纯文本
@ -356,6 +360,16 @@ public class NoteItemData {
public int getWidgetType() {
return mWidgetType;
}
/**
* 便
* @return true: , false:
*/
public boolean isPinned() {
return mPinned;
}
/**
* ID

@ -62,6 +62,7 @@ import android.widget.AdapterView.OnItemClickListener; // 列表项点击监听
import android.widget.AdapterView.OnItemLongClickListener; // 列表项长按监听
import android.widget.Button; // 按钮
import android.widget.EditText; // 编辑框
import android.widget.ImageView; // 图片视图
import android.widget.ListView; // 列表视图
import android.widget.PopupMenu; // 弹出菜单
import android.widget.TextView; // 文本视图
@ -72,6 +73,7 @@ import net.micode.notes.R; // 资源文件R类
// 应用数据模型
import net.micode.notes.data.Notes; // Notes主类
import net.micode.notes.data.Notes.NoteColumns; // 便签表列定义
import net.micode.notes.data.NotesDatabaseHelper; // 数据库帮助类
// 应用同步服务
import net.micode.notes.gtask.remote.GTaskSyncService; // Google任务同步服务
// 应用业务模型
@ -79,6 +81,7 @@ import net.micode.notes.model.WorkingNote; // 工作便签模型
// 应用工具
import net.micode.notes.tool.BackupUtils; // 备份工具
import net.micode.notes.tool.DataUtils; // 数据工具
import net.micode.notes.tool.ResourceParser; // 资源解析器
// 应用适配器
import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute; // 小部件属性
@ -93,6 +96,8 @@ import java.io.InputStream; // 输入流
import java.io.InputStreamReader; // 输入流读取器
// Java集合
import java.util.HashSet; // 哈希集合
import java.util.List; // 列表接口
import java.util.Map; // 映射接口
// ======================= 便签列表主Activity =======================
/**
@ -113,11 +118,17 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
/** 文件夹列表查询令牌 - 查询所有文件夹(用于移动操作) */
private static final int FOLDER_LIST_QUERY_TOKEN = 1;
// ======================= 上下文菜单项ID常量 =======================
// ======================= 文件夹上下文菜单常量 =======================
private static final int MENU_FOLDER_DELETE = 0; // 删除文件夹
private static final int MENU_FOLDER_VIEW = 1; // 查看文件夹
private static final int MENU_FOLDER_CHANGE_NAME = 2; // 重命名文件夹
// ======================= 便签上下文菜单常量 =======================
private static final int MENU_NOTE_PIN = 3; // 置顶便签
private static final int MENU_NOTE_UNPIN = 4; // 取消置顶便签
private static final int MENU_NOTE_BATCH_SELECT = 5; // 批量选择
// ======================= 偏好设置常量 =======================
@ -183,6 +194,9 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
/** 当前焦点便签数据项 - 用于上下文菜单和长按操作 */
private NoteItemData mFocusNoteDataItem;
/** 当前选中的标签ID - 用于按标签筛选便签,-1表示不筛选 */
// ======================= 数据库查询条件常量 =======================
/** 普通文件夹查询条件 - 查询指定父文件夹下的便签 */
@ -243,7 +257,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
* R.raw.introduction便
*/
private void setAppInfoFromRawRes() {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
SharedPreferences sp = getSharedPreferences(NotesPreferenceActivity.PREFERENCE_NAME, Context.MODE_PRIVATE);
// 检查是否已添加过介绍
if (!sp.getBoolean(PREFERENCE_ADD_INTRODUCTION, false)) {
StringBuilder sb = new StringBuilder();
@ -326,8 +340,12 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
mDispatchY = 0;
mOriginY = 0;
mTitleBar = (TextView) findViewById(R.id.tv_title_bar);
mState = ListEditState.NOTE_LIST; // 初始状态为便签列表
mModeCallBack = new ModeCallback(); // 创建多选模式回调
// 更新标题栏
updateTitleBar();
}
// ======================= 多选模式回调类 =======================
@ -358,6 +376,13 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
mMoveMenu.setVisible(true);
mMoveMenu.setOnMenuItemClickListener(this);
}
// 设置置顶和取消置顶菜单项的点击监听器
MenuItem pinMenuItem = menu.findItem(R.id.pin);
MenuItem unpinMenuItem = menu.findItem(R.id.unpin);
pinMenuItem.setOnMenuItemClickListener(this);
unpinMenuItem.setOnMenuItemClickListener(this);
mActionMode = mode;
mNotesListAdapter.setChoiceMode(true); // 进入选择模式
mNotesListView.setLongClickable(false); // 禁用长按
@ -473,6 +498,12 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
} else if (itemId == R.id.move) {
// 查询目标文件夹
startQueryDestinationFolders();
} else if (itemId == R.id.pin) {
// 批量置顶选中的便签
batchPinNotes(true);
} else if (itemId == R.id.unpin) {
// 批量取消置顶选中的便签
batchPinNotes(false);
} else {
return false;
}
@ -554,10 +585,11 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
private void startAsyncNotesListQuery() {
String selection = (mCurrentFolderId == Notes.ID_ROOT_FOLDER) ? ROOT_FOLDER_SELECTION
: NORMAL_SELECTION;
String[] selectionArgs = { String.valueOf(mCurrentFolderId) };
mBackgroundQueryHandler.startQuery(FOLDER_NOTE_LIST_QUERY_TOKEN, null,
Notes.CONTENT_NOTE_URI, NoteItemData.PROJECTION, selection, new String[] {
String.valueOf(mCurrentFolderId)
}, NoteColumns.TYPE + " DESC," + NoteColumns.MODIFIED_DATE + " DESC");
Notes.CONTENT_NOTE_URI, NoteItemData.PROJECTION, selection, selectionArgs,
NoteColumns.PINNED + " DESC," + NoteColumns.TYPE + " DESC," + NoteColumns.MODIFIED_DATE + " DESC");
}
/**
@ -640,21 +672,60 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
protected HashSet<AppWidgetAttribute> doInBackground(Void... unused) {
// 获取选中便签关联的小部件
HashSet<AppWidgetAttribute> widgets = mNotesListAdapter.getSelectedWidget();
if (!isSyncMode()) {
// 非同步模式:直接删除
if (DataUtils.batchDeleteNotes(mContentResolver, mNotesListAdapter
.getSelectedItemIds())) {
// 删除成功
} else {
Log.e(TAG, "Delete notes error, should not happens");
// 统一使用永久删除方式
if (!DataUtils.batchDeleteNotes(mContentResolver, mNotesListAdapter.getSelectedItemIds())) {
Log.e(TAG, "Delete notes error, should not happens");
}
return widgets;
}
@Override
protected void onPostExecute(HashSet<AppWidgetAttribute> widgets) {
// 更新关联的小部件
if (widgets != null) {
for (AppWidgetAttribute widget : widgets) {
if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID
&& widget.widgetType != Notes.TYPE_WIDGET_INVALIDE) {
updateWidget(widget.widgetId, widget.widgetType);
}
}
} else {
// 同步模式:移动到回收站
if (!DataUtils.batchMoveToFolder(mContentResolver, mNotesListAdapter
.getSelectedItemIds(), Notes.ID_TRASH_FOLER)) {
Log.e(TAG, "Move notes to trash folder error, should not happens");
}
mModeCallBack.finishActionMode();
}
}.execute();
}
/**
* 便
* @param pin true: , false:
*/
private void batchPinNotes(final boolean pin) {
new AsyncTask<Void, Void, HashSet<AppWidgetAttribute>>() {
protected HashSet<AppWidgetAttribute> doInBackground(Void... unused) {
// 获取选中便签关联的小部件
HashSet<AppWidgetAttribute> widgets = mNotesListAdapter.getSelectedWidget();
// 批量设置置顶状态
ContentValues values = new ContentValues();
values.put(NoteColumns.PINNED, pin ? 1 : 0);
values.put(NoteColumns.LOCAL_MODIFIED, 1);
StringBuffer selection = new StringBuffer();
HashSet<Long> ids = mNotesListAdapter.getSelectedItemIds();
String[] selectionArgs = new String[ids.size()];
int i = 0;
for (Long id : ids) {
if (i > 0) {
selection.append(" OR ");
}
selection.append(NoteColumns.ID + "=?");
selectionArgs[i++] = String.valueOf(id);
}
mContentResolver.update(Notes.CONTENT_NOTE_URI, values, selection.toString(), selectionArgs);
return widgets;
}
@ -676,6 +747,50 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
// ======================= 删除文件夹 =======================
/**
* 便
* @param noteId 便ID
* @param currentPinned
*/
private void togglePinNote(long noteId, boolean currentPinned) {
ContentValues values = new ContentValues();
values.put(NoteColumns.PINNED, currentPinned ? 0 : 1);
values.put(NoteColumns.LOCAL_MODIFIED, 1);
mContentResolver.update(Notes.CONTENT_NOTE_URI, values, NoteColumns.ID + "=?",
new String[] { String.valueOf(noteId) });
}
/**
*
*/
/**
*
*/
private void updateTitleBar() {
String title = getString(R.string.app_name);
// 如果不是根文件夹,显示文件夹名称
if (mCurrentFolderId != Notes.ID_ROOT_FOLDER) {
Cursor cursor = mContentResolver.query(Notes.CONTENT_NOTE_URI,
new String[] { NoteColumns.SNIPPET },
NoteColumns.ID + "=?",
new String[] { String.valueOf(mCurrentFolderId) },
null);
if (cursor != null && cursor.moveToFirst()) {
title = cursor.getString(0);
cursor.close();
}
}
mTitleBar.setText(title);
mTitleBar.setVisibility(View.VISIBLE);
}
/**
*
* @param folderId ID
@ -946,6 +1061,22 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
}
}
};
private final OnCreateContextMenuListener mNoteOnCreateContextMenuListener = new OnCreateContextMenuListener() {
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
if (mFocusNoteDataItem != null) {
menu.setHeaderTitle(R.string.app_name);
// 根据当前置顶状态添加不同的菜单项
if (mFocusNoteDataItem.isPinned()) {
menu.add(0, MENU_NOTE_UNPIN, 0, R.string.menu_unpin);
} else {
menu.add(0, MENU_NOTE_PIN, 0, R.string.menu_pin);
}
// 添加批量选择菜单项
menu.add(0, MENU_NOTE_BATCH_SELECT, 1, R.string.menu_batch_select);
}
}
};
/**
*
@ -985,7 +1116,18 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
builder.show();
} else if (itemId == MENU_FOLDER_CHANGE_NAME) {
showCreateOrModifyFolderDialog(false);
} else if (itemId == MENU_NOTE_PIN) {
togglePinNote(mFocusNoteDataItem.getId(), false);
} else if (itemId == MENU_NOTE_UNPIN) {
togglePinNote(mFocusNoteDataItem.getId(), true);
} else if (itemId == MENU_NOTE_BATCH_SELECT) {
// 进入批量选择模式
// 启动多选模式
mModeCallBack = new ModeCallback();
mNotesListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
mNotesListView.setMultiChoiceModeListener(mModeCallBack);
}
return true;
}
@ -1202,14 +1344,10 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
if (view instanceof NotesListItem) {
mFocusNoteDataItem = ((NotesListItem) view).getItemData();
// 便签长按:启动多选模式
if (mFocusNoteDataItem.getType() == Notes.TYPE_NOTE && !mNotesListAdapter.isInChoiceMode()) {
if (mNotesListView.startActionMode(mModeCallBack) != null) {
mModeCallBack.onItemCheckedStateChanged(null, position, id, true);
mNotesListView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
} else {
Log.e(TAG, "startActionMode fails");
}
if (mFocusNoteDataItem.getType() == Notes.TYPE_NOTE) {
// 便签长按:显示上下文菜单
mNotesListView.setOnCreateContextMenuListener(mNoteOnCreateContextMenuListener);
} else if (mFocusNoteDataItem.getType() == Notes.TYPE_FOLDER) {
// 文件夹长按:显示上下文菜单
mNotesListView.setOnCreateContextMenuListener(mFolderOnCreateContextMenuListener);

@ -53,6 +53,9 @@ public class NotesListItem extends LinearLayout {
/** 提醒图标 - 显示便签是否有提醒 */
private ImageView mAlert;
/** 置顶图标 - 显示便签是否置顶 */
private ImageView mPinned;
/** 标题文本 - 显示便签摘要或文件夹名称 */
private TextView mTitle;
@ -84,6 +87,7 @@ public class NotesListItem extends LinearLayout {
// 2. 查找并保存子视图引用
mAlert = (ImageView) findViewById(R.id.iv_alert_icon);
mPinned = (ImageView) findViewById(R.id.iv_pinned_icon);
mTitle = (TextView) findViewById(R.id.tv_title);
mTime = (TextView) findViewById(R.id.tv_time);
mCallName = (TextView) findViewById(R.id.tv_name);
@ -216,6 +220,12 @@ public class NotesListItem extends LinearLayout {
} else {
mAlert.setVisibility(View.GONE);
}
// 根据是否置顶设置置顶图标
if (data.isPinned()) {
mPinned.setVisibility(View.VISIBLE);
} else {
mPinned.setVisibility(View.GONE);
}
}
}

@ -41,6 +41,7 @@ import android.preference.Preference; // 偏好设置项
import android.preference.Preference.OnPreferenceClickListener; // 偏好设置点击监听
import android.preference.PreferenceActivity; // 偏好设置Activity基类
import android.preference.PreferenceCategory; // 偏好设置分类
import android.preference.PreferenceManager; // 偏好设置管理器
// Android工具
import android.text.TextUtils; // 文本工具
import android.text.format.DateFormat; // 日期格式化
@ -49,6 +50,7 @@ import android.view.LayoutInflater; // 布局加载器
import android.view.Menu; // 菜单
import android.view.MenuItem; // 菜单项
import android.view.View; // 视图基类
import android.widget.ListView; // 列表视图
// Android控件
import android.widget.Button; // 按钮
import android.widget.TextView; // 文本视图
@ -61,6 +63,8 @@ import net.micode.notes.data.Notes; // Notes主类
import net.micode.notes.data.Notes.NoteColumns; // 便签表列定义
// 应用同步服务
import net.micode.notes.gtask.remote.GTaskSyncService; // Google任务同步服务
// 应用工具
import net.micode.notes.tool.SearchHistoryManager; // 搜索历史管理器
// ======================= 便签设置Activity =======================
/**
@ -114,14 +118,25 @@ public class NotesPreferenceActivity extends PreferenceActivity {
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
/* 使用应用图标作为导航按钮 */
getActionBar().setDisplayHomeAsUpEnabled(true);
// 从XML加载偏好设置
addPreferencesFromResource(R.xml.preferences);
// 获取账户设置分类
mAccountCategory = (PreferenceCategory) findPreference(PREFERENCE_SYNC_ACCOUNT_KEY);
// 设置清除搜索历史按钮的点击事件
Preference clearSearchHistoryPref = findPreference("pref_key_clear_search_history");
if (clearSearchHistoryPref != null) {
clearSearchHistoryPref.setOnPreferenceClickListener(new OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
// 清除搜索历史
SearchHistoryManager.getInstance(NotesPreferenceActivity.this).clearSearchHistory();
Toast.makeText(NotesPreferenceActivity.this,
"Search history cleared", Toast.LENGTH_SHORT).show();
return true;
}
});
}
// 创建并注册同步广播接收器
mReceiver = new GTaskReceiver();
IntentFilter filter = new IntentFilter();
@ -131,7 +146,16 @@ public class NotesPreferenceActivity extends PreferenceActivity {
mOriAccounts = null; // 初始化原始账户列表
// 添加设置界面头部
View header = LayoutInflater.from(this).inflate(R.layout.settings_header, null);
getListView().addHeaderView(header, null, true);
ListView listView = getListView();
if (listView != null) {
listView.addHeaderView(header, null, true);
}
/* 使用应用图标作为导航按钮 */
ActionBar actionBar = getActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
}
}
/**

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
============================================================================
selector_color_btn.xml - 按钮颜色选择器资源文件
============================================================================
功能:定义按钮在不同状态下的文本颜色
用途:应用于便签应用中的按钮,提供视觉反馈
文件位置res/color/selector_color_btn.xml
============================================================================
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.
============================================================================
-->
<!--
======================= 颜色选择器定义 =======================
功能:根据按钮状态切换文本颜色
原理:系统根据按钮当前状态自动选择匹配的颜色
状态优先级:按定义顺序从上到下匹配,第一个匹配的状态生效
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!--
======================= 按下状态 =======================
状态android:state_pressed="true"
颜色:#88555555
说明:
- 当用户按下按钮时应用此颜色
- 颜色值:#88555555
- 前两位(88): Alpha通道约50%透明度
- 后六位(555555): RGB颜色深灰色
- 视觉反馈:半透明深灰色,表示按钮被按下
应用场景:用户触摸按钮时的即时反馈
-->
<item android:state_pressed="true" android:color="#88555555" />
<!--
======================= 选中状态 =======================
状态android:state_selected="true"
颜色:#ff999999
说明:
- 当按钮被选中时应用此颜色
- 颜色值:#ff999999
- 前两位(ff): Alpha通道完全不透明
- 后六位(999999): RGB颜色中灰色
- 视觉反馈:中灰色,表示按钮被选中
应用场景:导航选中、标签选中等状态
-->
<item android:state_selected="true" android:color="#ff999999" />
<!--
======================= 默认状态 =======================
状态:无状态条件(默认项)
颜色:#ff000000
说明:
- 当按钮处于普通状态时应用此颜色
- 颜色值:#ff000000
- 前两位(ff): Alpha通道完全不透明
- 后六位(000000): RGB颜色纯黑色
- 视觉反馈:纯黑色文本,正常显示
应用场景:按钮的默认外观
注意:必须放在最后,作为默认状态
-->
<item android:color="#ff000000" />
</selector>

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
============================================================================
selector_color_footer.xml - 底部区域颜色选择器资源文件
============================================================================
功能:定义便签列表底部区域的背景或文本颜色
用途:应用于便签列表的底部视图,提供半透明遮罩效果
文件位置res/color/selector_color_footer.xml
============================================================================
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.
============================================================================
-->
<!--
======================= 单状态颜色选择器 =======================
功能:提供固定的半透明黑色颜色
特点:只有默认状态,无状态切换
用途:创建半透明的遮罩或背景效果
设计理念:简约、一致、功能明确
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!--
======================= 唯一颜色项 =======================
状态:无状态条件(始终应用此颜色)
颜色:#50000000
说明:
- 这是一个固定颜色,不随状态变化
- 颜色值:#50000000
- 前两位(50): Alpha通道31.25%透明度
- 十六进制0x50 = 十进制 80
- 透明度80/255 ≈ 31.37%
- 后六位(000000): RGB颜色纯黑色
- 视觉效果31%透明度的黑色遮罩
应用场景:
1. 列表底部渐变遮罩
2. 半透明背景层
3. 视觉分隔效果
设计目的:
1. 创建视觉层次
2. 提供内容聚焦
3. 增强界面深度
-->
<item android:color="#50000000" />
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 554 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save