Compare commits

..

24 Commits
main ... master

@ -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>

@ -1,2 +0,0 @@
# git

@ -59,7 +59,6 @@ public class Notes {
* {@link Notes#ID_ROOT_FOLDER } * {@link Notes#ID_ROOT_FOLDER }
* {@link Notes#ID_TEMPARAY_FOLDER } * {@link Notes#ID_TEMPARAY_FOLDER }
* {@link Notes#ID_CALL_RECORD_FOLDER} * {@link Notes#ID_CALL_RECORD_FOLDER}
* {@link Notes#ID_TRASH_FOLER}/
*/ */
// 根文件夹ID默认文件夹所有无指定文件夹的笔记默认归属此文件夹 // 根文件夹ID默认文件夹所有无指定文件夹的笔记默认归属此文件夹
public static final int ID_ROOT_FOLDER = 0; public static final int ID_ROOT_FOLDER = 0;
@ -67,7 +66,7 @@ public class Notes {
public static final int ID_TEMPARAY_FOLDER = -1; public static final int ID_TEMPARAY_FOLDER = -1;
// 通话记录文件夹ID专门存储通话记录类型的笔记 // 通话记录文件夹ID专门存储通话记录类型的笔记
public static final int ID_CALL_RECORD_FOLDER = -2; public static final int ID_CALL_RECORD_FOLDER = -2;
// 回收站文件夹ID存放被用户删除的笔记或文件夹 // 回收站文件夹ID已废弃,用于兼容旧代码
public static final int ID_TRASH_FOLER = -3; public static final int ID_TRASH_FOLER = -3;
/** /**
@ -207,6 +206,12 @@ public class Notes {
* <P> : INTEGER </P> * <P> : INTEGER </P>
*/ */
public static final String TYPE = "type"; public static final String TYPE = "type";
/**
* 01
* <P> : INTEGER </P>
*/
public static final String PINNED = "pinned";
/** /**
* IDGTask * IDGTask
@ -220,6 +225,8 @@ public class Notes {
*/ */
public static final String LOCAL_MODIFIED = "local_modified"; public static final String LOCAL_MODIFIED = "local_modified";
/** /**
* ID * ID
* <P> : INTEGER (long) </P> * <P> : INTEGER (long) </P>
@ -381,4 +388,6 @@ public class Notes {
*/ */
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/call_note"); 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"; 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 { public interface TABLE {
// 笔记/文件夹表名称 // 笔记/文件夹表名称
@ -90,6 +90,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
NoteColumns.NOTES_COUNT + " INTEGER NOT NULL DEFAULT 0," + // 文件夹下的笔记数量 NoteColumns.NOTES_COUNT + " INTEGER NOT NULL DEFAULT 0," + // 文件夹下的笔记数量
NoteColumns.SNIPPET + " TEXT NOT NULL DEFAULT ''," + // 文件夹名称/笔记摘要 NoteColumns.SNIPPET + " TEXT NOT NULL DEFAULT ''," + // 文件夹名称/笔记摘要
NoteColumns.TYPE + " INTEGER NOT NULL DEFAULT 0," + // 类型(笔记/文件夹/系统) 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_ID + " INTEGER NOT NULL DEFAULT 0," + // 关联的Widget ID
NoteColumns.WIDGET_TYPE + " INTEGER NOT NULL DEFAULT -1," + // 关联的Widget类型 NoteColumns.WIDGET_TYPE + " INTEGER NOT NULL DEFAULT -1," + // 关联的Widget类型
NoteColumns.SYNC_ID + " INTEGER NOT NULL DEFAULT 0," + // 同步IDGTask NoteColumns.SYNC_ID + " INTEGER NOT NULL DEFAULT 0," + // 同步IDGTask
@ -122,10 +123,12 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
* dataNOTE_IDSQL * dataNOTE_IDSQL
* NOTE_IDdata * NOTE_IDdata
*/ */
private static final String CREATE_DATA_NOTE_ID_INDEX_SQL = private static final String CREATE_DATA_NOTE_ID_INDEX_SQL =
"CREATE INDEX IF NOT EXISTS note_id_index ON " + "CREATE INDEX IF NOT EXISTS note_id_index ON " +
TABLE.DATA + "(" + DataColumns.NOTE_ID + ");"; TABLE.DATA + "(" + DataColumns.NOTE_ID + ");";
// ====================== 数据库触发器SQL语句note表 ====================== // ====================== 数据库触发器SQL语句note表 ======================
/** /**
* ID * ID
@ -255,20 +258,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
" WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" + " WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" +
" END"; " 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 * 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 delete_data_on_delete");
db.execSQL("DROP TRIGGER IF EXISTS increase_folder_count_on_insert"); 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_delete_notes_on_delete");
db.execSQL("DROP TRIGGER IF EXISTS folder_move_notes_on_trash");
// 创建新触发器 // 创建新触发器
db.execSQL(NOTE_INCREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER); 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_DELETE_DATA_ON_DELETE_TRIGGER);
db.execSQL(NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER); db.execSQL(NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER);
db.execSQL(FOLDER_DELETE_NOTES_ON_DELETE_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); values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values); 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"); Log.d(TAG, "data table has been created");
} }
/** /**
* data * data
* *
@ -454,6 +438,16 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
upgradeToV4(db); upgradeToV4(db);
oldVersion++; 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) { if (reCreateTriggers) {
@ -468,6 +462,8 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
} }
} }
/** /**
* 12 * 12
* *
@ -486,8 +482,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
/** /**
* 23 * 23
* 1. * 1.
* 2. noteGTASK_ID * 2. noteGTASK_ID
* 3.
* *
* @param db SQLiteDatabase * @param db SQLiteDatabase
*/ */
@ -499,11 +494,6 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
// 为note表添加GTASK_ID列用于GTask同步 // 为note表添加GTASK_ID列用于GTask同步
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.GTASK_ID db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.GTASK_ID
+ " TEXT NOT NULL DEFAULT ''"); + " 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 android.util.Log;
import net.micode.notes.R; import net.micode.notes.R;
import net.micode.notes.tool.SearchHistoryManager;
import net.micode.notes.data.Notes.DataColumns; import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.NoteColumns; import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.NotesDatabaseHelper.TABLE; import net.micode.notes.data.NotesDatabaseHelper.TABLE;
@ -91,6 +92,7 @@ public class NotesProvider extends ContentProvider {
*/ */
private static final int URI_SEARCH_SUGGEST = 6; private static final int URI_SEARCH_SUGGEST = 6;
/** /**
* UriMatcherUri * UriMatcherUri
* authority + path -> * authority + path ->
@ -112,6 +114,7 @@ public class NotesProvider extends ContentProvider {
mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, URI_SEARCH_SUGGEST); mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, URI_SEARCH_SUGGEST);
// 匹配搜索建议带关键词content://micode_notes/suggestions/query/关键词 -> URI_SEARCH_SUGGEST // 匹配搜索建议带关键词content://micode_notes/suggestions/query/关键词 -> URI_SEARCH_SUGGEST
mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_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"); searchString = uri.getQueryParameter("pattern");
} }
// 关键词为空时返回null // 如果是搜索建议类型,且搜索关键词不为空,返回合并结果
if (TextUtils.isEmpty(searchString)) { if (mMatcher.match(uri) == URI_SEARCH_SUGGEST && !TextUtils.isEmpty(searchString)) {
return null; try {
} // 1. 获取搜索历史记录
SearchHistoryManager historyManager = SearchHistoryManager.getInstance(getContext());
try { java.util.List<String> historyList = historyManager.getSearchHistoryList();
// 拼接SQL的LIKE关键词%表示任意字符,如%笔记%
searchString = String.format("%%%s%%", searchString); // 2. 获取便签搜索结果
// 执行原生SQL查询获取搜索结果 String likeSearchString = String.format("%%%s%%", searchString);
c = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY, Cursor noteCursor = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY, new String[] { likeSearchString });
new String[] { searchString });
} catch (IllegalStateException ex) { // 3. 创建矩阵游标,用于合并结果
// 捕获异常,输出错误日志 String[] columns = { NoteColumns.ID, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA,
Log.e(TAG, "got exception: " + ex.toString()); 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; break;
default: default:
// 未知Uri抛出异常 // 未知Uri抛出异常
throw new IllegalArgumentException("Unknown URI " + uri); throw new IllegalArgumentException("Unknown URI " + uri);
@ -275,6 +337,7 @@ public class NotesProvider extends ContentProvider {
// 插入data表获取插入的ID // 插入data表获取插入的ID
insertedId = dataId = db.insert(TABLE.DATA, null, values); insertedId = dataId = db.insert(TABLE.DATA, null, values);
break; break;
default: default:
// 未知Uri抛出异常 // 未知Uri抛出异常
throw new IllegalArgumentException("Unknown URI " + uri); throw new IllegalArgumentException("Unknown URI " + uri);
@ -292,6 +355,8 @@ public class NotesProvider extends ContentProvider {
ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), null); ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), null);
} }
// 返回包含插入ID的新Uri // 返回包含插入ID的新Uri
return ContentUris.withAppendedId(uri, insertedId); return ContentUris.withAppendedId(uri, insertedId);
} }
@ -311,11 +376,12 @@ public class NotesProvider extends ContentProvider {
// 获取可写的SQLiteDatabase对象 // 获取可写的SQLiteDatabase对象
SQLiteDatabase db = mHelper.getWritableDatabase(); SQLiteDatabase db = mHelper.getWritableDatabase();
boolean deleteData = false; // 标记是否删除的是data表数据 boolean deleteData = false; // 标记是否删除的是data表数据
long noteId = 0; // 用于存储便签ID以便发送通知
// 根据Uri匹配的类型执行删除逻辑 // 根据Uri匹配的类型执行删除逻辑
switch (mMatcher.match(uri)) { switch (mMatcher.match(uri)) {
case URI_NOTE: case URI_NOTE:
// 删除note表数据条件传入的selection + ID>0排除系统文件夹 // 直接删除便签条件传入的selection + ID>0排除系统文件夹
selection = "(" + selection + ") AND " + NoteColumns.ID + ">0 "; selection = "(" + selection + ") AND " + NoteColumns.ID + ">0 ";
count = db.delete(TABLE.NOTE, selection, selectionArgs); count = db.delete(TABLE.NOTE, selection, selectionArgs);
break; break;
@ -325,13 +391,12 @@ public class NotesProvider extends ContentProvider {
/** /**
* ID0 * ID0
*/ */
long noteId = Long.valueOf(id); noteId = Long.valueOf(id);
if (noteId <= 0) { if (noteId <= 0) {
break; break;
} }
// 删除note表单条数据条件ID=id + 传入的selection // 直接删除便签条件ID=id + 传入的selection
count = db.delete(TABLE.NOTE, count = db.delete(TABLE.NOTE, NoteColumns.ID + "=" + id + parseSelection(selection), selectionArgs);
NoteColumns.ID + "=" + id + parseSelection(selection), selectionArgs);
break; break;
case URI_DATA: case URI_DATA:
// 删除data表数据 // 删除data表数据
@ -345,6 +410,9 @@ public class NotesProvider extends ContentProvider {
DataColumns.ID + "=" + id + parseSelection(selection), selectionArgs); DataColumns.ID + "=" + id + parseSelection(selection), selectionArgs);
deleteData = true; deleteData = true;
break; break;
default: default:
throw new IllegalArgumentException("Unknown URI " + uri); throw new IllegalArgumentException("Unknown URI " + uri);
} }
@ -355,6 +423,11 @@ public class NotesProvider extends ContentProvider {
if (deleteData) { if (deleteData) {
getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null); getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null);
} }
// 如果是便签相关操作通知对应的便签Uri
if (noteId > 0) {
getContext().getContentResolver().notifyChange(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null);
}
// 通知当前Uri的数据变更 // 通知当前Uri的数据变更
getContext().getContentResolver().notifyChange(uri, null); getContext().getContentResolver().notifyChange(uri, null);
} }
@ -377,6 +450,7 @@ public class NotesProvider extends ContentProvider {
// 获取可写的SQLiteDatabase对象 // 获取可写的SQLiteDatabase对象
SQLiteDatabase db = mHelper.getWritableDatabase(); SQLiteDatabase db = mHelper.getWritableDatabase();
boolean updateData = false; // 标记是否更新的是data表数据 boolean updateData = false; // 标记是否更新的是data表数据
long noteId = 0; // 用于存储便签ID以便发送通知
// 根据Uri匹配的类型执行更新逻辑 // 根据Uri匹配的类型执行更新逻辑
switch (mMatcher.match(uri)) { switch (mMatcher.match(uri)) {
@ -405,6 +479,9 @@ public class NotesProvider extends ContentProvider {
+ parseSelection(selection), selectionArgs); + parseSelection(selection), selectionArgs);
updateData = true; updateData = true;
break; break;
default: default:
throw new IllegalArgumentException("Unknown URI " + uri); throw new IllegalArgumentException("Unknown URI " + uri);
} }
@ -415,6 +492,11 @@ public class NotesProvider extends ContentProvider {
if (updateData) { if (updateData) {
getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null); getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null);
} }
// 如果是便签相关操作通知对应的便签Uri
if (noteId > 0) {
getContext().getContentResolver().notifyChange(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null);
}
// 通知当前Uri的数据变更 // 通知当前Uri的数据变更
getContext().getContentResolver().notifyChange(uri, null); getContext().getContentResolver().notifyChange(uri, null);
} }

@ -1,4 +1,3 @@
/* /*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
* *
@ -18,11 +17,15 @@
package net.micode.notes.gtask.remote; package net.micode.notes.gtask.remote;
import android.app.Notification; import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Build;
import androidx.core.app.NotificationCompat;
import net.micode.notes.R; import net.micode.notes.R;
import net.micode.notes.ui.NotesListActivity; import net.micode.notes.ui.NotesListActivity;
@ -32,6 +35,7 @@ import net.micode.notes.ui.NotesPreferenceActivity;
public class GTaskASyncTask extends AsyncTask<Void, String, Integer> { public class GTaskASyncTask extends AsyncTask<Void, String, Integer> {
private static int GTASK_SYNC_NOTIFICATION_ID = 5234235; private static int GTASK_SYNC_NOTIFICATION_ID = 5234235;
private static final String CHANNEL_ID = "gtask_sync_channel"; // 新增通知渠道ID
public interface OnCompleteListener { public interface OnCompleteListener {
void onComplete(); void onComplete();
@ -51,6 +55,23 @@ public class GTaskASyncTask extends AsyncTask<Void, String, Integer> {
mNotifiManager = (NotificationManager) mContext mNotifiManager = (NotificationManager) mContext
.getSystemService(Context.NOTIFICATION_SERVICE); .getSystemService(Context.NOTIFICATION_SERVICE);
mTaskManager = GTaskManager.getInstance(); 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() { public void cancelSync() {
@ -59,27 +80,34 @@ public class GTaskASyncTask extends AsyncTask<Void, String, Integer> {
public void publishProgess(String message) { public void publishProgess(String message) {
publishProgress(new String[] { publishProgress(new String[] {
message message
}); });
} }
private void showNotification(int tickerId, String content) { private void showNotification(int tickerId, String content) {
Notification notification = new Notification(R.drawable.notification, mContext // 替换为NotificationCompat.Builder构建通知
.getString(tickerId), System.currentTimeMillis()); NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext, CHANNEL_ID)
notification.defaults = Notification.DEFAULT_LIGHTS; .setSmallIcon(R.drawable.notification) // 保持原图标
notification.flags = Notification.FLAG_AUTO_CANCEL; .setContentTitle(mContext.getString(R.string.app_name)) // 原标题
.setContentText(content) // 原内容
.setTicker(mContext.getString(tickerId)) // 原滚动提示文字
.setWhen(System.currentTimeMillis()) // 原时间戳
.setDefaults(Notification.DEFAULT_LIGHTS) // 保持原灯光效果
.setAutoCancel(true); // 保持点击自动取消
// 设置跳转意图(保持原逻辑)
PendingIntent pendingIntent; PendingIntent pendingIntent;
if (tickerId != R.string.ticker_success) { if (tickerId != R.string.ticker_success) {
pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext, pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext,
NotesPreferenceActivity.class), 0); NotesPreferenceActivity.class), PendingIntent.FLAG_IMMUTABLE); // 适配高版本
} else { } else {
pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext, pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext,
NotesListActivity.class), 0); NotesListActivity.class), PendingIntent.FLAG_IMMUTABLE); // 适配高版本
} }
notification.setLatestEventInfo(mContext, mContext.getString(R.string.app_name), content, builder.setContentIntent(pendingIntent);
pendingIntent);
mNotifiManager.notify(GTASK_SYNC_NOTIFICATION_ID, notification); // 发送通知保持原ID
mNotifiManager.notify(GTASK_SYNC_NOTIFICATION_ID, builder.build());
} }
@Override @Override
@ -120,4 +148,4 @@ public class GTaskASyncTask extends AsyncTask<Void, String, Integer> {
}).start(); }).start();
} }
} }
} }

@ -29,6 +29,7 @@ import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.DataConstants; import net.micode.notes.data.Notes.DataConstants;
import net.micode.notes.data.Notes.NoteColumns; import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.Notes.TextNote; import net.micode.notes.data.Notes.TextNote;
import net.micode.notes.tool.ResourceParser.NoteBgResources; import net.micode.notes.tool.ResourceParser.NoteBgResources;
@ -188,6 +189,19 @@ public class WorkingNote {
} }
public synchronized boolean saveNote() { 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 (isWorthSaving()) {
if (!existInDatabase()) { if (!existInDatabase()) {
if ((mNoteId = Note.getNewNoteId(mContext, mFolderId)) == 0) { if ((mNoteId = Note.getNewNoteId(mContext, mFolderId)) == 0) {
@ -195,8 +209,12 @@ public class WorkingNote {
return false; return false;
} }
} }
mNote.syncNote(mContext, mNoteId); // 更新便签数据
mNote.setTextData(DataColumns.CONTENT, mContent);
// 同步到数据库
boolean result = mNote.syncNote(mContext, mNoteId);
/** /**
* Update widget content if there exist any widget of this note * Update widget content if there exist any widget of this note
@ -206,7 +224,7 @@ public class WorkingNote {
&& mNoteSettingStatusListener != null) { && mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onWidgetChanged(); mNoteSettingStatusListener.onWidgetChanged();
} }
return true; return result;
} else { } else {
return false; return false;
} }
@ -217,12 +235,24 @@ public class WorkingNote {
} }
private boolean isWorthSaving() { private boolean isWorthSaving() {
if (mIsDeleted || (!existInDatabase() && TextUtils.isEmpty(mContent)) if (mIsDeleted) {
|| (existInDatabase() && !mNote.isLocalModified())) { 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 false;
} else {
return true;
} }
return true;
} }
public void setOnSettingStatusChangedListener(NoteSettingChangedListener l) { public void setOnSettingStatusChangedListener(NoteSettingChangedListener l) {
@ -341,6 +371,8 @@ public class WorkingNote {
public int getWidgetType() { public int getWidgetType() {
return mWidgetType; return mWidgetType;
} }
public interface NoteSettingChangedListener { public interface NoteSettingChangedListener {
/** /**

@ -0,0 +1,528 @@
/*
* 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.
*/
// BackupUtils.java - 小米便签备份工具类
// 主要功能:将便签数据导出为文本文件格式,方便用户备份和查看
package net.micode.notes.tool;
// ======================= 导入区域 =======================
// Android系统相关类
import android.content.Context; // 上下文用于获取资源、ContentResolver等
import android.database.Cursor; // 数据库查询结果游标
import android.os.Environment; // 环境相关,用于访问外部存储
import android.text.TextUtils; // 文本处理工具类
import android.text.format.DateFormat; // 日期格式化工具
import android.util.Log; // 日志工具
// 应用内部资源
import net.micode.notes.R; // 资源文件R类
// 应用数据模型相关
import net.micode.notes.data.Notes; // Notes主类
import net.micode.notes.data.Notes.DataColumns; // 数据表列定义
import net.micode.notes.data.Notes.DataConstants;// 数据常量定义
import net.micode.notes.data.Notes.NoteColumns; // 便签表列定义
// Java IO相关
import java.io.File; // 文件操作
import java.io.FileNotFoundException; // 文件未找到异常
import java.io.FileOutputStream; // 文件输出流
import java.io.IOException; // IO异常
import java.io.PrintStream; // 打印输出流
// ======================= 备份工具主类 =======================
/**
* BackupUtils - 便
*
* 便
*/
public class BackupUtils {
// TAG - 日志标签用于Logcat日志筛选
private static final String TAG = "BackupUtils";
// ======================= 单例模式实现 =======================
// sInstance - 静态单例实例volatile确保多线程可见性
private static BackupUtils sInstance;
/**
* BackupUtils
* synchronized - 线线
* @param context
* @return BackupUtils
*/
public static synchronized BackupUtils getInstance(Context context) {
// 懒加载:首次调用时创建实例
if (sInstance == null) {
sInstance = new BackupUtils(context);
}
return sInstance;
}
// ======================= 备份状态常量定义 =======================
/**
* /
* 使
*/
public static final int STATE_SD_CARD_UNMOUONTED = 0; // SD卡未挂载无法访问外部存储
public static final int STATE_BACKUP_FILE_NOT_EXIST = 1; // 备份文件不存在
public static final int STATE_DATA_DESTROIED = 2; // 数据损坏,可能被其他程序修改
public static final int STATE_SYSTEM_ERROR = 3; // 系统运行时异常
public static final int STATE_SUCCESS = 4; // 操作成功完成
// mTextExport - 文本导出器实例,实际执行导出操作
private TextExport mTextExport;
/**
* -
* @param context TextExport
*/
private BackupUtils(Context context) {
// 创建文本导出器,传入上下文用于资源访问
mTextExport = new TextExport(context);
}
/**
*
* @return true: SD; false: SD
*/
private static boolean externalStorageAvailable() {
// Environment.MEDIA_MOUNTED - 存储介质已挂载且可读写
return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState());
}
/**
* -
* @return
*/
public int exportToText() {
return mTextExport.exportToText();
}
/**
*
* @return
*/
public String getExportedTextFileName() {
return mTextExport.mFileName;
}
/**
*
* @return
*/
public String getExportedTextFileDir() {
return mTextExport.mFileDirectory;
}
// ======================= 文本导出内部类 =======================
/**
* TextExport -
*
* -> ->
*/
private static class TextExport {
// ======================= 数据库查询配置 =======================
// NOTE_PROJECTION - 便签表查询字段投影
// 指定从数据库查询哪些字段,避免查询不必要的数据
private static final String[] NOTE_PROJECTION = {
NoteColumns.ID, // 0 - 便签ID主键
NoteColumns.MODIFIED_DATE, // 1 - 最后修改时间
NoteColumns.SNIPPET, // 2 - 便签摘要/标题
NoteColumns.TYPE // 3 - 便签类型(文件夹/便签)
};
// 便签表字段索引常量 - 提高代码可读性和维护性
private static final int NOTE_COLUMN_ID = 0; // ID列索引
private static final int NOTE_COLUMN_MODIFIED_DATE = 1; // 修改时间列索引
private static final int NOTE_COLUMN_SNIPPET = 2; // 摘要列索引
// DATA_PROJECTION - 便签数据表查询字段投影
// 便签具体内容存储在数据表中
private static final String[] DATA_PROJECTION = {
DataColumns.CONTENT, // 0 - 便签内容
DataColumns.MIME_TYPE, // 1 - 数据类型(普通便签/通话记录)
DataColumns.DATA1, // 2 - 扩展数据1通话记录时为通话时间
DataColumns.DATA2, // 3 - 扩展数据2
DataColumns.DATA3, // 4 - 扩展数据3
DataColumns.DATA4, // 5 - 扩展数据4通话记录时为电话号码
};
// 数据表字段索引常量
private static final int DATA_COLUMN_CONTENT = 0; // 内容列索引
private static final int DATA_COLUMN_MIME_TYPE = 1; // 数据类型列索引
private static final int DATA_COLUMN_CALL_DATE = 2; // 通话日期列索引
private static final int DATA_COLUMN_PHONE_NUMBER = 4; // 电话号码列索引
// ======================= 文本格式化配置 =======================
// TEXT_FORMAT - 文本格式化模板数组
// 从strings.xml的format_for_exported_note数组加载
private final String [] TEXT_FORMAT;
// 格式化模板索引常量
private static final int FORMAT_FOLDER_NAME = 0; // 文件夹名称格式化模板
private static final int FORMAT_NOTE_DATE = 1; // 便签日期格式化模板
private static final int FORMAT_NOTE_CONTENT = 2; // 便签内容格式化模板
// ======================= 成员变量 =======================
private Context mContext; // 上下文引用,用于访问资源
private String mFileName; // 导出的文件名
private String mFileDirectory; // 导出的文件目录
/**
* TextExport
* @param context
*/
public TextExport(Context context) {
// 从资源文件加载格式化模板
TEXT_FORMAT = context.getResources().getStringArray(R.array.format_for_exported_note);
mContext = context;
// 初始化为空字符串
mFileName = "";
mFileDirectory = "";
}
/**
*
* @param id FORMAT_FOLDER_NAME
* @return
*/
private String getFormat(int id) {
return TEXT_FORMAT[id];
}
/**
* 便
*
* @param folderId ID
* @param ps
*/
private void exportFolderToText(String folderId, PrintStream ps) {
// 查询该文件夹下的所有便签
// Notes.CONTENT_NOTE_URI - 便签内容URI
// NoteColumns.PARENT_ID - 父文件夹ID条件
Cursor notesCursor = mContext.getContentResolver().query(
Notes.CONTENT_NOTE_URI, // 便签表URI
NOTE_PROJECTION, // 查询的字段
NoteColumns.PARENT_ID + "=?", // 查询条件父ID等于指定文件夹
new String[] { folderId }, // 查询参数
null // 排序方式null表示默认
);
// 检查游标有效性
if (notesCursor != null) {
// 遍历查询结果
if (notesCursor.moveToFirst()) {
do {
// 1. 打印便签修改时间
// 使用格式化模板,将时间戳格式化为可读字符串
ps.println(String.format(
getFormat(FORMAT_NOTE_DATE),
DateFormat.format(
mContext.getString(R.string.format_datetime_mdhm), // 日期时间格式
notesCursor.getLong(NOTE_COLUMN_MODIFIED_DATE) // 时间戳
)
));
// 2. 导出该便签的具体内容
String noteId = notesCursor.getString(NOTE_COLUMN_ID);
exportNoteToText(noteId, ps);
} while (notesCursor.moveToNext()); // 继续下一个便签
}
// 关闭游标,释放数据库资源
notesCursor.close();
}
}
/**
* 便
* 便便
* @param noteId 便ID
* @param ps
*/
private void exportNoteToText(String noteId, PrintStream ps) {
// 查询该便签的所有数据项
Cursor dataCursor = mContext.getContentResolver().query(
Notes.CONTENT_DATA_URI, // 便签数据表URI
DATA_PROJECTION, // 查询字段
DataColumns.NOTE_ID + "=?", // 查询条件便签ID
new String[] { noteId }, // 查询参数
null // 排序
);
if (dataCursor != null) {
if (dataCursor.moveToFirst()) {
do {
// 获取数据类型
String mimeType = dataCursor.getString(DATA_COLUMN_MIME_TYPE);
// 根据数据类型分别处理
if (DataConstants.CALL_NOTE.equals(mimeType)) {
// ===== 通话记录类型 =====
// 1. 获取通话相关数据
String phoneNumber = dataCursor.getString(DATA_COLUMN_PHONE_NUMBER);
long callDate = dataCursor.getLong(DATA_COLUMN_CALL_DATE);
String location = dataCursor.getString(DATA_COLUMN_CONTENT);
// 2. 打印电话号码(如果有)
if (!TextUtils.isEmpty(phoneNumber)) {
ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT), phoneNumber));
}
// 3. 打印通话时间
ps.println(String.format(
getFormat(FORMAT_NOTE_CONTENT),
DateFormat.format(
mContext.getString(R.string.format_datetime_mdhm),
callDate
)
));
// 4. 打印位置信息(如果有)
if (!TextUtils.isEmpty(location)) {
ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT), location));
}
} else if (DataConstants.NOTE.equals(mimeType)) {
// ===== 普通便签类型 =====
String content = dataCursor.getString(DATA_COLUMN_CONTENT);
if (!TextUtils.isEmpty(content)) {
ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT), content));
}
}
} while (dataCursor.moveToNext()); // 继续下一个数据项
}
// 关闭游标
dataCursor.close();
}
// 在便签之间添加分隔符
// 使用换行符分隔不同便签,提高可读性
try {
ps.write(new byte[] {
Character.LINE_SEPARATOR, // 行分隔符
Character.LETTER_NUMBER // 字母数字
});
} catch (IOException e) {
// 记录异常,但不中断导出流程
Log.e(TAG, e.toString());
}
}
/**
* 便 -
*
* 1. SD
* 2.
* 3. 便
* 4. 便
* 5.
* @return
*/
public int exportToText() {
// 1. 检查SD卡是否可用
if (!externalStorageAvailable()) {
Log.d(TAG, "Media was not mounted");
return STATE_SD_CARD_UNMOUONTED;
}
// 2. 获取输出流
PrintStream ps = getExportToTextPrintStream();
if (ps == null) {
Log.e(TAG, "get print stream error");
return STATE_SYSTEM_ERROR;
}
// 3. 导出文件夹及其便签
// 查询条件说明:
// - 类型为文件夹 且 不在回收站中
// - 或者 ID是通话记录文件夹
Cursor folderCursor = mContext.getContentResolver().query(
Notes.CONTENT_NOTE_URI, // 便签表
NOTE_PROJECTION, // 查询字段
"(" + NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER + ") OR "
+ NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER,
null, // 查询参数
null // 排序
);
if (folderCursor != null) {
if (folderCursor.moveToFirst()) {
do {
// 获取文件夹名称
String folderName = "";
// 判断是否为通话记录文件夹
if(folderCursor.getLong(NOTE_COLUMN_ID) == Notes.ID_CALL_RECORD_FOLDER) {
// 使用预设的通话记录文件夹名称
folderName = mContext.getString(R.string.call_record_folder_name);
} else {
// 使用便签摘要作为文件夹名称
folderName = folderCursor.getString(NOTE_COLUMN_SNIPPET);
}
// 打印文件夹标题
if (!TextUtils.isEmpty(folderName)) {
ps.println(String.format(getFormat(FORMAT_FOLDER_NAME), folderName));
}
// 导出该文件夹下的所有便签
String folderId = folderCursor.getString(NOTE_COLUMN_ID);
exportFolderToText(folderId, ps);
} while (folderCursor.moveToNext());
}
folderCursor.close();
}
// 4. 导出根目录下的便签(没有父文件夹的便签)
// 查询条件:类型为普通便签 且 父ID为0根目录
Cursor noteCursor = mContext.getContentResolver().query(
Notes.CONTENT_NOTE_URI,
NOTE_PROJECTION,
NoteColumns.TYPE + "=" + Notes.TYPE_NOTE + " AND " + NoteColumns.PARENT_ID + "=0",
null,
null
);
if (noteCursor != null) {
if (noteCursor.moveToFirst()) {
do {
// 打印便签修改时间
ps.println(String.format(
getFormat(FORMAT_NOTE_DATE),
DateFormat.format(
mContext.getString(R.string.format_datetime_mdhm),
noteCursor.getLong(NOTE_COLUMN_MODIFIED_DATE)
)
));
// 导出便签内容
String noteId = noteCursor.getString(NOTE_COLUMN_ID);
exportNoteToText(noteId, ps);
} while (noteCursor.moveToNext());
}
noteCursor.close();
}
// 5. 关闭输出流
ps.close();
return STATE_SUCCESS;
}
/**
*
* SD + +
* @return PrintStream null
*/
private PrintStream getExportToTextPrintStream() {
// 生成输出文件
File file = generateFileMountedOnSDcard(
mContext,
R.string.file_path, // 文件路径资源ID
R.string.file_name_txt_format // 文件名格式资源ID
);
if (file == null) {
Log.e(TAG, "create file to exported failed");
return null;
}
// 保存文件信息
mFileName = file.getName(); // 文件名
mFileDirectory = mContext.getString(R.string.file_path); // 文件目录
PrintStream ps = null;
try {
// 创建文件输出流
FileOutputStream fos = new FileOutputStream(file);
// 创建打印流,方便文本输出
ps = new PrintStream(fos);
} catch (FileNotFoundException e) {
// 文件未找到异常
e.printStackTrace();
return null;
} catch (NullPointerException e) {
// 空指针异常
e.printStackTrace();
return null;
}
return ps;
}
}
// ======================= 静态工具方法 =======================
/**
* SD
* 使
* @param context
* @param filePathResId ID"/backup/"
* @param fileNameFormatResId ID"notes_%s.txt"
* @return null
*/
private static File generateFileMountedOnSDcard(
Context context,
int filePathResId,
int fileNameFormatResId
) {
// 使用StringBuilder构建文件路径提高性能
StringBuilder sb = new StringBuilder();
// 1. SD卡根目录
sb.append(Environment.getExternalStorageDirectory());
// 2. 应用指定的文件路径
sb.append(context.getString(filePathResId));
// 创建目录对象
File filedir = new File(sb.toString());
// 3. 添加文件名(使用当前日期格式化)
// 格式如notes_20231215.txt
sb.append(context.getString(
fileNameFormatResId,
DateFormat.format(
context.getString(R.string.format_date_ymd), // 日期格式
System.currentTimeMillis() // 当前时间戳
)
));
// 创建文件对象
File file = new File(sb.toString());
try {
// 创建目录(如果不存在)
if (!filedir.exists()) {
filedir.mkdir(); // 创建单级目录
}
// 创建文件(如果不存在)
if (!file.exists()) {
file.createNewFile();
}
return file;
} catch (SecurityException e) {
// 权限异常无SD卡写入权限
e.printStackTrace();
} catch (IOException e) {
// IO异常磁盘空间不足等
e.printStackTrace();
}
return null; // 创建失败返回null
}
}

@ -0,0 +1,448 @@
/*
* 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.
*/
// DataUtils.java - 小米便签数据工具类
// 主要功能:提供便签数据的批量操作、查询、验证等工具方法
package net.micode.notes.tool;
// ======================= 导入区域 =======================
// Android数据操作相关类
import android.content.ContentProviderOperation; // 内容提供者批量操作
import android.content.ContentProviderResult; // 批量操作结果
import android.content.ContentResolver; // 内容解析器用于访问ContentProvider
import android.content.ContentUris; // URI工具用于构建带ID的URI
import android.content.ContentValues; // 键值对容器,用于插入/更新数据
import android.content.OperationApplicationException; // 批量操作异常
import android.database.Cursor; // 数据库查询结果游标
import android.os.RemoteException; // 远程调用异常
import android.util.Log; // 日志工具
// 应用内部数据模型
import net.micode.notes.data.Notes; // Notes主类
import net.micode.notes.data.Notes.CallNote; // 通话记录相关常量
import net.micode.notes.data.Notes.NoteColumns; // 便签表列定义
import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute; // 小部件属性类
// Java集合类
import java.util.ArrayList; // 动态数组
import java.util.HashSet; // 哈希集合用于存储唯一ID
// ======================= 数据工具主类 =======================
/**
* DataUtils - 便
* 便
* 使
*/
public class DataUtils {
// TAG - 日志标签用于Logcat日志筛选
public static final String TAG = "DataUtils";
/**
* 便
* 使ContentProvider
* @param resolver 访ContentProvider
* @param ids 便IDHashSetID
* @return true: ; false:
*/
public static boolean batchDeleteNotes(ContentResolver resolver, HashSet<Long> ids) {
// 参数验证
if (ids == null) {
Log.d(TAG, "the ids is null");
return true; // 空集合视为成功,无需操作
}
if (ids.size() == 0) {
Log.d(TAG, "no id is in the hashset");
return true; // 空集合视为成功
}
// 创建批量操作列表
ArrayList<ContentProviderOperation> operationList =
new ArrayList<ContentProviderOperation>();
// 遍历ID集合为每个便签创建删除操作
for (long id : ids) {
// 禁止删除系统根文件夹
if(id == Notes.ID_ROOT_FOLDER) {
Log.e(TAG, "Don't delete system folder root");
continue; // 跳过系统文件夹
}
// 构建删除操作
ContentProviderOperation.Builder builder = ContentProviderOperation
.newDelete(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id));
operationList.add(builder.build());
}
// 执行批量操作
try {
ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operationList);
// 验证操作结果
if (results == null || results.length == 0 || results[0] == null) {
Log.d(TAG, "delete notes failed, ids:" + ids.toString());
return false;
}
return true;
} catch (RemoteException e) {
// 远程调用异常
Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage()));
} catch (OperationApplicationException e) {
// 批量操作应用异常
Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage()));
}
return false;
}
/**
* 便
* 便ID
* @param resolver
* @param id 便ID
* @param srcFolderId ID
* @param desFolderId ID
*/
public static void moveNoteToFoler(ContentResolver resolver, long id, long srcFolderId, long desFolderId) {
// 创建更新数据
ContentValues values = new ContentValues();
values.put(NoteColumns.PARENT_ID, desFolderId); // 新父文件夹ID
values.put(NoteColumns.ORIGIN_PARENT_ID, srcFolderId); // 原父文件夹ID用于同步
values.put(NoteColumns.LOCAL_MODIFIED, 1); // 标记为本地已修改
// 执行更新操作
resolver.update(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id), values, null, null);
}
/**
* 便
* 使
* @param resolver
* @param ids 便ID
* @param folderId ID
* @return true: ; false:
*/
public static boolean batchMoveToFolder(ContentResolver resolver, HashSet<Long> ids,
long folderId) {
// 参数验证
if (ids == null) {
Log.d(TAG, "the ids is null");
return true; // 空集合视为成功
}
// 创建批量操作列表
ArrayList<ContentProviderOperation> operationList =
new ArrayList<ContentProviderOperation>();
// 为每个便签创建更新操作
for (long id : ids) {
ContentProviderOperation.Builder builder = ContentProviderOperation
.newUpdate(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id));
builder.withValue(NoteColumns.PARENT_ID, folderId); // 更新父文件夹ID
builder.withValue(NoteColumns.LOCAL_MODIFIED, 1); // 标记为本地已修改
operationList.add(builder.build());
}
// 执行批量操作
try {
ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operationList);
// 验证操作结果
if (results == null || results.length == 0 || results[0] == null) {
Log.d(TAG, "delete notes failed, ids:" + ids.toString());
return false;
}
return true;
} catch (RemoteException e) {
Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage()));
} catch (OperationApplicationException e) {
Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage()));
}
return false;
}
/**
*
*
* @param resolver
* @return
*/
public static int getUserFolderCount(ContentResolver resolver) {
// 查询条件:类型为文件夹
Cursor cursor =resolver.query(Notes.CONTENT_NOTE_URI,
new String[] { "COUNT(*)" }, // 只查询数量
NoteColumns.TYPE + "=?", // 查询条件
new String[] {
String.valueOf(Notes.TYPE_FOLDER) // 参数1文件夹类型
},
null);
int count = 0;
if(cursor != null) {
if(cursor.moveToFirst()) {
try {
count = cursor.getInt(0); // 获取第一列COUNT(*)结果)
} catch (IndexOutOfBoundsException e) {
Log.e(TAG, "get folder count failed:" + e.toString());
} finally {
cursor.close(); // 确保游标关闭
}
}
}
return count;
}
/**
* 便
*
* @param resolver
* @param noteId 便ID
* @param type 便Notes.TYPE_NOTE Notes.TYPE_FOLDER
* @return true: ; false:
*/
public static boolean visibleInNoteDatabase(ContentResolver resolver, long noteId, int type) {
// 查询指定便签
Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId),
null, // 查询所有列
NoteColumns.TYPE + "=?",
new String [] {String.valueOf(type)}, // 类型参数
null);
boolean exist = false;
if (cursor != null) {
// 只要查询到记录就表示可见
if (cursor.getCount() > 0) {
exist = true;
}
cursor.close();
}
return exist;
}
/**
* 便
* true
* @param resolver
* @param noteId 便ID
* @return true: ; false:
*/
public static boolean existInNoteDatabase(ContentResolver resolver, long noteId) {
// 查询指定便签,无过滤条件
Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId),
null, null, null, null);
boolean exist = false;
if (cursor != null) {
if (cursor.getCount() > 0) {
exist = true;
}
cursor.close();
}
return exist;
}
/**
*
* 便
* @param resolver
* @param dataId ID
* @return true: ; false:
*/
public static boolean existInDataDatabase(ContentResolver resolver, long dataId) {
Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId),
null, null, null, null);
boolean exist = false;
if (cursor != null) {
if (cursor.getCount() > 0) {
exist = true;
}
cursor.close();
}
return exist;
}
/**
*
*
* @param resolver
* @param name
* @return true: ; false:
*/
public static boolean checkVisibleFolderName(ContentResolver resolver, String name) {
// 查询条件:文件夹类型 且 名称匹配
Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI, null,
NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER +
" AND " + NoteColumns.SNIPPET + "=?", // SNIPPET字段存储文件夹名称
new String[] { name }, null);
boolean exist = false;
if(cursor != null) {
if(cursor.getCount() > 0) {
exist = true;
}
cursor.close();
}
return exist;
}
/**
* 便
*
* @param resolver
* @param folderId ID
* @return null
*/
public static HashSet<AppWidgetAttribute> getFolderNoteWidget(ContentResolver resolver, long folderId) {
// 查询文件夹下所有便签的小部件信息
Cursor c = resolver.query(Notes.CONTENT_NOTE_URI,
new String[] { NoteColumns.WIDGET_ID, NoteColumns.WIDGET_TYPE }, // 小部件相关字段
NoteColumns.PARENT_ID + "=?", // 父文件夹条件
new String[] { String.valueOf(folderId) },
null);
HashSet<AppWidgetAttribute> set = null;
if (c != null) {
if (c.moveToFirst()) {
set = new HashSet<AppWidgetAttribute>(); // 创建集合
do {
try {
// 创建小部件属性对象
AppWidgetAttribute widget = new AppWidgetAttribute();
widget.widgetId = c.getInt(0); // 小部件ID
widget.widgetType = c.getInt(1); // 小部件类型
set.add(widget); // 添加到集合
} catch (IndexOutOfBoundsException e) {
Log.e(TAG, e.toString()); // 字段索引异常
}
} while (c.moveToNext()); // 遍历所有便签
}
c.close();
}
return set;
}
/**
* 便ID
* 便
* @param resolver
* @param noteId 便ID
* @return
*/
public static String getCallNumberByNoteId(ContentResolver resolver, long noteId) {
// 查询条件便签ID匹配 且 类型为通话记录
Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI,
new String [] { CallNote.PHONE_NUMBER }, // 只查询电话号码
CallNote.NOTE_ID + "=? AND " + CallNote.MIME_TYPE + "=?",
new String [] {
String.valueOf(noteId), // 参数1便签ID
CallNote.CONTENT_ITEM_TYPE // 参数2通话记录类型
},
null);
if (cursor != null && cursor.moveToFirst()) {
try {
return cursor.getString(0); // 返回电话号码
} catch (IndexOutOfBoundsException e) {
Log.e(TAG, "Get call number fails " + e.toString());
} finally {
cursor.close(); // finally块确保游标关闭
}
}
return ""; // 未找到返回空字符串
}
/**
* 便ID
*
* @param resolver
* @param phoneNumber
* @param callDate
* @return 便ID0
*/
public static long getNoteIdByPhoneNumberAndCallDate(ContentResolver resolver, String phoneNumber, long callDate) {
// 查询条件:通话时间匹配 且 类型为通话记录 且 电话号码匹配
// PHONE_NUMBERS_EQUAL是自定义函数用于比较电话号码可能处理格式差异
Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI,
new String [] { CallNote.NOTE_ID }, // 只查询便签ID
CallNote.CALL_DATE + "=? AND " + CallNote.MIME_TYPE + "=? AND PHONE_NUMBERS_EQUAL("
+ CallNote.PHONE_NUMBER + ",?)",
new String [] {
String.valueOf(callDate), // 参数1通话时间
CallNote.CONTENT_ITEM_TYPE, // 参数2通话记录类型
phoneNumber // 参数3电话号码
},
null);
if (cursor != null) {
if (cursor.moveToFirst()) {
try {
return cursor.getLong(0); // 返回便签ID
} catch (IndexOutOfBoundsException e) {
Log.e(TAG, "Get call note id fails " + e.toString());
}
}
cursor.close();
}
return 0; // 未找到返回0
}
/**
* 便ID便
*
* @param resolver
* @param noteId 便ID
* @return 便
* @throws IllegalArgumentException 便
*/
public static String getSnippetById(ContentResolver resolver, long noteId) {
Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI,
new String [] { NoteColumns.SNIPPET }, // 只查询摘要字段
NoteColumns.ID + "=?",
new String [] { String.valueOf(noteId)},
null);
if (cursor != null) {
String snippet = "";
if (cursor.moveToFirst()) {
snippet = cursor.getString(0); // 获取摘要
}
cursor.close();
return snippet;
}
throw new IllegalArgumentException("Note is not found with id: " + noteId);
}
/**
* 便
*
* @param snippet
* @return
*/
public static String getFormattedSnippet(String snippet) {
if (snippet != null) {
snippet = snippet.trim(); // 去除首尾空格
// 查找第一个换行符
int index = snippet.indexOf('\n');
if (index != -1) {
// 只保留第一行
snippet = snippet.substring(0, index);
}
}
return snippet;
}
}

@ -0,0 +1,197 @@
/*
* 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.
*/
// GTaskStringUtils.java - Google任务GTasks同步相关的字符串常量工具类
// 主要功能定义与Google Tasks API交互时使用的JSON键名、操作类型、元数据标识等常量
// 这些常量用于序列化和反序列化Google Tasks的JSON数据
package net.micode.notes.tool;
// ======================= Google Tasks JSON键名常量 =======================
/**
* GTaskStringUtils - Google Tasks
* Google Tasks API使JSON
* public static final
*/
public class GTaskStringUtils {
// ======================= 操作相关键名 =======================
/** 操作ID键名 - JSON中标识操作的唯一ID */
public final static String GTASK_JSON_ACTION_ID = "action_id";
/** 操作列表键名 - JSON中包含多个操作的数组 */
public final static String GTASK_JSON_ACTION_LIST = "action_list";
/** 操作类型键名 - JSON中标识操作类型的字段 */
public final static String GTASK_JSON_ACTION_TYPE = "action_type";
// ======================= 操作类型值 =======================
/** 创建操作类型值 - 表示创建新项的操作 */
public final static String GTASK_JSON_ACTION_TYPE_CREATE = "create";
/** 获取全部操作类型值 - 表示获取所有数据的操作 */
public final static String GTASK_JSON_ACTION_TYPE_GETALL = "get_all";
/** 移动操作类型值 - 表示移动项到其他位置的操作 */
public final static String GTASK_JSON_ACTION_TYPE_MOVE = "move";
/** 更新操作类型值 - 表示更新现有项的操作 */
public final static String GTASK_JSON_ACTION_TYPE_UPDATE = "update";
// ======================= 创建者和版本信息 =======================
/** 创建者ID键名 - JSON中标识创建者的字段 */
public final static String GTASK_JSON_CREATOR_ID = "creator_id";
/** 客户端版本键名 - JSON中标识客户端版本的字段 */
public final static String GTASK_JSON_CLIENT_VERSION = "client_version";
// ======================= 实体和子项相关 =======================
/** 子实体键名 - JSON中表示子项的列表 */
public final static String GTASK_JSON_CHILD_ENTITY = "child_entity";
/** 实体增量键名 - JSON中表示实体变更的字段 */
public final static String GTASK_JSON_ENTITY_DELTA = "entity_delta";
/** 实体类型键名 - JSON中标识实体类型的字段 */
public final static String GTASK_JSON_ENTITY_TYPE = "entity_type";
// ======================= 状态和删除相关 =======================
/** 完成状态键名 - JSON中标识任务是否完成的字段 */
public final static String GTASK_JSON_COMPLETED = "completed";
/** 删除状态键名 - JSON中标识项是否被删除的字段 */
public final static String GTASK_JSON_DELETED = "deleted";
/** 获取删除项键名 - JSON中标识是否获取已删除项的字段 */
public final static String GTASK_JSON_GET_DELETED = "get_deleted";
// ======================= ID和索引相关 =======================
/** ID键名 - JSON中最基本的ID字段 */
public final static String GTASK_JSON_ID = "id";
/** 新ID键名 - JSON中表示新生成的ID字段用于创建操作 */
public final static String GTASK_JSON_NEW_ID = "new_id";
/** 索引键名 - JSON中表示项在列表中的位置索引 */
public final static String GTASK_JSON_INDEX = "index";
/** 父项ID键名 - JSON中表示父项的ID */
public final static String GTASK_JSON_PARENT_ID = "parent_id";
/** 前一个兄弟项ID键名 - JSON中表示同一层级中前一项的ID用于确定位置 */
public final static String GTASK_JSON_PRIOR_SIBLING_ID = "prior_sibling_id";
// ======================= 列表相关 =======================
/** 当前列表ID键名 - JSON中表示当前活动列表的ID */
public final static String GTASK_JSON_CURRENT_LIST_ID = "current_list_id";
/** 默认列表ID键名 - JSON中表示默认列表的ID */
public final static String GTASK_JSON_DEFAULT_LIST_ID = "default_list_id";
/** 列表ID键名 - JSON中表示列表的ID */
public final static String GTASK_JSON_LIST_ID = "list_id";
/** 列表集合键名 - JSON中包含多个列表的数组 */
public final static String GTASK_JSON_LISTS = "lists";
/** 源列表键名 - JSON中表示移动操作的源列表 */
public final static String GTASK_JSON_SOURCE_LIST = "source_list";
/** 目标列表键名 - JSON中表示移动操作的目标列表 */
public final static String GTASK_JSON_DEST_LIST = "dest_list";
// ======================= 移动操作相关 =======================
/** 目标父项键名 - JSON中表示移动操作的目标父项 */
public final static String GTASK_JSON_DEST_PARENT = "dest_parent";
/** 目标父项类型键名 - JSON中表示移动操作的目标父项类型 */
public final static String GTASK_JSON_DEST_PARENT_TYPE = "dest_parent_type";
// ======================= 时间和同步相关 =======================
/** 最后修改时间键名 - JSON中表示项的最后修改时间戳 */
public final static String GTASK_JSON_LAST_MODIFIED = "last_modified";
/** 最新同步点键名 - JSON中表示最新的同步时间点用于增量同步 */
public final static String GTASK_JSON_LATEST_SYNC_POINT = "latest_sync_point";
// ======================= 名称和内容相关 =======================
/** 名称键名 - JSON中表示项的名称/标题字段 */
public final static String GTASK_JSON_NAME = "name";
/** 便签内容键名 - JSON中表示便签具体内容的字段 */
public final static String GTASK_JSON_NOTES = "notes";
// ======================= 结果和任务相关 =======================
/** 结果键名 - JSON中表示操作结果集的字段 */
public final static String GTASK_JSON_RESULTS = "results";
/** 任务集合键名 - JSON中包含多个任务的数组 */
public final static String GTASK_JSON_TASKS = "tasks";
// ======================= 类型相关 =======================
/** 类型键名 - JSON中表示项的类型字段 */
public final static String GTASK_JSON_TYPE = "type";
/** 组类型值 - JSON中表示组/文件夹类型的值 */
public final static String GTASK_JSON_TYPE_GROUP = "GROUP";
/** 任务类型值 - JSON中表示任务/便签类型的值 */
public final static String GTASK_JSON_TYPE_TASK = "TASK";
// ======================= 用户相关 =======================
/** 用户键名 - JSON中表示用户信息的字段 */
public final static String GTASK_JSON_USER = "user";
// ======================= MIUI便签专用文件夹前缀 =======================
/** MIUI文件夹前缀 - 用于标识MIUI便签创建的Google Tasks文件夹 */
public final static String MIUI_FOLDER_PREFFIX = "[MIUI_Notes]";
/** 默认文件夹名称 - 在Google Tasks中创建的默认文件夹名称 */
public final static String FOLDER_DEFAULT = "Default";
/** 通话记录文件夹名称 - 在Google Tasks中创建的通话记录专用文件夹 */
public final static String FOLDER_CALL_NOTE = "Call_Note";
// ======================= 元数据相关 =======================
/** 元数据文件夹标识 - 用于标识存储同步元数据的特殊文件夹 */
public final static String FOLDER_META = "METADATA";
/** 元数据头-GTaskID - 元数据中存储Google Task ID的字段前缀 */
public final static String META_HEAD_GTASK_ID = "meta_gid";
/** 元数据头-便签 - 元数据中存储便签信息的字段前缀 */
public final static String META_HEAD_NOTE = "meta_note";
/** 元数据头-数据 - 元数据中存储便签详细数据的字段前缀 */
public final static String META_HEAD_DATA = "meta_data";
/** 元数据便签名称 - 用于存储同步元数据的特殊便签的名称(提示用户不要修改) */
public final static String META_NOTE_NAME = "[META INFO] DON'T UPDATE AND DELETE";
}

@ -0,0 +1,310 @@
/*
* 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.
*/
// ResourceParser.java - 资源解析工具类
// 主要功能管理便签应用的颜色主题、背景资源、字体大小等UI相关资源的映射和获取
package net.micode.notes.tool;
// ======================= 导入区域 =======================
// Android相关类
import android.content.Context; // 上下文,用于访问偏好设置
import android.preference.PreferenceManager; // 偏好设置管理器
// 应用内部资源
import net.micode.notes.R; // 资源文件R类
import net.micode.notes.ui.NotesPreferenceActivity; // 设置Activity包含偏好设置键名
// ======================= 资源解析主类 =======================
/**
* ResourceParser -
* 便UI
*
*/
public class ResourceParser {
// ======================= 便签背景颜色常量 =======================
/**
* 便
* 便
*/
public static final int YELLOW = 0; // 黄色主题
public static final int BLUE = 1; // 蓝色主题
public static final int WHITE = 2; // 白色主题
public static final int GREEN = 3; // 绿色主题
public static final int RED = 4; // 红色主题
/** 默认背景颜色索引 - 应用启动时的默认颜色 */
public static final int BG_DEFAULT_COLOR = YELLOW; // 默认为黄色
// ======================= 字体大小常量 =======================
/**
*
*
*/
public static final int TEXT_SMALL = 0; // 小号字体
public static final int TEXT_MEDIUM = 1; // 中号字体
public static final int TEXT_LARGE = 2; // 大号字体
public static final int TEXT_SUPER = 3; // 超大号字体
/** 默认字体大小索引 - 应用启动时的默认字体大小 */
public static final int BG_DEFAULT_FONT_SIZE = TEXT_MEDIUM; // 默认为中号字体
// ======================= 编辑界面背景资源类 =======================
/**
* NoteBgResources - 便
* 便
*/
public static class NoteBgResources {
// 便签编辑区域背景资源数组
// 数组索引对应颜色常量0=黄, 1=蓝, 2=白, 3=绿, 4=红
private final static int [] BG_EDIT_RESOURCES = new int [] {
R.drawable.edit_yellow, // 黄色编辑背景
R.drawable.edit_blue, // 蓝色编辑背景
R.drawable.edit_white, // 白色编辑背景
R.drawable.edit_green, // 绿色编辑背景
R.drawable.edit_red // 红色编辑背景
};
// 便签标题栏背景资源数组
// 与编辑区域背景对应,但用于标题栏部分
private final static int [] BG_EDIT_TITLE_RESOURCES = new int [] {
R.drawable.edit_title_yellow, // 黄色标题背景
R.drawable.edit_title_blue, // 蓝色标题背景
R.drawable.edit_title_white, // 白色标题背景
R.drawable.edit_title_green, // 绿色标题背景
R.drawable.edit_title_red // 红色标题背景
};
/**
* 便ID
* @param id YELLOW/BLUE/WHITE/GREEN/RED
* @return drawableID
*/
public static int getNoteBgResource(int id) {
return BG_EDIT_RESOURCES[id];
}
/**
* 便ID
* @param id YELLOW/BLUE/WHITE/GREEN/RED
* @return drawableID
*/
public static int getNoteTitleBgResource(int id) {
return BG_EDIT_TITLE_RESOURCES[id];
}
}
/**
* ID
*
* @param context
* @return
*/
public static int getDefaultBgId(Context context) {
// 从偏好设置读取是否启用随机背景颜色
if (context.getSharedPreferences(NotesPreferenceActivity.PREFERENCE_NAME, Context.MODE_PRIVATE).getBoolean(
NotesPreferenceActivity.PREFERENCE_SET_BG_COLOR_KEY, // 偏好设置键名
false)) { // 默认值为false不随机
// 随机选择一种颜色
return (int) (Math.random() * NoteBgResources.BG_EDIT_RESOURCES.length);
} else {
// 使用默认颜色
return BG_DEFAULT_COLOR;
}
}
// ======================= 列表项背景资源类 =======================
/**
* NoteItemBgResources - 便
* 便
* 使
*/
public static class NoteItemBgResources {
// 列表首项背景资源数组
// 用于列表中的第一个项(顶部圆角)
private final static int [] BG_FIRST_RESOURCES = new int [] {
R.drawable.list_yellow_up, // 黄色首项背景
R.drawable.list_blue_up, // 蓝色首项背景
R.drawable.list_white_up, // 白色首项背景
R.drawable.list_green_up, // 绿色首项背景
R.drawable.list_red_up // 红色首项背景
};
// 列表中间项背景资源数组
// 用于列表中非首尾的中间项(直角)
private final static int [] BG_NORMAL_RESOURCES = new int [] {
R.drawable.list_yellow_middle, // 黄色中间项背景
R.drawable.list_blue_middle, // 蓝色中间项背景
R.drawable.list_white_middle, // 白色中间项背景
R.drawable.list_green_middle, // 绿色中间项背景
R.drawable.list_red_middle // 红色中间项背景
};
// 列表末项背景资源数组
// 用于列表中的最后一个项(底部圆角)
private final static int [] BG_LAST_RESOURCES = new int [] {
R.drawable.list_yellow_down, // 黄色末项背景
R.drawable.list_blue_down, // 蓝色末项背景
R.drawable.list_white_down, // 白色末项背景
R.drawable.list_green_down, // 绿色末项背景
R.drawable.list_red_down, // 红色末项背景
};
// 列表单独项背景资源数组
// 用于列表中只有一个项的情况(上下都有圆角)
private final static int [] BG_SINGLE_RESOURCES = new int [] {
R.drawable.list_yellow_single, // 黄色单独项背景
R.drawable.list_blue_single, // 蓝色单独项背景
R.drawable.list_white_single, // 白色单独项背景
R.drawable.list_green_single, // 绿色单独项背景
R.drawable.list_red_single // 红色单独项背景
};
/**
* ID
* @param id
* @return drawableID
*/
public static int getNoteBgFirstRes(int id) {
return BG_FIRST_RESOURCES[id];
}
/**
* ID
* @param id
* @return drawableID
*/
public static int getNoteBgLastRes(int id) {
return BG_LAST_RESOURCES[id];
}
/**
* ID
* @param id
* @return drawableID
*/
public static int getNoteBgSingleRes(int id) {
return BG_SINGLE_RESOURCES[id];
}
/**
* ID
* @param id
* @return drawableID
*/
public static int getNoteBgNormalRes(int id) {
return BG_NORMAL_RESOURCES[id];
}
/**
* ID
* 使
* @return ID
*/
public static int getFolderBgRes() {
return R.drawable.list_folder;
}
}
// ======================= 小部件背景资源类 =======================
/**
* WidgetBgResources -
*
*/
public static class WidgetBgResources {
// 2x尺寸小部件背景资源数组
// 用于2x2尺寸的桌面小部件
private final static int [] BG_2X_RESOURCES = new int [] {
R.drawable.widget_2x_yellow, // 黄色2x小部件背景
R.drawable.widget_2x_blue, // 蓝色2x小部件背景
R.drawable.widget_2x_white, // 白色2x小部件背景
R.drawable.widget_2x_green, // 绿色2x小部件背景
R.drawable.widget_2x_red, // 红色2x小部件背景
};
/**
* 2xID
* @param id
* @return drawableID
*/
public static int getWidget2xBgResource(int id) {
return BG_2X_RESOURCES[id];
}
// 4x尺寸小部件背景资源数组
// 用于4x4尺寸的桌面小部件
private final static int [] BG_4X_RESOURCES = new int [] {
R.drawable.widget_4x_yellow, // 黄色4x小部件背景
R.drawable.widget_4x_blue, // 蓝色4x小部件背景
R.drawable.widget_4x_white, // 白色4x小部件背景
R.drawable.widget_4x_green, // 绿色4x小部件背景
R.drawable.widget_4x_red // 红色4x小部件背景
};
/**
* 4xID
* @param id
* @return drawableID
*/
public static int getWidget4xBgResource(int id) {
return BG_4X_RESOURCES[id];
}
}
// ======================= 文本外观资源类 =======================
/**
* TextAppearanceResources -
* 便
*
*/
public static class TextAppearanceResources {
// 文本外观样式资源数组
// 索引对应字体大小常量0=小, 1=中, 2=大, 3=超大
private final static int [] TEXTAPPEARANCE_RESOURCES = new int [] {
R.style.TextAppearanceNormal, // 小号字体样式
R.style.TextAppearanceMedium, // 中号字体样式
R.style.TextAppearanceLarge, // 大号字体样式
R.style.TextAppearanceSuper // 超大号字体样式
};
/**
* ID
* ID
* @param id
* @return styleID
*/
public static int getTexAppearanceResource(int id) {
/**
* HACKME: IDbug
* id
* id
*/
if (id >= TEXTAPPEARANCE_RESOURCES.length) {
return BG_DEFAULT_FONT_SIZE; // 返回默认字体大小索引
}
return TEXTAPPEARANCE_RESOURCES[id];
}
/**
*
*
* @return
*/
public static int getResourcesSize() {
return TEXTAPPEARANCE_RESOURCES.length;
}
}
}

@ -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());
}
}
}

@ -0,0 +1,270 @@
/*
* 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.
*/
// AlarmAlertActivity.java - 闹钟提醒Activity
// 主要功能:显示便签闹钟提醒,在指定时间弹出提醒对话框并播放提示音
package net.micode.notes.ui;
// ======================= 导入区域 =======================
// Android基础类
import android.app.Activity; // Activity基类
import android.app.AlertDialog; // 警告对话框
import android.content.Context; // 上下文
import android.content.DialogInterface; // 对话框接口
import android.content.DialogInterface.OnClickListener; // 对话框点击监听
import android.content.DialogInterface.OnDismissListener; // 对话框关闭监听
import android.content.Intent; // Intent用于跳转
import android.media.AudioManager; // 音频管理
import android.media.MediaPlayer; // 媒体播放器
import android.media.RingtoneManager; // 铃声管理器
import android.net.Uri; // URI
import android.os.Bundle; // Bundle用于保存状态
import android.os.PowerManager; // 电源管理
import android.provider.Settings; // 系统设置
import android.view.Window; // 窗口
import android.view.WindowManager; // 窗口管理器
// 应用内部资源
import net.micode.notes.R; // 资源文件R类
import net.micode.notes.data.Notes; // Notes主类
import net.micode.notes.tool.DataUtils; // 数据工具类
// Java IO
import java.io.IOException; // IO异常
// ======================= 闹钟提醒Activity =======================
/**
* AlarmAlertActivity - Activity
* 使
* 便
* OnClickListenerOnDismissListener
*/
public class AlarmAlertActivity extends Activity implements OnClickListener, OnDismissListener {
// 成员变量
private long mNoteId; // 便签ID从Intent中获取
private String mSnippet; // 便签摘要,用于对话框显示
private static final int SNIPPET_PREW_MAX_LEN = 60; // 摘要预览最大长度
MediaPlayer mPlayer; // 媒体播放器,用于播放提示音
// ======================= 生命周期方法 =======================
/**
* onCreate - Activity
*
* 1.
* 2. 便ID
* 3. 便
* 4.
* 5.
* @param savedInstanceState nullActivity
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 1. 设置窗口属性
// 请求无标题栏
requestWindowFeature(Window.FEATURE_NO_TITLE);
// 获取窗口对象
final Window win = getWindow();
// 添加标志:在锁屏状态下显示
win.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
// 如果屏幕是关闭状态,添加更多标志
if (!isScreenOn()) {
win.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON // 保持屏幕常亮
| WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON // 点亮屏幕
| WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON // 允许锁屏
| WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR); // 布局装饰
}
// 2. 获取Intent数据
Intent intent = getIntent();
try {
// 从Intent的URI中解析便签ID
// URI格式示例content://micode_notes/note/123
// getPathSegments()返回["note", "123"]取索引1得到"123"
mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1));
// 通过工具类获取便签摘要
mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId);
// 3. 处理摘要显示长度
// 如果摘要过长截取前60个字符并添加省略号
mSnippet = mSnippet.length() > SNIPPET_PREW_MAX_LEN ? mSnippet.substring(0,
SNIPPET_PREW_MAX_LEN) + getResources().getString(R.string.notelist_string_info)
: mSnippet;
} catch (IllegalArgumentException e) {
// URI解析或ID转换异常
e.printStackTrace();
return; // 异常时直接返回,不继续执行
}
// 4. 初始化媒体播放器
mPlayer = new MediaPlayer();
// 5. 检查便签是否可见(存在且不在回收站)
if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) {
// 便签存在,显示对话框
showActionDialog();
// 播放提示音
playAlarmSound();
} else {
// 便签不存在可能已被删除直接结束Activity
finish();
}
}
/**
*
* @return true: ; false:
*/
private boolean isScreenOn() {
// 获取电源管理器服务
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
// 判断屏幕是否亮屏(已废弃,但兼容旧版本)
return pm.isScreenOn();
}
// ======================= 提示音播放 =======================
/**
*
* 使
*/
private void playAlarmSound() {
// 1. 获取系统默认闹钟铃声URI
Uri url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM);
// 2. 获取静音模式设置
// 检查哪些音频流受静音模式影响
int silentModeStreams = Settings.System.getInt(getContentResolver(),
Settings.System.MODE_RINGER_STREAMS_AFFECTED, 0);
// 3. 设置音频流类型
// 判断闹钟音频流是否受静音模式影响
if ((silentModeStreams & (1 << AudioManager.STREAM_ALARM)) != 0) {
// 受静音影响,使用系统设置的类型
mPlayer.setAudioStreamType(silentModeStreams);
} else {
// 不受静音影响,使用闹钟音频流
mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM);
}
// 4. 设置数据源并播放
try {
mPlayer.setDataSource(this, url); // 设置铃声URI
mPlayer.prepare(); // 准备播放
mPlayer.setLooping(true); // 循环播放
mPlayer.start(); // 开始播放
} catch (IllegalArgumentException e) {
// URI格式错误
e.printStackTrace();
} catch (SecurityException e) {
// 权限不足
e.printStackTrace();
} catch (IllegalStateException e) {
// 播放器状态错误
e.printStackTrace();
} catch (IOException e) {
// IO错误文件读取失败
e.printStackTrace();
}
}
// ======================= 提醒对话框 =======================
/**
*
* + 便 +
*/
private void showActionDialog() {
// 1. 创建对话框构建器
AlertDialog.Builder dialog = new AlertDialog.Builder(this);
// 2. 设置对话框内容
dialog.setTitle(R.string.app_name); // 应用名称作为标题
dialog.setMessage(mSnippet); // 便签摘要作为内容
// 3. 设置按钮
// 确定按钮 - 关闭提醒
dialog.setPositiveButton(R.string.notealert_ok, this);
// 如果屏幕已亮,显示"进入"按钮(跳转到便签编辑)
if (isScreenOn()) {
dialog.setNegativeButton(R.string.notealert_enter, this);
}
// 4. 显示对话框并设置关闭监听
dialog.show().setOnDismissListener(this);
}
// ======================= 对话框事件处理 =======================
/**
*
* OnClickListener
* @param dialog
* @param which
* BUTTON_POSITIVE:
* BUTTON_NEGATIVE:
* BUTTON_NEUTRAL: 使
*/
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case DialogInterface.BUTTON_NEGATIVE:
// "进入"按钮:跳转到便签编辑界面
Intent intent = new Intent(this, NoteEditActivity.class);
intent.setAction(Intent.ACTION_VIEW); // 查看动作
intent.putExtra(Intent.EXTRA_UID, mNoteId); // 传递便签ID
startActivity(intent);
break;
default:
// 其他按钮(确定按钮)不执行额外操作
// 对话框关闭后会触发onDismiss自动结束Activity
break;
}
}
/**
*
* OnDismissListener
*
* @param dialog
*/
public void onDismiss(DialogInterface dialog) {
// 停止播放提示音
stopAlarmSound();
// 结束Activity
finish();
}
// ======================= 资源清理 =======================
/**
*
* MediaPlayer
*/
private void stopAlarmSound() {
if (mPlayer != null) {
mPlayer.stop(); // 停止播放
mPlayer.release(); // 释放资源
mPlayer = null; // 置空引用帮助GC
}
}
}

@ -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.
*/
// AlarmInitReceiver.java - 闹钟初始化广播接收器
// 主要功能:在设备启动后重新注册所有未触发的便签闹钟提醒
package net.micode.notes.ui;
// ======================= 导入区域 =======================
// Android闹钟相关
import android.app.AlarmManager; // 闹钟管理器,用于设置系统闹钟
import android.app.PendingIntent; // 待定意图,用于延迟执行
// Android广播相关
import android.content.BroadcastReceiver; // 广播接收器基类
import android.content.ContentUris; // URI工具用于构建带ID的URI
import android.content.Context; // 上下文
import android.content.Intent; // 意图
// Android数据库相关
import android.database.Cursor; // 数据库查询结果游标
// 应用内部数据模型
import net.micode.notes.data.Notes; // Notes主类
import net.micode.notes.data.Notes.NoteColumns; // 便签表列定义
// ======================= 闹钟初始化广播接收器 =======================
/**
* AlarmInitReceiver - 广
* BroadcastReceiver广
*
*
* AndroidManifest.xml
* <receiver android:name=".ui.AlarmInitReceiver" android:exported="true">
* <intent-filter>
* <action android:name="android.intent.action.BOOT_COMPLETED" />
* </intent-filter>
* </receiver>
*/
public class AlarmInitReceiver extends BroadcastReceiver {
// ======================= 数据库查询配置 =======================
/**
* -
*
*/
private static final String [] PROJECTION = new String [] {
NoteColumns.ID, // 0 - 便签ID用于构建PendingIntent
NoteColumns.ALERTED_DATE // 1 - 提醒时间,用于设置闹钟
};
// 字段索引常量 - 提高代码可读性和维护性
private static final int COLUMN_ID = 0; // ID字段索引
private static final int COLUMN_ALERTED_DATE = 1; // 提醒时间字段索引
// ======================= 广播接收回调方法 =======================
/**
* onReceive - 广
* android.intent.action.BOOT_COMPLETED广
*
* 1. 便
* 2. 便
* 3.
*
* @param context 广
* @param intent 广BOOT_COMPLETED
*/
@Override
public void onReceive(Context context, Intent intent) {
// 1. 获取当前时间戳,用于查询未来提醒
long currentDate = System.currentTimeMillis();
// 2. 查询数据库,获取所有未来需要提醒的便签
// 查询条件:提醒时间 > 当前时间 且 类型为普通便签
Cursor c = context.getContentResolver().query(
Notes.CONTENT_NOTE_URI, // 便签表内容URI
PROJECTION, // 查询字段ID和提醒时间
NoteColumns.ALERTED_DATE + ">? AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE,
new String[] { String.valueOf(currentDate) }, // 查询参数:当前时间
null); // 排序方式null表示默认
// 3. 检查查询结果是否有效
if (c != null) {
// 4. 遍历查询结果,为每个便签设置闹钟
if (c.moveToFirst()) {
do {
// 4.1 获取便签的提醒时间
long alertDate = c.getLong(COLUMN_ALERTED_DATE);
// 4.2 创建AlarmReceiver的Intent
// AlarmReceiver是实际处理闹钟触发的广播接收器
Intent sender = new Intent(context, AlarmReceiver.class);
// 4.3 设置Intent的数据URI包含便签ID
// URI格式content://micode_notes/note/{noteId}
sender.setData(ContentUris.withAppendedId(
Notes.CONTENT_NOTE_URI,
c.getLong(COLUMN_ID)
));
// 4.4 创建PendingIntent
// 参数说明:
// - context: 上下文
// - requestCode: 请求码0表示默认
// - intent: 要执行的Intent
// - flags: 标志位0表示默认
PendingIntent pendingIntent = PendingIntent.getBroadcast(
context,
0,
sender,
0
);
// 4.5 获取系统闹钟管理器
AlarmManager alarmManager = (AlarmManager) context
.getSystemService(Context.ALARM_SERVICE);
// 4.6 设置系统闹钟
// 参数说明:
// - AlarmManager.RTC_WAKEUP: 使用实时时钟,触发时唤醒设备
// - alertDate: 触发时间(毫秒时间戳)
// - pendingIntent: 触发时执行的PendingIntent
alarmManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent);
} while (c.moveToNext()); // 继续处理下一个便签
}
// 5. 关闭游标,释放数据库资源
c.close();
}
}
}

@ -0,0 +1,77 @@
/*
* 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.
*/
// AlarmReceiver.java - 闹钟触发广播接收器
// 主要功能接收系统闹钟触发广播启动闹钟提醒Activity
package net.micode.notes.ui;
// ======================= 导入区域 =======================
// Android广播相关
import android.content.BroadcastReceiver; // 广播接收器基类
import android.content.Context; // 上下文
import android.content.Intent; // 意图用于启动Activity
// ======================= 闹钟触发广播接收器 =======================
/**
* AlarmReceiver - 广
* BroadcastReceiver广
* 广AlarmAlertActivity
*
* AndroidManifest.xml
* <receiver
* android:name="net.micode.notes.ui.AlarmReceiver"
* android:process=":remote" />
*
* 使
*/
public class AlarmReceiver extends BroadcastReceiver {
/**
* onReceive - 广
*
*
* 1. IntentAlarmAlertActivity
* 2. NEW_TASK
* 3. Activity
*
*
* AlarmReceiver AlarmAlertActivity
*
* @param context 广
* @param intent 广
* - Data URI: content://micode_notes/note/{noteId} (来自AlarmInitReceiver的设置)
* -
*/
@Override
public void onReceive(Context context, Intent intent) {
// 1. 修改Intent的目标Activity类
// 原始Intent来自AlarmInitReceiver设置的PendingIntent
// 这里将目标类改为AlarmAlertActivity
intent.setClass(context, AlarmAlertActivity.class);
// 2. 添加NEW_TASK标志
// 原因从广播接收器启动Activity必须在独立任务栈中
// FLAG_ACTIVITY_NEW_TASK作用
// - 创建新的任务栈
// - 允许从非Activity上下文BroadcastReceiver启动Activity
// - 避免与现有Activity的任务栈冲突
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
// 3. 启动闹钟提醒Activity
// 启动后AlarmAlertActivity将显示全屏提醒对话框
context.startActivity(intent);
}
}

@ -0,0 +1,680 @@
/*
* 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.
*/
// DateTimePicker.java - 日期时间选择器自定义控件
// 主要功能:提供便签闹钟设置所需的日期和时间选择界面
package net.micode.notes.ui;
// ======================= 导入区域 =======================
// Java日期时间相关
import java.text.DateFormatSymbols; // 日期格式符号用于获取AM/PM字符串
import java.util.Calendar; // 日历类,用于日期时间计算
// 应用内部资源
import net.micode.notes.R; // 资源文件R类
// Android相关
import android.content.Context; // 上下文
import android.text.format.DateFormat; // 日期格式化工具
import android.view.View; // 视图基类
import android.widget.FrameLayout; // 帧布局容器
import android.widget.NumberPicker; // 数字选择器控件
// ======================= 日期时间选择器控件 =======================
/**
* DateTimePicker -
* FrameLayoutNumberPicker
* 7AM/PM
* 1224
*/
public class DateTimePicker extends FrameLayout {
// ======================= 常量定义 =======================
/** 默认启用状态 - 控件初始化时的默认可用状态 */
private static final boolean DEFAULT_ENABLE_STATE = true;
/** 半天的小时数 - 12小时制使用 */
private static final int HOURS_IN_HALF_DAY = 12;
/** 全天的小时数 - 24小时制使用 */
private static final int HOURS_IN_ALL_DAY = 24;
/** 一周的天数 - 日期选择器显示的天数范围 */
private static final int DAYS_IN_ALL_WEEK = 7;
// 日期选择器范围常量
private static final int DATE_SPINNER_MIN_VAL = 0; // 日期选择器最小值
private static final int DATE_SPINNER_MAX_VAL = DAYS_IN_ALL_WEEK - 1; // 日期选择器最大值
// 24小时制小时选择器范围常量
private static final int HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW = 0; // 24小时制最小值
private static final int HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW = 23; // 24小时制最大值
// 12小时制小时选择器范围常量
private static final int HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW = 1; // 12小时制最小值
private static final int HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW = 12; // 12小时制最大值
// 分钟选择器范围常量
private static final int MINUT_SPINNER_MIN_VAL = 0; // 分钟最小值
private static final int MINUT_SPINNER_MAX_VAL = 59; // 分钟最大值
// AM/PM选择器范围常量
private static final int AMPM_SPINNER_MIN_VAL = 0; // AM索引
private static final int AMPM_SPINNER_MAX_VAL = 1; // PM索引
// ======================= 控件成员变量 =======================
/** 日期选择器 - 显示近7天的日期 */
private final NumberPicker mDateSpinner;
/** 小时选择器 - 显示小时12/24小时制 */
private final NumberPicker mHourSpinner;
/** 分钟选择器 - 显示分钟 */
private final NumberPicker mMinuteSpinner;
/** AM/PM选择器 - 显示上午/下午12小时制时显示 */
private final NumberPicker mAmPmSpinner;
/** 当前日期时间 - Calendar对象保存当前选择的时间 */
private Calendar mDate;
/** 日期显示值数组 - 存储近7天的格式化字符串 */
private String[] mDateDisplayValues = new String[DAYS_IN_ALL_WEEK];
// ======================= 状态标志 =======================
/** AM/PM标志 - true: 上午; false: 下午 */
private boolean mIsAm;
/** 24小时制标志 - true: 24小时制; false: 12小时制 */
private boolean mIs24HourView;
/** 启用状态标志 - 控件是否可用 */
private boolean mIsEnabled = DEFAULT_ENABLE_STATE;
/** 初始化标志 - 防止初始化时触发回调 */
private boolean mInitialising;
// ======================= 回调监听器 =======================
/** 日期时间变化监听器 - 当选择的时间发生变化时回调 */
private OnDateTimeChangedListener mOnDateTimeChangedListener;
// ======================= 日期选择器值变化监听器 =======================
/**
*
*
*/
private NumberPicker.OnValueChangeListener mOnDateChangedListener =
new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
// 计算日期变化量(新值-旧值并更新Calendar
mDate.add(Calendar.DAY_OF_YEAR, newVal - oldVal);
// 更新日期控件显示
updateDateControl();
// 触发日期时间变化回调
onDateTimeChanged();
}
};
// ======================= 小时选择器值变化监听器 =======================
/**
*
* AM/PM
*/
private NumberPicker.OnValueChangeListener mOnHourChangedListener =
new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
boolean isDateChanged = false; // 日期是否变化标志
Calendar cal = Calendar.getInstance(); // 临时Calendar用于计算
if (!mIs24HourView) {
// ===== 12小时制处理逻辑 =====
// 情况1PM 11点 -> PM 12点需要加1天
if (!mIsAm && oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) {
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, 1);
isDateChanged = true;
}
// 情况2AM 12点 -> AM 11点需要减1天
else if (mIsAm && oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) {
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, -1);
isDateChanged = true;
}
// 处理AM/PM切换12点前后切换时
if (oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY ||
oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) {
mIsAm = !mIsAm; // 切换AM/PM标志
updateAmPmControl(); // 更新AM/PM控件
}
} else {
// ===== 24小时制处理逻辑 =====
// 情况123点 -> 0点需要加1天
if (oldVal == HOURS_IN_ALL_DAY - 1 && newVal == 0) {
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, 1);
isDateChanged = true;
}
// 情况20点 -> 23点需要减1天
else if (oldVal == 0 && newVal == HOURS_IN_ALL_DAY - 1) {
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, -1);
isDateChanged = true;
}
}
// 计算实际的小时值12小时制需要转换
int newHour = mHourSpinner.getValue() % HOURS_IN_HALF_DAY + (mIsAm ? 0 : HOURS_IN_HALF_DAY);
mDate.set(Calendar.HOUR_OF_DAY, newHour);
// 触发回调
onDateTimeChanged();
// 如果日期发生变化,更新年月日
if (isDateChanged) {
setCurrentYear(cal.get(Calendar.YEAR));
setCurrentMonth(cal.get(Calendar.MONTH));
setCurrentDay(cal.get(Calendar.DAY_OF_MONTH));
}
}
};
// ======================= 分钟选择器值变化监听器 =======================
/**
*
* 590059
*/
private NumberPicker.OnValueChangeListener mOnMinuteChangedListener =
new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
int minValue = mMinuteSpinner.getMinValue(); // 分钟最小值0
int maxValue = mMinuteSpinner.getMaxValue(); // 分钟最大值59
int offset = 0; // 小时偏移量
// 情况159分 -> 0分小时加1
if (oldVal == maxValue && newVal == minValue) {
offset += 1;
}
// 情况20分 -> 59分小时减1
else if (oldVal == minValue && newVal == maxValue) {
offset -= 1;
}
// 如果需要调整小时
if (offset != 0) {
mDate.add(Calendar.HOUR_OF_DAY, offset); // 调整小时
mHourSpinner.setValue(getCurrentHour()); // 更新小时选择器
updateDateControl(); // 更新日期控件
// 更新AM/PM状态
int newHour = getCurrentHourOfDay();
if (newHour >= HOURS_IN_HALF_DAY) {
mIsAm = false;
updateAmPmControl();
} else {
mIsAm = true;
updateAmPmControl();
}
}
// 设置新的分钟值
mDate.set(Calendar.MINUTE, newVal);
onDateTimeChanged(); // 触发回调
}
};
// ======================= AM/PM选择器值变化监听器 =======================
/**
* AM/PM
* /
*/
private NumberPicker.OnValueChangeListener mOnAmPmChangedListener =
new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
mIsAm = !mIsAm; // 切换AM/PM标志
// 根据AM/PM调整小时
if (mIsAm) {
mDate.add(Calendar.HOUR_OF_DAY, -HOURS_IN_HALF_DAY); // PM转AM减12小时
} else {
mDate.add(Calendar.HOUR_OF_DAY, HOURS_IN_HALF_DAY); // AM转PM加12小时
}
updateAmPmControl(); // 更新AM/PM控件
onDateTimeChanged(); // 触发回调
}
};
// ======================= 回调接口定义 =======================
/**
* OnDateTimeChangedListener -
*
*/
public interface OnDateTimeChangedListener {
/**
*
* @param view DateTimePicker
* @param year
* @param month 0-11
* @param dayOfMonth 1-31
* @param hourOfDay 0-23
* @param minute 0-59
*/
void onDateTimeChanged(DateTimePicker view, int year, int month,
int dayOfMonth, int hourOfDay, int minute);
}
// ======================= 构造函数 =======================
/**
* 1 - 使
* @param context
*/
public DateTimePicker(Context context) {
this(context, System.currentTimeMillis()); // 调用构造函数2
}
/**
* 2 -
* @param context
* @param date
*/
public DateTimePicker(Context context, long date) {
this(context, date, DateFormat.is24HourFormat(context)); // 调用构造函数3
}
/**
* 3 -
* @param context
* @param date
* @param is24HourView 24
*/
public DateTimePicker(Context context, long date, boolean is24HourView) {
super(context);
// 1. 初始化成员变量
mDate = Calendar.getInstance(); // 创建Calendar实例
mInitialising = true; // 标记为初始化中
mIsAm = getCurrentHourOfDay() >= HOURS_IN_HALF_DAY; // 根据当前时间判断AM/PM
// 2. 加载布局文件
inflate(context, R.layout.datetime_picker, this);
// 3. 初始化日期选择器
mDateSpinner = (NumberPicker) findViewById(R.id.date);
mDateSpinner.setMinValue(DATE_SPINNER_MIN_VAL);
mDateSpinner.setMaxValue(DATE_SPINNER_MAX_VAL);
mDateSpinner.setOnValueChangedListener(mOnDateChangedListener);
// 4. 初始化小时选择器
mHourSpinner = (NumberPicker) findViewById(R.id.hour);
mHourSpinner.setOnValueChangedListener(mOnHourChangedListener);
// 5. 初始化分钟选择器
mMinuteSpinner = (NumberPicker) findViewById(R.id.minute);
mMinuteSpinner.setMinValue(MINUT_SPINNER_MIN_VAL);
mMinuteSpinner.setMaxValue(MINUT_SPINNER_MAX_VAL);
mMinuteSpinner.setOnLongPressUpdateInterval(100); // 长按滚动间隔100ms
mMinuteSpinner.setOnValueChangedListener(mOnMinuteChangedListener);
// 6. 初始化AM/PM选择器
String[] stringsForAmPm = new DateFormatSymbols().getAmPmStrings(); // 获取AM/PM本地化字符串
mAmPmSpinner = (NumberPicker) findViewById(R.id.amPm);
mAmPmSpinner.setMinValue(AMPM_SPINNER_MIN_VAL);
mAmPmSpinner.setMaxValue(AMPM_SPINNER_MAX_VAL);
mAmPmSpinner.setDisplayedValues(stringsForAmPm); // 设置显示值
mAmPmSpinner.setOnValueChangedListener(mOnAmPmChangedListener);
// 7. 更新控件初始状态
updateDateControl(); // 更新日期显示
updateHourControl(); // 更新小时范围
updateAmPmControl(); // 更新AM/PM显示
// 8. 设置24小时制模式
set24HourView(is24HourView);
// 9. 设置当前时间
setCurrentDate(date);
// 10. 设置启用状态
setEnabled(isEnabled());
// 11. 初始化完成
mInitialising = false;
}
// ======================= 控件状态管理 =======================
/**
*
* @param enabled true: ; false:
*/
@Override
public void setEnabled(boolean enabled) {
if (mIsEnabled == enabled) {
return; // 状态未变化,直接返回
}
super.setEnabled(enabled);
// 设置所有子控件的启用状态
mDateSpinner.setEnabled(enabled);
mMinuteSpinner.setEnabled(enabled);
mHourSpinner.setEnabled(enabled);
mAmPmSpinner.setEnabled(enabled);
mIsEnabled = enabled;
}
/**
*
* @return true: ; false:
*/
@Override
public boolean isEnabled() {
return mIsEnabled;
}
// ======================= 获取当前日期时间 =======================
/**
*
* @return
*/
public long getCurrentDateInTimeMillis() {
return mDate.getTimeInMillis();
}
/**
*
* @param date
*/
public void setCurrentDate(long date) {
Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(date);
setCurrentDate(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH),
cal.get(Calendar.DAY_OF_MONTH), cal.get(Calendar.HOUR_OF_DAY),
cal.get(Calendar.MINUTE));
}
/**
*
* @param year
* @param month 0-11
* @param dayOfMonth 1-31
* @param hourOfDay 0-23
* @param minute 0-59
*/
public void setCurrentDate(int year, int month,
int dayOfMonth, int hourOfDay, int minute) {
setCurrentYear(year);
setCurrentMonth(month);
setCurrentDay(dayOfMonth);
setCurrentHour(hourOfDay);
setCurrentMinute(minute);
}
// ======================= 年、月、日操作方法 =======================
/**
*
* @return
*/
public int getCurrentYear() {
return mDate.get(Calendar.YEAR);
}
/**
*
* @param year
*/
public void setCurrentYear(int year) {
if (!mInitialising && year == getCurrentYear()) {
return; // 初始化时或值未变化时不触发回调
}
mDate.set(Calendar.YEAR, year);
updateDateControl(); // 更新日期显示
onDateTimeChanged(); // 触发回调
}
/**
*
* @return 0-11
*/
public int getCurrentMonth() {
return mDate.get(Calendar.MONTH);
}
/**
*
* @param month 0-11
*/
public void setCurrentMonth(int month) {
if (!mInitialising && month == getCurrentMonth()) {
return;
}
mDate.set(Calendar.MONTH, month);
updateDateControl();
onDateTimeChanged();
}
/**
*
* @return 1-31
*/
public int getCurrentDay() {
return mDate.get(Calendar.DAY_OF_MONTH);
}
/**
*
* @param dayOfMonth 1-31
*/
public void setCurrentDay(int dayOfMonth) {
if (!mInitialising && dayOfMonth == getCurrentDay()) {
return;
}
mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth);
updateDateControl();
onDateTimeChanged();
}
// ======================= 小时操作方法 =======================
/**
* 24
* @return 0-23
*/
public int getCurrentHourOfDay() {
return mDate.get(Calendar.HOUR_OF_DAY);
}
/**
* 12/24
* @return 121-12240-23
*/
private int getCurrentHour() {
if (mIs24HourView){
return getCurrentHourOfDay(); // 24小时制直接返回
} else {
int hour = getCurrentHourOfDay();
if (hour > HOURS_IN_HALF_DAY) {
return hour - HOURS_IN_HALF_DAY; // PM13-23转换为1-11
} else {
return hour == 0 ? HOURS_IN_HALF_DAY : hour; // AM0点转为12点
}
}
}
/**
* 24
* @param hourOfDay 0-23
*/
public void setCurrentHour(int hourOfDay) {
if (!mInitialising && hourOfDay == getCurrentHourOfDay()) {
return;
}
mDate.set(Calendar.HOUR_OF_DAY, hourOfDay);
// 12小时制时需要额外处理AM/PM
if (!mIs24HourView) {
if (hourOfDay >= HOURS_IN_HALF_DAY) {
mIsAm = false; // PM
if (hourOfDay > HOURS_IN_HALF_DAY) {
hourOfDay -= HOURS_IN_HALF_DAY; // 13-23转换为1-11
}
} else {
mIsAm = true; // AM
if (hourOfDay == 0) {
hourOfDay = HOURS_IN_HALF_DAY; // 0点转为12点
}
}
updateAmPmControl(); // 更新AM/PM控件
}
mHourSpinner.setValue(hourOfDay); // 设置小时选择器值
onDateTimeChanged(); // 触发回调
}
// ======================= 分钟操作方法 =======================
/**
*
* @return 0-59
*/
public int getCurrentMinute() {
return mDate.get(Calendar.MINUTE);
}
/**
*
* @param minute 0-59
*/
public void setCurrentMinute(int minute) {
if (!mInitialising && minute == getCurrentMinute()) {
return;
}
mMinuteSpinner.setValue(minute); // 设置分钟选择器
mDate.set(Calendar.MINUTE, minute);
onDateTimeChanged();
}
// ======================= 24小时制管理 =======================
/**
* 24
* @return true: 24; false: 12
*/
public boolean is24HourView () {
return mIs24HourView;
}
/**
* 24
* @param is24HourView true: 24; false: 12
*/
public void set24HourView(boolean is24HourView) {
if (mIs24HourView == is24HourView) {
return; // 模式未变化
}
mIs24HourView = is24HourView;
// 显示/隐藏AM/PM选择器
mAmPmSpinner.setVisibility(is24HourView ? View.GONE : View.VISIBLE);
// 获取当前小时并更新控件
int hour = getCurrentHourOfDay();
updateHourControl(); // 更新小时选择器范围
setCurrentHour(hour); // 重新设置小时
updateAmPmControl(); // 更新AM/PM状态
}
// ======================= 控件更新方法 =======================
/**
*
* 3
*/
private void updateDateControl() {
Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(mDate.getTimeInMillis());
// 从当前日期向前推4天-3-1 = -4
cal.add(Calendar.DAY_OF_YEAR, -DAYS_IN_ALL_WEEK / 2 - 1);
mDateSpinner.setDisplayedValues(null); // 清空显示值
// 生成7天的日期显示字符串
for (int i = 0; i < DAYS_IN_ALL_WEEK; ++i) {
cal.add(Calendar.DAY_OF_YEAR, 1);
mDateDisplayValues[i] = (String) DateFormat.format("MM.dd EEEE", cal);
}
mDateSpinner.setDisplayedValues(mDateDisplayValues); // 设置显示值
mDateSpinner.setValue(DAYS_IN_ALL_WEEK / 2); // 设置当前为中间位置
mDateSpinner.invalidate(); // 重绘控件
}
/**
* AM/PM
* 24/AM/PM
*/
private void updateAmPmControl() {
if (mIs24HourView) {
mAmPmSpinner.setVisibility(View.GONE); // 24小时制隐藏
} else {
int index = mIsAm ? Calendar.AM : Calendar.PM; // AM=0, PM=1
mAmPmSpinner.setValue(index); // 设置AM/PM选择器值
mAmPmSpinner.setVisibility(View.VISIBLE); // 12小时制显示
}
}
/**
*
* 12/24
*/
private void updateHourControl() {
if (mIs24HourView) {
mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW);
mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW);
} else {
mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW);
mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW);
}
}
// ======================= 回调管理 =======================
/**
*
* @param callback
*/
public void setOnDateTimeChangedListener(OnDateTimeChangedListener callback) {
mOnDateTimeChangedListener = callback;
}
/**
*
*
*/
private void onDateTimeChanged() {
if (mOnDateTimeChangedListener != null) {
mOnDateTimeChangedListener.onDateTimeChanged(this, getCurrentYear(),
getCurrentMonth(), getCurrentDay(), getCurrentHourOfDay(),
getCurrentMinute());
}
}
}

@ -0,0 +1,202 @@
/*
* 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.
*/
// DateTimePickerDialog.java - 日期时间选择对话框
// 主要功能封装DateTimePicker控件提供完整的日期时间选择对话框界面
package net.micode.notes.ui;
// ======================= 导入区域 =======================
// Java日期时间
import java.util.Calendar; // 日历类,用于日期时间计算
// 应用内部资源
import net.micode.notes.R; // 资源文件R类
// 应用内部组件
import net.micode.notes.ui.DateTimePicker; // 日期时间选择器控件
import net.micode.notes.ui.DateTimePicker.OnDateTimeChangedListener; // 日期时间变化监听器
// Android对话框相关
import android.app.AlertDialog; // 警告对话框基类
import android.content.Context; // 上下文
import android.content.DialogInterface; // 对话框接口
import android.content.DialogInterface.OnClickListener; // 对话框点击监听器
// Android日期时间相关
import android.text.format.DateFormat; // 日期格式化工具
import android.text.format.DateUtils; // 日期工具类
// ======================= 日期时间选择对话框 =======================
/**
* DateTimePickerDialog -
* AlertDialogDateTimePicker
* /
* OnClickListener
*/
public class DateTimePickerDialog extends AlertDialog implements OnClickListener {
// ======================= 成员变量 =======================
/** 当前选择的日期时间 - Calendar对象存储用户选择的时间 */
private Calendar mDate = Calendar.getInstance();
/** 24小时制标志 - true: 24小时制; false: 12小时制 */
private boolean mIs24HourView;
/** 日期时间设置回调接口 - 当用户点击确定时回调 */
private OnDateTimeSetListener mOnDateTimeSetListener;
/** 日期时间选择器控件 - 核心选择组件 */
private DateTimePicker mDateTimePicker;
// ======================= 回调接口定义 =======================
/**
* OnDateTimeSetListener -
*
*/
public interface OnDateTimeSetListener {
/**
*
* @param dialog
* @param date
*/
void OnDateTimeSet(AlertDialog dialog, long date);
}
// ======================= 构造函数 =======================
/**
*
*
* @param context
* @param date
*/
public DateTimePickerDialog(Context context, long date) {
super(context);
// 1. 创建日期时间选择器控件
mDateTimePicker = new DateTimePicker(context);
// 2. 将选择器控件设置为对话框的内容视图
setView(mDateTimePicker);
// 3. 设置日期时间变化监听器
mDateTimePicker.setOnDateTimeChangedListener(new OnDateTimeChangedListener() {
/**
*
* DateTimePicker
* @param view DateTimePicker
* @param year
* @param month 0-11
* @param dayOfMonth 1-31
* @param hourOfDay 0-23
* @param minute 0-59
*/
public void onDateTimeChanged(DateTimePicker view, int year, int month,
int dayOfMonth, int hourOfDay, int minute) {
// 更新内部Calendar对象
mDate.set(Calendar.YEAR, year);
mDate.set(Calendar.MONTH, month);
mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth);
mDate.set(Calendar.HOUR_OF_DAY, hourOfDay);
mDate.set(Calendar.MINUTE, minute);
// 更新对话框标题显示
updateTitle(mDate.getTimeInMillis());
}
});
// 4. 设置初始日期时间
mDate.setTimeInMillis(date); // 设置时间戳
mDate.set(Calendar.SECOND, 0); // 秒数设为0只精确到分钟
// 5. 更新日期时间选择器控件的当前值
mDateTimePicker.setCurrentDate(mDate.getTimeInMillis());
// 6. 设置对话框按钮
// 确定按钮 - 点击后触发回调
setButton(context.getString(R.string.datetime_dialog_ok), this);
// 取消按钮 - 点击后直接关闭对话框
setButton2(context.getString(R.string.datetime_dialog_cancel), (OnClickListener)null);
// 7. 设置24小时制模式根据系统设置
set24HourView(DateFormat.is24HourFormat(this.getContext()));
// 8. 更新对话框标题显示
updateTitle(mDate.getTimeInMillis());
}
// ======================= 24小时制设置 =======================
/**
* 24
* @param is24HourView true: 24; false: 12
*/
public void set24HourView(boolean is24HourView) {
mIs24HourView = is24HourView;
}
// ======================= 回调监听器设置 =======================
/**
*
* @param callBack
*/
public void setOnDateTimeSetListener(OnDateTimeSetListener callBack) {
mOnDateTimeSetListener = callBack;
}
// ======================= 对话框标题更新 =======================
/**
*
*
* @param date
*/
private void updateTitle(long date) {
// 设置日期时间显示格式标志
int flag =
DateUtils.FORMAT_SHOW_YEAR | // 显示年份
DateUtils.FORMAT_SHOW_DATE | // 显示日期
DateUtils.FORMAT_SHOW_TIME; // 显示时间
// 根据24小时制标志添加相应格式
flag |= mIs24HourView ? DateUtils.FORMAT_24HOUR : DateUtils.FORMAT_24HOUR;
// 注意这里代码有bug应该是 FORMAT_24HOUR 或 FORMAT_12HOUR
// 格式化日期时间并设置为对话框标题
setTitle(DateUtils.formatDateTime(this.getContext(), date, flag));
}
// ======================= 按钮点击处理 =======================
/**
*
* OnClickListener
*
* @param arg0
* @param arg1
* DialogInterface.BUTTON_POSITIVE:
* DialogInterface.BUTTON_NEGATIVE:
* DialogInterface.BUTTON_NEUTRAL:
*/
public void onClick(DialogInterface arg0, int arg1) {
// 只有确定按钮会触发此回调
if (mOnDateTimeSetListener != null) {
// 触发日期时间设置回调
mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis());
}
}
}

@ -0,0 +1,172 @@
/*
* 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.
*/
// DropdownMenu.java - 下拉菜单包装类
// 主要功能将Button和PopupMenu组合封装提供简洁的下拉菜单实现
package net.micode.notes.ui;
// ======================= 导入区域 =======================
// Android上下文
import android.content.Context; // 上下文
// Android菜单相关
import android.view.Menu; // 菜单接口
import android.view.MenuItem; // 菜单项
// Android视图相关
import android.view.View; // 视图基类
import android.view.View.OnClickListener; // 点击监听器
// Android控件
import android.widget.Button; // 按钮控件
import android.widget.PopupMenu; // 弹出菜单控件
import android.widget.PopupMenu.OnMenuItemClickListener; // 弹出菜单项点击监听器
// 应用内部资源
import net.micode.notes.R; // 资源文件R类
// ======================= 下拉菜单包装类 =======================
/**
* DropdownMenu -
* ButtonPopupMenu
* 使Button
* FacadePopupMenu使
*/
public class DropdownMenu {
// ======================= 成员变量 =======================
/** 按钮控件 - 显示为下拉菜单的触发器按钮 */
private Button mButton;
/** 弹出菜单 - 实际的PopupMenu控件包含菜单项 */
private PopupMenu mPopupMenu;
/** 菜单对象 - 对应PopupMenu中的菜单用于查找菜单项 */
private Menu mMenu;
// ======================= 构造函数 =======================
/**
*
* Button
*
*
* 1. Button
* 2. Button
* 3. PopupMenuButton
* 4. Menu
* 5.
* 6. Button
*
* @param context PopupMenu
* @param button
* @param menuId IDres/menuXML
*
*
* new DropdownMenu(context, button, R.menu.note_operation_menu);
*/
public DropdownMenu(Context context, Button button, int menuId) {
// 1. 保存Button引用
mButton = button;
// 2. 设置Button背景为下拉箭头图标
// R.drawable.dropdown_icon: 下拉箭头图标
// 使Button看起来像下拉菜单按钮
mButton.setBackgroundResource(R.drawable.dropdown_icon);
// 3. 创建PopupMenu并关联到Button
// 第二个参数是锚点视图菜单会显示在Button下方
mPopupMenu = new PopupMenu(context, mButton);
// 4. 获取PopupMenu的Menu对象
mMenu = mPopupMenu.getMenu();
// 5. 加载菜单布局文件
// 从XML资源文件加载菜单项到Menu对象
mPopupMenu.getMenuInflater().inflate(menuId, mMenu);
// 6. 设置Button点击事件
mButton.setOnClickListener(new OnClickListener() {
/**
* Button
* Button
* @param v Button
*/
public void onClick(View v) {
mPopupMenu.show(); // 显示弹出菜单
}
});
}
// ======================= 菜单项点击监听器设置 =======================
/**
*
*
*
* @param listener
* OnMenuItemClickListener
*
* 使
* dropdownMenu.setOnDropdownMenuItemClickListener(new OnMenuItemClickListener() {
* @Override
* public boolean onMenuItemClick(MenuItem item) {
* // 处理菜单项点击
* return true;
* }
* });
*/
public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) {
// 安全检查确保PopupMenu已创建
if (mPopupMenu != null) {
mPopupMenu.setOnMenuItemClickListener(listener);
}
}
// ======================= 菜单项查找 =======================
/**
* ID
*
*
* @param id ID
* @return MenuItemnull
*
* 使
* MenuItem item = dropdownMenu.findItem(R.id.menu_delete);
* if (item != null) {
* item.setEnabled(false); // 禁用删除菜单项
* }
*/
public MenuItem findItem(int id) {
return mMenu.findItem(id);
}
// ======================= 按钮标题设置 =======================
/**
*
*
*
* @param title
*
* 使
* dropdownMenu.setTitle("操作菜单");
*
* dropdownMenu.setTitle(getString(R.string.menu_title));
*/
public void setTitle(CharSequence title) {
mButton.setText(title);
}
}

@ -0,0 +1,183 @@
/*
* 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.
*/
// FoldersListAdapter.java - 文件夹列表适配器
// 主要功能将数据库中的文件夹数据适配到ListView显示
package net.micode.notes.ui;
// ======================= 导入区域 =======================
// Android基础
import android.content.Context; // 上下文
// Android数据库
import android.database.Cursor; // 数据库查询结果游标
// Android视图
import android.view.View; // 视图基类
import android.view.ViewGroup; // 视图容器
// Android适配器
import android.widget.CursorAdapter; // 游标适配器基类
// Android布局
import android.widget.LinearLayout; // 线性布局
// Android控件
import android.widget.TextView; // 文本视图
// 应用内部资源
import net.micode.notes.R; // 资源文件R类
// 应用数据模型
import net.micode.notes.data.Notes; // Notes主类
import net.micode.notes.data.Notes.NoteColumns; // 便签表列定义
// ======================= 文件夹列表适配器 =======================
/**
* FoldersListAdapter -
* CursorAdapterListView
*
* 使便
*/
public class FoldersListAdapter extends CursorAdapter {
// ======================= 数据库查询配置 =======================
/**
* -
*
*/
public static final String [] PROJECTION = {
NoteColumns.ID, // 0 - 文件夹ID
NoteColumns.SNIPPET // 1 - 文件夹名称存储在SNIPPET字段中
};
// 字段索引常量 - 提高代码可读性
public static final int ID_COLUMN = 0; // ID字段索引
public static final int NAME_COLUMN = 1; // 名称字段索引
// ======================= 构造函数 =======================
/**
*
* CursorAdapter
* @param context
* @param c
*/
public FoldersListAdapter(Context context, Cursor c) {
super(context, c);
// TODO: 可在此处添加额外初始化代码
}
// ======================= 适配器核心方法 =======================
/**
*
* ListView
* @param context
* @param cursor
* @param parent ListView
* @return
*/
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
// 创建并返回FolderListItem对象
return new FolderListItem(context);
}
/**
*
*
* @param view
* @param context
* @param cursor
*/
@Override
public void bindView(View view, Context context, Cursor cursor) {
// 安全检查确保视图是FolderListItem类型
if (view instanceof FolderListItem) {
// 获取文件夹名称
String folderName = null;
// 特殊处理如果是根文件夹ID_ROOT_FOLDER使用预设名称
if (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) {
// 根文件夹显示为"主文件夹"
folderName = context.getString(R.string.menu_move_parent_folder);
} else {
// 普通文件夹使用数据库中的名称
folderName = cursor.getString(NAME_COLUMN);
}
// 调用FolderListItem的bind方法设置名称
((FolderListItem) view).bind(folderName);
}
}
// ======================= 辅助方法 =======================
/**
*
*
*
*
* @param context
* @param position
* @return
*/
public String getFolderName(Context context, int position) {
// 获取指定位置的Cursor对象
Cursor cursor = (Cursor) getItem(position);
// 与bindView中相同的逻辑处理文件夹名称
if (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) {
return context.getString(R.string.menu_move_parent_folder);
} else {
return cursor.getString(NAME_COLUMN);
}
}
// ======================= 内部类:文件夹列表项视图 =======================
/**
* FolderListItem -
* LinearLayout
*
*/
private class FolderListItem extends LinearLayout {
/** 文件夹名称文本视图 */
private TextView mName;
/**
*
*
* @param context
*/
public FolderListItem(Context context) {
super(context);
// 1. 加载布局文件
// R.layout.folder_list_item: 列表项布局
// 第三个参数true: 将布局添加到当前视图
inflate(context, R.layout.folder_list_item, this);
// 2. 查找并保存TextView引用
mName = (TextView) findViewById(R.id.tv_folder_name);
}
/**
*
* TextView
* @param name
*/
public void bind(String name) {
mName.setText(name);
}
}
}

@ -0,0 +1,457 @@
/*
* 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.
*/
// NoteEditText.java - 自定义便签编辑文本控件
// 主要功能扩展EditText支持清单模式的特殊按键处理、超链接识别和上下文菜单
package net.micode.notes.ui;
// ======================= 导入区域 =======================
// Android基础
import android.content.Context; // 上下文
import android.content.ContentResolver; // 内容解析器
import android.graphics.Rect; // 矩形区域
// Android文本处理
import android.text.Layout; // 文本布局
import android.text.Selection; // 文本选择
import android.text.Spanned; // 可设置样式的文本
import android.text.TextUtils; // 文本工具
import android.text.style.URLSpan; // URL超链接样式
import android.util.AttributeSet; // 属性集
import android.util.Log; // 日志工具
// Android菜单
import android.view.ContextMenu; // 上下文菜单
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.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类
// Java集合
import java.util.HashMap; // 哈希映射
import java.util.Map; // 映射接口
// ======================= 便签编辑文本控件 =======================
/**
* NoteEditText - 便
* EditText便
*
* 1.
* 2.
* 3.
* 4.
*/
public class NoteEditText extends EditText {
// ======================= 常量定义 =======================
private static final String TAG = "NoteEditText"; // 日志标签
/** 当前项索引 - 在清单模式中标识第几个列表项 */
private int mIndex;
/** 删除前的选择起始位置 - 用于判断是否在开头删除 */
private int mSelectionStartBeforeDelete;
// 超链接协议常量
private static final String SCHEME_TEL = "tel:"; // 电话协议
private static final String SCHEME_HTTP = "http:"; // HTTP协议
private static final String SCHEME_EMAIL = "mailto:"; // 邮件协议
/**
*
* ID
*
*/
private static final Map<String, Integer> sSchemaActionResMap = new HashMap<String, Integer>();
static {
sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel); // 电话链接
sSchemaActionResMap.put(SCHEME_HTTP, R.string.note_link_web); // 网页链接
sSchemaActionResMap.put(SCHEME_EMAIL, R.string.note_link_email); // 邮件链接
}
// ======================= 文本变化监听器接口 =======================
/**
* OnTextViewChangeListener -
* NoteEditActivity
*/
public interface OnTextViewChangeListener {
/**
*
*
* @param index
* @param text
*/
void onEditTextDelete(int index, String text);
/**
*
*
* @param index
* @param text
*/
void onEditTextEnter(int index, String text);
/**
*
* /
* @param index
* @param hasText
*/
void onTextChange(int index, boolean hasText);
}
/** 文本变化监听器实例 */
private OnTextViewChangeListener mOnTextViewChangeListener;
// ======================= 构造函数 =======================
/**
* 1 -
* @param context
*/
public NoteEditText(Context context) {
this(context, null);
mIndex = 0; // 默认索引为0
}
/**
* 2 -
* @param context
* @param attrs
*/
public NoteEditText(Context context, AttributeSet attrs) {
this(context, attrs, android.R.attr.editTextStyle);
}
/**
* 3 -
* @param context
* @param attrs
* @param defStyle
*/
public NoteEditText(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// 确保输入法支持中文输入
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);
}
}
// ======================= 设置方法 =======================
/**
*
*
* @param index
*/
public void setIndex(int index) {
mIndex = index;
}
/**
*
* @param listener
*/
public void setOnTextViewChangeListener(OnTextViewChangeListener listener) {
mOnTextViewChangeListener = listener;
}
// ======================= 触摸事件处理 =======================
/**
*
*
* @param event
* @return true: ; false:
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 计算触摸点在文本中的精确位置
int x = (int) event.getX();
int y = (int) event.getY();
x -= getTotalPaddingLeft(); // 减去内边距
y -= getTotalPaddingTop();
x += getScrollX(); // 加上滚动偏移
y += getScrollY();
// 获取布局并计算字符偏移
Layout layout = getLayout();
int line = layout.getLineForVertical(y);
int off = layout.getOffsetForHorizontal(line, x);
// 设置选择位置
Selection.setSelection(getText(), off);
break;
}
return super.onTouchEvent(event);
}
// ======================= 按键按下事件 =======================
/**
*
*
* @param keyCode
* @param event
* @return true: ; false:
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_ENTER:
// 拦截回车键在onKeyUp中处理
if (mOnTextViewChangeListener != null) {
return false; // 返回false让系统继续处理
}
break;
case KeyEvent.KEYCODE_DEL:
// 记录删除前的选择位置
mSelectionStartBeforeDelete = getSelectionStart();
break;
default:
break;
}
return super.onKeyDown(keyCode, event);
}
// ======================= 按键抬起事件 =======================
/**
*
*
* @param keyCode
* @param event
* @return true: ; false:
*/
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
switch(keyCode) {
case KeyEvent.KEYCODE_DEL:
// 删除键处理
if (mOnTextViewChangeListener != null) {
// 条件:在文本开头删除 且 不是第一项
if (0 == mSelectionStartBeforeDelete && mIndex != 0) {
// 触发删除回调
mOnTextViewChangeListener.onEditTextDelete(mIndex, getText().toString());
return true; // 已处理
}
} else {
Log.d(TAG, "OnTextViewChangeListener was not seted");
}
break;
case KeyEvent.KEYCODE_ENTER:
// 回车键处理
if (mOnTextViewChangeListener != null) {
int selectionStart = getSelectionStart();
// 分割文本:光标前保留,光标后移到新项
String text = getText().subSequence(selectionStart, length()).toString();
setText(getText().subSequence(0, selectionStart));
// 触发新增回调
mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, text);
} else {
Log.d(TAG, "OnTextViewChangeListener was not seted");
}
break;
default:
break;
}
return super.onKeyUp(keyCode, event);
}
// ======================= 焦点变化处理 =======================
/**
*
*
* @param focused
* @param direction
* @param previouslyFocusedRect
*/
@Override
protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
if (mOnTextViewChangeListener != null) {
// 失去焦点且文本为空时,通知隐藏操作控件
if (!focused && TextUtils.isEmpty(getText())) {
mOnTextViewChangeListener.onTextChange(mIndex, false);
} else {
// 其他情况通知显示操作控件
mOnTextViewChangeListener.onTextChange(mIndex, true);
}
}
super.onFocusChanged(focused, direction, previouslyFocusedRect);
}
// ======================= 上下文菜单创建 =======================
/**
*
*
* @param menu
*/
@Override
protected void onCreateContextMenu(ContextMenu menu) {
// 检查文本是否包含样式(可能包含超链接)
if (getText() instanceof Spanned) {
int selStart = getSelectionStart();
int selEnd = getSelectionEnd();
// 计算选择范围
int min = Math.min(selStart, selEnd);
int max = Math.max(selStart, selEnd);
// 获取选择范围内的超链接
final URLSpan[] urls = ((Spanned) getText()).getSpans(min, max, URLSpan.class);
// 只处理单个超链接的情况
if (urls.length == 1) {
int defaultResId = 0;
// 根据URL协议确定菜单文本
for(String schema: sSchemaActionResMap.keySet()) {
if(urls[0].getURL().indexOf(schema) >= 0) {
defaultResId = sSchemaActionResMap.get(schema);
break;
}
}
// 未知协议使用默认文本
if (defaultResId == 0) {
defaultResId = R.string.note_link_other;
}
// 添加菜单项
menu.add(0, 0, 0, defaultResId).setOnMenuItemClickListener(
new OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
// 触发超链接点击
urls[0].onClick(NoteEditText.this);
return true;
}
});
}
}
super.onCreateContextMenu(menu);
}
}

@ -0,0 +1,421 @@
/*
* 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.
*/
// NoteItemData.java - 便签项数据模型类
// 主要功能:封装便签列表项的数据,提供便捷的访问方法和位置判断逻辑
package net.micode.notes.ui;
// ======================= 导入区域 =======================
// Android基础
import android.content.Context; // 上下文
import android.database.Cursor; // 数据库查询结果游标
import android.text.TextUtils; // 文本工具
// 应用内部工具
import net.micode.notes.data.Contact; // 联系人工具类
import net.micode.notes.data.Notes; // Notes主类
import net.micode.notes.data.Notes.NoteColumns; // 便签表列定义
import net.micode.notes.tool.DataUtils; // 数据工具
// ======================= 便签项数据模型 =======================
/**
* NoteItemData - 便
* Cursor便
* 便便
*
*/
public class NoteItemData {
// ======================= 数据库查询配置 =======================
/**
* -
* 便
*/
static final String [] PROJECTION = new String [] {
NoteColumns.ID, // 0 - 便签ID
NoteColumns.ALERTED_DATE, // 1 - 提醒时间
NoteColumns.BG_COLOR_ID, // 2 - 背景颜色ID
NoteColumns.CREATED_DATE, // 3 - 创建时间
NoteColumns.HAS_ATTACHMENT, // 4 - 是否有附件
NoteColumns.MODIFIED_DATE, // 5 - 修改时间
NoteColumns.NOTES_COUNT, // 6 - 包含的便签数(文件夹用)
NoteColumns.PARENT_ID, // 7 - 父文件夹ID
NoteColumns.SNIPPET, // 8 - 便签摘要
NoteColumns.TYPE, // 9 - 便签类型
NoteColumns.WIDGET_ID, // 10 - 小部件ID
NoteColumns.WIDGET_TYPE, // 11 - 小部件类型
NoteColumns.PINNED // 12 - 是否置顶
};
// ======================= 字段索引常量 =======================
// 使用常量而非魔法数字,提高代码可读性
private static final int ID_COLUMN = 0; // ID字段索引
private static final int ALERTED_DATE_COLUMN = 1; // 提醒时间字段索引
private static final int BG_COLOR_ID_COLUMN = 2; // 背景颜色字段索引
private static final int CREATED_DATE_COLUMN = 3; // 创建时间字段索引
private static final int HAS_ATTACHMENT_COLUMN = 4; // 附件字段索引
private static final int MODIFIED_DATE_COLUMN = 5; // 修改时间字段索引
private static final int NOTES_COUNT_COLUMN = 6; // 便签数字段索引
private static final int PARENT_ID_COLUMN = 7; // 父ID字段索引
private static final int SNIPPET_COLUMN = 8; // 摘要字段索引
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; // 是否置顶字段索引
// ======================= 数据成员 =======================
// 基本数据字段
private long mId; // 便签ID
private long mAlertDate; // 提醒时间戳
private int mBgColorId; // 背景颜色索引
private long mCreatedDate; // 创建时间戳
private boolean mHasAttachment; // 是否有附件
private long mModifiedDate; // 修改时间戳
private int mNotesCount; // 包含的便签数(文件夹)
private long mParentId; // 父文件夹ID
private String mSnippet; // 便签摘要
private int mType; // 便签类型
private int mWidgetId; // 小部件ID
private int mWidgetType; // 小部件类型
private boolean mPinned; // 是否置顶
// 通话记录相关
private String mName; // 联系人姓名
private String mPhoneNumber; // 电话号码
// 位置状态标志
private boolean mIsLastItem; // 是否是列表最后一项
private boolean mIsFirstItem; // 是否是列表第一项
private boolean mIsOnlyOneItem; // 是否是唯一一项
private boolean mIsOneNoteFollowingFolder; // 是否是文件夹后的唯一便签
private boolean mIsMultiNotesFollowingFolder; // 是否是文件夹后的多个便签之一
// ======================= 构造函数 =======================
/**
*
* Cursor
*
* 1. Cursor
* 2.
* 3.
* 4.
*
* @param context
* @param cursor PROJECTION
*/
public NoteItemData(Context context, Cursor cursor) {
// 1. 读取基本字段
mId = cursor.getLong(ID_COLUMN);
mAlertDate = cursor.getLong(ALERTED_DATE_COLUMN);
mBgColorId = cursor.getInt(BG_COLOR_ID_COLUMN);
mCreatedDate = cursor.getLong(CREATED_DATE_COLUMN);
mHasAttachment = (cursor.getInt(HAS_ATTACHMENT_COLUMN) > 0) ? true : false;
mModifiedDate = cursor.getLong(MODIFIED_DATE_COLUMN);
mNotesCount = cursor.getInt(NOTES_COUNT_COLUMN);
mParentId = cursor.getLong(PARENT_ID_COLUMN);
mSnippet = cursor.getString(SNIPPET_COLUMN);
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. 清理摘要中的清单标记
// 移除已勾选(√)和未勾选(□)标记,只显示纯文本
mSnippet = mSnippet.replace(NoteEditActivity.TAG_CHECKED, "").replace(
NoteEditActivity.TAG_UNCHECKED, "");
// 3. 处理通话记录
mPhoneNumber = ""; // 初始化电话号码
if (mParentId == Notes.ID_CALL_RECORD_FOLDER) {
// 从通话记录文件夹中获取电话号码
mPhoneNumber = DataUtils.getCallNumberByNoteId(context.getContentResolver(), mId);
if (!TextUtils.isEmpty(mPhoneNumber)) {
// 根据电话号码查询联系人姓名
mName = Contact.getContact(context, mPhoneNumber);
if (mName == null) {
// 未找到联系人,使用电话号码作为显示名称
mName = mPhoneNumber;
}
}
}
// 确保名称不为null
if (mName == null) {
mName = "";
}
// 4. 检查位置状态
checkPostion(cursor);
}
// ======================= 位置检查方法 =======================
/**
* 便
* 使
*
* 1.
* 2. 便便
*
* @param cursor
*/
private void checkPostion(Cursor cursor) {
// 1. 基本位置判断
mIsLastItem = cursor.isLast() ? true : false; // 是否是最后一项
mIsFirstItem = cursor.isFirst() ? true : false; // 是否是第一项
mIsOnlyOneItem = (cursor.getCount() == 1); // 是否是唯一一项
// 初始化文件夹后位置标志
mIsMultiNotesFollowingFolder = false;
mIsOneNoteFollowingFolder = false;
// 2. 判断是否是文件夹后的便签
// 条件:当前是便签类型 且 不是第一项
if (mType == Notes.TYPE_NOTE && !mIsFirstItem) {
int position = cursor.getPosition(); // 获取当前游标位置
// 向前移动游标检查前一项
if (cursor.moveToPrevious()) {
// 判断前一项是否是文件夹或系统文件夹
if (cursor.getInt(TYPE_COLUMN) == Notes.TYPE_FOLDER
|| cursor.getInt(TYPE_COLUMN) == Notes.TYPE_SYSTEM) {
// 判断后面是否还有更多项
if (cursor.getCount() > (position + 1)) {
// 文件夹后有多个便签
mIsMultiNotesFollowingFolder = true;
} else {
// 文件夹后只有一个便签
mIsOneNoteFollowingFolder = true;
}
}
// 移动回原位置
if (!cursor.moveToNext()) {
throw new IllegalStateException("cursor move to previous but can't move back");
}
}
}
}
// ======================= 位置状态获取方法 =======================
/**
* 便
* list_{color}_single
* @return true: 便
*/
public boolean isOneFollowingFolder() {
return mIsOneNoteFollowingFolder;
}
/**
* 便
* list_{color}_middle
* @return true: 便
*/
public boolean isMultiFollowingFolder() {
return mIsMultiNotesFollowingFolder;
}
/**
*
* list_{color}_down
* @return true:
*/
public boolean isLast() {
return mIsLastItem;
}
/**
*
* 便
* @return
*/
public String getCallName() {
return mName;
}
/**
*
* list_{color}_up
* @return true:
*/
public boolean isFirst() {
return mIsFirstItem;
}
/**
*
* list_{color}_single
* @return true:
*/
public boolean isSingle() {
return mIsOnlyOneItem;
}
// ======================= 基本属性获取方法 =======================
/**
* 便ID
* @return 便ID
*/
public long getId() {
return mId;
}
/**
*
* @return 0
*/
public long getAlertDate() {
return mAlertDate;
}
/**
*
* @return
*/
public long getCreatedDate() {
return mCreatedDate;
}
/**
*
* @return true:
*/
public boolean hasAttachment() {
return mHasAttachment;
}
/**
*
* @return
*/
public long getModifiedDate() {
return mModifiedDate;
}
/**
* ID
* @return
*/
public int getBgColorId() {
return mBgColorId;
}
/**
* ID
* @return ID
*/
public long getParentId() {
return mParentId;
}
/**
* 便
*
* @return 便
*/
public int getNotesCount() {
return mNotesCount;
}
/**
* IDgetParentId
* 访
* @return ID
*/
public long getFolderId () {
return mParentId;
}
/**
* 便
* @return TYPE_NOTE, TYPE_FOLDER, TYPE_SYSTEM
*/
public int getType() {
return mType;
}
/**
*
* @return TYPE_WIDGET_INVALIDE, TYPE_WIDGET_2X, TYPE_WIDGET_4X
*/
public int getWidgetType() {
return mWidgetType;
}
/**
* 便
* @return true: , false:
*/
public boolean isPinned() {
return mPinned;
}
/**
* ID
* @return ID
*/
public int getWidgetId() {
return mWidgetId;
}
/**
* 便
*
* @return 便
*/
public String getSnippet() {
return mSnippet;
}
// ======================= 状态判断方法 =======================
/**
*
* @return true:
*/
public boolean hasAlert() {
return (mAlertDate > 0);
}
/**
*
*
* @return true:
*/
public boolean isCallRecord() {
return (mParentId == Notes.ID_CALL_RECORD_FOLDER && !TextUtils.isEmpty(mPhoneNumber));
}
// ======================= 静态工具方法 =======================
/**
* Cursor便
*
* @param cursor
* @return 便
*/
public static int getNoteType(Cursor cursor) {
return cursor.getInt(TYPE_COLUMN);
}
}

@ -0,0 +1,318 @@
/*
* 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.
*/
// NotesListAdapter.java - 便签列表适配器
// 主要功能:为便签列表提供数据适配,支持多选模式和选中状态管理
package net.micode.notes.ui;
// ======================= 导入区域 =======================
// Android基础
import android.content.Context; // 上下文
import android.database.Cursor; // 数据库查询结果游标
import android.util.Log; // 日志工具
// Android视图
import android.view.View; // 视图基类
import android.view.ViewGroup; // 视图容器
// Android适配器
import android.widget.CursorAdapter; // 游标适配器基类
// 应用数据模型
import net.micode.notes.data.Notes; // Notes主类
// Java集合
import java.util.Collection; // 集合接口
import java.util.HashMap; // 哈希映射
import java.util.HashSet; // 哈希集合
import java.util.Iterator; // 迭代器
// ======================= 便签列表适配器 =======================
/**
* NotesListAdapter - 便
* CursorAdapterListView便
*
* 1. 便
* 2.
* 3.
* 4.
*/
public class NotesListAdapter extends CursorAdapter {
// ======================= 常量定义 =======================
private static final String TAG = "NotesListAdapter"; // 日志标签
// ======================= 成员变量 =======================
/** 上下文 */
private Context mContext;
/** 选中索引映射 - 存储列表位置与选中状态的映射 */
private HashMap<Integer, Boolean> mSelectedIndex;
/** 便签总数 - 不包括文件夹,用于全选判断 */
private int mNotesCount;
/** 选择模式标志 - true: 多选模式; false: 普通模式 */
private boolean mChoiceMode;
// ======================= 小部件属性内部类 =======================
/**
* AppWidgetAttribute -
* 便
*/
public static class AppWidgetAttribute {
public int widgetId; // 小部件ID
public int widgetType; // 小部件类型
};
// ======================= 构造函数 =======================
/**
*
*
* @param context
*/
public NotesListAdapter(Context context) {
super(context, null); // 初始游标为null
mSelectedIndex = new HashMap<Integer, Boolean>();
mContext = context;
mNotesCount = 0; // 初始便签数为0
}
// ======================= 适配器核心方法 =======================
/**
*
* ListView
* @param context
* @param cursor
* @param parent ListView
* @return NotesListItem
*/
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return new NotesListItem(context);
}
/**
*
*
* @param view
* @param context
* @param cursor
*/
@Override
public void bindView(View view, Context context, Cursor cursor) {
if (view instanceof NotesListItem) {
// 创建便签数据项
NoteItemData itemData = new NoteItemData(context, cursor);
// 绑定数据到列表项
((NotesListItem) view).bind(context, itemData, mChoiceMode,
isSelectedItem(cursor.getPosition()));
}
}
// ======================= 选择模式管理 =======================
/**
*
*
* @param position
* @param checked true: ; false:
*/
public void setCheckedItem(final int position, final boolean checked) {
mSelectedIndex.put(position, checked);
notifyDataSetChanged(); // 通知视图更新
}
/**
*
* @return true: ; false:
*/
public boolean isInChoiceMode() {
return mChoiceMode;
}
/**
*
* 退
* @param mode true: ; false: 退
*/
public void setChoiceMode(boolean mode) {
mSelectedIndex.clear(); // 清除所有选中状态
mChoiceMode = mode;
}
/**
* /
* 便
* @param checked true: ; false:
*/
public void selectAll(boolean checked) {
Cursor cursor = getCursor();
for (int i = 0; i < getCount(); i++) {
if (cursor.moveToPosition(i)) {
// 只选择便签类型,不选择文件夹
if (NoteItemData.getNoteType(cursor) == Notes.TYPE_NOTE) {
setCheckedItem(i, checked);
}
}
}
}
// ======================= 选中项ID获取 =======================
/**
* ID
*
* @return ID
*/
public HashSet<Long> getSelectedItemIds() {
HashSet<Long> itemSet = new HashSet<Long>();
for (Integer position : mSelectedIndex.keySet()) {
if (mSelectedIndex.get(position) == true) {
Long id = getItemId(position);
// 安全检查:排除根文件夹
if (id == Notes.ID_ROOT_FOLDER) {
Log.d(TAG, "Wrong item id, should not happen");
} else {
itemSet.add(id);
}
}
}
return itemSet;
}
// ======================= 选中项小部件获取 =======================
/**
*
*
* @return
*/
public HashSet<AppWidgetAttribute> getSelectedWidget() {
HashSet<AppWidgetAttribute> itemSet = new HashSet<AppWidgetAttribute>();
for (Integer position : mSelectedIndex.keySet()) {
if (mSelectedIndex.get(position) == true) {
Cursor c = (Cursor) getItem(position);
if (c != null) {
AppWidgetAttribute widget = new AppWidgetAttribute();
NoteItemData item = new NoteItemData(mContext, c);
widget.widgetId = item.getWidgetId();
widget.widgetType = item.getWidgetType();
itemSet.add(widget);
/**
*
*
*/
} else {
Log.e(TAG, "Invalid cursor");
return null;
}
}
}
return itemSet;
}
// ======================= 选中计数 =======================
/**
*
* @return
*/
public int getSelectedCount() {
Collection<Boolean> values = mSelectedIndex.values();
if (null == values) {
return 0;
}
Iterator<Boolean> iter = values.iterator();
int count = 0;
while (iter.hasNext()) {
if (true == iter.next()) {
count++;
}
}
return count;
}
/**
*
* 便
* @return true: 便; false: 便
*/
public boolean isAllSelected() {
int checkedCount = getSelectedCount();
return (checkedCount != 0 && checkedCount == mNotesCount);
}
/**
*
* @param position
* @return true: ; false:
*/
public boolean isSelectedItem(final int position) {
if (null == mSelectedIndex.get(position)) {
return false;
}
return mSelectedIndex.get(position);
}
// ======================= 数据变化处理 =======================
/**
*
* 便
*/
@Override
protected void onContentChanged() {
super.onContentChanged();
calcNotesCount(); // 重新计算便签数
}
/**
*
* 便
* @param cursor
*/
@Override
public void changeCursor(Cursor cursor) {
super.changeCursor(cursor);
calcNotesCount(); // 重新计算便签数
}
// ======================= 便签数量计算 =======================
/**
* 便
* TYPE_NOTE
*/
private void calcNotesCount() {
mNotesCount = 0; // 重置计数
for (int i = 0; i < getCount(); i++) {
Cursor c = (Cursor) getItem(i);
if (c != null) {
// 只统计便签类型,不统计文件夹
if (NoteItemData.getNoteType(c) == Notes.TYPE_NOTE) {
mNotesCount++;
}
} else {
Log.e(TAG, "Invalid cursor");
return;
}
}
}
}

@ -0,0 +1,277 @@
/*
* 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.
*/
// NotesListItem.java - 便签列表项自定义视图
// 主要功能:显示便签或文件夹列表项的完整视图,包括图标、文本、时间等
package net.micode.notes.ui;
// ======================= 导入区域 =======================
// Android基础
import android.content.Context; // 上下文
import android.text.format.DateUtils; // 日期工具,用于相对时间显示
// Android视图
import android.view.View; // 视图基类
// Android控件
import android.widget.CheckBox; // 复选框
import android.widget.ImageView; // 图片视图
import android.widget.LinearLayout; // 线性布局容器
import android.widget.TextView; // 文本视图
// 应用内部资源
import net.micode.notes.R; // 资源文件R类
// 应用数据模型
import net.micode.notes.data.Notes; // Notes主类
// 应用工具
import net.micode.notes.tool.DataUtils; // 数据工具
// 应用资源解析
import net.micode.notes.tool.ResourceParser.NoteItemBgResources; // 列表项背景资源
// ======================= 便签列表项视图 =======================
/**
* NotesListItem - 便
* LinearLayout便
* 便/
*
*/
public class NotesListItem extends LinearLayout {
// ======================= 成员变量 =======================
/** 提醒图标 - 显示便签是否有提醒 */
private ImageView mAlert;
/** 置顶图标 - 显示便签是否置顶 */
private ImageView mPinned;
/** 标题文本 - 显示便签摘要或文件夹名称 */
private TextView mTitle;
/** 时间文本 - 显示修改时间(相对时间) */
private TextView mTime;
/** 联系人姓名文本 - 通话记录专用,显示联系人姓名 */
private TextView mCallName;
/** 便签数据项 - 当前项绑定的数据 */
private NoteItemData mItemData;
/** 复选框 - 多选模式下显示,用于选择操作 */
private CheckBox mCheckBox;
// ======================= 构造函数 =======================
/**
*
*
* @param context
*/
public NotesListItem(Context context) {
super(context);
// 1. 加载布局文件
// R.layout.note_item: 列表项布局
// 第三个参数true: 将布局添加到当前LinearLayout
inflate(context, R.layout.note_item, this);
// 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);
mCheckBox = (CheckBox) findViewById(android.R.id.checkbox);
}
// ======================= 数据绑定方法 =======================
/**
*
* 便UI
*
* 1.
* 2. 便
* 3.
* 4.
*
* @param context
* @param data 便
* @param choiceMode
* @param checked
*/
public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked) {
// 1. 处理选择模式复选框
if (choiceMode && data.getType() == Notes.TYPE_NOTE) {
// 选择模式且是便签类型:显示复选框
mCheckBox.setVisibility(View.VISIBLE);
mCheckBox.setChecked(checked);
} else {
// 非选择模式或非便签类型:隐藏复选框
mCheckBox.setVisibility(View.GONE);
}
// 保存数据引用
mItemData = data;
// 2. 根据便签类型设置不同显示
if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) {
// 情况1通话记录文件夹
bindCallRecordFolder(context, data);
} else if (data.getParentId() == Notes.ID_CALL_RECORD_FOLDER) {
// 情况2通话记录文件夹中的便签
bindCallRecordNote(context, data);
} else {
// 情况3普通文件夹或便签
bindNormalItem(context, data);
}
// 3. 更新时间显示(所有类型都显示)
mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate()));
// 4. 设置背景
setBackground(data);
}
// ======================= 通话记录文件夹绑定 =======================
/**
*
* + 便
* @param context
* @param data
*/
private void bindCallRecordFolder(Context context, NoteItemData data) {
// 隐藏联系人姓名
mCallName.setVisibility(View.GONE);
// 显示提醒图标(文件夹图标)
mAlert.setVisibility(View.VISIBLE);
// 设置主标题样式
mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem);
// 设置标题文本:文件夹名称 + 文件数量
mTitle.setText(context.getString(R.string.call_record_folder_name)
+ context.getString(R.string.format_folder_files_count, data.getNotesCount()));
// 设置文件夹图标
mAlert.setImageResource(R.drawable.call_record);
}
// ======================= 通话记录便签绑定 =======================
/**
* 便
* +
* @param context
* @param data 便
*/
private void bindCallRecordNote(Context context, NoteItemData data) {
// 显示联系人姓名
mCallName.setVisibility(View.VISIBLE);
mCallName.setText(data.getCallName());
// 设置副标题样式
mTitle.setTextAppearance(context,R.style.TextAppearanceSecondaryItem);
// 设置标题文本(格式化摘要)
mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet()));
// 根据是否有提醒设置图标
if (data.hasAlert()) {
mAlert.setImageResource(R.drawable.clock);
mAlert.setVisibility(View.VISIBLE);
} else {
mAlert.setVisibility(View.GONE);
}
}
// ======================= 普通项绑定 =======================
/**
* 便
*
* @param context
* @param data 便/
*/
private void bindNormalItem(Context context, NoteItemData data) {
// 隐藏联系人姓名
mCallName.setVisibility(View.GONE);
// 设置主标题样式
mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem);
if (data.getType() == Notes.TYPE_FOLDER) {
// 文件夹:显示名称 + 文件数量
mTitle.setText(data.getSnippet()
+ context.getString(R.string.format_folder_files_count,
data.getNotesCount()));
mAlert.setVisibility(View.GONE); // 文件夹不显示提醒图标
} else {
// 普通便签:显示格式化摘要
mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet()));
// 根据是否有提醒设置图标
if (data.hasAlert()) {
mAlert.setImageResource(R.drawable.clock);
mAlert.setVisibility(View.VISIBLE);
} else {
mAlert.setVisibility(View.GONE);
}
// 根据是否置顶设置置顶图标
if (data.isPinned()) {
mPinned.setVisibility(View.VISIBLE);
} else {
mPinned.setVisibility(View.GONE);
}
}
}
// ======================= 背景设置 =======================
/**
*
* 便
*
* 1. 便
* 2. 使
*
* @param data 便
*/
private void setBackground(NoteItemData data) {
int id = data.getBgColorId(); // 获取背景颜色ID
if (data.getType() == Notes.TYPE_NOTE) {
// 便签类型:根据位置选择背景
if (data.isSingle() || data.isOneFollowingFolder()) {
// 单独项或文件夹后的唯一便签:单独项背景
setBackgroundResource(NoteItemBgResources.getNoteBgSingleRes(id));
} else if (data.isLast()) {
// 最后一项:底部圆角背景
setBackgroundResource(NoteItemBgResources.getNoteBgLastRes(id));
} else if (data.isFirst() || data.isMultiFollowingFolder()) {
// 第一项或文件夹后的多个便签之一:顶部圆角背景
setBackgroundResource(NoteItemBgResources.getNoteBgFirstRes(id));
} else {
// 中间项:普通直角背景
setBackgroundResource(NoteItemBgResources.getNoteBgNormalRes(id));
}
} else {
// 文件夹类型:统一使用文件夹背景
setBackgroundResource(NoteItemBgResources.getFolderBgRes());
}
}
// ======================= 数据获取方法 =======================
/**
* 便
*
* @return 便
*/
public NoteItemData getItemData() {
return mItemData;
}
}

@ -14,88 +14,162 @@
* limitations under the License. * limitations under the License.
*/ */
// NotesPreferenceActivity.java - 便签设置Activity
// 主要功能管理应用的偏好设置特别是Google Tasks同步账户管理
package net.micode.notes.ui; package net.micode.notes.ui;
import android.accounts.Account; // ======================= 导入区域 =======================
import android.accounts.AccountManager; // Android账户管理
import android.app.ActionBar; import android.accounts.Account; // 账户对象
import android.app.AlertDialog; import android.accounts.AccountManager; // 账户管理器
import android.content.BroadcastReceiver; // Android界面
import android.content.ContentValues; import android.app.ActionBar; // 操作栏
import android.content.Context; import android.app.AlertDialog; // 警告对话框
import android.content.DialogInterface; // Android广播
import android.content.Intent; import android.content.BroadcastReceiver; // 广播接收器
import android.content.IntentFilter; // Android数据
import android.content.SharedPreferences; import android.content.ContentValues; // 内容值
import android.os.Bundle; // Android基础
import android.preference.Preference; import android.content.Context; // 上下文
import android.preference.Preference.OnPreferenceClickListener; import android.content.DialogInterface; // 对话框接口
import android.preference.PreferenceActivity; import android.content.Intent; // 意图
import android.preference.PreferenceCategory; import android.content.IntentFilter; // 意图过滤器
import android.text.TextUtils; import android.content.SharedPreferences; // 偏好设置
import android.text.format.DateFormat; import android.os.Bundle; // 状态保存
import android.view.LayoutInflater; // Android偏好设置
import android.view.Menu; import android.preference.Preference; // 偏好设置项
import android.view.MenuItem; import android.preference.Preference.OnPreferenceClickListener; // 偏好设置点击监听
import android.view.View; import android.preference.PreferenceActivity; // 偏好设置Activity基类
import android.widget.Button; import android.preference.PreferenceCategory; // 偏好设置分类
import android.widget.TextView; import android.preference.PreferenceManager; // 偏好设置管理器
import android.widget.Toast; // Android工具
import android.text.TextUtils; // 文本工具
import net.micode.notes.R; import android.text.format.DateFormat; // 日期格式化
import net.micode.notes.data.Notes; // Android视图
import net.micode.notes.data.Notes.NoteColumns; import android.view.LayoutInflater; // 布局加载器
import net.micode.notes.gtask.remote.GTaskSyncService; 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; // 文本视图
import android.widget.Toast; // 提示信息
// 应用内部资源
import net.micode.notes.R; // 资源文件R类
// 应用数据模型
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 =======================
/**
* NotesPreferenceActivity - 便Activity
* PreferenceActivity
* Google Tasks
*/
public class NotesPreferenceActivity extends PreferenceActivity {
// ======================= 偏好设置常量 =======================
public class NotesPreferenceActivity extends PreferenceActivity { /** 偏好设置文件名 */
public static final String PREFERENCE_NAME = "notes_preferences"; public static final String PREFERENCE_NAME = "notes_preferences";
/** 同步账户名称偏好键 - 存储当前设置的同步账户名 */
public static final String PREFERENCE_SYNC_ACCOUNT_NAME = "pref_key_account_name"; public static final String PREFERENCE_SYNC_ACCOUNT_NAME = "pref_key_account_name";
/** 最后同步时间偏好键 - 存储上次成功同步的时间戳 */
public static final String PREFERENCE_LAST_SYNC_TIME = "pref_last_sync_time"; public static final String PREFERENCE_LAST_SYNC_TIME = "pref_last_sync_time";
/** 背景颜色随机显示偏好键 - 控制是否随机显示背景颜色 */
public static final String PREFERENCE_SET_BG_COLOR_KEY = "pref_key_bg_random_appear"; public static final String PREFERENCE_SET_BG_COLOR_KEY = "pref_key_bg_random_appear";
/** 同步账户偏好键 - 用于XML中定义账户设置分类 */
private static final String PREFERENCE_SYNC_ACCOUNT_KEY = "pref_sync_account_key"; private static final String PREFERENCE_SYNC_ACCOUNT_KEY = "pref_sync_account_key";
/** 权限过滤器键 - 用于添加账户时过滤账户类型 */
private static final String AUTHORITIES_FILTER_KEY = "authorities"; private static final String AUTHORITIES_FILTER_KEY = "authorities";
// ======================= 成员变量 =======================
/** 账户设置分类 - 显示账户相关设置 */
private PreferenceCategory mAccountCategory; private PreferenceCategory mAccountCategory;
/** Google Tasks同步广播接收器 - 监听同步状态变化 */
private GTaskReceiver mReceiver; private GTaskReceiver mReceiver;
/** 原始账户列表 - 用于检测新添加的账户 */
private Account[] mOriAccounts; private Account[] mOriAccounts;
/** 是否添加了新账户标志 - 标记用户是否添加了新账户 */
private boolean mHasAddedAccount; private boolean mHasAddedAccount;
// ======================= 生命周期方法 =======================
/**
* onCreate - Activity
*
* @param icicle
*/
@Override @Override
protected void onCreate(Bundle icicle) { protected void onCreate(Bundle icicle) {
super.onCreate(icicle); super.onCreate(icicle);
// 从XML加载偏好设置
/* using the app icon for navigation */
getActionBar().setDisplayHomeAsUpEnabled(true);
addPreferencesFromResource(R.xml.preferences); addPreferencesFromResource(R.xml.preferences);
// 获取账户设置分类
mAccountCategory = (PreferenceCategory) findPreference(PREFERENCE_SYNC_ACCOUNT_KEY); 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(); mReceiver = new GTaskReceiver();
IntentFilter filter = new IntentFilter(); IntentFilter filter = new IntentFilter();
filter.addAction(GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME); filter.addAction(GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME);
registerReceiver(mReceiver, filter); registerReceiver(mReceiver, filter);
mOriAccounts = null; mOriAccounts = null; // 初始化原始账户列表
// 添加设置界面头部
View header = LayoutInflater.from(this).inflate(R.layout.settings_header, 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);
}
} }
/**
* onResume - Activity
*
*/
@Override @Override
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
// need to set sync account automatically if user has added a new // 如果用户添加了新账户,需要自动设置为同步账户
// account
if (mHasAddedAccount) { if (mHasAddedAccount) {
Account[] accounts = getGoogleAccounts(); Account[] accounts = getGoogleAccounts();
// 比较新旧账户数量,检测新账户
if (mOriAccounts != null && accounts.length > mOriAccounts.length) { if (mOriAccounts != null && accounts.length > mOriAccounts.length) {
for (Account accountNew : accounts) { for (Account accountNew : accounts) {
boolean found = false; boolean found = false;
@ -106,6 +180,7 @@ public class NotesPreferenceActivity extends PreferenceActivity {
} }
} }
if (!found) { if (!found) {
// 找到新账户,设置为同步账户
setSyncAccount(accountNew.name); setSyncAccount(accountNew.name);
break; break;
} }
@ -113,9 +188,13 @@ public class NotesPreferenceActivity extends PreferenceActivity {
} }
} }
refreshUI(); refreshUI(); // 刷新界面
} }
/**
* onDestroy - Activity
* 广
*/
@Override @Override
protected void onDestroy() { protected void onDestroy() {
if (mReceiver != null) { if (mReceiver != null) {
@ -124,9 +203,16 @@ public class NotesPreferenceActivity extends PreferenceActivity {
super.onDestroy(); super.onDestroy();
} }
// ======================= 账户偏好设置加载 =======================
/**
*
*
*/
private void loadAccountPreference() { private void loadAccountPreference() {
mAccountCategory.removeAll(); mAccountCategory.removeAll(); // 清空现有设置项
// 创建账户设置项
Preference accountPref = new Preference(this); Preference accountPref = new Preference(this);
final String defaultAccount = getSyncAccountName(this); final String defaultAccount = getSyncAccountName(this);
accountPref.setTitle(getString(R.string.preferences_account_title)); accountPref.setTitle(getString(R.string.preferences_account_title));
@ -135,16 +221,16 @@ public class NotesPreferenceActivity extends PreferenceActivity {
public boolean onPreferenceClick(Preference preference) { public boolean onPreferenceClick(Preference preference) {
if (!GTaskSyncService.isSyncing()) { if (!GTaskSyncService.isSyncing()) {
if (TextUtils.isEmpty(defaultAccount)) { if (TextUtils.isEmpty(defaultAccount)) {
// the first time to set account // 首次设置账户:显示选择账户对话框
showSelectAccountAlertDialog(); showSelectAccountAlertDialog();
} else { } else {
// if the account has already been set, we need to promp // 已设置账户:显示更改账户确认对话框
// user about the risk
showChangeAccountConfirmAlertDialog(); showChangeAccountConfirmAlertDialog();
} }
} else { } else {
// 同步进行中,不能更改账户
Toast.makeText(NotesPreferenceActivity.this, Toast.makeText(NotesPreferenceActivity.this,
R.string.preferences_toast_cannot_change_account, Toast.LENGTH_SHORT) R.string.preferences_toast_cannot_change_account, Toast.LENGTH_SHORT)
.show(); .show();
} }
return true; return true;
@ -154,12 +240,19 @@ public class NotesPreferenceActivity extends PreferenceActivity {
mAccountCategory.addPreference(accountPref); mAccountCategory.addPreference(accountPref);
} }
// ======================= 同步按钮加载 =======================
/**
*
*
*/
private void loadSyncButton() { private void loadSyncButton() {
Button syncButton = (Button) findViewById(R.id.preference_sync_button); Button syncButton = (Button) findViewById(R.id.preference_sync_button);
TextView lastSyncTimeView = (TextView) findViewById(R.id.prefenerece_sync_status_textview); TextView lastSyncTimeView = (TextView) findViewById(R.id.prefenerece_sync_status_textview);
// set button state // 设置按钮状态
if (GTaskSyncService.isSyncing()) { if (GTaskSyncService.isSyncing()) {
// 同步中:显示取消按钮
syncButton.setText(getString(R.string.preferences_button_sync_cancel)); syncButton.setText(getString(R.string.preferences_button_sync_cancel));
syncButton.setOnClickListener(new View.OnClickListener() { syncButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) { public void onClick(View v) {
@ -167,6 +260,7 @@ public class NotesPreferenceActivity extends PreferenceActivity {
} }
}); });
} else { } else {
// 未同步:显示立即同步按钮
syncButton.setText(getString(R.string.preferences_button_sync_immediately)); syncButton.setText(getString(R.string.preferences_button_sync_immediately));
syncButton.setOnClickListener(new View.OnClickListener() { syncButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) { public void onClick(View v) {
@ -174,13 +268,16 @@ public class NotesPreferenceActivity extends PreferenceActivity {
} }
}); });
} }
// 有账户时启用按钮,无账户时禁用
syncButton.setEnabled(!TextUtils.isEmpty(getSyncAccountName(this))); syncButton.setEnabled(!TextUtils.isEmpty(getSyncAccountName(this)));
// set last sync time // 设置最后同步时间
if (GTaskSyncService.isSyncing()) { if (GTaskSyncService.isSyncing()) {
// 同步中:显示同步进度
lastSyncTimeView.setText(GTaskSyncService.getProgressString()); lastSyncTimeView.setText(GTaskSyncService.getProgressString());
lastSyncTimeView.setVisibility(View.VISIBLE); lastSyncTimeView.setVisibility(View.VISIBLE);
} else { } else {
// 未同步:显示最后同步时间
long lastSyncTime = getLastSyncTime(this); long lastSyncTime = getLastSyncTime(this);
if (lastSyncTime != 0) { if (lastSyncTime != 0) {
lastSyncTimeView.setText(getString(R.string.preferences_last_sync_time, lastSyncTimeView.setText(getString(R.string.preferences_last_sync_time,
@ -188,19 +285,33 @@ public class NotesPreferenceActivity extends PreferenceActivity {
lastSyncTime))); lastSyncTime)));
lastSyncTimeView.setVisibility(View.VISIBLE); lastSyncTimeView.setVisibility(View.VISIBLE);
} else { } else {
// 从未同步:隐藏时间显示
lastSyncTimeView.setVisibility(View.GONE); lastSyncTimeView.setVisibility(View.GONE);
} }
} }
} }
// ======================= 界面刷新 =======================
/**
*
*
*/
private void refreshUI() { private void refreshUI() {
loadAccountPreference(); loadAccountPreference();
loadSyncButton(); loadSyncButton();
} }
// ======================= 选择账户对话框 =======================
/**
*
*
*/
private void showSelectAccountAlertDialog() { private void showSelectAccountAlertDialog() {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
// 自定义标题视图
View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null); View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null);
TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title); TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title);
titleTextView.setText(getString(R.string.preferences_dialog_select_account_title)); titleTextView.setText(getString(R.string.preferences_dialog_select_account_title));
@ -208,45 +319,57 @@ public class NotesPreferenceActivity extends PreferenceActivity {
subtitleTextView.setText(getString(R.string.preferences_dialog_select_account_tips)); subtitleTextView.setText(getString(R.string.preferences_dialog_select_account_tips));
dialogBuilder.setCustomTitle(titleView); dialogBuilder.setCustomTitle(titleView);
dialogBuilder.setPositiveButton(null, null); dialogBuilder.setPositiveButton(null, null); // 不显示确定按钮
// 获取Google账户列表
Account[] accounts = getGoogleAccounts(); Account[] accounts = getGoogleAccounts();
String defAccount = getSyncAccountName(this); String defAccount = getSyncAccountName(this);
// 保存原始账户列表,用于检测新账户
mOriAccounts = accounts; mOriAccounts = accounts;
mHasAddedAccount = false; mHasAddedAccount = false;
if (accounts.length > 0) { if (accounts.length > 0) {
// 创建账户列表项
CharSequence[] items = new CharSequence[accounts.length]; CharSequence[] items = new CharSequence[accounts.length];
final CharSequence[] itemMapping = items; final CharSequence[] itemMapping = items; // 用于内部类访问
int checkedItem = -1; int checkedItem = -1; // 默认未选中
// 填充账户列表
int index = 0; int index = 0;
for (Account account : accounts) { for (Account account : accounts) {
if (TextUtils.equals(account.name, defAccount)) { if (TextUtils.equals(account.name, defAccount)) {
checkedItem = index; checkedItem = index; // 选中已设置的账户
} }
items[index++] = account.name; items[index++] = account.name;
} }
// 设置单选列表
dialogBuilder.setSingleChoiceItems(items, checkedItem, dialogBuilder.setSingleChoiceItems(items, checkedItem,
new DialogInterface.OnClickListener() { new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
// 选择账户
setSyncAccount(itemMapping[which].toString()); setSyncAccount(itemMapping[which].toString());
dialog.dismiss(); dialog.dismiss();
refreshUI(); refreshUI(); // 刷新界面
} }
}); });
} }
// 添加"添加账户"视图
View addAccountView = LayoutInflater.from(this).inflate(R.layout.add_account_text, null); View addAccountView = LayoutInflater.from(this).inflate(R.layout.add_account_text, null);
dialogBuilder.setView(addAccountView); dialogBuilder.setView(addAccountView);
// 显示对话框
final AlertDialog dialog = dialogBuilder.show(); final AlertDialog dialog = dialogBuilder.show();
addAccountView.setOnClickListener(new View.OnClickListener() { addAccountView.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) { public void onClick(View v) {
mHasAddedAccount = true; mHasAddedAccount = true; // 标记为添加账户
// 启动系统添加账户界面
Intent intent = new Intent("android.settings.ADD_ACCOUNT_SETTINGS"); Intent intent = new Intent("android.settings.ADD_ACCOUNT_SETTINGS");
// 只显示Gmail账户类型
intent.putExtra(AUTHORITIES_FILTER_KEY, new String[] { intent.putExtra(AUTHORITIES_FILTER_KEY, new String[] {
"gmail-ls" "gmail-ls"
}); });
startActivityForResult(intent, -1); startActivityForResult(intent, -1);
dialog.dismiss(); dialog.dismiss();
@ -254,42 +377,69 @@ public class NotesPreferenceActivity extends PreferenceActivity {
}); });
} }
// ======================= 更改账户确认对话框 =======================
/**
*
*
*/
private void showChangeAccountConfirmAlertDialog() { private void showChangeAccountConfirmAlertDialog() {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
// 自定义标题视图
View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null); View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null);
TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title); TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title);
// 标题包含当前账户名
titleTextView.setText(getString(R.string.preferences_dialog_change_account_title, titleTextView.setText(getString(R.string.preferences_dialog_change_account_title,
getSyncAccountName(this))); getSyncAccountName(this)));
TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle); TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle);
subtitleTextView.setText(getString(R.string.preferences_dialog_change_account_warn_msg)); subtitleTextView.setText(getString(R.string.preferences_dialog_change_account_warn_msg));
dialogBuilder.setCustomTitle(titleView); dialogBuilder.setCustomTitle(titleView);
// 设置菜单项
CharSequence[] menuItemArray = new CharSequence[] { CharSequence[] menuItemArray = new CharSequence[] {
getString(R.string.preferences_menu_change_account), getString(R.string.preferences_menu_change_account), // 更改账户
getString(R.string.preferences_menu_remove_account), getString(R.string.preferences_menu_remove_account), // 移除账户
getString(R.string.preferences_menu_cancel) getString(R.string.preferences_menu_cancel) // 取消
}; };
dialogBuilder.setItems(menuItemArray, new DialogInterface.OnClickListener() { dialogBuilder.setItems(menuItemArray, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
if (which == 0) { if (which == 0) {
// 更改账户:显示选择账户对话框
showSelectAccountAlertDialog(); showSelectAccountAlertDialog();
} else if (which == 1) { } else if (which == 1) {
// 移除账户:清除账户设置
removeSyncAccount(); removeSyncAccount();
refreshUI(); refreshUI();
} }
// 取消:不执行任何操作
} }
}); });
dialogBuilder.show(); dialogBuilder.show();
} }
// ======================= 获取Google账户 =======================
/**
* Google
* @return Google
*/
private Account[] getGoogleAccounts() { private Account[] getGoogleAccounts() {
AccountManager accountManager = AccountManager.get(this); AccountManager accountManager = AccountManager.get(this);
// 获取类型为"com.google"的账户Google账户
return accountManager.getAccountsByType("com.google"); return accountManager.getAccountsByType("com.google");
} }
// ======================= 设置同步账户 =======================
/**
*
*
* @param account
*/
private void setSyncAccount(String account) { private void setSyncAccount(String account) {
if (!getSyncAccountName(this).equals(account)) { if (!getSyncAccountName(this).equals(account)) {
// 保存到偏好设置
SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit(); SharedPreferences.Editor editor = settings.edit();
if (account != null) { if (account != null) {
@ -299,53 +449,76 @@ public class NotesPreferenceActivity extends PreferenceActivity {
} }
editor.commit(); editor.commit();
// clean up last sync time // 清理最后同步时间
setLastSyncTime(this, 0); setLastSyncTime(this, 0);
// clean up local gtask related info // 清理本地同步相关数据(在后台线程执行)
new Thread(new Runnable() { new Thread(new Runnable() {
public void run() { public void run() {
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put(NoteColumns.GTASK_ID, ""); values.put(NoteColumns.GTASK_ID, ""); // 清空GTASK ID
values.put(NoteColumns.SYNC_ID, 0); values.put(NoteColumns.SYNC_ID, 0); // 清空同步ID
getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null); getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null);
} }
}).start(); }).start();
// 显示成功提示
Toast.makeText(NotesPreferenceActivity.this, Toast.makeText(NotesPreferenceActivity.this,
getString(R.string.preferences_toast_success_set_accout, account), getString(R.string.preferences_toast_success_set_accout, account),
Toast.LENGTH_SHORT).show(); Toast.LENGTH_SHORT).show();
} }
} }
// ======================= 移除同步账户 =======================
/**
*
*
*/
private void removeSyncAccount() { private void removeSyncAccount() {
SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit(); SharedPreferences.Editor editor = settings.edit();
// 移除账户名
if (settings.contains(PREFERENCE_SYNC_ACCOUNT_NAME)) { if (settings.contains(PREFERENCE_SYNC_ACCOUNT_NAME)) {
editor.remove(PREFERENCE_SYNC_ACCOUNT_NAME); editor.remove(PREFERENCE_SYNC_ACCOUNT_NAME);
} }
// 移除最后同步时间
if (settings.contains(PREFERENCE_LAST_SYNC_TIME)) { if (settings.contains(PREFERENCE_LAST_SYNC_TIME)) {
editor.remove(PREFERENCE_LAST_SYNC_TIME); editor.remove(PREFERENCE_LAST_SYNC_TIME);
} }
editor.commit(); editor.commit();
// clean up local gtask related info // 清理本地同步相关数据(在后台线程执行)
new Thread(new Runnable() { new Thread(new Runnable() {
public void run() { public void run() {
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put(NoteColumns.GTASK_ID, ""); values.put(NoteColumns.GTASK_ID, ""); // 清空GTASK ID
values.put(NoteColumns.SYNC_ID, 0); values.put(NoteColumns.SYNC_ID, 0); // 清空同步ID
getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null); getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null);
} }
}).start(); }).start();
} }
// ======================= 静态工具方法 =======================
/**
*
*
* @param context
* @return
*/
public static String getSyncAccountName(Context context) { public static String getSyncAccountName(Context context) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE); Context.MODE_PRIVATE);
return settings.getString(PREFERENCE_SYNC_ACCOUNT_NAME, ""); return settings.getString(PREFERENCE_SYNC_ACCOUNT_NAME, "");
} }
/**
*
*
* @param context
* @param time
*/
public static void setLastSyncTime(Context context, long time) { public static void setLastSyncTime(Context context, long time) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE); Context.MODE_PRIVATE);
@ -354,29 +527,52 @@ public class NotesPreferenceActivity extends PreferenceActivity {
editor.commit(); editor.commit();
} }
/**
*
*
* @param context
* @return 0
*/
public static long getLastSyncTime(Context context) { public static long getLastSyncTime(Context context) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE); Context.MODE_PRIVATE);
return settings.getLong(PREFERENCE_LAST_SYNC_TIME, 0); return settings.getLong(PREFERENCE_LAST_SYNC_TIME, 0);
} }
private class GTaskReceiver extends BroadcastReceiver { // ======================= 同步广播接收器 =======================
/**
* GTaskReceiver - Google Tasks广
*
*/
private class GTaskReceiver extends BroadcastReceiver {
/**
* 广
*
*/
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
refreshUI(); refreshUI(); // 刷新界面
// 如果正在同步,更新进度信息
if (intent.getBooleanExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_IS_SYNCING, false)) { if (intent.getBooleanExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_IS_SYNCING, false)) {
TextView syncStatus = (TextView) findViewById(R.id.prefenerece_sync_status_textview); TextView syncStatus = (TextView) findViewById(R.id.prefenerece_sync_status_textview);
syncStatus.setText(intent syncStatus.setText(intent
.getStringExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_PROGRESS_MSG)); .getStringExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_PROGRESS_MSG));
} }
} }
} }
// ======================= 菜单项处理 =======================
/**
*
*
*/
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) { switch (item.getItemId()) {
case android.R.id.home: case android.R.id.home:
// 返回按钮:回到主界面
Intent intent = new Intent(this, NotesListActivity.class); Intent intent = new Intent(this, NotesListActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent); startActivity(intent);
@ -385,4 +581,4 @@ public class NotesPreferenceActivity extends PreferenceActivity {
return false; return false;
} }
} }
} }

@ -0,0 +1,257 @@
/*
* 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.
*/
// NoteWidgetProvider.java - 便签小部件提供者抽象基类
// 主要功能:为便签小部件提供基础实现,包括数据加载、视图更新、点击处理
package net.micode.notes.widget;
// ======================= 导入区域 =======================
// Android小部件
import android.app.PendingIntent; // 待定意图
import android.appwidget.AppWidgetManager; // 小部件管理器
import android.appwidget.AppWidgetProvider; // 小部件提供者基类
// Android数据
import android.content.ContentValues; // 内容值
import android.content.Context; // 上下文
import android.content.Intent; // 意图
import android.database.Cursor; // 数据库查询结果游标
import android.util.Log; // 日志工具
// Android小部件视图
import android.widget.RemoteViews; // 远程视图
// 应用内部资源
import net.micode.notes.R; // 资源文件R类
// 应用数据模型
import net.micode.notes.data.Notes; // Notes主类
import net.micode.notes.data.Notes.NoteColumns; // 便签表列定义
// 应用资源解析
import net.micode.notes.tool.ResourceParser; // 资源解析器
// 应用界面
import net.micode.notes.ui.NoteEditActivity; // 便签编辑Activity
import net.micode.notes.ui.NotesListActivity; // 便签列表Activity
// ======================= 便签小部件提供者抽象基类 =======================
/**
* NoteWidgetProvider - 便
* AppWidgetProvider便
* 2x4x
*
* 1.
* 2.
* 3.
* 4.
*/
public abstract class NoteWidgetProvider extends AppWidgetProvider {
// ======================= 数据库查询配置 =======================
/**
* -
*
*/
public static final String [] PROJECTION = new String [] {
NoteColumns.ID, // 0 - 便签ID
NoteColumns.BG_COLOR_ID, // 1 - 背景颜色ID
NoteColumns.SNIPPET // 2 - 便签摘要
};
// ======================= 字段索引常量 =======================
public static final int COLUMN_ID = 0; // ID字段索引
public static final int COLUMN_BG_COLOR_ID = 1; // 背景颜色字段索引
public static final int COLUMN_SNIPPET = 2; // 摘要字段索引
// ======================= 常量定义 =======================
private static final String TAG = "NoteWidgetProvider"; // 日志标签
// ======================= 小部件生命周期方法 =======================
/**
*
*
* 便ID
*
* @param context
* @param appWidgetIds ID
*/
@Override
public void onDeleted(Context context, int[] appWidgetIds) {
// 创建内容值,用于更新数据库
ContentValues values = new ContentValues();
// 将小部件ID设置为无效值
values.put(NoteColumns.WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
// 遍历所有被删除的小部件
for (int i = 0; i < appWidgetIds.length; i++) {
// 更新数据库清理关联的小部件ID
context.getContentResolver().update(Notes.CONTENT_NOTE_URI,
values,
NoteColumns.WIDGET_ID + "=?", // 条件小部件ID匹配
new String[] { String.valueOf(appWidgetIds[i])});
}
}
// ======================= 小部件信息查询 =======================
/**
* 便
* ID便
* 便
*
* @param context
* @param widgetId ID
* @return 便ID
*/
private Cursor getNoteWidgetInfo(Context context, int widgetId) {
return context.getContentResolver().query(Notes.CONTENT_NOTE_URI,
PROJECTION,
// 条件小部件ID匹配 且 不在回收站中
NoteColumns.WIDGET_ID + "=? AND " + NoteColumns.PARENT_ID + "<>?",
new String[] { String.valueOf(widgetId), String.valueOf(Notes.ID_TRASH_FOLER) },
null);
}
// ======================= 小部件更新方法 =======================
/**
*
* 使
*
* @param context
* @param appWidgetManager
* @param appWidgetIds ID
*/
protected void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
update(context, appWidgetManager, appWidgetIds, false); // 默认非隐私模式
}
/**
*
*
*
* 1. 便
* 2.
* 3.
* 4.
*
* @param context
* @param appWidgetManager
* @param appWidgetIds ID
* @param privacyMode
* true: "浏览中"
* false: 便
*/
private void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds,
boolean privacyMode) {
// 遍历所有要更新的小部件
for (int i = 0; i < appWidgetIds.length; i++) {
if (appWidgetIds[i] != AppWidgetManager.INVALID_APPWIDGET_ID) {
int bgId = ResourceParser.getDefaultBgId(context); // 默认背景颜色
String snippet = ""; // 便签摘要
// 构建点击意图
Intent intent = new Intent(context, NoteEditActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
intent.putExtra(Notes.INTENT_EXTRA_WIDGET_ID, appWidgetIds[i]);
intent.putExtra(Notes.INTENT_EXTRA_WIDGET_TYPE, getWidgetType());
// 查询小部件关联的便签信息
Cursor c = getNoteWidgetInfo(context, appWidgetIds[i]);
if (c != null && c.moveToFirst()) {
// 安全检查:确保没有多个便签关联到同一小部件
if (c.getCount() > 1) {
Log.e(TAG, "Multiple message with same widget id:" + appWidgetIds[i]);
c.close();
return; // 数据异常,直接返回
}
// 从游标获取数据
snippet = c.getString(COLUMN_SNIPPET);
bgId = c.getInt(COLUMN_BG_COLOR_ID);
intent.putExtra(Intent.EXTRA_UID, c.getLong(COLUMN_ID)); // 便签ID
intent.setAction(Intent.ACTION_VIEW); // 查看模式
} else {
// 无关联便签:显示默认文本
snippet = context.getResources().getString(R.string.widget_havenot_content);
intent.setAction(Intent.ACTION_INSERT_OR_EDIT); // 创建/编辑模式
}
// 关闭游标
if (c != null) {
c.close();
}
// 创建远程视图
RemoteViews rv = new RemoteViews(context.getPackageName(), getLayoutId());
// 设置背景图片
rv.setImageViewResource(R.id.widget_bg_image, getBgResourceId(bgId));
intent.putExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, bgId);
/**
* Activity
*/
PendingIntent pendingIntent = null;
if (privacyMode) {
// 隐私模式:显示"浏览中"文本,点击进入列表
rv.setTextViewText(R.id.widget_text,
context.getString(R.string.widget_under_visit_mode));
pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], new Intent(
context, NotesListActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);
} else {
// 正常模式:显示便签内容,点击编辑便签
rv.setTextViewText(R.id.widget_text, snippet);
pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], intent,
PendingIntent.FLAG_UPDATE_CURRENT);
}
// 设置点击事件
rv.setOnClickPendingIntent(R.id.widget_text, pendingIntent);
// 更新小部件
appWidgetManager.updateAppWidget(appWidgetIds[i], rv);
}
}
}
// ======================= 抽象方法 =======================
/**
* ID
* IDDrawableID
*
*
* @param bgId ID
* @return DrawableID
*/
protected abstract int getBgResourceId(int bgId);
/**
* ID
* 使ID
*
*
* @return ID
*/
protected abstract int getLayoutId();
/**
*
*
*
*
* @return TYPE_WIDGET_2X TYPE_WIDGET_4X
*/
protected abstract int getWidgetType();
}

@ -0,0 +1,99 @@
/*
* 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.
*/
// NoteWidgetProvider_2x.java - 2x2便签小部件提供者
// 主要功能实现2x2尺寸便签小部件的具体逻辑
package net.micode.notes.widget;
// ======================= 导入区域 =======================
// Android小部件
import android.appwidget.AppWidgetManager; // 小部件管理器
// Android基础
import android.content.Context; // 上下文
// 应用内部资源
import net.micode.notes.R; // 资源文件R类
// 应用数据模型
import net.micode.notes.data.Notes; // Notes主类
// 应用资源解析
import net.micode.notes.tool.ResourceParser; // 资源解析器
// ======================= 2x2便签小部件提供者 =======================
/**
* NoteWidgetProvider_2x - 2x2便
* NoteWidgetProvider
* 2x2便
* 2x222
* widget_2x.xml
*/
public class NoteWidgetProvider_2x extends NoteWidgetProvider {
// ======================= 小部件生命周期方法 =======================
/**
*
*
* update()
*
* @param context
* @param appWidgetManager
* @param appWidgetIds ID
*/
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
// 调用父类的update()方法执行通用更新逻辑
super.update(context, appWidgetManager, appWidgetIds);
}
// ======================= 抽象方法实现 =======================
/**
* ID
* 2x2使ID
* R.layout.widget_2x
*
* @return 2x2ID
*/
@Override
protected int getLayoutId() {
return R.layout.widget_2x;
}
/**
* ID
* ID2x2DrawableID
* ResourceParser.WidgetBgResources
*
* @param bgId ID
* @return 2x2DrawableID
*/
@Override
protected int getBgResourceId(int bgId) {
return ResourceParser.WidgetBgResources.getWidget2xBgResource(bgId);
}
/**
*
* 2x2
* Notes
*
* @return Notes.TYPE_WIDGET_2X
*/
@Override
protected int getWidgetType() {
return Notes.TYPE_WIDGET_2X;
}
}

@ -0,0 +1,104 @@
/*
* 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.
*/
// NoteWidgetProvider_4x.java - 4x4便签小部件提供者
// 主要功能实现4x4尺寸便签小部件的具体逻辑
package net.micode.notes.widget;
// ======================= 导入区域 =======================
// Android小部件
import android.appwidget.AppWidgetManager; // 小部件管理器
// Android基础
import android.content.Context; // 上下文
// 应用内部资源
import net.micode.notes.R; // 资源文件R类
// 应用数据模型
import net.micode.notes.data.Notes; // Notes主类
// 应用资源解析
import net.micode.notes.tool.ResourceParser; // 资源解析器
// ======================= 4x4便签小部件提供者 =======================
/**
* NoteWidgetProvider_4x - 4x4便
* NoteWidgetProvider
* 4x4便
* 4x444
* widget_4x.xml
* 便
*/
public class NoteWidgetProvider_4x extends NoteWidgetProvider {
// ======================= 小部件生命周期方法 =======================
/**
*
*
* update()
* 2x
*
* @param context
* @param appWidgetManager
* @param appWidgetIds ID
*/
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
// 调用父类的update()方法执行通用更新逻辑
super.update(context, appWidgetManager, appWidgetIds);
}
// ======================= 抽象方法实现 =======================
/**
* ID
* 4x4使ID
* R.layout.widget_4x
*
*
* @return 4x4ID
*/
@Override
protected int getLayoutId() {
return R.layout.widget_4x;
}
/**
* ID
* ID4x4DrawableID
* ResourceParser.WidgetBgResources
* 使4x4x4
*
* @param bgId ID
* @return 4x4DrawableID
*/
@Override
protected int getBgResourceId(int bgId) {
return ResourceParser.WidgetBgResources.getWidget4xBgResource(bgId);
}
/**
*
* 4x4
* Notes
*
*
* @return Notes.TYPE_WIDGET_4X
*/
@Override
protected int getWidgetType() {
return Notes.TYPE_WIDGET_4X;
}
}

@ -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>

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

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

Loading…
Cancel
Save