chennan_branch
linue 2 weeks ago
parent 38f26b4f9c
commit 3f936d0461

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

@ -0,0 +1,125 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.data;
import android.content.Context;
import android.database.Cursor;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.Data;
import android.telephony.PhoneNumberUtils;
import android.util.Log;
import java.util.HashMap;
/**
*
*
* HashMap
*
*/
public class Contact {
/**
*
* KeyStringValueString
* HashMap
*/
private static HashMap<String, String> sContactCache;
/**
* Logcat便
*/
private static final String TAG = "Contact";
/**
* SQL WHERE
*
* 1. PHONE_NUMBERS_EQUAL(Phone.NUMBER,?)使
* 2. Data.MIMETYPE = Phone.CONTENT_ITEM_TYPE
* 3. Data.RAW_CONTACT_ID IN ()phone_lookupID"+"
*/
private static final String CALLER_ID_SELECTION = "PHONE_NUMBERS_EQUAL(" + Phone.NUMBER
+ ",?) AND " + Data.MIMETYPE + "='" + Phone.CONTENT_ITEM_TYPE + "'"
+ " AND " + Data.RAW_CONTACT_ID + " IN "
+ "(SELECT raw_contact_id "
+ " FROM phone_lookup"
+ " WHERE min_match = '+')";
/**
*
*
* 1.
* 2.
* 3.
* 4. ContentResolver
* 5. null
*
* @param context ContentResolver访
* @param phoneNumber String
* @return null
*/
public static String getContact(Context context, String phoneNumber) {
// 首次调用方法时,初始化静态缓存集合
if(sContactCache == null) {
sContactCache = new HashMap<String, String>();
}
// 缓存命中:直接返回缓存中的联系人名称,避免数据库查询
if(sContactCache.containsKey(phoneNumber)) {
return sContactCache.get(phoneNumber);
}
// 替换筛选条件模板中的"+"占位符为电话号码的最小匹配值(系统工具类处理,提升号码匹配准确性)
String selection = CALLER_ID_SELECTION.replace("+",
PhoneNumberUtils.toCallerIDMinMatch(phoneNumber));
// 调用ContentResolver查询联系人数据库
// 参数说明:
// 1. Data.CONTENT_URI安卓联系人数据的总URI
// 2. 投影数组仅查询Phone.DISPLAY_NAME列联系人名称减少数据传输
// 3. selection处理后的筛选条件
// 4. selectionArgs传入电话号码作为查询参数防止SQL注入
// 5. sortOrder排序规则null表示无需排序
Cursor cursor = context.getContentResolver().query(
Data.CONTENT_URI,
new String [] { Phone.DISPLAY_NAME },
selection,
new String[] { phoneNumber },
null);
// 游标非空且包含数据时,处理查询结果
if (cursor != null && cursor.moveToFirst()) {
try {
// 获取联系人名称索引0对应投影数组中的Phone.DISPLAY_NAME
String name = cursor.getString(0);
// 将电话号码和名称存入缓存,供后续调用使用
sContactCache.put(phoneNumber, name);
// 返回查询到的联系人名称
return name;
} catch (IndexOutOfBoundsException e) {
// 捕获索引越界异常(如游标数据异常时),输出错误日志
Log.e(TAG, " Cursor get string error " + e.toString());
return null;
} finally {
// 最终关闭游标,释放数据库资源(必须执行,避免资源泄漏)
cursor.close();
}
} else {
// 游标为空或无数据时输出调试日志返回null
Log.d(TAG, "No contact matched with number:" + phoneNumber);
return null;
}
}
}

@ -0,0 +1,393 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.data;
import android.net.Uri;
/**
*
* ContentProviderURI/ID
* IntentWidget/
* ContentProviderIntent/
*
* @author MiCode Open Source Community
* @date 2010-2011
*/
public class Notes {
/**
* ContentProviderAuthorityContentProvider
* ContentResolver访NotesProvidercontent://micode_notes/...
*/
public static final String AUTHORITY = "micode_notes";
/**
* Logcat便
*/
public static final String TAG = "Notes";
/**
*
*/
public static final int TYPE_NOTE = 0;
/**
*
*/
public static final int TYPE_FOLDER = 1;
/**
* /
*/
public static final int TYPE_SYSTEM = 2;
/**
* ID
* {@link Notes#ID_ROOT_FOLDER }
* {@link Notes#ID_TEMPARAY_FOLDER }
* {@link Notes#ID_CALL_RECORD_FOLDER}
*/
// 根文件夹ID默认文件夹所有无指定文件夹的笔记默认归属此文件夹
public static final int ID_ROOT_FOLDER = 0;
// 临时文件夹ID存放临时、无归属的笔记
public static final int ID_TEMPARAY_FOLDER = -1;
// 通话记录文件夹ID专门存储通话记录类型的笔记
public static final int ID_CALL_RECORD_FOLDER = -2;
// 回收站文件夹ID已废弃用于兼容旧代码
public static final int ID_TRASH_FOLER = -3;
/**
* Intentalert_date
*/
public static final String INTENT_EXTRA_ALERT_DATE = "net.micode.notes.alert_date";
/**
* IntentIDbackground_color_id
*/
public static final String INTENT_EXTRA_BACKGROUND_ID = "net.micode.notes.background_color_id";
/**
* IntentWidgetIDwidget_id
*/
public static final String INTENT_EXTRA_WIDGET_ID = "net.micode.notes.widget_id";
/**
* IntentWidgetwidget_type
*/
public static final String INTENT_EXTRA_WIDGET_TYPE = "net.micode.notes.widget_type";
/**
* IntentIDfolder_id
*/
public static final String INTENT_EXTRA_FOLDER_ID = "net.micode.notes.folder_id";
/**
* Intentcall_date
*/
public static final String INTENT_EXTRA_CALL_DATE = "net.micode.notes.call_date";
/**
* WidgetWidget
*/
public static final int TYPE_WIDGET_INVALIDE = -1;
/**
* Widget2x2Widget
*/
public static final int TYPE_WIDGET_2X = 0;
/**
* Widget4x4Widget
*/
public static final int TYPE_WIDGET_4X = 1;
/**
* MIME
* ContentProviderMIME
*/
public static class DataConstants {
// 文本笔记的MIME类型对应TextNote的CONTENT_ITEM_TYPE
public static final String NOTE = TextNote.CONTENT_ITEM_TYPE;
// 通话笔记的MIME类型对应CallNote的CONTENT_ITEM_TYPE
public static final String CALL_NOTE = CallNote.CONTENT_ITEM_TYPE;
}
/**
* ContentProviderURIURI
* content://micode_notes/note
*/
public static final Uri CONTENT_NOTE_URI = Uri.parse("content://" + AUTHORITY + "/note");
/**
* ContentProviderURIURI
* content://micode_notes/data
*/
public static final Uri CONTENT_DATA_URI = Uri.parse("content://" + AUTHORITY + "/data");
/**
* /
* noteContentProvider
*/
public interface NoteColumns {
/**
* ID
* <P> : INTEGER (long) </P>
*/
public static final String ID = "_id";
/**
* /ID/
* <P> : INTEGER (long) </P>
*/
public static final String PARENT_ID = "parent_id";
/**
* /
* <P> : INTEGER (long) </P>
*/
public static final String CREATED_DATE = "created_date";
/**
* /
* <P> : INTEGER (long) </P>
*/
public static final String MODIFIED_DATE = "modified_date";
/**
*
* <P> : INTEGER (long) </P>
*/
public static final String ALERTED_DATE = "alert_date";
/**
* /
* <P> : TEXT </P>
*/
public static final String SNIPPET = "snippet";
/**
* Widget IDWidgetWidget ID
* <P> : INTEGER (long) </P>
*/
public static final String WIDGET_ID = "widget_id";
/**
* Widget2x2/4x4TYPE_WIDGET_2X/TYPE_WIDGET_4X
* <P> : INTEGER (long) </P>
*/
public static final String WIDGET_TYPE = "widget_type";
/**
* IDID
* <P> : INTEGER (long) </P>
*/
public static final String BG_COLOR_ID = "bg_color_id";
/**
*
* <P> : INTEGER10 </P>
*/
public static final String HAS_ATTACHMENT = "has_attachment";
/**
*
* <P> : INTEGER (long) </P>
*/
public static final String NOTES_COUNT = "notes_count";
/**
* //TYPE_NOTE/TYPE_FOLDER/TYPE_SYSTEM
* <P> : INTEGER </P>
*/
public static final String TYPE = "type";
/**
* 01
* <P> : INTEGER </P>
*/
public static final String PINNED = "pinned";
/**
* IDGTask
* <P> : INTEGER (long) </P>
*/
public static final String SYNC_ID = "sync_id";
/**
* /GTask
* <P> : INTEGER10 </P>
*/
public static final String LOCAL_MODIFIED = "local_modified";
/**
* ID
* <P> : INTEGER (long) </P>
*/
public static final String ORIGIN_PARENT_ID = "origin_parent_id";
/**
* GTaskID
* <P> : TEXT </P>
*/
public static final String GTASK_ID = "gtask_id";
/**
*
* <P> : INTEGER (long) </P>
*/
public static final String VERSION = "version";
}
/**
*
* datadata
*/
public interface DataColumns {
/**
* ID
* <P> : INTEGER (long) </P>
*/
public static final String ID = "_id";
/**
* MIME/TextNote/CALLNoteCONTENT_ITEM_TYPE
* <P> : Text </P>
*/
public static final String MIME_TYPE = "mime_type";
/**
* IDnote_iddatanote
* <P> : INTEGER (long) </P>
*/
public static final String NOTE_ID = "note_id";
/**
*
* <P> : INTEGER (long) </P>
*/
public static final String CREATED_DATE = "created_date";
/**
*
* <P> : INTEGER (long) </P>
*/
public static final String MODIFIED_DATE = "modified_date";
/**
*
* <P> : TEXT </P>
*/
public static final String CONTENT = "content";
/**
* 1MIME_TYPE
* <P> : INTEGER </P>
*/
public static final String DATA1 = "data1";
/**
* 2MIME_TYPE
* <P> : INTEGER </P>
*/
public static final String DATA2 = "data2";
/**
* 3MIME_TYPE
* <P> : TEXT </P>
*/
public static final String DATA3 = "data3";
/**
* 4MIME_TYPE
* <P> : TEXT </P>
*/
public static final String DATA4 = "data4";
/**
* 5MIME_TYPE
* <P> : TEXT </P>
*/
public static final String DATA5 = "data5";
}
/**
* DataColumns
* /MIMEContentProvider URI
*/
public static final class TextNote implements DataColumns {
/**
* DATA1
* <P> : Integer10 </P>
*/
public static final String MODE = DATA1;
/**
*
*/
public static final int MODE_CHECK_LIST = 1;
/**
* MIMEContentProviderdir
* vnd.android.cursor.dir/text_note
*/
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/text_note";
/**
* MIMEContentProvideritem
* vnd.android.cursor.item/text_note
*/
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/text_note";
/**
* ContentProvider URI
* content://micode_notes/text_note
*/
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/text_note");
}
/**
* DataColumns
* MIMEContentProvider URI
*/
public static final class CallNote implements DataColumns {
/**
* DATA1
* <P> : INTEGER (long) </P>
*/
public static final String CALL_DATE = DATA1;
/**
* DATA3
* <P> : TEXT </P>
*/
public static final String PHONE_NUMBER = DATA3;
/**
* MIMEContentProviderdir
* vnd.android.cursor.dir/call_note
*/
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/call_note";
/**
* MIMEContentProvideritem
* vnd.android.cursor.item/call_note
*/
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/call_note";
/**
* ContentProvider URI
* content://micode_notes/call_note
*/
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/call_note");
}
}

@ -0,0 +1,509 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.data;
import android.content.ContentValues;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.DataConstants;
import net.micode.notes.data.Notes.NoteColumns;
/**
* SQLiteAndroidSQLiteOpenHelper
*
* 1.
* 2. notedata
* 3.
* 4.
* 5.
*
* @author MiCode Open Source Community
* @date 2010-2011
*/
public class NotesDatabaseHelper extends SQLiteOpenHelper {
/**
*
*/
private static final String DB_NAME = "note.db";
/**
* 6
*/
private static final int DB_VERSION = 6;
/**
* notedata便-
*/
public interface TABLE {
// 笔记/文件夹表名称
public static final String NOTE = "note";
// 笔记明细数据表名称(存储文本、通话记录等具体内容)
public static final String DATA = "data";
}
/**
* Logcat便
*/
private static final String TAG = "NotesDatabaseHelper";
/**
* NotesDatabaseHelper
*
*/
private static NotesDatabaseHelper mInstance;
// ====================== 数据表创建SQL语句 ======================
/**
* noteSQL
* noteIDID
* ID
*/
private static final String CREATE_NOTE_TABLE_SQL =
"CREATE TABLE " + TABLE.NOTE + "(" +
NoteColumns.ID + " INTEGER PRIMARY KEY," + // 主键ID
NoteColumns.PARENT_ID + " INTEGER NOT NULL DEFAULT 0," + // 父级ID关联文件夹
NoteColumns.ALERTED_DATE + " INTEGER NOT NULL DEFAULT 0," + // 提醒时间
NoteColumns.BG_COLOR_ID + " INTEGER NOT NULL DEFAULT 0," + // 背景颜色ID
// 创建时间:默认值为当前时间戳(秒转毫秒)
NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
NoteColumns.HAS_ATTACHMENT + " INTEGER NOT NULL DEFAULT 0," + // 是否有附件
// 修改时间:默认值为当前时间戳
NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
NoteColumns.NOTES_COUNT + " INTEGER NOT NULL DEFAULT 0," + // 文件夹下的笔记数量
NoteColumns.SNIPPET + " TEXT NOT NULL DEFAULT ''," + // 文件夹名称/笔记摘要
NoteColumns.TYPE + " INTEGER NOT NULL DEFAULT 0," + // 类型(笔记/文件夹/系统)
NoteColumns.PINNED + " INTEGER NOT NULL DEFAULT 0," + // 是否置顶0不置顶1置顶
NoteColumns.WIDGET_ID + " INTEGER NOT NULL DEFAULT 0," + // 关联的Widget ID
NoteColumns.WIDGET_TYPE + " INTEGER NOT NULL DEFAULT -1," + // 关联的Widget类型
NoteColumns.SYNC_ID + " INTEGER NOT NULL DEFAULT 0," + // 同步IDGTask
NoteColumns.LOCAL_MODIFIED + " INTEGER NOT NULL DEFAULT 0," + // 本地修改标记
NoteColumns.ORIGIN_PARENT_ID + " INTEGER NOT NULL DEFAULT 0," + // 原始父级ID
NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''," + // GTask ID
NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0" + // 版本号
")";
/**
* dataSQL
* datanoteNOTE_ID
*/
private static final String CREATE_DATA_TABLE_SQL =
"CREATE TABLE " + TABLE.DATA + "(" +
DataColumns.ID + " INTEGER PRIMARY KEY," + // 主键ID
DataColumns.MIME_TYPE + " TEXT NOT NULL," + // 数据类型(文本/通话记录)
DataColumns.NOTE_ID + " INTEGER NOT NULL DEFAULT 0," + // 关联的note表ID
NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + // 创建时间
NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + // 修改时间
DataColumns.CONTENT + " TEXT NOT NULL DEFAULT ''," + // 数据内容
DataColumns.DATA1 + " INTEGER," + // 通用整型列1
DataColumns.DATA2 + " INTEGER," + // 通用整型列2
DataColumns.DATA3 + " TEXT NOT NULL DEFAULT ''," + // 通用文本列3
DataColumns.DATA4 + " TEXT NOT NULL DEFAULT ''," + // 通用文本列4
DataColumns.DATA5 + " TEXT NOT NULL DEFAULT ''" + // 通用文本列5
")";
/**
* dataNOTE_IDSQL
* NOTE_IDdata
*/
private static final String CREATE_DATA_NOTE_ID_INDEX_SQL =
"CREATE INDEX IF NOT EXISTS note_id_index ON " +
TABLE.DATA + "(" + DataColumns.NOTE_ID + ");";
// ====================== 数据库触发器SQL语句note表 ======================
/**
* ID
* notePARENT_ID
* NOTES_COUNT1
*/
private static final String NOTE_INCREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER =
"CREATE TRIGGER increase_folder_count_on_update "+
" AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE +
" BEGIN " +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" +
" WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" +
" END";
/**
* ID
* notePARENT_ID
* NOTES_COUNT10
*/
private static final String NOTE_DECREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER =
"CREATE TRIGGER decrease_folder_count_on_update " +
" AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE +
" BEGIN " +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" +
" WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID +
" AND " + NoteColumns.NOTES_COUNT + ">0" + ";" +
" END";
/**
*
* note
* NOTES_COUNT1
*/
private static final String NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER =
"CREATE TRIGGER increase_folder_count_on_insert " +
" AFTER INSERT ON " + TABLE.NOTE +
" BEGIN " +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" +
" WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" +
" END";
/**
*
* note
* NOTES_COUNT10
*/
private static final String NOTE_DECREASE_FOLDER_COUNT_ON_DELETE_TRIGGER =
"CREATE TRIGGER decrease_folder_count_on_delete " +
" AFTER DELETE ON " + TABLE.NOTE +
" BEGIN " +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" +
" WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID +
" AND " + NoteColumns.NOTES_COUNT + ">0;" +
" END";
// ====================== 数据库触发器SQL语句data表 ======================
/**
* note
* data
* noteSNIPPETdataCONTENT
*/
private static final String DATA_UPDATE_NOTE_CONTENT_ON_INSERT_TRIGGER =
"CREATE TRIGGER update_note_content_on_insert " +
" AFTER INSERT ON " + TABLE.DATA +
" WHEN new." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" +
" BEGIN" +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT +
" WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" +
" END";
/**
* note
* data
*/
private static final String DATA_UPDATE_NOTE_CONTENT_ON_UPDATE_TRIGGER =
"CREATE TRIGGER update_note_content_on_update " +
" AFTER UPDATE ON " + TABLE.DATA +
" WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" +
" BEGIN" +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT +
" WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" +
" END";
/**
* note
* data
*/
private static final String DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER =
"CREATE TRIGGER update_note_content_on_delete " +
" AFTER delete ON " + TABLE.DATA +
" WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" +
" BEGIN" +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.SNIPPET + "=''" +
" WHERE " + NoteColumns.ID + "=old." + DataColumns.NOTE_ID + ";" +
" END";
/**
* data
* note
* dataNOTE_IDID
*/
private static final String NOTE_DELETE_DATA_ON_DELETE_TRIGGER =
"CREATE TRIGGER delete_data_on_delete " +
" AFTER DELETE ON " + TABLE.NOTE +
" BEGIN" +
" DELETE FROM " + TABLE.DATA +
" WHERE " + DataColumns.NOTE_ID + "=old." + NoteColumns.ID + ";" +
" END";
/**
*
* note
* notePARENT_IDID
*/
private static final String FOLDER_DELETE_NOTES_ON_DELETE_TRIGGER =
"CREATE TRIGGER folder_delete_notes_on_delete " +
" AFTER DELETE ON " + TABLE.NOTE +
" BEGIN" +
" DELETE FROM " + TABLE.NOTE +
" WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" +
" END";
/**
* SQLiteOpenHelper
*
* @param context 访
*/
public NotesDatabaseHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
/**
* note
*
* @param db SQLiteDatabaseSQL
*/
public void createNoteTable(SQLiteDatabase db) {
// 执行创建note表的SQL
db.execSQL(CREATE_NOTE_TABLE_SQL);
// 重新创建note表的触发器
reCreateNoteTableTriggers(db);
// 初始化系统文件夹
createSystemFolder(db);
Log.d(TAG, "note table has been created");
}
/**
* note
*
*
* @param db SQLiteDatabase
*/
private void reCreateNoteTableTriggers(SQLiteDatabase db) {
// 删除旧触发器(如果存在)
db.execSQL("DROP TRIGGER IF EXISTS increase_folder_count_on_update");
db.execSQL("DROP TRIGGER IF EXISTS decrease_folder_count_on_update");
db.execSQL("DROP TRIGGER IF EXISTS decrease_folder_count_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 folder_delete_notes_on_delete");
// 创建新触发器
db.execSQL(NOTE_INCREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER);
db.execSQL(NOTE_DECREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER);
db.execSQL(NOTE_DECREASE_FOLDER_COUNT_ON_DELETE_TRIGGER);
db.execSQL(NOTE_DELETE_DATA_ON_DELETE_TRIGGER);
db.execSQL(NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER);
db.execSQL(FOLDER_DELETE_NOTES_ON_DELETE_TRIGGER);
}
/**
*
* noteTYPE_SYSTEM
*
* @param db SQLiteDatabase
*/
private void createSystemFolder(SQLiteDatabase db) {
ContentValues values = new ContentValues();
/**
* 1.
*/
values.put(NoteColumns.ID, Notes.ID_CALL_RECORD_FOLDER); // 设置固定ID
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); // 类型为系统文件夹
db.insert(TABLE.NOTE, null, values);
/**
* 2.
*/
values.clear(); // 清空ContentValues
values.put(NoteColumns.ID, Notes.ID_ROOT_FOLDER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
/**
* 3.
*/
values.clear();
values.put(NoteColumns.ID, Notes.ID_TEMPARAY_FOLDER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
}
/**
* data
*
* @param db SQLiteDatabase
*/
public void createDataTable(SQLiteDatabase db) {
// 执行创建data表的SQL
db.execSQL(CREATE_DATA_TABLE_SQL);
// 重新创建data表的触发器
reCreateDataTableTriggers(db);
// 创建note_id索引提升查询性能
db.execSQL(CREATE_DATA_NOTE_ID_INDEX_SQL);
Log.d(TAG, "data table has been created");
}
/**
* data
*
* @param db SQLiteDatabase
*/
private void reCreateDataTableTriggers(SQLiteDatabase db) {
// 删除旧触发器
db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_insert");
db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_update");
db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_delete");
// 创建新触发器
db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_INSERT_TRIGGER);
db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_UPDATE_TRIGGER);
db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER);
}
/**
* NotesDatabaseHelper线
* synchronized线
*
* @param context 使Application Context
* @return NotesDatabaseHelper
*/
static synchronized NotesDatabaseHelper getInstance(Context context) {
// 懒汉式加载实例为null时才创建
if (mInstance == null) {
mInstance = new NotesDatabaseHelper(context);
}
return mInstance;
}
/**
*
* notedata
*
* @param db SQLiteDatabase
*/
@Override
public void onCreate(SQLiteDatabase db) {
createNoteTable(db);
createDataTable(db);
}
/**
*
* 1/2/34
*
* @param db SQLiteDatabase
* @param oldVersion
* @param newVersion
*/
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// 是否需要重建触发器的标记
boolean reCreateTriggers = false;
// 是否跳过版本2升级的标记版本1升级包含版本2的逻辑
boolean skipV2 = false;
// 从版本1升级到版本2
if (oldVersion == 1) {
upgradeToV2(db);
skipV2 = true; // 版本1的升级已包含版本2到3的逻辑
oldVersion++;
}
// 从版本2升级到版本3未跳过的情况
if (oldVersion == 2 && !skipV2) {
upgradeToV3(db);
reCreateTriggers = true; // 需要重建触发器
oldVersion++;
}
// 从版本3升级到版本4
if (oldVersion == 3) {
upgradeToV4(db);
oldVersion++;
}
// 从版本4升级到版本5
if (oldVersion == 4) {
// 为note表添加pinned字段用于置顶功能
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.PINNED
+ " INTEGER NOT NULL DEFAULT 0");
oldVersion++;
}
// 如果需要,重建触发器
if (reCreateTriggers) {
reCreateNoteTableTriggers(db);
reCreateDataTableTriggers(db);
}
// 升级失败时抛出异常,提示版本升级错误
if (oldVersion != newVersion) {
throw new IllegalStateException("Upgrade notes database to version " + newVersion
+ "fails");
}
}
/**
* 12
*
*
* @param db SQLiteDatabase
*/
private void upgradeToV2(SQLiteDatabase db) {
// 删除旧的note表和data表
db.execSQL("DROP TABLE IF EXISTS " + TABLE.NOTE);
db.execSQL("DROP TABLE IF EXISTS " + TABLE.DATA);
// 重新创建表和初始化数据
createNoteTable(db);
createDataTable(db);
}
/**
* 23
* 1.
* 2. noteGTASK_ID
*
* @param db SQLiteDatabase
*/
private void upgradeToV3(SQLiteDatabase db) {
// 删除无用的触发器(更新笔记修改时间的触发器)
db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_insert");
db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_delete");
db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_update");
// 为note表添加GTASK_ID列用于GTask同步
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.GTASK_ID
+ " TEXT NOT NULL DEFAULT ''");
}
/**
* 34
* noteVERSION
*
* @param db SQLiteDatabase
*/
private void upgradeToV4(SQLiteDatabase db) {
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.VERSION
+ " INTEGER NOT NULL DEFAULT 0");
}
}

@ -0,0 +1,571 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.data;
import android.app.SearchManager;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Intent;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
import net.micode.notes.R;
import net.micode.notes.tool.SearchHistoryManager;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.NotesDatabaseHelper.TABLE;
/**
* ContentProviderAndroidContentProvider
*
* 1. 访SQLiteCRUD
* 2. UriMatcherUrinotedata/
* 3.
* 4. notifyChangeUI
* 5.
*
* ContentProvider/访
*
* @author MiCode Open Source Community
* @date 2010-2011
*/
public class NotesProvider extends ContentProvider {
/**
* UriUri
*
*/
private static final UriMatcher mMatcher;
/**
* SQLiteDatabase
*/
private NotesDatabaseHelper mHelper;
/**
* Logcat便
*/
private static final String TAG = "NotesProvider";
// ====================== Uri匹配类型常量 ======================
/**
* Uri/note
*/
private static final int URI_NOTE = 1;
/**
* Uri/noteIDnote/1
*/
private static final int URI_NOTE_ITEM = 2;
/**
* Uri/data
*/
private static final int URI_DATA = 3;
/**
* Uri/dataIDdata/1
*/
private static final int URI_DATA_ITEM = 4;
/**
* Uri
*/
private static final int URI_SEARCH = 5;
/**
* UriSearchManager
*/
private static final int URI_SEARCH_SUGGEST = 6;
/**
* UriMatcherUri
* authority + path ->
*/
static {
// 初始化UriMatcher默认匹配失败返回UriMatcher.NO_MATCH
mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
// 匹配note表所有数据content://micode_notes/note -> URI_NOTE
mMatcher.addURI(Notes.AUTHORITY, "note", URI_NOTE);
// 匹配note表单条数据content://micode_notes/note/##表示数字ID -> URI_NOTE_ITEM
mMatcher.addURI(Notes.AUTHORITY, "note/#", URI_NOTE_ITEM);
// 匹配data表所有数据content://micode_notes/data -> URI_DATA
mMatcher.addURI(Notes.AUTHORITY, "data", URI_DATA);
// 匹配data表单条数据content://micode_notes/data/# -> URI_DATA_ITEM
mMatcher.addURI(Notes.AUTHORITY, "data/#", URI_DATA_ITEM);
// 匹配搜索操作content://micode_notes/search -> URI_SEARCH
mMatcher.addURI(Notes.AUTHORITY, "search", URI_SEARCH);
// 匹配搜索建议空查询content://micode_notes/suggestions/query -> URI_SEARCH_SUGGEST
mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, URI_SEARCH_SUGGEST);
// 匹配搜索建议带关键词content://micode_notes/suggestions/query/关键词 -> URI_SEARCH_SUGGEST
mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", URI_SEARCH_SUGGEST);
}
/**
* Projection
* SearchManager
* 1. x'0A'SQLite\n
* 2. SearchManagerSUGGEST_COLUMN_TEXT_1SUGGEST_COLUMN_ICON_1使
* 3. ID
*/
private static final String NOTES_SEARCH_PROJECTION = NoteColumns.ID + ","
+ NoteColumns.ID + " AS " + SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA + ","
+ "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_1 + ","
+ "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_2 + ","
+ R.drawable.search_result + " AS " + SearchManager.SUGGEST_COLUMN_ICON_1 + ","
+ "'" + Intent.ACTION_VIEW + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_ACTION + ","
+ "'" + Notes.TextNote.CONTENT_TYPE + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA;
/**
* SQL
* 1. NOTES_SEARCH_PROJECTION
* 2. note
* 3. LIKE ?PARENT_ID != IDTYPE=NOTE
*/
private static String NOTES_SNIPPET_SEARCH_QUERY = "SELECT " + NOTES_SEARCH_PROJECTION
+ " FROM " + TABLE.NOTE
+ " WHERE " + NoteColumns.SNIPPET + " LIKE ?"
+ " AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER
+ " AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE;
/**
* ContentProvider
* @return trueContentProviderfalse
*/
@Override
public boolean onCreate() {
// 获取NotesDatabaseHelper的单例实例上下文使用ContentProvider的上下文
mHelper = NotesDatabaseHelper.getInstance(getContext());
return true;
}
/**
* ContentProvider
* UriCursor
*
* @param uri Uri
* @param projection null
* @param selection WHERE?
* @param selectionArgs selection?
* @param sortOrder ORDER BY
* @return Cursornull
*/
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
Cursor c = null;
// 获取只读的SQLiteDatabase对象查询操作使用只读数据库提升性能
SQLiteDatabase db = mHelper.getReadableDatabase();
String id = null; // 存储Uri中的ID如note/1中的1
// 根据Uri匹配的类型执行不同的查询逻辑
switch (mMatcher.match(uri)) {
case URI_NOTE:
// 查询note表的所有数据
c = db.query(TABLE.NOTE, projection, selection, selectionArgs, null, null,
sortOrder);
break;
case URI_NOTE_ITEM:
// 获取Uri中的ID路径的第二个分段如note/1的路径分段是["note", "1"]
id = uri.getPathSegments().get(1);
// 查询note表的单条数据条件ID=id + 传入的selection
c = db.query(TABLE.NOTE, projection, NoteColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs, null, null, sortOrder);
break;
case URI_DATA:
// 查询data表的所有数据
c = db.query(TABLE.DATA, projection, selection, selectionArgs, null, null,
sortOrder);
break;
case URI_DATA_ITEM:
// 获取Uri中的ID
id = uri.getPathSegments().get(1);
// 查询data表的单条数据条件ID=id + 传入的selection
c = db.query(TABLE.DATA, projection, DataColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs, null, null, sortOrder);
break;
case URI_SEARCH:
case URI_SEARCH_SUGGEST:
// 搜索操作不允许指定sortOrder和projection否则抛出异常
if (sortOrder != null || projection != null) {
throw new IllegalArgumentException(
"do not specify sortOrder, selection, selectionArgs, or projection" + "with this query");
}
// 获取搜索关键词:搜索建议从路径获取,普通搜索从查询参数获取
String searchString = null;
if (mMatcher.match(uri) == URI_SEARCH_SUGGEST) {
// 搜索建议路径分段大于1时第二个分段是关键词如query/笔记)
if (uri.getPathSegments().size() > 1) {
searchString = uri.getPathSegments().get(1);
}
} else {
// 普通搜索:从查询参数"pattern"中获取关键词
searchString = uri.getQueryParameter("pattern");
}
// 如果是搜索建议类型,且搜索关键词不为空,返回合并结果
if (mMatcher.match(uri) == URI_SEARCH_SUGGEST && !TextUtils.isEmpty(searchString)) {
try {
// 1. 获取搜索历史记录
SearchHistoryManager historyManager = SearchHistoryManager.getInstance(getContext());
java.util.List<String> historyList = historyManager.getSearchHistoryList();
// 2. 获取便签搜索结果
String likeSearchString = String.format("%%%s%%", searchString);
Cursor noteCursor = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY, new String[] { likeSearchString });
// 3. 创建矩阵游标,用于合并结果
String[] columns = { NoteColumns.ID, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA,
SearchManager.SUGGEST_COLUMN_TEXT_1, SearchManager.SUGGEST_COLUMN_TEXT_2,
SearchManager.SUGGEST_COLUMN_ICON_1, SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
SearchManager.SUGGEST_COLUMN_INTENT_DATA };
android.database.MatrixCursor matrixCursor = new android.database.MatrixCursor(columns);
// 4. 添加搜索历史记录(只添加匹配的历史)
for (String history : historyList) {
if (history.toLowerCase().contains(searchString.toLowerCase())) {
matrixCursor.addRow(new Object[] {
-1, // ID为-1表示是历史记录
history, // 历史记录作为Intent Extra数据
history, // 显示的文本1
getContext().getString(R.string.search_history), // 显示的文本2
R.drawable.search_result, // 图标
Intent.ACTION_SEARCH, // Intent动作
Notes.TextNote.CONTENT_TYPE // Intent数据类型
});
}
}
// 5. 添加便签搜索结果
if (noteCursor != null && noteCursor.moveToFirst()) {
do {
// 从便签搜索结果中获取列数据
long noteId = noteCursor.getLong(noteCursor.getColumnIndexOrThrow(NoteColumns.ID));
String extraData = noteCursor.getString(noteCursor.getColumnIndexOrThrow(SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA));
String text1 = noteCursor.getString(noteCursor.getColumnIndexOrThrow(SearchManager.SUGGEST_COLUMN_TEXT_1));
String text2 = noteCursor.getString(noteCursor.getColumnIndexOrThrow(SearchManager.SUGGEST_COLUMN_TEXT_2));
int icon = noteCursor.getInt(noteCursor.getColumnIndexOrThrow(SearchManager.SUGGEST_COLUMN_ICON_1));
String action = noteCursor.getString(noteCursor.getColumnIndexOrThrow(SearchManager.SUGGEST_COLUMN_INTENT_ACTION));
String data = noteCursor.getString(noteCursor.getColumnIndexOrThrow(SearchManager.SUGGEST_COLUMN_INTENT_DATA));
matrixCursor.addRow(new Object[] { noteId, extraData, text1, text2, icon, action, data });
} while (noteCursor.moveToNext());
}
// 6. 关闭便签搜索结果游标
if (noteCursor != null) {
noteCursor.close();
}
// 7. 设置矩阵游标为结果
c = matrixCursor;
} catch (IllegalStateException ex) {
// 捕获异常,输出错误日志
Log.e(TAG, "got exception: " + ex.toString());
}
} else if (!TextUtils.isEmpty(searchString)) {
// 普通搜索或搜索建议但关键词为空,只返回便签搜索结果
try {
// 拼接SQL的LIKE关键词%表示任意字符,如%笔记%
searchString = String.format("%%%s%%", searchString);
// 执行原生SQL查询获取搜索结果
c = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY,
new String[] { searchString });
} catch (IllegalStateException ex) {
// 捕获异常,输出错误日志
Log.e(TAG, "got exception: " + ex.toString());
}
}
break;
default:
// 未知Uri抛出异常
throw new IllegalArgumentException("Unknown URI " + uri);
}
// 为Cursor设置通知Uri当数据变更时Cursor会收到通知并更新
if (c != null) {
c.setNotificationUri(getContext().getContentResolver(), uri);
}
return c;
}
/**
* notedata
*
* @param uri Uri
* @param values ContentValues
* @return UriIDcontent://micode_notes/note/1
*/
@Override
public Uri insert(Uri uri, ContentValues values) {
// 获取可写的SQLiteDatabase对象插入操作需要写权限
SQLiteDatabase db = mHelper.getWritableDatabase();
long dataId = 0, noteId = 0, insertedId = 0; // 存储插入的ID
// 根据Uri匹配的类型执行插入逻辑
switch (mMatcher.match(uri)) {
case URI_NOTE:
// 插入note表获取插入的ID
insertedId = noteId = db.insert(TABLE.NOTE, null, values);
break;
case URI_DATA:
// 插入data表时先获取关联的noteId必须包含NOTE_ID列
if (values.containsKey(DataColumns.NOTE_ID)) {
noteId = values.getAsLong(DataColumns.NOTE_ID);
} else {
// 无NOTE_ID时输出调试日志
Log.d(TAG, "Wrong data format without note id:" + values.toString());
}
// 插入data表获取插入的ID
insertedId = dataId = db.insert(TABLE.DATA, null, values);
break;
default:
// 未知Uri抛出异常
throw new IllegalArgumentException("Unknown URI " + uri);
}
// 发送通知note表数据变更通知对应的Uri
if (noteId > 0) {
getContext().getContentResolver().notifyChange(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null);
}
// 发送通知data表数据变更通知对应的Uri
if (dataId > 0) {
getContext().getContentResolver().notifyChange(
ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), null);
}
// 返回包含插入ID的新Uri
return ContentUris.withAppendedId(uri, insertedId);
}
/**
* notedata
*
* @param uri Uri
* @param selection WHERE
* @param selectionArgs
* @return
*/
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
int count = 0; // 存储删除的行数
String id = null; // 存储Uri中的ID
// 获取可写的SQLiteDatabase对象
SQLiteDatabase db = mHelper.getWritableDatabase();
boolean deleteData = false; // 标记是否删除的是data表数据
long noteId = 0; // 用于存储便签ID以便发送通知
// 根据Uri匹配的类型执行删除逻辑
switch (mMatcher.match(uri)) {
case URI_NOTE:
// 直接删除便签条件传入的selection + ID>0排除系统文件夹
selection = "(" + selection + ") AND " + NoteColumns.ID + ">0 ";
count = db.delete(TABLE.NOTE, selection, selectionArgs);
break;
case URI_NOTE_ITEM:
// 获取Uri中的ID
id = uri.getPathSegments().get(1);
/**
* ID0
*/
noteId = Long.valueOf(id);
if (noteId <= 0) {
break;
}
// 直接删除便签条件ID=id + 传入的selection
count = db.delete(TABLE.NOTE, NoteColumns.ID + "=" + id + parseSelection(selection), selectionArgs);
break;
case URI_DATA:
// 删除data表数据
count = db.delete(TABLE.DATA, selection, selectionArgs);
deleteData = true;
break;
case URI_DATA_ITEM:
// 获取Uri中的ID删除data表单条数据
id = uri.getPathSegments().get(1);
count = db.delete(TABLE.DATA,
DataColumns.ID + "=" + id + parseSelection(selection), selectionArgs);
deleteData = true;
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
// 数据删除成功时发送通知更新UI
if (count > 0) {
// 删除data表数据时同时通知note表的Uri因为data表变更会影响note表的摘要
if (deleteData) {
getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null);
}
// 如果是便签相关操作通知对应的便签Uri
if (noteId > 0) {
getContext().getContentResolver().notifyChange(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null);
}
// 通知当前Uri的数据变更
getContext().getContentResolver().notifyChange(uri, null);
}
return count;
}
/**
* notedata
*
* @param uri Uri
* @param values ContentValues
* @param selection WHERE
* @param selectionArgs
* @return
*/
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
int count = 0; // 存储更新的行数
String id = null; // 存储Uri中的ID
// 获取可写的SQLiteDatabase对象
SQLiteDatabase db = mHelper.getWritableDatabase();
boolean updateData = false; // 标记是否更新的是data表数据
long noteId = 0; // 用于存储便签ID以便发送通知
// 根据Uri匹配的类型执行更新逻辑
switch (mMatcher.match(uri)) {
case URI_NOTE:
// 更新note表前增加笔记的版本号
increaseNoteVersion(-1, selection, selectionArgs);
count = db.update(TABLE.NOTE, values, selection, selectionArgs);
break;
case URI_NOTE_ITEM:
// 获取Uri中的ID
id = uri.getPathSegments().get(1);
// 更新note表单条数据前增加版本号
increaseNoteVersion(Long.valueOf(id), selection, selectionArgs);
count = db.update(TABLE.NOTE, values, NoteColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs);
break;
case URI_DATA:
// 更新data表数据
count = db.update(TABLE.DATA, values, selection, selectionArgs);
updateData = true;
break;
case URI_DATA_ITEM:
// 获取Uri中的ID更新data表单条数据
id = uri.getPathSegments().get(1);
count = db.update(TABLE.DATA, values, DataColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs);
updateData = true;
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
// 数据更新成功时发送通知更新UI
if (count > 0) {
// 更新data表数据时同时通知note表的Uri
if (updateData) {
getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null);
}
// 如果是便签相关操作通知对应的便签Uri
if (noteId > 0) {
getContext().getContentResolver().notifyChange(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null);
}
// 通知当前Uri的数据变更
getContext().getContentResolver().notifyChange(uri, null);
}
return count;
}
/**
* AND
* selection" AND (selection)"
* UriIDselectionSQL
*
* @param selection
* @return
*/
private String parseSelection(String selection) {
return (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : "");
}
/**
* VERSION
* SQLVERSION1
*
* @param id ID-1
* @param selection
* @param selectionArgs
*/
private void increaseNoteVersion(long id, String selection, String[] selectionArgs) {
StringBuilder sql = new StringBuilder(120); // 构建SQL语句的字符串构建器
// 拼接UPDATE语句UPDATE note SET VERSION=VERSION+1 [WHERE 条件]
sql.append("UPDATE ");
sql.append(TABLE.NOTE);
sql.append(" SET ");
sql.append(NoteColumns.VERSION);
sql.append("=" + NoteColumns.VERSION + "+1 ");
// 添加WHERE子句ID>0或selection不为空时
if (id > 0 || !TextUtils.isEmpty(selection)) {
sql.append(" WHERE ");
}
// 添加ID条件id>0时
if (id > 0) {
sql.append(NoteColumns.ID + "=" + String.valueOf(id));
}
// 添加传入的selection条件替换占位符?为实际参数)
if (!TextUtils.isEmpty(selection)) {
// 处理selection已有ID时拼接否则直接使用
String selectString = id > 0 ? parseSelection(selection) : selection;
// 替换selection中的?为selectionArgs的参数简单替换适用于基础场景
for (String args : selectionArgs) {
selectString = selectString.replaceFirst("\\?", args);
}
sql.append(selectString);
}
// 执行SQL语句
mHelper.getWritableDatabase().execSQL(sql.toString());
}
/**
* UriMIMEContentProvider
* null
*
* @param uri Uri
* @return UriMIME
*/
@Override
public String getType(Uri uri) {
// TODO Auto-generated method stub
return null;
}
}

@ -0,0 +1,159 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.gtask.data;
import android.database.Cursor;
import android.util.Log;
import net.micode.notes.tool.GTaskStringUtils;
import org.json.JSONException;
import org.json.JSONObject;
/**
* GTask
* {@link Task}GTaskGTask ID
* JSONGTaskJSONID
* JSON
*
* @author MiCode Open Source Community
* @date 2010-2011
* @see Task
*/
public class MetaData extends Task {
/**
* 使MetaData便Logcat
*/
private final static String TAG = MetaData.class.getSimpleName();
/**
* GTaskGid
* GidGTaskIDGTask
*/
private String mRelatedGid = null;
/**
* GTask IDJSON
*
* 1. JSONGTask ID
* 2. JSONnotes
* 3. {@link GTaskStringUtils#META_NOTE_NAME}
*
* @param gid GTaskGid
* @param metaInfo JSON
*/
public void setMeta(String gid, JSONObject metaInfo) {
try {
// 向JSON对象中添加GTask ID的键值对键为META_HEAD_GTASK_ID
metaInfo.put(GTaskStringUtils.META_HEAD_GTASK_ID, gid);
} catch (JSONException e) {
// 捕获JSON操作异常输出错误日志
Log.e(TAG, "failed to put related gid");
}
// 将JSON对象转为字符串设置为任务的笔记内容父类Task的notes属性
setNotes(metaInfo.toString());
// 设置任务名称为元数据专用名称(区分普通任务和元数据任务)
setName(GTaskStringUtils.META_NOTE_NAME);
}
/**
* GTask ID
*
* @return GTask IDnull
*/
public String getRelatedGid() {
return mRelatedGid;
}
/**
* /
* JSONnull
*
*
* @return truefalse
*/
@Override
public boolean isWorthSaving() {
return getNotes() != null;
}
/**
* GTaskJSON
*
* 1.
* 2. JSONJSONGTask ID
* 3. JSONGTask IDnull
*
* @param js GTaskJSON
*/
@Override
public void setContentByRemoteJSON(JSONObject js) {
// 调用父类方法,处理基础的任务内容(如名称、笔记等)
super.setContentByRemoteJSON(js);
// 若笔记内容元数据JSON字符串非空则解析关联的GTask ID
if (getNotes() != null) {
try {
// 将笔记内容转为JSON对象去除首尾空格避免解析错误
JSONObject metaInfo = new JSONObject(getNotes().trim());
// 从JSON对象中获取GTask ID并赋值给成员变量
mRelatedGid = metaInfo.getString(GTaskStringUtils.META_HEAD_GTASK_ID);
} catch (JSONException e) {
// 捕获JSON解析异常输出警告日志将GTask ID置为null
Log.w(TAG, "failed to get related gid");
mRelatedGid = null;
}
}
}
/**
* JSON
* 访
*
* @throws IllegalAccessError
*/
@Override
public void setContentByLocalJSON(JSONObject js) {
// 抛出异常说明该方法不应被调用元数据仅处理远程GTask的JSON不处理本地JSON
throw new IllegalAccessError("MetaData:setContentByLocalJSON should not be called");
}
/**
* JSON
* 访
*
* @throws IllegalAccessError
* @return
*/
@Override
public JSONObject getLocalJSONFromContent() {
throw new IllegalAccessError("MetaData:getLocalJSONFromContent should not be called");
}
/**
*
* 访
*
* @param c 使
* @throws IllegalAccessError
* @return
*/
@Override
public int getSyncAction(Cursor c) {
throw new IllegalAccessError("MetaData:getSyncAction should not be called");
}
}

@ -0,0 +1,217 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.gtask.data;
import android.database.Cursor;
import org.json.JSONObject;
/**
* GTask
* GTaskGID
* JSON/
* GTask/
*
* @author MiCode Open Source Community
* @date 2010-2011
*/
public abstract class Node {
// ====================== 同步动作常量 ======================
/**
*
*/
public static final int SYNC_ACTION_NONE = 0;
/**
* GTask
*/
public static final int SYNC_ACTION_ADD_REMOTE = 1;
/**
* GTask
*/
public static final int SYNC_ACTION_ADD_LOCAL = 2;
/**
* GTask
*/
public static final int SYNC_ACTION_DEL_REMOTE = 3;
/**
* GTask
*/
public static final int SYNC_ACTION_DEL_LOCAL = 4;
/**
* GTask
*/
public static final int SYNC_ACTION_UPDATE_REMOTE = 5;
/**
* GTask
*/
public static final int SYNC_ACTION_UPDATE_LOCAL = 6;
/**
*
*/
public static final int SYNC_ACTION_UPDATE_CONFLICT = 7;
/**
*
*/
public static final int SYNC_ACTION_ERROR = 8;
// ====================== 成员变量 ======================
/**
* GTaskIDGidGTask
*/
private String mGid;
/**
*
*/
private String mName;
/**
*
*/
private long mLastModified;
/**
* true
*/
private boolean mDeleted;
/**
*
* Gidnull0false
*/
public Node() {
mGid = null;
mName = "";
mLastModified = 0;
mDeleted = false;
}
// ====================== 抽象方法(子类需实现) ======================
/**
* JSONGTask
* @param actionId IDID
* @return JSONObject
*/
public abstract JSONObject getCreateAction(int actionId);
/**
* JSONGTask
* @param actionId IDID
* @return JSONObject
*/
public abstract JSONObject getUpdateAction(int actionId);
/**
* GTaskJSON
* @param js GTaskJSON
*/
public abstract void setContentByRemoteJSON(JSONObject js);
/**
* JSON
* @param js JSON
*/
public abstract void setContentByLocalJSON(JSONObject js);
/**
* JSONJSON
* @return JSONObject
*/
public abstract JSONObject getLocalJSONFromContent();
/**
* Cursor
* GTask
* @param c Cursor
* @return SYNC_ACTION_*
*/
public abstract int getSyncAction(Cursor c);
// ====================== 成员变量的setter/getter方法 ======================
/**
* GTaskIDGid
* @param gid GTaskGid
*/
public void setGid(String gid) {
this.mGid = gid;
}
/**
*
* @param name /
*/
public void setName(String name) {
this.mName = name;
}
/**
*
* @param lastModified
*/
public void setLastModified(long lastModified) {
this.mLastModified = lastModified;
}
/**
*
* @param deleted truefalse
*/
public void setDeleted(boolean deleted) {
this.mDeleted = deleted;
}
/**
* GTaskIDGid
* @return Gidnull
*/
public String getGid() {
return this.mGid;
}
/**
*
* @return
*/
public String getName() {
return this.mName;
}
/**
*
* @return
*/
public long getLastModified() {
return this.mLastModified;
}
/**
*
* @return truefalse
*/
public boolean getDeleted() {
return this.mDeleted;
}
}

@ -0,0 +1,335 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.gtask.data;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.util.Log;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.DataConstants;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.NotesDatabaseHelper.TABLE;
import net.micode.notes.gtask.exception.ActionFailureException;
import org.json.JSONException;
import org.json.JSONObject;
/**
* data
* Android ContentProviderdataJSON/
* GTaskdatamDiffDataValues
*
*
* @author MiCode Open Source Community
* @date 2010-2011
*/
public class SqlData {
/**
* 使便
*/
private static final String TAG = SqlData.class.getSimpleName();
/**
* IDIDID
*/
private static final int INVALID_ID = -99999;
/**
* dataProjection
* IDMIME_TYPECONTENTDATA1DATA3
*/
public static final String[] PROJECTION_DATA = new String[] {
DataColumns.ID, DataColumns.MIME_TYPE, DataColumns.CONTENT, DataColumns.DATA1,
DataColumns.DATA3
};
// ====================== PROJECTION_DATA的列索引常量 ======================
/**
* PROJECTION_DATAIDDataColumns.ID
*/
public static final int DATA_ID_COLUMN = 0;
/**
* PROJECTION_DATAMIME_TYPEDataColumns.MIME_TYPE
*/
public static final int DATA_MIME_TYPE_COLUMN = 1;
/**
* PROJECTION_DATACONTENTDataColumns.CONTENT
*/
public static final int DATA_CONTENT_COLUMN = 2;
/**
* PROJECTION_DATADATA1DataColumns.DATA1
*/
public static final int DATA_CONTENT_DATA_1_COLUMN = 3;
/**
* PROJECTION_DATADATA3DataColumns.DATA3
*/
public static final int DATA_CONTENT_DATA_3_COLUMN = 4;
// ====================== 成员变量 ======================
/**
* Android访ContentProvider
*/
private ContentResolver mContentResolver;
/**
* truefalse
*/
private boolean mIsCreate;
/**
* dataID
*/
private long mDataId;
/**
* MIMEDataConstants.NOTEDataConstants.CALL_NOTE
*/
private String mDataMimeType;
/**
*
*/
private String mDataContent;
/**
* DATA1
*/
private long mDataContentData1;
/**
* DATA3
*/
private String mDataContentData3;
/**
* ContentValues
*/
private ContentValues mDiffDataValues;
/**
* SqlData
*
* @param context ContentResolver
*/
public SqlData(Context context) {
// 获取ContentResolver实例用于访问ContentProvider
mContentResolver = context.getContentResolver();
// 标记为新数据,需要插入数据库
mIsCreate = true;
// 初始化ID为无效值
mDataId = INVALID_ID;
// 默认MIME类型为文本笔记
mDataMimeType = DataConstants.NOTE;
// 初始化内容为空字符串
mDataContent = "";
// 初始化DATA1为0
mDataContentData1 = 0;
// 初始化DATA3为空字符串
mDataContentData3 = "";
// 初始化数据差异容器
mDiffDataValues = new ContentValues();
}
/**
* CursorSqlData
*
* @param context ContentResolver
* @param c dataCursor使PROJECTION_DATA
*/
public SqlData(Context context, Cursor c) {
mContentResolver = context.getContentResolver();
// 标记为已有数据,需要更新数据库
mIsCreate = false;
// 从Cursor中加载数据到成员变量
loadFromCursor(c);
// 初始化数据差异容器
mDiffDataValues = new ContentValues();
}
/**
* Cursor
* Cursor使PROJECTION_DATA
*
* @param c dataCursor
*/
private void loadFromCursor(Cursor c) {
// 从Cursor中获取ID长整型
mDataId = c.getLong(DATA_ID_COLUMN);
// 获取MIME类型字符串
mDataMimeType = c.getString(DATA_MIME_TYPE_COLUMN);
// 获取内容(字符串)
mDataContent = c.getString(DATA_CONTENT_COLUMN);
// 获取DATA1长整型
mDataContentData1 = c.getLong(DATA_CONTENT_DATA_1_COLUMN);
// 获取DATA3字符串
mDataContentData3 = c.getString(DATA_CONTENT_DATA_3_COLUMN);
}
/**
* JSONmDiffDataValues
* JSON
*
* @param js dataJSON
* @throws JSONException JSON
*/
public void setContent(JSONObject js) throws JSONException {
// 从JSON中获取ID默认值为INVALID_ID
long dataId = js.has(DataColumns.ID) ? js.getLong(DataColumns.ID) : INVALID_ID;
// 新数据或ID变化时将ID加入差异容器
if (mIsCreate || mDataId != dataId) {
mDiffDataValues.put(DataColumns.ID, dataId);
}
// 更新当前ID
mDataId = dataId;
// 从JSON中获取MIME类型默认值为文本笔记
String dataMimeType = js.has(DataColumns.MIME_TYPE) ? js.getString(DataColumns.MIME_TYPE)
: DataConstants.NOTE;
// 新数据或MIME类型变化时将MIME_TYPE加入差异容器
if (mIsCreate || !mDataMimeType.equals(dataMimeType)) {
mDiffDataValues.put(DataColumns.MIME_TYPE, dataMimeType);
}
// 更新当前MIME类型
mDataMimeType = dataMimeType;
// 从JSON中获取内容默认值为空字符串
String dataContent = js.has(DataColumns.CONTENT) ? js.getString(DataColumns.CONTENT) : "";
// 新数据或内容变化时将CONTENT加入差异容器
if (mIsCreate || !mDataContent.equals(dataContent)) {
mDiffDataValues.put(DataColumns.CONTENT, dataContent);
}
// 更新当前内容
mDataContent = dataContent;
// 从JSON中获取DATA1默认值为0
long dataContentData1 = js.has(DataColumns.DATA1) ? js.getLong(DataColumns.DATA1) : 0;
// 新数据或DATA1变化时将DATA1加入差异容器
if (mIsCreate || mDataContentData1 != dataContentData1) {
mDiffDataValues.put(DataColumns.DATA1, dataContentData1);
}
// 更新当前DATA1
mDataContentData1 = dataContentData1;
// 从JSON中获取DATA3默认值为空字符串
String dataContentData3 = js.has(DataColumns.DATA3) ? js.getString(DataColumns.DATA3) : "";
// 新数据或DATA3变化时将DATA3加入差异容器
if (mIsCreate || !mDataContentData3.equals(dataContentData3)) {
mDiffDataValues.put(DataColumns.DATA3, dataContentData3);
}
// 更新当前DATA3
mDataContentData3 = dataContentData3;
}
/**
* JSON
* null
*
* @return dataJSON
* @throws JSONException JSON
*/
public JSONObject getContent() throws JSONException {
// 新数据尚未持久化输出错误日志并返回null
if (mIsCreate) {
Log.e(TAG, "it seems that we haven't created this in database yet");
return null;
}
// 创建JSON对象并写入所有数据字段
JSONObject js = new JSONObject();
js.put(DataColumns.ID, mDataId);
js.put(DataColumns.MIME_TYPE, mDataMimeType);
js.put(DataColumns.CONTENT, mDataContent);
js.put(DataColumns.DATA1, mDataContentData1);
js.put(DataColumns.DATA3, mDataContentData3);
return js;
}
/**
*
* note
*
* @param noteId noteIDdataNOTE_ID
* @param validateVersion truefalse
* @param version notevalidateVersiontrue
*/
public void commit(long noteId, boolean validateVersion, long version) {
// 新数据:执行插入操作
if (mIsCreate) {
// 若ID为无效值且差异容器中包含ID移除该ID数据库自增ID无需手动设置
if (mDataId == INVALID_ID && mDiffDataValues.containsKey(DataColumns.ID)) {
mDiffDataValues.remove(DataColumns.ID);
}
// 添加关联的note表ID到差异容器
mDiffDataValues.put(DataColumns.NOTE_ID, noteId);
// 插入数据到ContentProvider获取返回的Uri包含新数据的ID
Uri uri = mContentResolver.insert(Notes.CONTENT_DATA_URI, mDiffDataValues);
try {
// 从Uri中解析出新数据的IDUri路径分段的第二个元素如data/123中的123
mDataId = Long.valueOf(uri.getPathSegments().get(1));
} catch (NumberFormatException e) {
// 解析ID失败输出错误日志并抛出同步失败异常
Log.e(TAG, "Get note id error :" + e.toString());
throw new ActionFailureException("create note failed");
}
} else {
// 已有数据:仅当存在差异时执行更新操作
if (mDiffDataValues.size() > 0) {
int result = 0;
if (!validateVersion) {
// 不验证版本直接更新数据根据data表ID
result = mContentResolver.update(ContentUris.withAppendedId(
Notes.CONTENT_DATA_URI, mDataId), mDiffDataValues, null, null);
} else {
// 验证版本仅当note表中对应ID的版本号匹配时才更新
result = mContentResolver.update(ContentUris.withAppendedId(
Notes.CONTENT_DATA_URI, mDataId), mDiffDataValues,
" ? in (SELECT " + NoteColumns.ID + " FROM " + TABLE.NOTE
+ " WHERE " + NoteColumns.VERSION + "=?)", new String[] {
String.valueOf(noteId), String.valueOf(version)
});
}
// 更新结果为0说明数据未更新可能同步时用户修改了数据输出警告日志
if (result == 0) {
Log.w(TAG, "there is no update. maybe user updates note when syncing");
}
}
}
// 清空差异容器,标记数据为已存在(非新数据)
mDiffDataValues.clear();
mIsCreate = false;
}
/**
* dataID
*
* @return dataIDINVALID_ID
*/
public long getId() {
return mDataId;
}
}

@ -0,0 +1,505 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.gtask.data;
import android.appwidget.AppWidgetManager;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.util.Log;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.gtask.exception.ActionFailureException;
import net.micode.notes.tool.GTaskStringUtils;
import net.micode.notes.tool.ResourceParser;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
public class SqlNote {
private static final String TAG = SqlNote.class.getSimpleName();
private static final int INVALID_ID = -99999;
public static final String[] PROJECTION_NOTE = new String[] {
NoteColumns.ID, NoteColumns.ALERTED_DATE, NoteColumns.BG_COLOR_ID,
NoteColumns.CREATED_DATE, NoteColumns.HAS_ATTACHMENT, NoteColumns.MODIFIED_DATE,
NoteColumns.NOTES_COUNT, NoteColumns.PARENT_ID, NoteColumns.SNIPPET, NoteColumns.TYPE,
NoteColumns.WIDGET_ID, NoteColumns.WIDGET_TYPE, NoteColumns.SYNC_ID,
NoteColumns.LOCAL_MODIFIED, NoteColumns.ORIGIN_PARENT_ID, NoteColumns.GTASK_ID,
NoteColumns.VERSION
};
public static final int ID_COLUMN = 0;
public static final int ALERTED_DATE_COLUMN = 1;
public static final int BG_COLOR_ID_COLUMN = 2;
public static final int CREATED_DATE_COLUMN = 3;
public static final int HAS_ATTACHMENT_COLUMN = 4;
public static final int MODIFIED_DATE_COLUMN = 5;
public static final int NOTES_COUNT_COLUMN = 6;
public static final int PARENT_ID_COLUMN = 7;
public static final int SNIPPET_COLUMN = 8;
public static final int TYPE_COLUMN = 9;
public static final int WIDGET_ID_COLUMN = 10;
public static final int WIDGET_TYPE_COLUMN = 11;
public static final int SYNC_ID_COLUMN = 12;
public static final int LOCAL_MODIFIED_COLUMN = 13;
public static final int ORIGIN_PARENT_ID_COLUMN = 14;
public static final int GTASK_ID_COLUMN = 15;
public static final int VERSION_COLUMN = 16;
private Context mContext;
private ContentResolver mContentResolver;
private boolean mIsCreate;
private long mId;
private long mAlertDate;
private int mBgColorId;
private long mCreatedDate;
private int mHasAttachment;
private long mModifiedDate;
private long mParentId;
private String mSnippet;
private int mType;
private int mWidgetId;
private int mWidgetType;
private long mOriginParent;
private long mVersion;
private ContentValues mDiffNoteValues;
private ArrayList<SqlData> mDataList;
public SqlNote(Context context) {
mContext = context;
mContentResolver = context.getContentResolver();
mIsCreate = true;
mId = INVALID_ID;
mAlertDate = 0;
mBgColorId = ResourceParser.getDefaultBgId(context);
mCreatedDate = System.currentTimeMillis();
mHasAttachment = 0;
mModifiedDate = System.currentTimeMillis();
mParentId = 0;
mSnippet = "";
mType = Notes.TYPE_NOTE;
mWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
mWidgetType = Notes.TYPE_WIDGET_INVALIDE;
mOriginParent = 0;
mVersion = 0;
mDiffNoteValues = new ContentValues();
mDataList = new ArrayList<SqlData>();
}
public SqlNote(Context context, Cursor c) {
mContext = context;
mContentResolver = context.getContentResolver();
mIsCreate = false;
loadFromCursor(c);
mDataList = new ArrayList<SqlData>();
if (mType == Notes.TYPE_NOTE)
loadDataContent();
mDiffNoteValues = new ContentValues();
}
public SqlNote(Context context, long id) {
mContext = context;
mContentResolver = context.getContentResolver();
mIsCreate = false;
loadFromCursor(id);
mDataList = new ArrayList<SqlData>();
if (mType == Notes.TYPE_NOTE)
loadDataContent();
mDiffNoteValues = new ContentValues();
}
private void loadFromCursor(long id) {
Cursor c = null;
try {
c = mContentResolver.query(Notes.CONTENT_NOTE_URI, PROJECTION_NOTE, "(_id=?)",
new String[] {
String.valueOf(id)
}, null);
if (c != null) {
c.moveToNext();
loadFromCursor(c);
} else {
Log.w(TAG, "loadFromCursor: cursor = null");
}
} finally {
if (c != null)
c.close();
}
}
private void loadFromCursor(Cursor c) {
mId = c.getLong(ID_COLUMN);
mAlertDate = c.getLong(ALERTED_DATE_COLUMN);
mBgColorId = c.getInt(BG_COLOR_ID_COLUMN);
mCreatedDate = c.getLong(CREATED_DATE_COLUMN);
mHasAttachment = c.getInt(HAS_ATTACHMENT_COLUMN);
mModifiedDate = c.getLong(MODIFIED_DATE_COLUMN);
mParentId = c.getLong(PARENT_ID_COLUMN);
mSnippet = c.getString(SNIPPET_COLUMN);
mType = c.getInt(TYPE_COLUMN);
mWidgetId = c.getInt(WIDGET_ID_COLUMN);
mWidgetType = c.getInt(WIDGET_TYPE_COLUMN);
mVersion = c.getLong(VERSION_COLUMN);
}
private void loadDataContent() {
Cursor c = null;
mDataList.clear();
try {
c = mContentResolver.query(Notes.CONTENT_DATA_URI, SqlData.PROJECTION_DATA,
"(note_id=?)", new String[] {
String.valueOf(mId)
}, null);
if (c != null) {
if (c.getCount() == 0) {
Log.w(TAG, "it seems that the note has not data");
return;
}
while (c.moveToNext()) {
SqlData data = new SqlData(mContext, c);
mDataList.add(data);
}
} else {
Log.w(TAG, "loadDataContent: cursor = null");
}
} finally {
if (c != null)
c.close();
}
}
public boolean setContent(JSONObject js) {
try {
JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_SYSTEM) {
Log.w(TAG, "cannot set system folder");
} else if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_FOLDER) {
// for folder we can only update the snnipet and type
String snippet = note.has(NoteColumns.SNIPPET) ? note
.getString(NoteColumns.SNIPPET) : "";
if (mIsCreate || !mSnippet.equals(snippet)) {
mDiffNoteValues.put(NoteColumns.SNIPPET, snippet);
}
mSnippet = snippet;
int type = note.has(NoteColumns.TYPE) ? note.getInt(NoteColumns.TYPE)
: Notes.TYPE_NOTE;
if (mIsCreate || mType != type) {
mDiffNoteValues.put(NoteColumns.TYPE, type);
}
mType = type;
} else if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_NOTE) {
JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA);
long id = note.has(NoteColumns.ID) ? note.getLong(NoteColumns.ID) : INVALID_ID;
if (mIsCreate || mId != id) {
mDiffNoteValues.put(NoteColumns.ID, id);
}
mId = id;
long alertDate = note.has(NoteColumns.ALERTED_DATE) ? note
.getLong(NoteColumns.ALERTED_DATE) : 0;
if (mIsCreate || mAlertDate != alertDate) {
mDiffNoteValues.put(NoteColumns.ALERTED_DATE, alertDate);
}
mAlertDate = alertDate;
int bgColorId = note.has(NoteColumns.BG_COLOR_ID) ? note
.getInt(NoteColumns.BG_COLOR_ID) : ResourceParser.getDefaultBgId(mContext);
if (mIsCreate || mBgColorId != bgColorId) {
mDiffNoteValues.put(NoteColumns.BG_COLOR_ID, bgColorId);
}
mBgColorId = bgColorId;
long createDate = note.has(NoteColumns.CREATED_DATE) ? note
.getLong(NoteColumns.CREATED_DATE) : System.currentTimeMillis();
if (mIsCreate || mCreatedDate != createDate) {
mDiffNoteValues.put(NoteColumns.CREATED_DATE, createDate);
}
mCreatedDate = createDate;
int hasAttachment = note.has(NoteColumns.HAS_ATTACHMENT) ? note
.getInt(NoteColumns.HAS_ATTACHMENT) : 0;
if (mIsCreate || mHasAttachment != hasAttachment) {
mDiffNoteValues.put(NoteColumns.HAS_ATTACHMENT, hasAttachment);
}
mHasAttachment = hasAttachment;
long modifiedDate = note.has(NoteColumns.MODIFIED_DATE) ? note
.getLong(NoteColumns.MODIFIED_DATE) : System.currentTimeMillis();
if (mIsCreate || mModifiedDate != modifiedDate) {
mDiffNoteValues.put(NoteColumns.MODIFIED_DATE, modifiedDate);
}
mModifiedDate = modifiedDate;
long parentId = note.has(NoteColumns.PARENT_ID) ? note
.getLong(NoteColumns.PARENT_ID) : 0;
if (mIsCreate || mParentId != parentId) {
mDiffNoteValues.put(NoteColumns.PARENT_ID, parentId);
}
mParentId = parentId;
String snippet = note.has(NoteColumns.SNIPPET) ? note
.getString(NoteColumns.SNIPPET) : "";
if (mIsCreate || !mSnippet.equals(snippet)) {
mDiffNoteValues.put(NoteColumns.SNIPPET, snippet);
}
mSnippet = snippet;
int type = note.has(NoteColumns.TYPE) ? note.getInt(NoteColumns.TYPE)
: Notes.TYPE_NOTE;
if (mIsCreate || mType != type) {
mDiffNoteValues.put(NoteColumns.TYPE, type);
}
mType = type;
int widgetId = note.has(NoteColumns.WIDGET_ID) ? note.getInt(NoteColumns.WIDGET_ID)
: AppWidgetManager.INVALID_APPWIDGET_ID;
if (mIsCreate || mWidgetId != widgetId) {
mDiffNoteValues.put(NoteColumns.WIDGET_ID, widgetId);
}
mWidgetId = widgetId;
int widgetType = note.has(NoteColumns.WIDGET_TYPE) ? note
.getInt(NoteColumns.WIDGET_TYPE) : Notes.TYPE_WIDGET_INVALIDE;
if (mIsCreate || mWidgetType != widgetType) {
mDiffNoteValues.put(NoteColumns.WIDGET_TYPE, widgetType);
}
mWidgetType = widgetType;
long originParent = note.has(NoteColumns.ORIGIN_PARENT_ID) ? note
.getLong(NoteColumns.ORIGIN_PARENT_ID) : 0;
if (mIsCreate || mOriginParent != originParent) {
mDiffNoteValues.put(NoteColumns.ORIGIN_PARENT_ID, originParent);
}
mOriginParent = originParent;
for (int i = 0; i < dataArray.length(); i++) {
JSONObject data = dataArray.getJSONObject(i);
SqlData sqlData = null;
if (data.has(DataColumns.ID)) {
long dataId = data.getLong(DataColumns.ID);
for (SqlData temp : mDataList) {
if (dataId == temp.getId()) {
sqlData = temp;
}
}
}
if (sqlData == null) {
sqlData = new SqlData(mContext);
mDataList.add(sqlData);
}
sqlData.setContent(data);
}
}
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
return false;
}
return true;
}
public JSONObject getContent() {
try {
JSONObject js = new JSONObject();
if (mIsCreate) {
Log.e(TAG, "it seems that we haven't created this in database yet");
return null;
}
JSONObject note = new JSONObject();
if (mType == Notes.TYPE_NOTE) {
note.put(NoteColumns.ID, mId);
note.put(NoteColumns.ALERTED_DATE, mAlertDate);
note.put(NoteColumns.BG_COLOR_ID, mBgColorId);
note.put(NoteColumns.CREATED_DATE, mCreatedDate);
note.put(NoteColumns.HAS_ATTACHMENT, mHasAttachment);
note.put(NoteColumns.MODIFIED_DATE, mModifiedDate);
note.put(NoteColumns.PARENT_ID, mParentId);
note.put(NoteColumns.SNIPPET, mSnippet);
note.put(NoteColumns.TYPE, mType);
note.put(NoteColumns.WIDGET_ID, mWidgetId);
note.put(NoteColumns.WIDGET_TYPE, mWidgetType);
note.put(NoteColumns.ORIGIN_PARENT_ID, mOriginParent);
js.put(GTaskStringUtils.META_HEAD_NOTE, note);
JSONArray dataArray = new JSONArray();
for (SqlData sqlData : mDataList) {
JSONObject data = sqlData.getContent();
if (data != null) {
dataArray.put(data);
}
}
js.put(GTaskStringUtils.META_HEAD_DATA, dataArray);
} else if (mType == Notes.TYPE_FOLDER || mType == Notes.TYPE_SYSTEM) {
note.put(NoteColumns.ID, mId);
note.put(NoteColumns.TYPE, mType);
note.put(NoteColumns.SNIPPET, mSnippet);
js.put(GTaskStringUtils.META_HEAD_NOTE, note);
}
return js;
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
}
return null;
}
public void setParentId(long id) {
mParentId = id;
mDiffNoteValues.put(NoteColumns.PARENT_ID, id);
}
public void setGtaskId(String gid) {
mDiffNoteValues.put(NoteColumns.GTASK_ID, gid);
}
public void setSyncId(long syncId) {
mDiffNoteValues.put(NoteColumns.SYNC_ID, syncId);
}
public void resetLocalModified() {
mDiffNoteValues.put(NoteColumns.LOCAL_MODIFIED, 0);
}
public long getId() {
return mId;
}
public long getParentId() {
return mParentId;
}
public String getSnippet() {
return mSnippet;
}
public boolean isNoteType() {
return mType == Notes.TYPE_NOTE;
}
public void commit(boolean validateVersion) {
if (mIsCreate) {
if (mId == INVALID_ID && mDiffNoteValues.containsKey(NoteColumns.ID)) {
mDiffNoteValues.remove(NoteColumns.ID);
}
Uri uri = mContentResolver.insert(Notes.CONTENT_NOTE_URI, mDiffNoteValues);
try {
mId = Long.valueOf(uri.getPathSegments().get(1));
} catch (NumberFormatException e) {
Log.e(TAG, "Get note id error :" + e.toString());
throw new ActionFailureException("create note failed");
}
if (mId == 0) {
throw new IllegalStateException("Create thread id failed");
}
if (mType == Notes.TYPE_NOTE) {
for (SqlData sqlData : mDataList) {
sqlData.commit(mId, false, -1);
}
}
} else {
if (mId <= 0 && mId != Notes.ID_ROOT_FOLDER && mId != Notes.ID_CALL_RECORD_FOLDER) {
Log.e(TAG, "No such note");
throw new IllegalStateException("Try to update note with invalid id");
}
if (mDiffNoteValues.size() > 0) {
mVersion ++;
int result = 0;
if (!validateVersion) {
result = mContentResolver.update(Notes.CONTENT_NOTE_URI, mDiffNoteValues, "("
+ NoteColumns.ID + "=?)", new String[] {
String.valueOf(mId)
});
} else {
result = mContentResolver.update(Notes.CONTENT_NOTE_URI, mDiffNoteValues, "("
+ NoteColumns.ID + "=?) AND (" + NoteColumns.VERSION + "<=?)",
new String[] {
String.valueOf(mId), String.valueOf(mVersion)
});
}
if (result == 0) {
Log.w(TAG, "there is no update. maybe user updates note when syncing");
}
}
if (mType == Notes.TYPE_NOTE) {
for (SqlData sqlData : mDataList) {
sqlData.commit(mId, validateVersion, mVersion);
}
}
}
// refresh local info
loadFromCursor(mId);
if (mType == Notes.TYPE_NOTE)
loadDataContent();
mDiffNoteValues.clear();
mIsCreate = false;
}
}

@ -0,0 +1,351 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.gtask.data;
import android.database.Cursor;
import android.text.TextUtils;
import android.util.Log;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.DataConstants;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.gtask.exception.ActionFailureException;
import net.micode.notes.tool.GTaskStringUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
public class Task extends Node {
private static final String TAG = Task.class.getSimpleName();
private boolean mCompleted;
private String mNotes;
private JSONObject mMetaInfo;
private Task mPriorSibling;
private TaskList mParent;
public Task() {
super();
mCompleted = false;
mNotes = null;
mPriorSibling = null;
mParent = null;
mMetaInfo = null;
}
public JSONObject getCreateAction(int actionId) {
JSONObject js = new JSONObject();
try {
// action_type
js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE);
// action_id
js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId);
// index
js.put(GTaskStringUtils.GTASK_JSON_INDEX, mParent.getChildTaskIndex(this));
// entity_delta
JSONObject entity = new JSONObject();
entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName());
entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null");
entity.put(GTaskStringUtils.GTASK_JSON_ENTITY_TYPE,
GTaskStringUtils.GTASK_JSON_TYPE_TASK);
if (getNotes() != null) {
entity.put(GTaskStringUtils.GTASK_JSON_NOTES, getNotes());
}
js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity);
// parent_id
js.put(GTaskStringUtils.GTASK_JSON_PARENT_ID, mParent.getGid());
// dest_parent_type
js.put(GTaskStringUtils.GTASK_JSON_DEST_PARENT_TYPE,
GTaskStringUtils.GTASK_JSON_TYPE_GROUP);
// list_id
js.put(GTaskStringUtils.GTASK_JSON_LIST_ID, mParent.getGid());
// prior_sibling_id
if (mPriorSibling != null) {
js.put(GTaskStringUtils.GTASK_JSON_PRIOR_SIBLING_ID, mPriorSibling.getGid());
}
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to generate task-create jsonobject");
}
return js;
}
public JSONObject getUpdateAction(int actionId) {
JSONObject js = new JSONObject();
try {
// action_type
js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE);
// action_id
js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId);
// id
js.put(GTaskStringUtils.GTASK_JSON_ID, getGid());
// entity_delta
JSONObject entity = new JSONObject();
entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName());
if (getNotes() != null) {
entity.put(GTaskStringUtils.GTASK_JSON_NOTES, getNotes());
}
entity.put(GTaskStringUtils.GTASK_JSON_DELETED, getDeleted());
js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity);
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to generate task-update jsonobject");
}
return js;
}
public void setContentByRemoteJSON(JSONObject js) {
if (js != null) {
try {
// id
if (js.has(GTaskStringUtils.GTASK_JSON_ID)) {
setGid(js.getString(GTaskStringUtils.GTASK_JSON_ID));
}
// last_modified
if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) {
setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED));
}
// name
if (js.has(GTaskStringUtils.GTASK_JSON_NAME)) {
setName(js.getString(GTaskStringUtils.GTASK_JSON_NAME));
}
// notes
if (js.has(GTaskStringUtils.GTASK_JSON_NOTES)) {
setNotes(js.getString(GTaskStringUtils.GTASK_JSON_NOTES));
}
// deleted
if (js.has(GTaskStringUtils.GTASK_JSON_DELETED)) {
setDeleted(js.getBoolean(GTaskStringUtils.GTASK_JSON_DELETED));
}
// completed
if (js.has(GTaskStringUtils.GTASK_JSON_COMPLETED)) {
setCompleted(js.getBoolean(GTaskStringUtils.GTASK_JSON_COMPLETED));
}
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to get task content from jsonobject");
}
}
}
public void setContentByLocalJSON(JSONObject js) {
if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE)
|| !js.has(GTaskStringUtils.META_HEAD_DATA)) {
Log.w(TAG, "setContentByLocalJSON: nothing is avaiable");
}
try {
JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA);
if (note.getInt(NoteColumns.TYPE) != Notes.TYPE_NOTE) {
Log.e(TAG, "invalid type");
return;
}
for (int i = 0; i < dataArray.length(); i++) {
JSONObject data = dataArray.getJSONObject(i);
if (TextUtils.equals(data.getString(DataColumns.MIME_TYPE), DataConstants.NOTE)) {
setName(data.getString(DataColumns.CONTENT));
break;
}
}
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
}
}
public JSONObject getLocalJSONFromContent() {
String name = getName();
try {
if (mMetaInfo == null) {
// new task created from web
if (name == null) {
Log.w(TAG, "the note seems to be an empty one");
return null;
}
JSONObject js = new JSONObject();
JSONObject note = new JSONObject();
JSONArray dataArray = new JSONArray();
JSONObject data = new JSONObject();
data.put(DataColumns.CONTENT, name);
dataArray.put(data);
js.put(GTaskStringUtils.META_HEAD_DATA, dataArray);
note.put(NoteColumns.TYPE, Notes.TYPE_NOTE);
js.put(GTaskStringUtils.META_HEAD_NOTE, note);
return js;
} else {
// synced task
JSONObject note = mMetaInfo.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
JSONArray dataArray = mMetaInfo.getJSONArray(GTaskStringUtils.META_HEAD_DATA);
for (int i = 0; i < dataArray.length(); i++) {
JSONObject data = dataArray.getJSONObject(i);
if (TextUtils.equals(data.getString(DataColumns.MIME_TYPE), DataConstants.NOTE)) {
data.put(DataColumns.CONTENT, getName());
break;
}
}
note.put(NoteColumns.TYPE, Notes.TYPE_NOTE);
return mMetaInfo;
}
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
return null;
}
}
public void setMetaInfo(MetaData metaData) {
if (metaData != null && metaData.getNotes() != null) {
try {
mMetaInfo = new JSONObject(metaData.getNotes());
} catch (JSONException e) {
Log.w(TAG, e.toString());
mMetaInfo = null;
}
}
}
public int getSyncAction(Cursor c) {
try {
JSONObject noteInfo = null;
if (mMetaInfo != null && mMetaInfo.has(GTaskStringUtils.META_HEAD_NOTE)) {
noteInfo = mMetaInfo.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
}
if (noteInfo == null) {
Log.w(TAG, "it seems that note meta has been deleted");
return SYNC_ACTION_UPDATE_REMOTE;
}
if (!noteInfo.has(NoteColumns.ID)) {
Log.w(TAG, "remote note id seems to be deleted");
return SYNC_ACTION_UPDATE_LOCAL;
}
// validate the note id now
if (c.getLong(SqlNote.ID_COLUMN) != noteInfo.getLong(NoteColumns.ID)) {
Log.w(TAG, "note id doesn't match");
return SYNC_ACTION_UPDATE_LOCAL;
}
if (c.getInt(SqlNote.LOCAL_MODIFIED_COLUMN) == 0) {
// there is no local update
if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) {
// no update both side
return SYNC_ACTION_NONE;
} else {
// apply remote to local
return SYNC_ACTION_UPDATE_LOCAL;
}
} else {
// validate gtask id
if (!c.getString(SqlNote.GTASK_ID_COLUMN).equals(getGid())) {
Log.e(TAG, "gtask id doesn't match");
return SYNC_ACTION_ERROR;
}
if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) {
// local modification only
return SYNC_ACTION_UPDATE_REMOTE;
} else {
return SYNC_ACTION_UPDATE_CONFLICT;
}
}
} catch (Exception e) {
Log.e(TAG, e.toString());
e.printStackTrace();
}
return SYNC_ACTION_ERROR;
}
public boolean isWorthSaving() {
return mMetaInfo != null || (getName() != null && getName().trim().length() > 0)
|| (getNotes() != null && getNotes().trim().length() > 0);
}
public void setCompleted(boolean completed) {
this.mCompleted = completed;
}
public void setNotes(String notes) {
this.mNotes = notes;
}
public void setPriorSibling(Task priorSibling) {
this.mPriorSibling = priorSibling;
}
public void setParent(TaskList parent) {
this.mParent = parent;
}
public boolean getCompleted() {
return this.mCompleted;
}
public String getNotes() {
return this.mNotes;
}
public Task getPriorSibling() {
return this.mPriorSibling;
}
public TaskList getParent() {
return this.mParent;
}
}

@ -0,0 +1,343 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.gtask.data;
import android.database.Cursor;
import android.util.Log;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.gtask.exception.ActionFailureException;
import net.micode.notes.tool.GTaskStringUtils;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
public class TaskList extends Node {
private static final String TAG = TaskList.class.getSimpleName();
private int mIndex;
private ArrayList<Task> mChildren;
public TaskList() {
super();
mChildren = new ArrayList<Task>();
mIndex = 1;
}
public JSONObject getCreateAction(int actionId) {
JSONObject js = new JSONObject();
try {
// action_type
js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE);
// action_id
js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId);
// index
js.put(GTaskStringUtils.GTASK_JSON_INDEX, mIndex);
// entity_delta
JSONObject entity = new JSONObject();
entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName());
entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null");
entity.put(GTaskStringUtils.GTASK_JSON_ENTITY_TYPE,
GTaskStringUtils.GTASK_JSON_TYPE_GROUP);
js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity);
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to generate tasklist-create jsonobject");
}
return js;
}
public JSONObject getUpdateAction(int actionId) {
JSONObject js = new JSONObject();
try {
// action_type
js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE);
// action_id
js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId);
// id
js.put(GTaskStringUtils.GTASK_JSON_ID, getGid());
// entity_delta
JSONObject entity = new JSONObject();
entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName());
entity.put(GTaskStringUtils.GTASK_JSON_DELETED, getDeleted());
js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity);
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to generate tasklist-update jsonobject");
}
return js;
}
public void setContentByRemoteJSON(JSONObject js) {
if (js != null) {
try {
// id
if (js.has(GTaskStringUtils.GTASK_JSON_ID)) {
setGid(js.getString(GTaskStringUtils.GTASK_JSON_ID));
}
// last_modified
if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) {
setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED));
}
// name
if (js.has(GTaskStringUtils.GTASK_JSON_NAME)) {
setName(js.getString(GTaskStringUtils.GTASK_JSON_NAME));
}
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to get tasklist content from jsonobject");
}
}
}
public void setContentByLocalJSON(JSONObject js) {
if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE)) {
Log.w(TAG, "setContentByLocalJSON: nothing is avaiable");
}
try {
JSONObject folder = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
if (folder.getInt(NoteColumns.TYPE) == Notes.TYPE_FOLDER) {
String name = folder.getString(NoteColumns.SNIPPET);
setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + name);
} else if (folder.getInt(NoteColumns.TYPE) == Notes.TYPE_SYSTEM) {
if (folder.getLong(NoteColumns.ID) == Notes.ID_ROOT_FOLDER)
setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT);
else if (folder.getLong(NoteColumns.ID) == Notes.ID_CALL_RECORD_FOLDER)
setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX
+ GTaskStringUtils.FOLDER_CALL_NOTE);
else
Log.e(TAG, "invalid system folder");
} else {
Log.e(TAG, "error type");
}
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
}
}
public JSONObject getLocalJSONFromContent() {
try {
JSONObject js = new JSONObject();
JSONObject folder = new JSONObject();
String folderName = getName();
if (getName().startsWith(GTaskStringUtils.MIUI_FOLDER_PREFFIX))
folderName = folderName.substring(GTaskStringUtils.MIUI_FOLDER_PREFFIX.length(),
folderName.length());
folder.put(NoteColumns.SNIPPET, folderName);
if (folderName.equals(GTaskStringUtils.FOLDER_DEFAULT)
|| folderName.equals(GTaskStringUtils.FOLDER_CALL_NOTE))
folder.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
else
folder.put(NoteColumns.TYPE, Notes.TYPE_FOLDER);
js.put(GTaskStringUtils.META_HEAD_NOTE, folder);
return js;
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
return null;
}
}
public int getSyncAction(Cursor c) {
try {
if (c.getInt(SqlNote.LOCAL_MODIFIED_COLUMN) == 0) {
// there is no local update
if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) {
// no update both side
return SYNC_ACTION_NONE;
} else {
// apply remote to local
return SYNC_ACTION_UPDATE_LOCAL;
}
} else {
// validate gtask id
if (!c.getString(SqlNote.GTASK_ID_COLUMN).equals(getGid())) {
Log.e(TAG, "gtask id doesn't match");
return SYNC_ACTION_ERROR;
}
if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) {
// local modification only
return SYNC_ACTION_UPDATE_REMOTE;
} else {
// for folder conflicts, just apply local modification
return SYNC_ACTION_UPDATE_REMOTE;
}
}
} catch (Exception e) {
Log.e(TAG, e.toString());
e.printStackTrace();
}
return SYNC_ACTION_ERROR;
}
public int getChildTaskCount() {
return mChildren.size();
}
public boolean addChildTask(Task task) {
boolean ret = false;
if (task != null && !mChildren.contains(task)) {
ret = mChildren.add(task);
if (ret) {
// need to set prior sibling and parent
task.setPriorSibling(mChildren.isEmpty() ? null : mChildren
.get(mChildren.size() - 1));
task.setParent(this);
}
}
return ret;
}
public boolean addChildTask(Task task, int index) {
if (index < 0 || index > mChildren.size()) {
Log.e(TAG, "add child task: invalid index");
return false;
}
int pos = mChildren.indexOf(task);
if (task != null && pos == -1) {
mChildren.add(index, task);
// update the task list
Task preTask = null;
Task afterTask = null;
if (index != 0)
preTask = mChildren.get(index - 1);
if (index != mChildren.size() - 1)
afterTask = mChildren.get(index + 1);
task.setPriorSibling(preTask);
if (afterTask != null)
afterTask.setPriorSibling(task);
}
return true;
}
public boolean removeChildTask(Task task) {
boolean ret = false;
int index = mChildren.indexOf(task);
if (index != -1) {
ret = mChildren.remove(task);
if (ret) {
// reset prior sibling and parent
task.setPriorSibling(null);
task.setParent(null);
// update the task list
if (index != mChildren.size()) {
mChildren.get(index).setPriorSibling(
index == 0 ? null : mChildren.get(index - 1));
}
}
}
return ret;
}
public boolean moveChildTask(Task task, int index) {
if (index < 0 || index >= mChildren.size()) {
Log.e(TAG, "move child task: invalid index");
return false;
}
int pos = mChildren.indexOf(task);
if (pos == -1) {
Log.e(TAG, "move child task: the task should in the list");
return false;
}
if (pos == index)
return true;
return (removeChildTask(task) && addChildTask(task, index));
}
public Task findChildTaskByGid(String gid) {
for (int i = 0; i < mChildren.size(); i++) {
Task t = mChildren.get(i);
if (t.getGid().equals(gid)) {
return t;
}
}
return null;
}
public int getChildTaskIndex(Task task) {
return mChildren.indexOf(task);
}
public Task getChildTaskByIndex(int index) {
if (index < 0 || index >= mChildren.size()) {
Log.e(TAG, "getTaskByIndex: invalid index");
return null;
}
return mChildren.get(index);
}
public Task getChilTaskByGid(String gid) {
for (Task task : mChildren) {
if (task.getGid().equals(gid))
return task;
}
return null;
}
public ArrayList<Task> getChildTaskList() {
return this.mChildren;
}
public void setIndex(int index) {
this.mIndex = index;
}
public int getIndex() {
return this.mIndex;
}
}

@ -0,0 +1,33 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.gtask.exception;
public class ActionFailureException extends RuntimeException {
private static final long serialVersionUID = 4425249765923293627L;
public ActionFailureException() {
super();
}
public ActionFailureException(String paramString) {
super(paramString);
}
public ActionFailureException(String paramString, Throwable paramThrowable) {
super(paramString, paramThrowable);
}
}

@ -0,0 +1,33 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.gtask.exception;
public class NetworkFailureException extends Exception {
private static final long serialVersionUID = 2107610287180234136L;
public NetworkFailureException() {
super();
}
public NetworkFailureException(String paramString) {
super(paramString);
}
public NetworkFailureException(String paramString, Throwable paramThrowable) {
super(paramString, paramThrowable);
}
}

@ -0,0 +1,151 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.gtask.remote;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Build;
import androidx.core.app.NotificationCompat;
import net.micode.notes.R;
import net.micode.notes.ui.NotesListActivity;
import net.micode.notes.ui.NotesPreferenceActivity;
public class GTaskASyncTask extends AsyncTask<Void, String, Integer> {
private static int GTASK_SYNC_NOTIFICATION_ID = 5234235;
private static final String CHANNEL_ID = "gtask_sync_channel"; // 新增通知渠道ID
public interface OnCompleteListener {
void onComplete();
}
private Context mContext;
private NotificationManager mNotifiManager;
private GTaskManager mTaskManager;
private OnCompleteListener mOnCompleteListener;
public GTaskASyncTask(Context context, OnCompleteListener listener) {
mContext = context;
mOnCompleteListener = listener;
mNotifiManager = (NotificationManager) mContext
.getSystemService(Context.NOTIFICATION_SERVICE);
mTaskManager = GTaskManager.getInstance();
// 初始化通知渠道仅Android 8.0+需要)
createNotificationChannel();
}
// 新增:创建通知渠道
private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
CharSequence name = mContext.getString(R.string.app_name); // 渠道名称
String description = "GTask同步通知"; // 渠道描述
int importance = NotificationManager.IMPORTANCE_DEFAULT;
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance);
channel.setDescription(description);
// 向系统注册渠道
if (mNotifiManager != null) {
mNotifiManager.createNotificationChannel(channel);
}
}
}
public void cancelSync() {
mTaskManager.cancelSync();
}
public void publishProgess(String message) {
publishProgress(new String[] {
message
});
}
private void showNotification(int tickerId, String content) {
// 替换为NotificationCompat.Builder构建通知
NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext, CHANNEL_ID)
.setSmallIcon(R.drawable.notification) // 保持原图标
.setContentTitle(mContext.getString(R.string.app_name)) // 原标题
.setContentText(content) // 原内容
.setTicker(mContext.getString(tickerId)) // 原滚动提示文字
.setWhen(System.currentTimeMillis()) // 原时间戳
.setDefaults(Notification.DEFAULT_LIGHTS) // 保持原灯光效果
.setAutoCancel(true); // 保持点击自动取消
// 设置跳转意图(保持原逻辑)
PendingIntent pendingIntent;
if (tickerId != R.string.ticker_success) {
pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext,
NotesPreferenceActivity.class), PendingIntent.FLAG_IMMUTABLE); // 适配高版本
} else {
pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext,
NotesListActivity.class), PendingIntent.FLAG_IMMUTABLE); // 适配高版本
}
builder.setContentIntent(pendingIntent);
// 发送通知保持原ID
mNotifiManager.notify(GTASK_SYNC_NOTIFICATION_ID, builder.build());
}
@Override
protected Integer doInBackground(Void... unused) {
publishProgess(mContext.getString(R.string.sync_progress_login, NotesPreferenceActivity
.getSyncAccountName(mContext)));
return mTaskManager.sync(mContext, this);
}
@Override
protected void onProgressUpdate(String... progress) {
showNotification(R.string.ticker_syncing, progress[0]);
if (mContext instanceof GTaskSyncService) {
((GTaskSyncService) mContext).sendBroadcast(progress[0]);
}
}
@Override
protected void onPostExecute(Integer result) {
if (result == GTaskManager.STATE_SUCCESS) {
showNotification(R.string.ticker_success, mContext.getString(
R.string.success_sync_account, mTaskManager.getSyncAccount()));
NotesPreferenceActivity.setLastSyncTime(mContext, System.currentTimeMillis());
} else if (result == GTaskManager.STATE_NETWORK_ERROR) {
showNotification(R.string.ticker_fail, mContext.getString(R.string.error_sync_network));
} else if (result == GTaskManager.STATE_INTERNAL_ERROR) {
showNotification(R.string.ticker_fail, mContext.getString(R.string.error_sync_internal));
} else if (result == GTaskManager.STATE_SYNC_CANCELLED) {
showNotification(R.string.ticker_cancel, mContext
.getString(R.string.error_sync_cancelled));
}
if (mOnCompleteListener != null) {
new Thread(new Runnable() {
public void run() {
mOnCompleteListener.onComplete();
}
}).start();
}
}
}

@ -0,0 +1,585 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.gtask.remote;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerFuture;
import android.app.Activity;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import net.micode.notes.gtask.data.Node;
import net.micode.notes.gtask.data.Task;
import net.micode.notes.gtask.data.TaskList;
import net.micode.notes.gtask.exception.ActionFailureException;
import net.micode.notes.gtask.exception.NetworkFailureException;
import net.micode.notes.tool.GTaskStringUtils;
import net.micode.notes.ui.NotesPreferenceActivity;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.cookie.Cookie;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.LinkedList;
import java.util.List;
import java.util.zip.GZIPInputStream;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
public class GTaskClient {
private static final String TAG = GTaskClient.class.getSimpleName();
private static final String GTASK_URL = "https://mail.google.com/tasks/";
private static final String GTASK_GET_URL = "https://mail.google.com/tasks/ig";
private static final String GTASK_POST_URL = "https://mail.google.com/tasks/r/ig";
private static GTaskClient mInstance = null;
private DefaultHttpClient mHttpClient;
private String mGetUrl;
private String mPostUrl;
private long mClientVersion;
private boolean mLoggedin;
private long mLastLoginTime;
private int mActionId;
private Account mAccount;
private JSONArray mUpdateArray;
private GTaskClient() {
mHttpClient = null;
mGetUrl = GTASK_GET_URL;
mPostUrl = GTASK_POST_URL;
mClientVersion = -1;
mLoggedin = false;
mLastLoginTime = 0;
mActionId = 1;
mAccount = null;
mUpdateArray = null;
}
public static synchronized GTaskClient getInstance() {
if (mInstance == null) {
mInstance = new GTaskClient();
}
return mInstance;
}
public boolean login(Activity activity) {
// we suppose that the cookie would expire after 5 minutes
// then we need to re-login
final long interval = 1000 * 60 * 5;
if (mLastLoginTime + interval < System.currentTimeMillis()) {
mLoggedin = false;
}
// need to re-login after account switch
if (mLoggedin
&& !TextUtils.equals(getSyncAccount().name, NotesPreferenceActivity
.getSyncAccountName(activity))) {
mLoggedin = false;
}
if (mLoggedin) {
Log.d(TAG, "already logged in");
return true;
}
mLastLoginTime = System.currentTimeMillis();
String authToken = loginGoogleAccount(activity, false);
if (authToken == null) {
Log.e(TAG, "login google account failed");
return false;
}
// login with custom domain if necessary
if (!(mAccount.name.toLowerCase().endsWith("gmail.com") || mAccount.name.toLowerCase()
.endsWith("googlemail.com"))) {
StringBuilder url = new StringBuilder(GTASK_URL).append("a/");
int index = mAccount.name.indexOf('@') + 1;
String suffix = mAccount.name.substring(index);
url.append(suffix + "/");
mGetUrl = url.toString() + "ig";
mPostUrl = url.toString() + "r/ig";
if (tryToLoginGtask(activity, authToken)) {
mLoggedin = true;
}
}
// try to login with google official url
if (!mLoggedin) {
mGetUrl = GTASK_GET_URL;
mPostUrl = GTASK_POST_URL;
if (!tryToLoginGtask(activity, authToken)) {
return false;
}
}
mLoggedin = true;
return true;
}
private String loginGoogleAccount(Activity activity, boolean invalidateToken) {
String authToken;
AccountManager accountManager = AccountManager.get(activity);
Account[] accounts = accountManager.getAccountsByType("com.google");
if (accounts.length == 0) {
Log.e(TAG, "there is no available google account");
return null;
}
String accountName = NotesPreferenceActivity.getSyncAccountName(activity);
Account account = null;
for (Account a : accounts) {
if (a.name.equals(accountName)) {
account = a;
break;
}
}
if (account != null) {
mAccount = account;
} else {
Log.e(TAG, "unable to get an account with the same name in the settings");
return null;
}
// get the token now
AccountManagerFuture<Bundle> accountManagerFuture = accountManager.getAuthToken(account,
"goanna_mobile", null, activity, null, null);
try {
Bundle authTokenBundle = accountManagerFuture.getResult();
authToken = authTokenBundle.getString(AccountManager.KEY_AUTHTOKEN);
if (invalidateToken) {
accountManager.invalidateAuthToken("com.google", authToken);
loginGoogleAccount(activity, false);
}
} catch (Exception e) {
Log.e(TAG, "get auth token failed");
authToken = null;
}
return authToken;
}
private boolean tryToLoginGtask(Activity activity, String authToken) {
if (!loginGtask(authToken)) {
// maybe the auth token is out of date, now let's invalidate the
// token and try again
authToken = loginGoogleAccount(activity, true);
if (authToken == null) {
Log.e(TAG, "login google account failed");
return false;
}
if (!loginGtask(authToken)) {
Log.e(TAG, "login gtask failed");
return false;
}
}
return true;
}
private boolean loginGtask(String authToken) {
int timeoutConnection = 10000;
int timeoutSocket = 15000;
HttpParams httpParameters = new BasicHttpParams();
HttpConnectionParams.setConnectionTimeout(httpParameters, timeoutConnection);
HttpConnectionParams.setSoTimeout(httpParameters, timeoutSocket);
mHttpClient = new DefaultHttpClient(httpParameters);
BasicCookieStore localBasicCookieStore = new BasicCookieStore();
mHttpClient.setCookieStore(localBasicCookieStore);
HttpProtocolParams.setUseExpectContinue(mHttpClient.getParams(), false);
// login gtask
try {
String loginUrl = mGetUrl + "?auth=" + authToken;
HttpGet httpGet = new HttpGet(loginUrl);
HttpResponse response = null;
response = mHttpClient.execute(httpGet);
// get the cookie now
List<Cookie> cookies = mHttpClient.getCookieStore().getCookies();
boolean hasAuthCookie = false;
for (Cookie cookie : cookies) {
if (cookie.getName().contains("GTL")) {
hasAuthCookie = true;
}
}
if (!hasAuthCookie) {
Log.w(TAG, "it seems that there is no auth cookie");
}
// get the client version
String resString = getResponseContent(response.getEntity());
String jsBegin = "_setup(";
String jsEnd = ")}</script>";
int begin = resString.indexOf(jsBegin);
int end = resString.lastIndexOf(jsEnd);
String jsString = null;
if (begin != -1 && end != -1 && begin < end) {
jsString = resString.substring(begin + jsBegin.length(), end);
}
JSONObject js = new JSONObject(jsString);
mClientVersion = js.getLong("v");
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
return false;
} catch (Exception e) {
// simply catch all exceptions
Log.e(TAG, "httpget gtask_url failed");
return false;
}
return true;
}
private int getActionId() {
return mActionId++;
}
private HttpPost createHttpPost() {
HttpPost httpPost = new HttpPost(mPostUrl);
httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8");
httpPost.setHeader("AT", "1");
return httpPost;
}
private String getResponseContent(HttpEntity entity) throws IOException {
String contentEncoding = null;
if (entity.getContentEncoding() != null) {
contentEncoding = entity.getContentEncoding().getValue();
Log.d(TAG, "encoding: " + contentEncoding);
}
InputStream input = entity.getContent();
if (contentEncoding != null && contentEncoding.equalsIgnoreCase("gzip")) {
input = new GZIPInputStream(entity.getContent());
} else if (contentEncoding != null && contentEncoding.equalsIgnoreCase("deflate")) {
Inflater inflater = new Inflater(true);
input = new InflaterInputStream(entity.getContent(), inflater);
}
try {
InputStreamReader isr = new InputStreamReader(input);
BufferedReader br = new BufferedReader(isr);
StringBuilder sb = new StringBuilder();
while (true) {
String buff = br.readLine();
if (buff == null) {
return sb.toString();
}
sb = sb.append(buff);
}
} finally {
input.close();
}
}
private JSONObject postRequest(JSONObject js) throws NetworkFailureException {
if (!mLoggedin) {
Log.e(TAG, "please login first");
throw new ActionFailureException("not logged in");
}
HttpPost httpPost = createHttpPost();
try {
LinkedList<BasicNameValuePair> list = new LinkedList<BasicNameValuePair>();
list.add(new BasicNameValuePair("r", js.toString()));
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(list, "UTF-8");
httpPost.setEntity(entity);
// execute the post
HttpResponse response = mHttpClient.execute(httpPost);
String jsString = getResponseContent(response.getEntity());
return new JSONObject(jsString);
} catch (ClientProtocolException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new NetworkFailureException("postRequest failed");
} catch (IOException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new NetworkFailureException("postRequest failed");
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("unable to convert response content to jsonobject");
} catch (Exception e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("error occurs when posting request");
}
}
public void createTask(Task task) throws NetworkFailureException {
commitUpdate();
try {
JSONObject jsPost = new JSONObject();
JSONArray actionList = new JSONArray();
// action_list
actionList.put(task.getCreateAction(getActionId()));
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
// client_version
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
// post
JSONObject jsResponse = postRequest(jsPost);
JSONObject jsResult = (JSONObject) jsResponse.getJSONArray(
GTaskStringUtils.GTASK_JSON_RESULTS).get(0);
task.setGid(jsResult.getString(GTaskStringUtils.GTASK_JSON_NEW_ID));
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("create task: handing jsonobject failed");
}
}
public void createTaskList(TaskList tasklist) throws NetworkFailureException {
commitUpdate();
try {
JSONObject jsPost = new JSONObject();
JSONArray actionList = new JSONArray();
// action_list
actionList.put(tasklist.getCreateAction(getActionId()));
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
// client version
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
// post
JSONObject jsResponse = postRequest(jsPost);
JSONObject jsResult = (JSONObject) jsResponse.getJSONArray(
GTaskStringUtils.GTASK_JSON_RESULTS).get(0);
tasklist.setGid(jsResult.getString(GTaskStringUtils.GTASK_JSON_NEW_ID));
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("create tasklist: handing jsonobject failed");
}
}
public void commitUpdate() throws NetworkFailureException {
if (mUpdateArray != null) {
try {
JSONObject jsPost = new JSONObject();
// action_list
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, mUpdateArray);
// client_version
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
postRequest(jsPost);
mUpdateArray = null;
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("commit update: handing jsonobject failed");
}
}
}
public void addUpdateNode(Node node) throws NetworkFailureException {
if (node != null) {
// too many update items may result in an error
// set max to 10 items
if (mUpdateArray != null && mUpdateArray.length() > 10) {
commitUpdate();
}
if (mUpdateArray == null)
mUpdateArray = new JSONArray();
mUpdateArray.put(node.getUpdateAction(getActionId()));
}
}
public void moveTask(Task task, TaskList preParent, TaskList curParent)
throws NetworkFailureException {
commitUpdate();
try {
JSONObject jsPost = new JSONObject();
JSONArray actionList = new JSONArray();
JSONObject action = new JSONObject();
// action_list
action.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_MOVE);
action.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, getActionId());
action.put(GTaskStringUtils.GTASK_JSON_ID, task.getGid());
if (preParent == curParent && task.getPriorSibling() != null) {
// put prioring_sibing_id only if moving within the tasklist and
// it is not the first one
action.put(GTaskStringUtils.GTASK_JSON_PRIOR_SIBLING_ID, task.getPriorSibling());
}
action.put(GTaskStringUtils.GTASK_JSON_SOURCE_LIST, preParent.getGid());
action.put(GTaskStringUtils.GTASK_JSON_DEST_PARENT, curParent.getGid());
if (preParent != curParent) {
// put the dest_list only if moving between tasklists
action.put(GTaskStringUtils.GTASK_JSON_DEST_LIST, curParent.getGid());
}
actionList.put(action);
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
// client_version
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
postRequest(jsPost);
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("move task: handing jsonobject failed");
}
}
public void deleteNode(Node node) throws NetworkFailureException {
commitUpdate();
try {
JSONObject jsPost = new JSONObject();
JSONArray actionList = new JSONArray();
// action_list
node.setDeleted(true);
actionList.put(node.getUpdateAction(getActionId()));
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
// client_version
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
postRequest(jsPost);
mUpdateArray = null;
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("delete node: handing jsonobject failed");
}
}
public JSONArray getTaskLists() throws NetworkFailureException {
if (!mLoggedin) {
Log.e(TAG, "please login first");
throw new ActionFailureException("not logged in");
}
try {
HttpGet httpGet = new HttpGet(mGetUrl);
HttpResponse response = null;
response = mHttpClient.execute(httpGet);
// get the task list
String resString = getResponseContent(response.getEntity());
String jsBegin = "_setup(";
String jsEnd = ")}</script>";
int begin = resString.indexOf(jsBegin);
int end = resString.lastIndexOf(jsEnd);
String jsString = null;
if (begin != -1 && end != -1 && begin < end) {
jsString = resString.substring(begin + jsBegin.length(), end);
}
JSONObject js = new JSONObject(jsString);
return js.getJSONObject("t").getJSONArray(GTaskStringUtils.GTASK_JSON_LISTS);
} catch (ClientProtocolException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new NetworkFailureException("gettasklists: httpget failed");
} catch (IOException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new NetworkFailureException("gettasklists: httpget failed");
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("get task lists: handing jasonobject failed");
}
}
public JSONArray getTaskList(String listGid) throws NetworkFailureException {
commitUpdate();
try {
JSONObject jsPost = new JSONObject();
JSONArray actionList = new JSONArray();
JSONObject action = new JSONObject();
// action_list
action.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_GETALL);
action.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, getActionId());
action.put(GTaskStringUtils.GTASK_JSON_LIST_ID, listGid);
action.put(GTaskStringUtils.GTASK_JSON_GET_DELETED, false);
actionList.put(action);
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
// client_version
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
JSONObject jsResponse = postRequest(jsPost);
return jsResponse.getJSONArray(GTaskStringUtils.GTASK_JSON_TASKS);
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("get task list: handing jsonobject failed");
}
}
public Account getSyncAccount() {
return mAccount;
}
public void resetUpdateArray() {
mUpdateArray = null;
}
}

@ -0,0 +1,800 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.gtask.remote;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.util.Log;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.gtask.data.MetaData;
import net.micode.notes.gtask.data.Node;
import net.micode.notes.gtask.data.SqlNote;
import net.micode.notes.gtask.data.Task;
import net.micode.notes.gtask.data.TaskList;
import net.micode.notes.gtask.exception.ActionFailureException;
import net.micode.notes.gtask.exception.NetworkFailureException;
import net.micode.notes.tool.DataUtils;
import net.micode.notes.tool.GTaskStringUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
public class GTaskManager {
private static final String TAG = GTaskManager.class.getSimpleName();
public static final int STATE_SUCCESS = 0;
public static final int STATE_NETWORK_ERROR = 1;
public static final int STATE_INTERNAL_ERROR = 2;
public static final int STATE_SYNC_IN_PROGRESS = 3;
public static final int STATE_SYNC_CANCELLED = 4;
private static GTaskManager mInstance = null;
private Activity mActivity;
private Context mContext;
private ContentResolver mContentResolver;
private boolean mSyncing;
private boolean mCancelled;
private HashMap<String, TaskList> mGTaskListHashMap;
private HashMap<String, Node> mGTaskHashMap;
private HashMap<String, MetaData> mMetaHashMap;
private TaskList mMetaList;
private HashSet<Long> mLocalDeleteIdMap;
private HashMap<String, Long> mGidToNid;
private HashMap<Long, String> mNidToGid;
private GTaskManager() {
mSyncing = false;
mCancelled = false;
mGTaskListHashMap = new HashMap<String, TaskList>();
mGTaskHashMap = new HashMap<String, Node>();
mMetaHashMap = new HashMap<String, MetaData>();
mMetaList = null;
mLocalDeleteIdMap = new HashSet<Long>();
mGidToNid = new HashMap<String, Long>();
mNidToGid = new HashMap<Long, String>();
}
public static synchronized GTaskManager getInstance() {
if (mInstance == null) {
mInstance = new GTaskManager();
}
return mInstance;
}
public synchronized void setActivityContext(Activity activity) {
// used for getting authtoken
mActivity = activity;
}
public int sync(Context context, GTaskASyncTask asyncTask) {
if (mSyncing) {
Log.d(TAG, "Sync is in progress");
return STATE_SYNC_IN_PROGRESS;
}
mContext = context;
mContentResolver = mContext.getContentResolver();
mSyncing = true;
mCancelled = false;
mGTaskListHashMap.clear();
mGTaskHashMap.clear();
mMetaHashMap.clear();
mLocalDeleteIdMap.clear();
mGidToNid.clear();
mNidToGid.clear();
try {
GTaskClient client = GTaskClient.getInstance();
client.resetUpdateArray();
// login google task
if (!mCancelled) {
if (!client.login(mActivity)) {
throw new NetworkFailureException("login google task failed");
}
}
// get the task list from google
asyncTask.publishProgess(mContext.getString(R.string.sync_progress_init_list));
initGTaskList();
// do content sync work
asyncTask.publishProgess(mContext.getString(R.string.sync_progress_syncing));
syncContent();
} catch (NetworkFailureException e) {
Log.e(TAG, e.toString());
return STATE_NETWORK_ERROR;
} catch (ActionFailureException e) {
Log.e(TAG, e.toString());
return STATE_INTERNAL_ERROR;
} catch (Exception e) {
Log.e(TAG, e.toString());
e.printStackTrace();
return STATE_INTERNAL_ERROR;
} finally {
mGTaskListHashMap.clear();
mGTaskHashMap.clear();
mMetaHashMap.clear();
mLocalDeleteIdMap.clear();
mGidToNid.clear();
mNidToGid.clear();
mSyncing = false;
}
return mCancelled ? STATE_SYNC_CANCELLED : STATE_SUCCESS;
}
private void initGTaskList() throws NetworkFailureException {
if (mCancelled)
return;
GTaskClient client = GTaskClient.getInstance();
try {
JSONArray jsTaskLists = client.getTaskLists();
// init meta list first
mMetaList = null;
for (int i = 0; i < jsTaskLists.length(); i++) {
JSONObject object = jsTaskLists.getJSONObject(i);
String gid = object.getString(GTaskStringUtils.GTASK_JSON_ID);
String name = object.getString(GTaskStringUtils.GTASK_JSON_NAME);
if (name
.equals(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_META)) {
mMetaList = new TaskList();
mMetaList.setContentByRemoteJSON(object);
// load meta data
JSONArray jsMetas = client.getTaskList(gid);
for (int j = 0; j < jsMetas.length(); j++) {
object = (JSONObject) jsMetas.getJSONObject(j);
MetaData metaData = new MetaData();
metaData.setContentByRemoteJSON(object);
if (metaData.isWorthSaving()) {
mMetaList.addChildTask(metaData);
if (metaData.getGid() != null) {
mMetaHashMap.put(metaData.getRelatedGid(), metaData);
}
}
}
}
}
// create meta list if not existed
if (mMetaList == null) {
mMetaList = new TaskList();
mMetaList.setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX
+ GTaskStringUtils.FOLDER_META);
GTaskClient.getInstance().createTaskList(mMetaList);
}
// init task list
for (int i = 0; i < jsTaskLists.length(); i++) {
JSONObject object = jsTaskLists.getJSONObject(i);
String gid = object.getString(GTaskStringUtils.GTASK_JSON_ID);
String name = object.getString(GTaskStringUtils.GTASK_JSON_NAME);
if (name.startsWith(GTaskStringUtils.MIUI_FOLDER_PREFFIX)
&& !name.equals(GTaskStringUtils.MIUI_FOLDER_PREFFIX
+ GTaskStringUtils.FOLDER_META)) {
TaskList tasklist = new TaskList();
tasklist.setContentByRemoteJSON(object);
mGTaskListHashMap.put(gid, tasklist);
mGTaskHashMap.put(gid, tasklist);
// load tasks
JSONArray jsTasks = client.getTaskList(gid);
for (int j = 0; j < jsTasks.length(); j++) {
object = (JSONObject) jsTasks.getJSONObject(j);
gid = object.getString(GTaskStringUtils.GTASK_JSON_ID);
Task task = new Task();
task.setContentByRemoteJSON(object);
if (task.isWorthSaving()) {
task.setMetaInfo(mMetaHashMap.get(gid));
tasklist.addChildTask(task);
mGTaskHashMap.put(gid, task);
}
}
}
}
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("initGTaskList: handing JSONObject failed");
}
}
private void syncContent() throws NetworkFailureException {
int syncType;
Cursor c = null;
String gid;
Node node;
mLocalDeleteIdMap.clear();
if (mCancelled) {
return;
}
// for local deleted note
try {
c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE,
"(type<>? AND parent_id=?)", new String[] {
String.valueOf(Notes.TYPE_SYSTEM), String.valueOf(Notes.ID_TRASH_FOLER)
}, null);
if (c != null) {
while (c.moveToNext()) {
gid = c.getString(SqlNote.GTASK_ID_COLUMN);
node = mGTaskHashMap.get(gid);
if (node != null) {
mGTaskHashMap.remove(gid);
doContentSync(Node.SYNC_ACTION_DEL_REMOTE, node, c);
}
mLocalDeleteIdMap.add(c.getLong(SqlNote.ID_COLUMN));
}
} else {
Log.w(TAG, "failed to query trash folder");
}
} finally {
if (c != null) {
c.close();
c = null;
}
}
// sync folder first
syncFolder();
// for note existing in database
try {
c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE,
"(type=? AND parent_id<>?)", new String[] {
String.valueOf(Notes.TYPE_NOTE), String.valueOf(Notes.ID_TRASH_FOLER)
}, NoteColumns.TYPE + " DESC");
if (c != null) {
while (c.moveToNext()) {
gid = c.getString(SqlNote.GTASK_ID_COLUMN);
node = mGTaskHashMap.get(gid);
if (node != null) {
mGTaskHashMap.remove(gid);
mGidToNid.put(gid, c.getLong(SqlNote.ID_COLUMN));
mNidToGid.put(c.getLong(SqlNote.ID_COLUMN), gid);
syncType = node.getSyncAction(c);
} else {
if (c.getString(SqlNote.GTASK_ID_COLUMN).trim().length() == 0) {
// local add
syncType = Node.SYNC_ACTION_ADD_REMOTE;
} else {
// remote delete
syncType = Node.SYNC_ACTION_DEL_LOCAL;
}
}
doContentSync(syncType, node, c);
}
} else {
Log.w(TAG, "failed to query existing note in database");
}
} finally {
if (c != null) {
c.close();
c = null;
}
}
// go through remaining items
Iterator<Map.Entry<String, Node>> iter = mGTaskHashMap.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry<String, Node> entry = iter.next();
node = entry.getValue();
doContentSync(Node.SYNC_ACTION_ADD_LOCAL, node, null);
}
// mCancelled can be set by another thread, so we neet to check one by
// one
// clear local delete table
if (!mCancelled) {
if (!DataUtils.batchDeleteNotes(mContentResolver, mLocalDeleteIdMap)) {
throw new ActionFailureException("failed to batch-delete local deleted notes");
}
}
// refresh local sync id
if (!mCancelled) {
GTaskClient.getInstance().commitUpdate();
refreshLocalSyncId();
}
}
private void syncFolder() throws NetworkFailureException {
Cursor c = null;
String gid;
Node node;
int syncType;
if (mCancelled) {
return;
}
// for root folder
try {
c = mContentResolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI,
Notes.ID_ROOT_FOLDER), SqlNote.PROJECTION_NOTE, null, null, null);
if (c != null) {
c.moveToNext();
gid = c.getString(SqlNote.GTASK_ID_COLUMN);
node = mGTaskHashMap.get(gid);
if (node != null) {
mGTaskHashMap.remove(gid);
mGidToNid.put(gid, (long) Notes.ID_ROOT_FOLDER);
mNidToGid.put((long) Notes.ID_ROOT_FOLDER, gid);
// for system folder, only update remote name if necessary
if (!node.getName().equals(
GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT))
doContentSync(Node.SYNC_ACTION_UPDATE_REMOTE, node, c);
} else {
doContentSync(Node.SYNC_ACTION_ADD_REMOTE, node, c);
}
} else {
Log.w(TAG, "failed to query root folder");
}
} finally {
if (c != null) {
c.close();
c = null;
}
}
// for call-note folder
try {
c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, "(_id=?)",
new String[] {
String.valueOf(Notes.ID_CALL_RECORD_FOLDER)
}, null);
if (c != null) {
if (c.moveToNext()) {
gid = c.getString(SqlNote.GTASK_ID_COLUMN);
node = mGTaskHashMap.get(gid);
if (node != null) {
mGTaskHashMap.remove(gid);
mGidToNid.put(gid, (long) Notes.ID_CALL_RECORD_FOLDER);
mNidToGid.put((long) Notes.ID_CALL_RECORD_FOLDER, gid);
// for system folder, only update remote name if
// necessary
if (!node.getName().equals(
GTaskStringUtils.MIUI_FOLDER_PREFFIX
+ GTaskStringUtils.FOLDER_CALL_NOTE))
doContentSync(Node.SYNC_ACTION_UPDATE_REMOTE, node, c);
} else {
doContentSync(Node.SYNC_ACTION_ADD_REMOTE, node, c);
}
}
} else {
Log.w(TAG, "failed to query call note folder");
}
} finally {
if (c != null) {
c.close();
c = null;
}
}
// for local existing folders
try {
c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE,
"(type=? AND parent_id<>?)", new String[] {
String.valueOf(Notes.TYPE_FOLDER), String.valueOf(Notes.ID_TRASH_FOLER)
}, NoteColumns.TYPE + " DESC");
if (c != null) {
while (c.moveToNext()) {
gid = c.getString(SqlNote.GTASK_ID_COLUMN);
node = mGTaskHashMap.get(gid);
if (node != null) {
mGTaskHashMap.remove(gid);
mGidToNid.put(gid, c.getLong(SqlNote.ID_COLUMN));
mNidToGid.put(c.getLong(SqlNote.ID_COLUMN), gid);
syncType = node.getSyncAction(c);
} else {
if (c.getString(SqlNote.GTASK_ID_COLUMN).trim().length() == 0) {
// local add
syncType = Node.SYNC_ACTION_ADD_REMOTE;
} else {
// remote delete
syncType = Node.SYNC_ACTION_DEL_LOCAL;
}
}
doContentSync(syncType, node, c);
}
} else {
Log.w(TAG, "failed to query existing folder");
}
} finally {
if (c != null) {
c.close();
c = null;
}
}
// for remote add folders
Iterator<Map.Entry<String, TaskList>> iter = mGTaskListHashMap.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry<String, TaskList> entry = iter.next();
gid = entry.getKey();
node = entry.getValue();
if (mGTaskHashMap.containsKey(gid)) {
mGTaskHashMap.remove(gid);
doContentSync(Node.SYNC_ACTION_ADD_LOCAL, node, null);
}
}
if (!mCancelled)
GTaskClient.getInstance().commitUpdate();
}
private void doContentSync(int syncType, Node node, Cursor c) throws NetworkFailureException {
if (mCancelled) {
return;
}
MetaData meta;
switch (syncType) {
case Node.SYNC_ACTION_ADD_LOCAL:
addLocalNode(node);
break;
case Node.SYNC_ACTION_ADD_REMOTE:
addRemoteNode(node, c);
break;
case Node.SYNC_ACTION_DEL_LOCAL:
meta = mMetaHashMap.get(c.getString(SqlNote.GTASK_ID_COLUMN));
if (meta != null) {
GTaskClient.getInstance().deleteNode(meta);
}
mLocalDeleteIdMap.add(c.getLong(SqlNote.ID_COLUMN));
break;
case Node.SYNC_ACTION_DEL_REMOTE:
meta = mMetaHashMap.get(node.getGid());
if (meta != null) {
GTaskClient.getInstance().deleteNode(meta);
}
GTaskClient.getInstance().deleteNode(node);
break;
case Node.SYNC_ACTION_UPDATE_LOCAL:
updateLocalNode(node, c);
break;
case Node.SYNC_ACTION_UPDATE_REMOTE:
updateRemoteNode(node, c);
break;
case Node.SYNC_ACTION_UPDATE_CONFLICT:
// merging both modifications maybe a good idea
// right now just use local update simply
updateRemoteNode(node, c);
break;
case Node.SYNC_ACTION_NONE:
break;
case Node.SYNC_ACTION_ERROR:
default:
throw new ActionFailureException("unkown sync action type");
}
}
private void addLocalNode(Node node) throws NetworkFailureException {
if (mCancelled) {
return;
}
SqlNote sqlNote;
if (node instanceof TaskList) {
if (node.getName().equals(
GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT)) {
sqlNote = new SqlNote(mContext, Notes.ID_ROOT_FOLDER);
} else if (node.getName().equals(
GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_CALL_NOTE)) {
sqlNote = new SqlNote(mContext, Notes.ID_CALL_RECORD_FOLDER);
} else {
sqlNote = new SqlNote(mContext);
sqlNote.setContent(node.getLocalJSONFromContent());
sqlNote.setParentId(Notes.ID_ROOT_FOLDER);
}
} else {
sqlNote = new SqlNote(mContext);
JSONObject js = node.getLocalJSONFromContent();
try {
if (js.has(GTaskStringUtils.META_HEAD_NOTE)) {
JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
if (note.has(NoteColumns.ID)) {
long id = note.getLong(NoteColumns.ID);
if (DataUtils.existInNoteDatabase(mContentResolver, id)) {
// the id is not available, have to create a new one
note.remove(NoteColumns.ID);
}
}
}
if (js.has(GTaskStringUtils.META_HEAD_DATA)) {
JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA);
for (int i = 0; i < dataArray.length(); i++) {
JSONObject data = dataArray.getJSONObject(i);
if (data.has(DataColumns.ID)) {
long dataId = data.getLong(DataColumns.ID);
if (DataUtils.existInDataDatabase(mContentResolver, dataId)) {
// the data id is not available, have to create
// a new one
data.remove(DataColumns.ID);
}
}
}
}
} catch (JSONException e) {
Log.w(TAG, e.toString());
e.printStackTrace();
}
sqlNote.setContent(js);
Long parentId = mGidToNid.get(((Task) node).getParent().getGid());
if (parentId == null) {
Log.e(TAG, "cannot find task's parent id locally");
throw new ActionFailureException("cannot add local node");
}
sqlNote.setParentId(parentId.longValue());
}
// create the local node
sqlNote.setGtaskId(node.getGid());
sqlNote.commit(false);
// update gid-nid mapping
mGidToNid.put(node.getGid(), sqlNote.getId());
mNidToGid.put(sqlNote.getId(), node.getGid());
// update meta
updateRemoteMeta(node.getGid(), sqlNote);
}
private void updateLocalNode(Node node, Cursor c) throws NetworkFailureException {
if (mCancelled) {
return;
}
SqlNote sqlNote;
// update the note locally
sqlNote = new SqlNote(mContext, c);
sqlNote.setContent(node.getLocalJSONFromContent());
Long parentId = (node instanceof Task) ? mGidToNid.get(((Task) node).getParent().getGid())
: new Long(Notes.ID_ROOT_FOLDER);
if (parentId == null) {
Log.e(TAG, "cannot find task's parent id locally");
throw new ActionFailureException("cannot update local node");
}
sqlNote.setParentId(parentId.longValue());
sqlNote.commit(true);
// update meta info
updateRemoteMeta(node.getGid(), sqlNote);
}
private void addRemoteNode(Node node, Cursor c) throws NetworkFailureException {
if (mCancelled) {
return;
}
SqlNote sqlNote = new SqlNote(mContext, c);
Node n;
// update remotely
if (sqlNote.isNoteType()) {
Task task = new Task();
task.setContentByLocalJSON(sqlNote.getContent());
String parentGid = mNidToGid.get(sqlNote.getParentId());
if (parentGid == null) {
Log.e(TAG, "cannot find task's parent tasklist");
throw new ActionFailureException("cannot add remote task");
}
mGTaskListHashMap.get(parentGid).addChildTask(task);
GTaskClient.getInstance().createTask(task);
n = (Node) task;
// add meta
updateRemoteMeta(task.getGid(), sqlNote);
} else {
TaskList tasklist = null;
// we need to skip folder if it has already existed
String folderName = GTaskStringUtils.MIUI_FOLDER_PREFFIX;
if (sqlNote.getId() == Notes.ID_ROOT_FOLDER)
folderName += GTaskStringUtils.FOLDER_DEFAULT;
else if (sqlNote.getId() == Notes.ID_CALL_RECORD_FOLDER)
folderName += GTaskStringUtils.FOLDER_CALL_NOTE;
else
folderName += sqlNote.getSnippet();
Iterator<Map.Entry<String, TaskList>> iter = mGTaskListHashMap.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry<String, TaskList> entry = iter.next();
String gid = entry.getKey();
TaskList list = entry.getValue();
if (list.getName().equals(folderName)) {
tasklist = list;
if (mGTaskHashMap.containsKey(gid)) {
mGTaskHashMap.remove(gid);
}
break;
}
}
// no match we can add now
if (tasklist == null) {
tasklist = new TaskList();
tasklist.setContentByLocalJSON(sqlNote.getContent());
GTaskClient.getInstance().createTaskList(tasklist);
mGTaskListHashMap.put(tasklist.getGid(), tasklist);
}
n = (Node) tasklist;
}
// update local note
sqlNote.setGtaskId(n.getGid());
sqlNote.commit(false);
sqlNote.resetLocalModified();
sqlNote.commit(true);
// gid-id mapping
mGidToNid.put(n.getGid(), sqlNote.getId());
mNidToGid.put(sqlNote.getId(), n.getGid());
}
private void updateRemoteNode(Node node, Cursor c) throws NetworkFailureException {
if (mCancelled) {
return;
}
SqlNote sqlNote = new SqlNote(mContext, c);
// update remotely
node.setContentByLocalJSON(sqlNote.getContent());
GTaskClient.getInstance().addUpdateNode(node);
// update meta
updateRemoteMeta(node.getGid(), sqlNote);
// move task if necessary
if (sqlNote.isNoteType()) {
Task task = (Task) node;
TaskList preParentList = task.getParent();
String curParentGid = mNidToGid.get(sqlNote.getParentId());
if (curParentGid == null) {
Log.e(TAG, "cannot find task's parent tasklist");
throw new ActionFailureException("cannot update remote task");
}
TaskList curParentList = mGTaskListHashMap.get(curParentGid);
if (preParentList != curParentList) {
preParentList.removeChildTask(task);
curParentList.addChildTask(task);
GTaskClient.getInstance().moveTask(task, preParentList, curParentList);
}
}
// clear local modified flag
sqlNote.resetLocalModified();
sqlNote.commit(true);
}
private void updateRemoteMeta(String gid, SqlNote sqlNote) throws NetworkFailureException {
if (sqlNote != null && sqlNote.isNoteType()) {
MetaData metaData = mMetaHashMap.get(gid);
if (metaData != null) {
metaData.setMeta(gid, sqlNote.getContent());
GTaskClient.getInstance().addUpdateNode(metaData);
} else {
metaData = new MetaData();
metaData.setMeta(gid, sqlNote.getContent());
mMetaList.addChildTask(metaData);
mMetaHashMap.put(gid, metaData);
GTaskClient.getInstance().createTask(metaData);
}
}
}
private void refreshLocalSyncId() throws NetworkFailureException {
if (mCancelled) {
return;
}
// get the latest gtask list
mGTaskHashMap.clear();
mGTaskListHashMap.clear();
mMetaHashMap.clear();
initGTaskList();
Cursor c = null;
try {
c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE,
"(type<>? AND parent_id<>?)", new String[] {
String.valueOf(Notes.TYPE_SYSTEM), String.valueOf(Notes.ID_TRASH_FOLER)
}, NoteColumns.TYPE + " DESC");
if (c != null) {
while (c.moveToNext()) {
String gid = c.getString(SqlNote.GTASK_ID_COLUMN);
Node node = mGTaskHashMap.get(gid);
if (node != null) {
mGTaskHashMap.remove(gid);
ContentValues values = new ContentValues();
values.put(NoteColumns.SYNC_ID, node.getLastModified());
mContentResolver.update(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI,
c.getLong(SqlNote.ID_COLUMN)), values, null, null);
} else {
Log.e(TAG, "something is missed");
throw new ActionFailureException(
"some local items don't have gid after sync");
}
}
} else {
Log.w(TAG, "failed to query local note to refresh sync id");
}
} finally {
if (c != null) {
c.close();
c = null;
}
}
}
public String getSyncAccount() {
return GTaskClient.getInstance().getSyncAccount().name;
}
public void cancelSync() {
mCancelled = true;
}
}

@ -0,0 +1,128 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.gtask.remote;
import android.app.Activity;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.IBinder;
public class GTaskSyncService extends Service {
public final static String ACTION_STRING_NAME = "sync_action_type";
public final static int ACTION_START_SYNC = 0;
public final static int ACTION_CANCEL_SYNC = 1;
public final static int ACTION_INVALID = 2;
public final static String GTASK_SERVICE_BROADCAST_NAME = "net.micode.notes.gtask.remote.gtask_sync_service";
public final static String GTASK_SERVICE_BROADCAST_IS_SYNCING = "isSyncing";
public final static String GTASK_SERVICE_BROADCAST_PROGRESS_MSG = "progressMsg";
private static GTaskASyncTask mSyncTask = null;
private static String mSyncProgress = "";
private void startSync() {
if (mSyncTask == null) {
mSyncTask = new GTaskASyncTask(this, new GTaskASyncTask.OnCompleteListener() {
public void onComplete() {
mSyncTask = null;
sendBroadcast("");
stopSelf();
}
});
sendBroadcast("");
mSyncTask.execute();
}
}
private void cancelSync() {
if (mSyncTask != null) {
mSyncTask.cancelSync();
}
}
@Override
public void onCreate() {
mSyncTask = null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Bundle bundle = intent.getExtras();
if (bundle != null && bundle.containsKey(ACTION_STRING_NAME)) {
switch (bundle.getInt(ACTION_STRING_NAME, ACTION_INVALID)) {
case ACTION_START_SYNC:
startSync();
break;
case ACTION_CANCEL_SYNC:
cancelSync();
break;
default:
break;
}
return START_STICKY;
}
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onLowMemory() {
if (mSyncTask != null) {
mSyncTask.cancelSync();
}
}
public IBinder onBind(Intent intent) {
return null;
}
public void sendBroadcast(String msg) {
mSyncProgress = msg;
Intent intent = new Intent(GTASK_SERVICE_BROADCAST_NAME);
intent.putExtra(GTASK_SERVICE_BROADCAST_IS_SYNCING, mSyncTask != null);
intent.putExtra(GTASK_SERVICE_BROADCAST_PROGRESS_MSG, msg);
sendBroadcast(intent);
}
public static void startSync(Activity activity) {
GTaskManager.getInstance().setActivityContext(activity);
Intent intent = new Intent(activity, GTaskSyncService.class);
intent.putExtra(GTaskSyncService.ACTION_STRING_NAME, GTaskSyncService.ACTION_START_SYNC);
activity.startService(intent);
}
public static void cancelSync(Context context) {
Intent intent = new Intent(context, GTaskSyncService.class);
intent.putExtra(GTaskSyncService.ACTION_STRING_NAME, GTaskSyncService.ACTION_CANCEL_SYNC);
context.startService(intent);
}
public static boolean isSyncing() {
return mSyncTask != null;
}
public static String getProgressString() {
return mSyncProgress;
}
}

@ -0,0 +1,253 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.model;
import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.OperationApplicationException;
import android.net.Uri;
import android.os.RemoteException;
import android.util.Log;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.CallNote;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.Notes.TextNote;
import java.util.ArrayList;
public class Note {
private ContentValues mNoteDiffValues;
private NoteData mNoteData;
private static final String TAG = "Note";
/**
* Create a new note id for adding a new note to databases
*/
public static synchronized long getNewNoteId(Context context, long folderId) {
// Create a new note in the database
ContentValues values = new ContentValues();
long createdTime = System.currentTimeMillis();
values.put(NoteColumns.CREATED_DATE, createdTime);
values.put(NoteColumns.MODIFIED_DATE, createdTime);
values.put(NoteColumns.TYPE, Notes.TYPE_NOTE);
values.put(NoteColumns.LOCAL_MODIFIED, 1);
values.put(NoteColumns.PARENT_ID, folderId);
Uri uri = context.getContentResolver().insert(Notes.CONTENT_NOTE_URI, values);
long noteId = 0;
try {
noteId = Long.valueOf(uri.getPathSegments().get(1));
} catch (NumberFormatException e) {
Log.e(TAG, "Get note id error :" + e.toString());
noteId = 0;
}
if (noteId == -1) {
throw new IllegalStateException("Wrong note id:" + noteId);
}
return noteId;
}
public Note() {
mNoteDiffValues = new ContentValues();
mNoteData = new NoteData();
}
public void setNoteValue(String key, String value) {
mNoteDiffValues.put(key, value);
mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1);
mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis());
}
public void setTextData(String key, String value) {
mNoteData.setTextData(key, value);
}
public void setTextDataId(long id) {
mNoteData.setTextDataId(id);
}
public long getTextDataId() {
return mNoteData.mTextDataId;
}
public void setCallDataId(long id) {
mNoteData.setCallDataId(id);
}
public void setCallData(String key, String value) {
mNoteData.setCallData(key, value);
}
public boolean isLocalModified() {
return mNoteDiffValues.size() > 0 || mNoteData.isLocalModified();
}
public boolean syncNote(Context context, long noteId) {
if (noteId <= 0) {
throw new IllegalArgumentException("Wrong note id:" + noteId);
}
if (!isLocalModified()) {
return true;
}
/**
* In theory, once data changed, the note should be updated on {@link NoteColumns#LOCAL_MODIFIED} and
* {@link NoteColumns#MODIFIED_DATE}. For data safety, though update note fails, we also update the
* note data info
*/
if (context.getContentResolver().update(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), mNoteDiffValues, null,
null) == 0) {
Log.e(TAG, "Update note error, should not happen");
// Do not return, fall through
}
mNoteDiffValues.clear();
if (mNoteData.isLocalModified()
&& (mNoteData.pushIntoContentResolver(context, noteId) == null)) {
return false;
}
return true;
}
private class NoteData {
private long mTextDataId;
private ContentValues mTextDataValues;
private long mCallDataId;
private ContentValues mCallDataValues;
private static final String TAG = "NoteData";
public NoteData() {
mTextDataValues = new ContentValues();
mCallDataValues = new ContentValues();
mTextDataId = 0;
mCallDataId = 0;
}
boolean isLocalModified() {
return mTextDataValues.size() > 0 || mCallDataValues.size() > 0;
}
void setTextDataId(long id) {
if(id <= 0) {
throw new IllegalArgumentException("Text data id should larger than 0");
}
mTextDataId = id;
}
void setCallDataId(long id) {
if (id <= 0) {
throw new IllegalArgumentException("Call data id should larger than 0");
}
mCallDataId = id;
}
void setCallData(String key, String value) {
mCallDataValues.put(key, value);
mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1);
mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis());
}
void setTextData(String key, String value) {
mTextDataValues.put(key, value);
mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1);
mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis());
}
Uri pushIntoContentResolver(Context context, long noteId) {
/**
* Check for safety
*/
if (noteId <= 0) {
throw new IllegalArgumentException("Wrong note id:" + noteId);
}
ArrayList<ContentProviderOperation> operationList = new ArrayList<ContentProviderOperation>();
ContentProviderOperation.Builder builder = null;
if(mTextDataValues.size() > 0) {
mTextDataValues.put(DataColumns.NOTE_ID, noteId);
if (mTextDataId == 0) {
mTextDataValues.put(DataColumns.MIME_TYPE, TextNote.CONTENT_ITEM_TYPE);
Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI,
mTextDataValues);
try {
setTextDataId(Long.valueOf(uri.getPathSegments().get(1)));
} catch (NumberFormatException e) {
Log.e(TAG, "Insert new text data fail with noteId" + noteId);
mTextDataValues.clear();
return null;
}
} else {
builder = ContentProviderOperation.newUpdate(ContentUris.withAppendedId(
Notes.CONTENT_DATA_URI, mTextDataId));
builder.withValues(mTextDataValues);
operationList.add(builder.build());
}
mTextDataValues.clear();
}
if(mCallDataValues.size() > 0) {
mCallDataValues.put(DataColumns.NOTE_ID, noteId);
if (mCallDataId == 0) {
mCallDataValues.put(DataColumns.MIME_TYPE, CallNote.CONTENT_ITEM_TYPE);
Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI,
mCallDataValues);
try {
setCallDataId(Long.valueOf(uri.getPathSegments().get(1)));
} catch (NumberFormatException e) {
Log.e(TAG, "Insert new call data fail with noteId" + noteId);
mCallDataValues.clear();
return null;
}
} else {
builder = ContentProviderOperation.newUpdate(ContentUris.withAppendedId(
Notes.CONTENT_DATA_URI, mCallDataId));
builder.withValues(mCallDataValues);
operationList.add(builder.build());
}
mCallDataValues.clear();
}
if (operationList.size() > 0) {
try {
ContentProviderResult[] results = context.getContentResolver().applyBatch(
Notes.AUTHORITY, operationList);
return (results == null || results.length == 0 || results[0] == null) ? null
: ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId);
} catch (RemoteException e) {
Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage()));
return null;
} catch (OperationApplicationException e) {
Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage()));
return null;
}
}
return null;
}
}
}

@ -0,0 +1,400 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.model;
import android.appwidget.AppWidgetManager;
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.text.TextUtils;
import android.util.Log;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.CallNote;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.DataConstants;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.Notes.TextNote;
import net.micode.notes.tool.ResourceParser.NoteBgResources;
public class WorkingNote {
// Note for the working note
private Note mNote;
// Note Id
private long mNoteId;
// Note content
private String mContent;
// Note mode
private int mMode;
private long mAlertDate;
private long mModifiedDate;
private int mBgColorId;
private int mWidgetId;
private int mWidgetType;
private long mFolderId;
private Context mContext;
private static final String TAG = "WorkingNote";
private boolean mIsDeleted;
private NoteSettingChangedListener mNoteSettingStatusListener;
public static final String[] DATA_PROJECTION = new String[] {
DataColumns.ID,
DataColumns.CONTENT,
DataColumns.MIME_TYPE,
DataColumns.DATA1,
DataColumns.DATA2,
DataColumns.DATA3,
DataColumns.DATA4,
};
public static final String[] NOTE_PROJECTION = new String[] {
NoteColumns.PARENT_ID,
NoteColumns.ALERTED_DATE,
NoteColumns.BG_COLOR_ID,
NoteColumns.WIDGET_ID,
NoteColumns.WIDGET_TYPE,
NoteColumns.MODIFIED_DATE
};
private static final int DATA_ID_COLUMN = 0;
private static final int DATA_CONTENT_COLUMN = 1;
private static final int DATA_MIME_TYPE_COLUMN = 2;
private static final int DATA_MODE_COLUMN = 3;
private static final int NOTE_PARENT_ID_COLUMN = 0;
private static final int NOTE_ALERTED_DATE_COLUMN = 1;
private static final int NOTE_BG_COLOR_ID_COLUMN = 2;
private static final int NOTE_WIDGET_ID_COLUMN = 3;
private static final int NOTE_WIDGET_TYPE_COLUMN = 4;
private static final int NOTE_MODIFIED_DATE_COLUMN = 5;
// New note construct
private WorkingNote(Context context, long folderId) {
mContext = context;
mAlertDate = 0;
mModifiedDate = System.currentTimeMillis();
mFolderId = folderId;
mNote = new Note();
mNoteId = 0;
mIsDeleted = false;
mMode = 0;
mWidgetType = Notes.TYPE_WIDGET_INVALIDE;
}
// Existing note construct
private WorkingNote(Context context, long noteId, long folderId) {
mContext = context;
mNoteId = noteId;
mFolderId = folderId;
mIsDeleted = false;
mNote = new Note();
loadNote();
}
private void loadNote() {
Cursor cursor = mContext.getContentResolver().query(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mNoteId), NOTE_PROJECTION, null,
null, null);
if (cursor != null) {
if (cursor.moveToFirst()) {
mFolderId = cursor.getLong(NOTE_PARENT_ID_COLUMN);
mBgColorId = cursor.getInt(NOTE_BG_COLOR_ID_COLUMN);
mWidgetId = cursor.getInt(NOTE_WIDGET_ID_COLUMN);
mWidgetType = cursor.getInt(NOTE_WIDGET_TYPE_COLUMN);
mAlertDate = cursor.getLong(NOTE_ALERTED_DATE_COLUMN);
mModifiedDate = cursor.getLong(NOTE_MODIFIED_DATE_COLUMN);
}
cursor.close();
} else {
Log.e(TAG, "No note with id:" + mNoteId);
throw new IllegalArgumentException("Unable to find note with id " + mNoteId);
}
loadNoteData();
}
private void loadNoteData() {
Cursor cursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI, DATA_PROJECTION,
DataColumns.NOTE_ID + "=?", new String[] {
String.valueOf(mNoteId)
}, null);
if (cursor != null) {
if (cursor.moveToFirst()) {
do {
String type = cursor.getString(DATA_MIME_TYPE_COLUMN);
if (DataConstants.NOTE.equals(type)) {
mContent = cursor.getString(DATA_CONTENT_COLUMN);
mMode = cursor.getInt(DATA_MODE_COLUMN);
mNote.setTextDataId(cursor.getLong(DATA_ID_COLUMN));
} else if (DataConstants.CALL_NOTE.equals(type)) {
mNote.setCallDataId(cursor.getLong(DATA_ID_COLUMN));
} else {
Log.d(TAG, "Wrong note type with type:" + type);
}
} while (cursor.moveToNext());
}
cursor.close();
} else {
Log.e(TAG, "No data with id:" + mNoteId);
throw new IllegalArgumentException("Unable to find note's data with id " + mNoteId);
}
}
public static WorkingNote createEmptyNote(Context context, long folderId, int widgetId,
int widgetType, int defaultBgColorId) {
WorkingNote note = new WorkingNote(context, folderId);
note.setBgColorId(defaultBgColorId);
note.setWidgetId(widgetId);
note.setWidgetType(widgetType);
return note;
}
public static WorkingNote load(Context context, long id) {
return new WorkingNote(context, id, 0);
}
public synchronized boolean saveNote() {
if (mIsDeleted && existInDatabase()) {
// 如果便签已被标记为删除且存在于数据库中,则执行删除操作
int rows = mContext.getContentResolver().delete(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mNoteId),
null, null);
if (rows > 0) {
Log.d(TAG, "Deleted empty note with id:" + mNoteId);
} else {
Log.e(TAG, "Failed to delete empty note with id:" + mNoteId);
}
return true;
}
if (isWorthSaving()) {
if (!existInDatabase()) {
if ((mNoteId = Note.getNewNoteId(mContext, mFolderId)) == 0) {
Log.e(TAG, "Create new note fail with id:" + mNoteId);
return false;
}
}
// 更新便签数据
mNote.setTextData(DataColumns.CONTENT, mContent);
// 同步到数据库
boolean result = mNote.syncNote(mContext, mNoteId);
/**
* Update widget content if there exist any widget of this note
*/
if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID
&& mWidgetType != Notes.TYPE_WIDGET_INVALIDE
&& mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onWidgetChanged();
}
return result;
} else {
return false;
}
}
public boolean existInDatabase() {
return mNoteId > 0;
}
private boolean isWorthSaving() {
if (mIsDeleted) {
return false;
}
// 如果便签不存在于数据库且内容为空,则不值得保存
if (!existInDatabase() && TextUtils.isEmpty(mContent)) {
return false;
}
// 如果便签已存在于数据库但内容为空,则需要删除它
if (existInDatabase() && TextUtils.isEmpty(mContent)) {
// 标记为需要删除
mIsDeleted = true;
return false;
}
// 如果便签已存在于数据库但未被修改,则不需要保存
if (existInDatabase() && !mNote.isLocalModified()) {
return false;
}
return true;
}
public void setOnSettingStatusChangedListener(NoteSettingChangedListener l) {
mNoteSettingStatusListener = l;
}
public void setAlertDate(long date, boolean set) {
if (date != mAlertDate) {
mAlertDate = date;
mNote.setNoteValue(NoteColumns.ALERTED_DATE, String.valueOf(mAlertDate));
}
if (mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onClockAlertChanged(date, set);
}
}
public void markDeleted(boolean mark) {
mIsDeleted = mark;
if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID
&& mWidgetType != Notes.TYPE_WIDGET_INVALIDE && mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onWidgetChanged();
}
}
public void setBgColorId(int id) {
if (id != mBgColorId) {
mBgColorId = id;
if (mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onBackgroundColorChanged();
}
mNote.setNoteValue(NoteColumns.BG_COLOR_ID, String.valueOf(id));
}
}
public void setCheckListMode(int mode) {
if (mMode != mode) {
if (mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onCheckListModeChanged(mMode, mode);
}
mMode = mode;
mNote.setTextData(TextNote.MODE, String.valueOf(mMode));
}
}
public void setWidgetType(int type) {
if (type != mWidgetType) {
mWidgetType = type;
mNote.setNoteValue(NoteColumns.WIDGET_TYPE, String.valueOf(mWidgetType));
}
}
public void setWidgetId(int id) {
if (id != mWidgetId) {
mWidgetId = id;
mNote.setNoteValue(NoteColumns.WIDGET_ID, String.valueOf(mWidgetId));
}
}
public void setWorkingText(String text) {
if (!TextUtils.equals(mContent, text)) {
mContent = text;
mNote.setTextData(DataColumns.CONTENT, mContent);
}
}
public void convertToCallNote(String phoneNumber, long callDate) {
mNote.setCallData(CallNote.CALL_DATE, String.valueOf(callDate));
mNote.setCallData(CallNote.PHONE_NUMBER, phoneNumber);
mNote.setNoteValue(NoteColumns.PARENT_ID, String.valueOf(Notes.ID_CALL_RECORD_FOLDER));
}
public boolean hasClockAlert() {
return (mAlertDate > 0 ? true : false);
}
public String getContent() {
return mContent;
}
public long getAlertDate() {
return mAlertDate;
}
public long getModifiedDate() {
return mModifiedDate;
}
public int getBgColorResId() {
return NoteBgResources.getNoteBgResource(mBgColorId);
}
public int getBgColorId() {
return mBgColorId;
}
public int getTitleBgResId() {
return NoteBgResources.getNoteTitleBgResource(mBgColorId);
}
public int getCheckListMode() {
return mMode;
}
public long getNoteId() {
return mNoteId;
}
public long getFolderId() {
return mFolderId;
}
public int getWidgetId() {
return mWidgetId;
}
public int getWidgetType() {
return mWidgetType;
}
public interface NoteSettingChangedListener {
/**
* Called when the background color of current note has just changed
*/
void onBackgroundColorChanged();
/**
* Called when user set clock
*/
void onClockAlertChanged(long date, boolean set);
/**
* Call when user create note from widget
*/
void onWidgetChanged();
/**
* Call when switch between check list mode and normal mode
* @param oldMode is previous mode before change
* @param newMode is new mode
*/
void onCheckListModeChanged(int oldMode, int newMode);
}
}

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

@ -0,0 +1,584 @@
/*
* 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.
*/
// NotesPreferenceActivity.java - 便签设置Activity
// 主要功能管理应用的偏好设置特别是Google Tasks同步账户管理
package net.micode.notes.ui;
// ======================= 导入区域 =======================
// Android账户管理
import android.accounts.Account; // 账户对象
import android.accounts.AccountManager; // 账户管理器
// Android界面
import android.app.ActionBar; // 操作栏
import android.app.AlertDialog; // 警告对话框
// Android广播
import android.content.BroadcastReceiver; // 广播接收器
// Android数据
import android.content.ContentValues; // 内容值
// Android基础
import android.content.Context; // 上下文
import android.content.DialogInterface; // 对话框接口
import android.content.Intent; // 意图
import android.content.IntentFilter; // 意图过滤器
import android.content.SharedPreferences; // 偏好设置
import android.os.Bundle; // 状态保存
// Android偏好设置
import android.preference.Preference; // 偏好设置项
import android.preference.Preference.OnPreferenceClickListener; // 偏好设置点击监听
import android.preference.PreferenceActivity; // 偏好设置Activity基类
import android.preference.PreferenceCategory; // 偏好设置分类
import android.preference.PreferenceManager; // 偏好设置管理器
// Android工具
import android.text.TextUtils; // 文本工具
import android.text.format.DateFormat; // 日期格式化
// Android视图
import android.view.LayoutInflater; // 布局加载器
import android.view.Menu; // 菜单
import android.view.MenuItem; // 菜单项
import android.view.View; // 视图基类
import android.widget.ListView; // 列表视图
// Android控件
import android.widget.Button; // 按钮
import android.widget.TextView; // 文本视图
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 static final String PREFERENCE_NAME = "notes_preferences";
/** 同步账户名称偏好键 - 存储当前设置的同步账户名 */
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_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 AUTHORITIES_FILTER_KEY = "authorities";
// ======================= 成员变量 =======================
/** 账户设置分类 - 显示账户相关设置 */
private PreferenceCategory mAccountCategory;
/** Google Tasks同步广播接收器 - 监听同步状态变化 */
private GTaskReceiver mReceiver;
/** 原始账户列表 - 用于检测新添加的账户 */
private Account[] mOriAccounts;
/** 是否添加了新账户标志 - 标记用户是否添加了新账户 */
private boolean mHasAddedAccount;
// ======================= 生命周期方法 =======================
/**
* onCreate - Activity
*
* @param icicle
*/
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
// 从XML加载偏好设置
addPreferencesFromResource(R.xml.preferences);
// 获取账户设置分类
mAccountCategory = (PreferenceCategory) findPreference(PREFERENCE_SYNC_ACCOUNT_KEY);
// 设置清除搜索历史按钮的点击事件
Preference clearSearchHistoryPref = findPreference("pref_key_clear_search_history");
if (clearSearchHistoryPref != null) {
clearSearchHistoryPref.setOnPreferenceClickListener(new OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
// 清除搜索历史
SearchHistoryManager.getInstance(NotesPreferenceActivity.this).clearSearchHistory();
Toast.makeText(NotesPreferenceActivity.this,
"Search history cleared", Toast.LENGTH_SHORT).show();
return true;
}
});
}
// 创建并注册同步广播接收器
mReceiver = new GTaskReceiver();
IntentFilter filter = new IntentFilter();
filter.addAction(GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME);
registerReceiver(mReceiver, filter);
mOriAccounts = null; // 初始化原始账户列表
// 添加设置界面头部
View header = LayoutInflater.from(this).inflate(R.layout.settings_header, null);
ListView listView = getListView();
if (listView != null) {
listView.addHeaderView(header, null, true);
}
/* 使用应用图标作为导航按钮 */
ActionBar actionBar = getActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
}
}
/**
* onResume - Activity
*
*/
@Override
protected void onResume() {
super.onResume();
// 如果用户添加了新账户,需要自动设置为同步账户
if (mHasAddedAccount) {
Account[] accounts = getGoogleAccounts();
// 比较新旧账户数量,检测新账户
if (mOriAccounts != null && accounts.length > mOriAccounts.length) {
for (Account accountNew : accounts) {
boolean found = false;
for (Account accountOld : mOriAccounts) {
if (TextUtils.equals(accountOld.name, accountNew.name)) {
found = true;
break;
}
}
if (!found) {
// 找到新账户,设置为同步账户
setSyncAccount(accountNew.name);
break;
}
}
}
}
refreshUI(); // 刷新界面
}
/**
* onDestroy - Activity
* 广
*/
@Override
protected void onDestroy() {
if (mReceiver != null) {
unregisterReceiver(mReceiver);
}
super.onDestroy();
}
// ======================= 账户偏好设置加载 =======================
/**
*
*
*/
private void loadAccountPreference() {
mAccountCategory.removeAll(); // 清空现有设置项
// 创建账户设置项
Preference accountPref = new Preference(this);
final String defaultAccount = getSyncAccountName(this);
accountPref.setTitle(getString(R.string.preferences_account_title));
accountPref.setSummary(getString(R.string.preferences_account_summary));
accountPref.setOnPreferenceClickListener(new OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
if (!GTaskSyncService.isSyncing()) {
if (TextUtils.isEmpty(defaultAccount)) {
// 首次设置账户:显示选择账户对话框
showSelectAccountAlertDialog();
} else {
// 已设置账户:显示更改账户确认对话框
showChangeAccountConfirmAlertDialog();
}
} else {
// 同步进行中,不能更改账户
Toast.makeText(NotesPreferenceActivity.this,
R.string.preferences_toast_cannot_change_account, Toast.LENGTH_SHORT)
.show();
}
return true;
}
});
mAccountCategory.addPreference(accountPref);
}
// ======================= 同步按钮加载 =======================
/**
*
*
*/
private void loadSyncButton() {
Button syncButton = (Button) findViewById(R.id.preference_sync_button);
TextView lastSyncTimeView = (TextView) findViewById(R.id.prefenerece_sync_status_textview);
// 设置按钮状态
if (GTaskSyncService.isSyncing()) {
// 同步中:显示取消按钮
syncButton.setText(getString(R.string.preferences_button_sync_cancel));
syncButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
GTaskSyncService.cancelSync(NotesPreferenceActivity.this);
}
});
} else {
// 未同步:显示立即同步按钮
syncButton.setText(getString(R.string.preferences_button_sync_immediately));
syncButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
GTaskSyncService.startSync(NotesPreferenceActivity.this);
}
});
}
// 有账户时启用按钮,无账户时禁用
syncButton.setEnabled(!TextUtils.isEmpty(getSyncAccountName(this)));
// 设置最后同步时间
if (GTaskSyncService.isSyncing()) {
// 同步中:显示同步进度
lastSyncTimeView.setText(GTaskSyncService.getProgressString());
lastSyncTimeView.setVisibility(View.VISIBLE);
} else {
// 未同步:显示最后同步时间
long lastSyncTime = getLastSyncTime(this);
if (lastSyncTime != 0) {
lastSyncTimeView.setText(getString(R.string.preferences_last_sync_time,
DateFormat.format(getString(R.string.preferences_last_sync_time_format),
lastSyncTime)));
lastSyncTimeView.setVisibility(View.VISIBLE);
} else {
// 从未同步:隐藏时间显示
lastSyncTimeView.setVisibility(View.GONE);
}
}
}
// ======================= 界面刷新 =======================
/**
*
*
*/
private void refreshUI() {
loadAccountPreference();
loadSyncButton();
}
// ======================= 选择账户对话框 =======================
/**
*
*
*/
private void showSelectAccountAlertDialog() {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
// 自定义标题视图
View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null);
TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title);
titleTextView.setText(getString(R.string.preferences_dialog_select_account_title));
TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle);
subtitleTextView.setText(getString(R.string.preferences_dialog_select_account_tips));
dialogBuilder.setCustomTitle(titleView);
dialogBuilder.setPositiveButton(null, null); // 不显示确定按钮
// 获取Google账户列表
Account[] accounts = getGoogleAccounts();
String defAccount = getSyncAccountName(this);
// 保存原始账户列表,用于检测新账户
mOriAccounts = accounts;
mHasAddedAccount = false;
if (accounts.length > 0) {
// 创建账户列表项
CharSequence[] items = new CharSequence[accounts.length];
final CharSequence[] itemMapping = items; // 用于内部类访问
int checkedItem = -1; // 默认未选中
// 填充账户列表
int index = 0;
for (Account account : accounts) {
if (TextUtils.equals(account.name, defAccount)) {
checkedItem = index; // 选中已设置的账户
}
items[index++] = account.name;
}
// 设置单选列表
dialogBuilder.setSingleChoiceItems(items, checkedItem,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
// 选择账户
setSyncAccount(itemMapping[which].toString());
dialog.dismiss();
refreshUI(); // 刷新界面
}
});
}
// 添加"添加账户"视图
View addAccountView = LayoutInflater.from(this).inflate(R.layout.add_account_text, null);
dialogBuilder.setView(addAccountView);
// 显示对话框
final AlertDialog dialog = dialogBuilder.show();
addAccountView.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
mHasAddedAccount = true; // 标记为添加账户
// 启动系统添加账户界面
Intent intent = new Intent("android.settings.ADD_ACCOUNT_SETTINGS");
// 只显示Gmail账户类型
intent.putExtra(AUTHORITIES_FILTER_KEY, new String[] {
"gmail-ls"
});
startActivityForResult(intent, -1);
dialog.dismiss();
}
});
}
// ======================= 更改账户确认对话框 =======================
/**
*
*
*/
private void showChangeAccountConfirmAlertDialog() {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
// 自定义标题视图
View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null);
TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title);
// 标题包含当前账户名
titleTextView.setText(getString(R.string.preferences_dialog_change_account_title,
getSyncAccountName(this)));
TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle);
subtitleTextView.setText(getString(R.string.preferences_dialog_change_account_warn_msg));
dialogBuilder.setCustomTitle(titleView);
// 设置菜单项
CharSequence[] menuItemArray = new CharSequence[] {
getString(R.string.preferences_menu_change_account), // 更改账户
getString(R.string.preferences_menu_remove_account), // 移除账户
getString(R.string.preferences_menu_cancel) // 取消
};
dialogBuilder.setItems(menuItemArray, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
if (which == 0) {
// 更改账户:显示选择账户对话框
showSelectAccountAlertDialog();
} else if (which == 1) {
// 移除账户:清除账户设置
removeSyncAccount();
refreshUI();
}
// 取消:不执行任何操作
}
});
dialogBuilder.show();
}
// ======================= 获取Google账户 =======================
/**
* Google
* @return Google
*/
private Account[] getGoogleAccounts() {
AccountManager accountManager = AccountManager.get(this);
// 获取类型为"com.google"的账户Google账户
return accountManager.getAccountsByType("com.google");
}
// ======================= 设置同步账户 =======================
/**
*
*
* @param account
*/
private void setSyncAccount(String account) {
if (!getSyncAccountName(this).equals(account)) {
// 保存到偏好设置
SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
if (account != null) {
editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, account);
} else {
editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, "");
}
editor.commit();
// 清理最后同步时间
setLastSyncTime(this, 0);
// 清理本地同步相关数据(在后台线程执行)
new Thread(new Runnable() {
public void run() {
ContentValues values = new ContentValues();
values.put(NoteColumns.GTASK_ID, ""); // 清空GTASK ID
values.put(NoteColumns.SYNC_ID, 0); // 清空同步ID
getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null);
}
}).start();
// 显示成功提示
Toast.makeText(NotesPreferenceActivity.this,
getString(R.string.preferences_toast_success_set_accout, account),
Toast.LENGTH_SHORT).show();
}
}
// ======================= 移除同步账户 =======================
/**
*
*
*/
private void removeSyncAccount() {
SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
// 移除账户名
if (settings.contains(PREFERENCE_SYNC_ACCOUNT_NAME)) {
editor.remove(PREFERENCE_SYNC_ACCOUNT_NAME);
}
// 移除最后同步时间
if (settings.contains(PREFERENCE_LAST_SYNC_TIME)) {
editor.remove(PREFERENCE_LAST_SYNC_TIME);
}
editor.commit();
// 清理本地同步相关数据(在后台线程执行)
new Thread(new Runnable() {
public void run() {
ContentValues values = new ContentValues();
values.put(NoteColumns.GTASK_ID, ""); // 清空GTASK ID
values.put(NoteColumns.SYNC_ID, 0); // 清空同步ID
getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null);
}
}).start();
}
// ======================= 静态工具方法 =======================
/**
*
*
* @param context
* @return
*/
public static String getSyncAccountName(Context context) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
return settings.getString(PREFERENCE_SYNC_ACCOUNT_NAME, "");
}
/**
*
*
* @param context
* @param time
*/
public static void setLastSyncTime(Context context, long time) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
editor.putLong(PREFERENCE_LAST_SYNC_TIME, time);
editor.commit();
}
/**
*
*
* @param context
* @return 0
*/
public static long getLastSyncTime(Context context) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
return settings.getLong(PREFERENCE_LAST_SYNC_TIME, 0);
}
// ======================= 同步广播接收器 =======================
/**
* GTaskReceiver - Google Tasks广
*
*/
private class GTaskReceiver extends BroadcastReceiver {
/**
* 广
*
*/
@Override
public void onReceive(Context context, Intent intent) {
refreshUI(); // 刷新界面
// 如果正在同步,更新进度信息
if (intent.getBooleanExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_IS_SYNCING, false)) {
TextView syncStatus = (TextView) findViewById(R.id.prefenerece_sync_status_textview);
syncStatus.setText(intent
.getStringExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_PROGRESS_MSG));
}
}
}
// ======================= 菜单项处理 =======================
/**
*
*
*/
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
// 返回按钮:回到主界面
Intent intent = new Intent(this, NotesListActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
return true;
default:
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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 554 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

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

Loading…
Cancel
Save