Compare commits

...

30 Commits

Author SHA1 Message Date
peufo5rvf 712001e9a0 Merge pull request '补充' (#9) from xuningshuo_branch into master
2 months ago
xuningshuo 3c9021ab70 补充
2 months ago
peufo5rvf 7bd8e76b15 Merge pull request '按照标准进行格式修改' (#8) from xuningshuo_branch into master
2 months ago
xuningshuo 96ac2764bd 按照标准要求提交
2 months ago
peufo5rvf c44dc829e0 Merge pull request '功能添加+上个功能补充' (#7) from xuningshuo_branch into master
2 months ago
xuningshuo 00010a1411 新增了改变便签排列格式功能,补充了回收站未成功上传的xml文件
2 months ago
peufo5rvf 9a15ef0d2d Merge pull request '功能添加' (#6) from xuningshuo_branch into master
3 months ago
xuningshuo 66d1cc6d28 新增了保存并退出、返回上一级、回收站三个功能
3 months ago
xuningshuo 510a7a2c48 同步伙伴的修改
3 months ago
xuningshuo 78880f2551 。。。
3 months ago
xuningshuo 015913abbb ???
3 months ago
xuningshuo ded5811f58 修bug
3 months ago
xuningshuo 432b4e3780 Merge branch 'master' of https://bdgit.educoder.net/pjhm45b8a/tsrxns into xuningshuo_branch
3 months ago
xuningshuo f3aff56a2e bug了?
3 months ago
tsr ae41521f94 补充
3 months ago
tsr 2c9ee96f45 增加了便签置顶,四象限分类,深色模式功能
3 months ago
xuningshuo 37aee3a282 修错
3 months ago
xuningshuo 911d904eaa 修错
3 months ago
xuningshuo 9c5190d76a 修错
3 months ago
peufo5rvf f3164679c1 Merge pull request '2026.1.27' (#5) from xuningshuo_branch into master
3 months ago
xuningshuo 6e883216ea 修改了页边距使得ui不会遮挡第一行文字
3 months ago
pjhm45b8a 73142a15e5 Merge pull request '调整res文件夹位置' (#4) from xuningshuo_branch into master
3 months ago
xuningshuo 357df4670d 修正了小米便签中未能在功能区实现汉化的问题
3 months ago
xuningshuo ea0acc7f26 提交
3 months ago
xuningshuo 2d4d11befa 补充res文档
3 months ago
xuningshuo 28b47717a6 调整了res文件的位置
3 months ago
xuningshuo f76d088746 Merge branch 'master' of https://bdgit.educoder.net/pjhm45b8a/tsrxns into xuningshuo_branch
3 months ago
pjhm45b8a 7be2d10a46 Merge pull request '修改写便签按钮被阻挡问题' (#3) from taosiru_branch into master
3 months ago
pjhm45b8a f2fb7184f9 Merge pull request '1.7代码质量分析报告' (#2) from taosiru_branch into master
3 months ago
xuningshuo c524aaf6ea 1.6跟进
3 months ago

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

@ -0,0 +1,26 @@
package net.micode.notes;
import android.content.Context;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("net.micode.notes", appContext.getPackageName());
}
}

@ -1,83 +0,0 @@
/*
* 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;
public class Contact {
private static HashMap<String, String> sContactCache;//存储联系人信息。电话——姓名
private static final String TAG = "Contact";
//这个常量通常用于在日志输出或调试信息中标识所属类别或模块,帮助开发人员在查看日志时快速定位到相关信息的来源。
//这段代码的作用是构建一个复杂的查询条件,用于在查询联系人信息时精确匹配电话号码,并确保所查询到的数据符合特定条件。
//CALLER_ID_SELECTION 的含义是选择电话号码字段为特定号码MIME 类型为电话号码类型,且原始联系人 ID 符合特定条件(通过子查询获取)的联系人数据。
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 = '+')";
//根据电话号码查询联系人信息
public static String getContact(Context context, String phoneNumber) {
if(sContactCache == null) { //初始化联系人信息缓存
sContactCache = new HashMap<String, String>();
}
//查询联系人的号码,如果已有联系人信息,则直接返回
if(sContactCache.containsKey(phoneNumber)) {
return sContactCache.get(phoneNumber);
}
//使用 CALLER_ID_SELECTION 中定义的查询条件,并将其中的 + 替换为经过处理后的最小匹配号码
String selection = CALLER_ID_SELECTION.replace("+",
PhoneNumberUtils.toCallerIDMinMatch(phoneNumber));
//PhoneNumberUtils.toCallerIDMinMatch(phoneNumber) 是一个 Android 平台提供的用于处理电话号码的工具方法之一。
// 它通常用于将输入的电话号码转换为适合作为 CALLER_ID_SELECTION 筛选条件中的最小匹配号码。
//这段代码使用 Android 的内容解析器Content Resolver来执行查询操作并返回一个 Cursor 对象,用于获取符合查询条件的联系人信息。
Cursor cursor = context.getContentResolver().query(
Data.CONTENT_URI,
new String [] { Phone.DISPLAY_NAME },
selection,
new String[] { phoneNumber },
null);
if (cursor != null && cursor.moveToFirst()) { //如果查询返回的 cursor 不为空且能够移动到第一个结果位置(即有匹配的联系人信息)
try {
String name = cursor.getString(0);//获取第一个字段的值,即联系人的显示名称
sContactCache.put(phoneNumber, name);//将该信息存储在 sContactCache 中,以电话号码作为键,联系人名称作为值。
return name;
} catch (IndexOutOfBoundsException e) {
Log.e(TAG, " Cursor get string error " + e.toString()); //记录错误日志
return null;
} finally { //无论是否发生异常,都会在最终执行块中关闭查询结果的 cursor
cursor.close();
}
} else { //如果查询结果为空或未找到匹配的联系人信息,则记录调试日志表示未找到匹配的联系人信息,并返回 null。
Log.d(TAG, "No contact matched with number:" + phoneNumber);
return null;
}
}
}

@ -1,317 +0,0 @@
/*
* 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;
public class Notes {
public static final String AUTHORITY = "micode_notes";
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;//系统类型
//这段注释解释了一些系统文件夹的标识符,以及它们对应的含义
// Notes#ID_ROOT_FOLDER: 默认文件夹的标识符。
// Notes#ID_TEMPARAY_FOLDER: 用于存储不属于任何文件夹的临时笔记的标识符。
//Notes#ID_CALL_RECORD_FOLDER: 用于存储通话记录的文件夹的标识符。
/**
* Following IDs are system folders' identifiers
* {@link Notes#ID_ROOT_FOLDER } is default folder
* {@link Notes#ID_TEMPARAY_FOLDER } is for notes belonging no folder
* {@link Notes#ID_CALL_RECORD_FOLDER} is to store call records
*/
//这些标识符被赋予了特定的整数数值,用于在代码中唯一地标识不同的系统文件夹。
// 通过使用这些常量,可以方便地在代码中引用这些特殊文件夹的标识符,而无需硬编码数值。
public static final int ID_ROOT_FOLDER = 0;//默认文件夹的标识符
public static final int ID_TEMPARAY_FOLDER = -1;//用于存储不属于任何文件夹的临时笔记的标识符
public static final int ID_CALL_RECORD_FOLDER = -2;//用于存储通话记录的文件夹的标识符
public static final int ID_TRASH_FOLER = -3;//用于存储回收站文件夹的标识符
// Intent是Android应用程序组件之间进行通信的一种重要方式
// 这段代码定义了一些静态常量,用于表示在 Intent 中传递时所需的额外参数的键值对。这些静态常量的含义如下:
// INTENT_EXTRA_ALERT_DATE: 表示提醒日期的额外参数键值,在 Intent 中使用时对应的键名为 "net.micode.notes.alert_date"。
// INTENT_EXTRA_BACKGROUND_ID: 表示背景颜色 ID 的额外参数键值,在 Intent 中使用时对应的键名为 "net.micode.notes.background_color_id"。
// INTENT_EXTRA_WIDGET_ID: 表示小部件 ID 的额外参数键值,在 Intent 中使用时对应的键名为 "net.micode.notes.widget_id"。
// INTENT_EXTRA_WIDGET_TYPE: 表示小部件类型的额外参数键值,在 Intent 中使用时对应的键名为 "net.micode.notes.widget_type"。
// INTENT_EXTRA_FOLDER_ID: 表示文件夹 ID 的额外参数键值,在 Intent 中使用时对应的键名为 "net.micode.notes.folder_id"。
// INTENT_EXTRA_CALL_DATE: 表示通话日期的额外参数键值,在 Intent 中使用时对应的键名为 "net.micode.notes.call_date"。
// 通过定义这些静态常量,可以避免在代码中硬编码这些字符串,提高了代码的可读性和可维护性。
// 这些常量的命名也很清晰地表达了它们所代表的含义,让开发人员能够准确地理解它们在 Intent 中所承载的数据。
public static final String INTENT_EXTRA_ALERT_DATE = "net.micode.notes.alert_date";
public static final String INTENT_EXTRA_BACKGROUND_ID = "net.micode.notes.background_color_id";
public static final String INTENT_EXTRA_WIDGET_ID = "net.micode.notes.widget_id";
public static final String INTENT_EXTRA_WIDGET_TYPE = "net.micode.notes.widget_type";
public static final String INTENT_EXTRA_FOLDER_ID = "net.micode.notes.folder_id";
public static final String INTENT_EXTRA_CALL_DATE = "net.micode.notes.call_date";
//这段代码定义了一些静态常量,用于表示小部件的类型
public static final int TYPE_WIDGET_INVALIDE = -1; //无效的小部件类型
public static final int TYPE_WIDGET_2X = 0; //2x2小部件
public static final int TYPE_WIDGET_4X = 1; //4x4小部件
//MIMEMultipurpose Internet Mail Extensions类型是一种标准化的方法用于表示各种数据的类型和格式
//在 Android 开发中MIME 类型也经常用于指示 Intent 的数据类型,以便系统能够选择合适的应用程序来处理特定类型的数据。
//定义了一个静态内部类 DataConstants,将数据类型的常量集中定义在一个内部类中,有助于组织和管理相关的常量,并使得代码结构更清晰。
public static class DataConstants {
public static final String NOTE = TextNote.CONTENT_ITEM_TYPE;//表示笔记的 MIME 类型
public static final String CALL_NOTE = CallNote.CONTENT_ITEM_TYPE;//表示通话记录的 MIME 类型
}
//URI统一资源标识符是用于标识互联网上资源的字符串文本它提供了一种统一的方式来定位资源。
/**
* Uri to query all notes and folders
*/
//表示用于查询所有笔记和文件夹的 URI
public static final Uri CONTENT_NOTE_URI = Uri.parse("content://" + AUTHORITY + "/note");
/**
* Uri to query data
*/
//表示用于查询数据的 URI
public static final Uri CONTENT_DATA_URI = Uri.parse("content://" + AUTHORITY + "/data");
//定义了一个接口 NoteColumns其中包含了一系列常量用于表示笔记或文件夹的各种属性和信息。
// 这些常量提供了对笔记或文件夹数据结构中不同字段的访问,使得代码可以更清晰地操作和管理这些数据。
public interface NoteColumns {
/**
* The unique ID for a row
* <P> Type: INTEGER (long) </P>
*/
public static final String ID = "_id";
/**
* The parent's id for note or folder
* <P> Type: INTEGER (long) </P>
*/
public static final String PARENT_ID = "parent_id";
/**
* Created data for note or folder
* <P> Type: INTEGER (long) </P>
*/
public static final String CREATED_DATE = "created_date";
/**
* Latest modified date
* <P> Type: INTEGER (long) </P>
*/
public static final String MODIFIED_DATE = "modified_date";
/**
* Alert date
* <P> Type: INTEGER (long) </P>
*/
public static final String ALERTED_DATE = "alert_date";
/**
* Folder's name or text content of note
* <P> Type: TEXT </P>
*/
public static final String SNIPPET = "snippet";
/**
* Note's widget id
* <P> Type: INTEGER (long) </P>
*/
public static final String WIDGET_ID = "widget_id";
/**
* Note's widget type
* <P> Type: INTEGER (long) </P>
*/
public static final String WIDGET_TYPE = "widget_type";
/**
* Note's background color's id
* <P> Type: INTEGER (long) </P>
*/
public static final String BG_COLOR_ID = "bg_color_id";
/**
* For text note, it doesn't has attachment, for multi-media
* note, it has at least one attachment
* <P> Type: INTEGER </P>
*/
public static final String HAS_ATTACHMENT = "has_attachment";
/**
* Folder's count of notes
* <P> Type: INTEGER (long) </P>
*/
public static final String NOTES_COUNT = "notes_count";
/**
* The file type: folder or note
* <P> Type: INTEGER </P>
*/
public static final String TYPE = "type";
/**
* The last sync id
* <P> Type: INTEGER (long) </P>
*/
public static final String SYNC_ID = "sync_id";
/**
* Sign to indicate local modified or not
* <P> Type: INTEGER </P>
*/
public static final String LOCAL_MODIFIED = "local_modified";
/**
* Original parent id before moving into temporary folder
* <P> Type : INTEGER </P>
*/
public static final String ORIGIN_PARENT_ID = "origin_parent_id";
/**
* The gtask id
* <P> Type : TEXT </P>
*/
public static final String GTASK_ID = "gtask_id";
/**
* The version code
* <P> Type : INTEGER (long) </P>
*/
public static final String VERSION = "version";
}
//定义了另一个接口 DataColumns其中包含了一系列常量用于表示数据的各种属性和信息。
// 这些常量提供了对数据结构中不同字段的访问,使得代码可以更清晰地操作和管理这些数据。
public interface DataColumns {
/**
* The unique ID for a row
* <P> Type: INTEGER (long) </P>
*/
public static final String ID = "_id";
/**
* The MIME type of the item represented by this row.
* <P> Type: Text </P>
*/
public static final String MIME_TYPE = "mime_type";
/**
* The reference id to note that this data belongs to
* <P> Type: INTEGER (long) </P>
*/
public static final String NOTE_ID = "note_id";
/**
* Created data for note or folder
* <P> Type: INTEGER (long) </P>
*/
public static final String CREATED_DATE = "created_date";
/**
* Latest modified date
* <P> Type: INTEGER (long) </P>
*/
public static final String MODIFIED_DATE = "modified_date";
/**
* Data's content
* <P> Type: TEXT </P>
*/
public static final String CONTENT = "content";
/**
* Generic data column, the meaning is {@link #MIMETYPE} specific, used for
* integer data type
* <P> Type: INTEGER </P>
*/
public static final String DATA1 = "data1";
/**
* Generic data column, the meaning is {@link #MIMETYPE} specific, used for
* integer data type
* <P> Type: INTEGER </P>
*/
public static final String DATA2 = "data2";
/**
* Generic data column, the meaning is {@link #MIMETYPE} specific, used for
* TEXT data type
* <P> Type: TEXT </P>
*/
public static final String DATA3 = "data3";
/**
* Generic data column, the meaning is {@link #MIMETYPE} specific, used for
* TEXT data type
* <P> Type: TEXT </P>
*/
public static final String DATA4 = "data4";
/**
* Generic data column, the meaning is {@link #MIMETYPE} specific, used for
* TEXT data type
* <P> Type: TEXT </P>
*/
public static final String DATA5 = "data5";
}
//定义了一个静态内部类 TextNote它实现了接口 DataColumns。
// 这个内部类包含了一些常量和字段,用于表示文本笔记的相关信息。
public static final class TextNote implements DataColumns {
/**
* Mode to indicate the text in check list mode or not
* <P> Type: Integer 1:check list mode 0: normal mode </P>
*/
public static final String MODE = DATA1;
//用于指示文本笔记是否处于待办事项列表模式的模式。值为 1 表示待办事项列表模式,值为 0 表示普通模式
public static final int MODE_CHECK_LIST = 1;//待办事项列表模式
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/text_note";//内容类型,用于表示文本笔记的目录类型。
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/text_note";//内容项类型,用于表示文本笔记的项目类型。
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/text_note");//用于表示文本笔记的内容 URI
}
//静态内部类 CallNote它也实现了接口 DataColumns。这个内部类包含了一些常量和字段用于表示通话记录的相关信息。
public static final class CallNote implements DataColumns {
/**
* Call date for this record
* <P> Type: INTEGER (long) </P>
*/
public static final String CALL_DATE = DATA1;//通话日期
/**
* Phone number for this record
* <P> Type: TEXT </P>
*/
public static final String PHONE_NUMBER = DATA3;//电话号码
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/call_note";//内容类型,用于表示通话记录的目录类型。
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/call_note";//内容项类型,用于表示通话记录的项目类型。
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/call_note");//用于表示通话记录的内容 URI
}
}

@ -1,146 +0,0 @@
/*
* 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.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import net.micode.notes.R;
import net.micode.notes.ui.NotesListActivity;
import net.micode.notes.ui.NotesPreferenceActivity;
//定义一个异步任务类GTaskASyncTask用于在后台线程执行Google Tasks同步操作
public class GTaskASyncTask extends AsyncTask<Void, String, Integer> {
private static int GTASK_SYNC_NOTIFICATION_ID = 5234235;// 定义一个静态常量用于通知的唯一ID
// 定义一个接口,用于在同步完成时回调
public interface OnCompleteListener {
void onComplete();
}
private Context mContext;// 定义成员变量,存储上下文环境
private NotificationManager mNotifiManager;// 定义NotificationManager实例用于管理通知
private GTaskManager mTaskManager; // 定义GTaskManager实例负责与Google Tasks服务交互
private OnCompleteListener mOnCompleteListener;// 定义监听器当同步完成时触发onComplete方法
// 构造函数,初始化相关组件和变量
public GTaskASyncTask(Context context, OnCompleteListener listener) {
mContext = context;// 设置应用程序上下文
mOnCompleteListener = listener;// 设置完成监听器
mNotifiManager = (NotificationManager)mContext
.getSystemService(Context.NOTIFICATION_SERVICE);// 获取系统通知管理器
mTaskManager = GTaskManager.getInstance();// 获取GTaskManager单例
}
// 取消正在进行的同步任务
public void cancelSync() {
mTaskManager.cancelSync();
}
// 发布进度更新消息
public void publishProgess(String message) {
publishProgress(new String[]{
message
});// 调用父类方法发布进度更新
}
// private void showNotification(int tickerId, String content) {
// Notification notification = new Notification(R.drawable.notification, mContext
// .getString(tickerId), System.currentTimeMillis());
// notification.defaults = Notification.DEFAULT_LIGHTS;// notification.flags = Notification.FLAG_AUTO_CANCEL;
// PendingIntent pendingIntent;
// pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext,
// NotesPreferenceActivity.class), 0);
// } else {
// pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext,
// NotesListActivity.class), 0);
// }
// notification.setLatestEventInfo(mContext, mContext.getString(R.string.app_name), content,
// pendingIntent);
// mNotifiManager.notify(GTASK_SYNC_NOTIFICATION_ID, notification);
// }
// 创建并显示通知的方法
private void showNotification(int tickerId, String content) {
PendingIntent 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);// 同步成功后的通知跳转至笔记列表界面
}
Notification.Builder builder = new Notification.Builder(mContext)
.setAutoCancel(true)
.setContentTitle(mContext.getString(R.string.app_name))
.setContentText(content)
.setContentIntent(pendingIntent)
.setWhen(System.currentTimeMillis())
.setOngoing(true);
Notification notification = builder.getNotification();
mNotifiManager.notify(GTASK_SYNC_NOTIFICATION_ID, notification);// 显示通知
}
@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]);// 如果当前上下文是GTaskSyncService则发送广播更新
}
}
@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) { // 若存在监听器则在新线程中触发onComplete方法
new Thread(new Runnable(){
public void run() {
mOnCompleteListener.onComplete();
}
}).start();
}
}
}

@ -1,948 +0,0 @@
/*
* 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;
//GTaskManager类用于管理小米便签与Google Tasks之间的同步逻辑
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; // Google任务列表哈希映射
private HashMap<String, Node> mGTaskHashMap; // Google任务哈希映射
private HashMap<String, MetaData> mMetaHashMap; // 元数据哈希映射
private TaskList mMetaList; // 元数据列表
private HashSet<Long> mLocalDeleteIdMap; // 本地删除ID集合
private HashMap<String, Long> mGidToNid; // Google ID到本地ID的映射
private HashMap<Long, String> mNidToGid; // 本地ID到Google ID的映射
// 私有构造函数,初始化各变量
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) {
// 用于获取授权令牌
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();
// 登录Google任务
if (!mCancelled) {
if (!client.login(mActivity)) {
throw new NetworkFailureException("login google task failed");
}
}
// 从Google获取任务列表
asyncTask.publishProgess(mContext.getString(R.string.sync_progress_init_list));
initGTaskList();
// 执行内容同步工作
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 实例
GTaskClient client = GTaskClient.getInstance();
try {
// 获取任务列表的 JSON 数组
JSONArray jsTaskLists = client.getTaskLists();
// 初始化元数据列表
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);
// 加载元数据
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) {
// 将元数据与相关的元数据 ID 映射存储
mMetaHashMap.put(metaData.getRelatedGid(), metaData);
}
}
}
}
}
// 如果元数据列表为空,则创建一个新的元数据列表
if (mMetaList == null) {
mMetaList = new TaskList();
mMetaList.setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_META);
GTaskClient.getInstance().createTaskList(mMetaList);
}
// 初始化任务列表
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);
// 如果任务列表以 MIUI_FOLDER_PREFIX 开头且不是元数据文件夹
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);
// 加载任务
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; // 游标对象初始化为null
String gid; // Google Task ID
Node node; // 节点对象
mLocalDeleteIdMap.clear(); // 清空本地删除 ID 映射表
if (mCancelled) { // 如果同步已取消,则直接返回
return;
}
// 处理本地已删除的便签
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)); // 将本地删除 ID 添加到映射表中
}
} else {
Log.w(TAG, "failed to query trash folder");
}
} finally {
if (c != null) {
c.close(); // 关闭游标
c = null;
}
}
// 先同步文件夹
syncFolder();
// 处理数据库中存在的便签
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) {
// 本地添加
syncType = Node.SYNC_ACTION_ADD_REMOTE;
} else {
// 远程删除
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;
}
}
// 处理剩余的项
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所以我们需要逐个检查
// 清除本地删除表
if (!mCancelled) {
if (!DataUtils.batchDeleteNotes(mContentResolver, mLocalDeleteIdMap)) {
throw new ActionFailureException("failed to batch-delete local deleted notes");
}
}
// 刷新本地同步 ID
if (!mCancelled) {
GTaskClient.getInstance().commitUpdate();
refreshLocalSyncId();
}
}
private void syncFolder() throws NetworkFailureException {
Cursor c = null; // 定义游标对象
String gid; // Google 任务 ID
Node node; // 节点对象
int syncType; // 同步类型
if (mCancelled) { // 如果已取消同步,则直接返回
return;
}
// 对根文件夹进行处理
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);
// 对系统文件夹,仅在必要时更新远程名称
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;
}
}
// 对通话便签文件夹进行处理
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
// 对系统文件夹,仅在必要时更新远程名称
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;
}
}
// 对本地存在的文件夹进行处理
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;
}
}
// 遍历远程任务列表映射mGTaskListHashMap处理每个映射项
Iterator<Map.Entry<String, TaskList>> iter = mGTaskListHashMap.entrySet().iterator();
while (iter.hasNext()) {
// 获取当前映射项
Map.Entry<String, TaskList> entry = iter.next();
// 提取映射项的键Google 任务 ID与值TaskList 对象)
gid = entry.getKey();
node = entry.getValue();
// 检查本地任务哈希表mGTaskHashMap中是否存在该 Google 任务 ID
if (mGTaskHashMap.containsKey(gid)) {
// 若存在,从本地任务哈希表中移除该 ID 对应的任务
mGTaskHashMap.remove(gid);
// 执行内容同步操作,将远程任务添加到本地
doContentSync(Node.SYNC_ACTION_ADD_LOCAL, node, null);
}
}
// 如果同步过程未被取消
if (!mCancelled) {
// 提交更新至 Google 任务服务端
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:
// 获取待删除任务的元数据MetaData
meta = mMetaHashMap.get(c.getString(SqlNote.GTASK_ID_COLUMN));
// 如果元数据存在,调用 Google 任务客户端删除该任务
if (meta != null) {
GTaskClient.getInstance().deleteNode(meta);
}
// 将待删除的本地任务 ID 添加到本地删除 ID 集合
mLocalDeleteIdMap.add(c.getLong(SqlNote.ID_COLUMN));
break;
case Node.SYNC_ACTION_DEL_REMOTE:
// 获取待删除任务的元数据MetaData
meta = mMetaHashMap.get(node.getGid());
// 如果元数据存在,调用 Google 任务客户端删除该任务
if (meta != null) {
GTaskClient.getInstance().deleteNode(meta);
}
// 调用 Google 任务客户端删除指定任务
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:
// 处理冲突:目前仅使用本地更新简单处理
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;
// 处理不同类型的节点TaskList 或 Task
if (node instanceof TaskList) {
// 节点为 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 {
// 节点为 Task 类型
sqlNote = new SqlNote(mContext);
// 获取节点的本地 JSON 表示,并处理 ID 冲突
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)) {
// 当前 ID 已存在,需创建新 ID
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)) {
// 数据 ID 已存在,需创建新 ID
data.remove(DataColumns.ID);
}
}
}
}
} catch (JSONException e) {
Log.w(TAG, e.toString());
e.printStackTrace();
}
sqlNote.setContent(js);
// 设置父任务 ID
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());
}
// 创建本地节点
sqlNote.setGtaskId(node.getGid());
sqlNote.commit(false);
// 更新 GID-NID 映射
mGidToNid.put(node.getGid(), sqlNote.getId());
mNidToGid.put(sqlNote.getId(), node.getGid());
// 更新远程元数据
updateRemoteMeta(node.getGid(), sqlNote);
}
private void updateLocalNode(Node node, Cursor c) throws NetworkFailureException {
if (mCancelled) {
// 如果同步过程已被取消,直接返回,不再执行后续操作
return;
}
SqlNote sqlNote;
// 1. 创建并初始化 SqlNote 对象,用于更新本地节点
// 使用传入的上下文 mContext 和游标 c 初始化 SqlNote 实例
sqlNote = new SqlNote(mContext, c);
// 2. 设置 SqlNote 的内容为当前节点的本地 JSON 表示
// 调用 Node 对象的 getLocalJSONFromContent() 方法获取其本地 JSON 表示,并设置到 SqlNote 对象中
sqlNote.setContent(node.getLocalJSONFromContent());
// 3. 获取待更新节点的父任务 ID
// 根据节点类型Task 或其他类型)区分处理:
// - 如果节点是 Task 类型,通过 mGidToNid 映射表获取其父任务的 Google ID再转换为本地 ID
// - 否则,将其父任务 ID 默认设置为根文件夹 IDNotes.ID_ROOT_FOLDER
Long parentId;
if (node instanceof Task) {
parentId = mGidToNid.get(((Task) node).getParent().getGid());
} else {
parentId = new Long(Notes.ID_ROOT_FOLDER);
}
// 4. 检查父任务 ID 是否有效
// 如果父任务 ID 为 null说明无法找到对应的本地父任务记录错误信息并抛出异常
if (parentId == null) {
Log.e(TAG, "cannot find task's parent id locally");
throw new ActionFailureException("cannot update local node");
}
// 5. 设置 SqlNote 的父任务 ID并提交更新
// 将计算得到的父任务 ID 设置到 SqlNote 对象中,然后调用 commit(true) 方法提交更新至本地数据库
sqlNote.setParentId(parentId.longValue());
sqlNote.commit(true);
// 6. 更新远程元数据
// 调用 updateRemoteMeta() 方法,同步更新远程元数据以保持与本地数据一致
updateRemoteMeta(node.getGid(), sqlNote);
}
private void addRemoteNode(Node node, Cursor c) throws NetworkFailureException {
if (mCancelled) {
// 如果同步过程已被取消,直接返回,不再执行后续操作
return;
}
SqlNote sqlNote = new SqlNote(mContext, c);
Node n;
// 1. 根据 SqlNote 类型(任务或文件夹),执行相应的远程添加操作
// 如果 SqlNote 是任务类型
if (sqlNote.isNoteType()) {
// 创建 Task 对象并设置内容
Task task = new Task();
task.setContentByLocalJSON(sqlNote.getContent());
// 获取父任务列表的 Google ID
String parentGid = mNidToGid.get(sqlNote.getParentId());
// 检查父任务列表 ID 是否有效
if (parentGid == null) {
Log.e(TAG, "cannot find task's parent tasklist");
throw new ActionFailureException("cannot add remote task");
}
// 在对应的父任务列表中添加子任务
mGTaskListHashMap.get(parentGid).addChildTask(task);
// 调用 Google 任务客户端创建远程任务
GTaskClient.getInstance().createTask(task);
// 将 Task 对象赋值给 Node 变量 n
n = (Node) task;
// 更新远程元数据
updateRemoteMeta(task.getGid(), sqlNote);
} else {
// 如果 SqlNote 是文件夹类型
// 初始化 TaskList 变量 tasklist用于存储已匹配或新建的远程文件夹
TaskList tasklist = null;
// 构建文件夹名称
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;
}
}
// 如果未找到匹配的文件夹,创建新的 TaskList 对象并添加到远程
if (tasklist == null) {
tasklist = new TaskList();
tasklist.setContentByLocalJSON(sqlNote.getContent());
GTaskClient.getInstance().createTaskList(tasklist);
mGTaskListHashMap.put(tasklist.getGid(), tasklist);
}
// 将 TaskList 对象赋值给 Node 变量 n
n = (Node) tasklist;
}
// 2. 更新本地 SqlNote 记录
// 设置 SqlNote 的 Google ID
sqlNote.setGtaskId(n.getGid());
// 提交更新false不触发本地更新
sqlNote.commit(false);
// 清除本地修改标志
sqlNote.resetLocalModified();
// 提交更新true触发本地更新
sqlNote.commit(true);
// 3. 更新 GID-NID 映射表
// 将新添加的远程节点的 Google ID 和本地 ID 添加到映射表
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);
// 1. 更新远程节点
// 根据 SqlNote 设置 Node 对象的内容
node.setContentByLocalJSON(sqlNote.getContent());
// 调用 Google 任务客户端更新远程节点
GTaskClient.getInstance().addUpdateNode(node);
// 更新远程元数据
updateRemoteMeta(node.getGid(), sqlNote);
// 2. 若为任务类型,检查是否需要移动任务
if (sqlNote.isNoteType()) {
Task task = (Task) node;
TaskList preParentList = task.getParent();
// 获取当前父任务列表的 Google ID
String curParentGid = mNidToGid.get(sqlNote.getParentId());
// 检查当前父任务列表 ID 是否有效
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);
}
}
// 3. 清除本地修改标志并提交更新
// 清除 SqlNote 的本地修改标志
sqlNote.resetLocalModified();
// 提交更新true触发本地更新
sqlNote.commit(true);
}
private void updateRemoteMeta(String gid, SqlNote sqlNote) throws NetworkFailureException {
if (sqlNote != null && sqlNote.isNoteType()) {
// 1. 获取或创建 MetaData 对象
MetaData metaData = mMetaHashMap.get(gid);
if (metaData != null) {
// 若已存在 MetaData更新其内容并添加到更新队列
metaData.setMeta(gid, sqlNote.getContent());
GTaskClient.getInstance().addUpdateNode(metaData);
} else {
// 若不存在 MetaData创建新的 MetaData 对象,设置内容,添加到元数据列表、映射表和更新队列
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;
}
// 1. 获取最新的 Google 任务列表
// 清空本地缓存的 Google 任务哈希表、Google 任务列表哈希表和元数据哈希表
mGTaskHashMap.clear();
mGTaskListHashMap.clear();
mMetaHashMap.clear();
// 初始化 Google 任务列表
initGTaskList();
// 2. 查询本地便签数据,更新同步 ID
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) {
// 遍历查询结果,更新每个便签的同步 ID
while (c.moveToNext()) {
// 获取便签的 Google ID
String gid = c.getString(SqlNote.GTASK_ID_COLUMN);
// 查找对应的 Google 任务
Node node = mGTaskHashMap.get(gid);
if (node != null) {
// 移除已处理的 Google 任务
mGTaskHashMap.remove(gid);
// 构建用于更新同步 ID 的 ContentValues
ContentValues values = new ContentValues();
values.put(NoteColumns.SYNC_ID, node.getLastModified());
// 更新便签的同步 ID
mContentResolver.update(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI,
c.getLong(SqlNote.ID_COLUMN)), values, null, null);
} else {
// 记录错误信息并抛出异常,表示本地便签缺少 Google ID
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() {
// 返回 Google 任务客户端使用的同步账户名
return GTaskClient.getInstance().getSyncAccount().name;
}
public void cancelSync() {
// 标记同步过程已取消
mCancelled = true;
}
}

@ -0,0 +1,132 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/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" />
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
<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:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Notesmaster"
tools:targetApi="31">
<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>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ui.NoteEditActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:launchMode="singleTop"
android:theme="@style/NoteTheme"
android:windowSoftInputMode="adjustPan"
android:exported="true">
<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 >
<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>
<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>
<provider
android:name="net.micode.notes.data.NotesProvider"
android:authorities="micode_notes"
android:multiprocess="true" />
<receiver
android:name=".widget.NoteWidgetProvider_2x"
android:label="@string/app_widget2x2"
android:exported="true">
<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>
<receiver
android:name=".widget.NoteWidgetProvider_4x"
android:label="@string/app_widget4x4"
android:exported="true">
<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>
<receiver
android:name="net.micode.notes.ui.AlarmReceiver"
android:process=":remote" >
</receiver>
<activity
android:name=".ui.AlarmAlertActivity"
android:label="@string/app_name"
android:launchMode="singleInstance"
android:theme="@android:style/Theme.Holo.Wallpaper.NoTitleBar" >
</activity>
<activity
android:name="net.micode.notes.ui.NotesPreferenceActivity"
android:label="@string/preferences_title"
android:launchMode="singleTop"
android:theme="@android:style/Theme.Holo.Light" >
</activity>
<service
android:name="net.micode.notes.gtask.remote.GTaskSyncService"
android:exported="false" >
</service>
<meta-data
android:name="android.app.default_searchable"
android:value=".ui.NoteEditActivity" />
<!-- <activity-->
<!-- android:name=".MainActivity"-->
<!-- android:exported="true">-->
<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.MAIN" />-->
<!-- <category android:name="android.intent.category.LAUNCHER" />-->
<!-- </intent-filter>-->
<!-- </activity>-->
</application>
</manifest>

@ -0,0 +1,24 @@
package net.micode.notes;
import android.os.Bundle;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
}
}

@ -0,0 +1,93 @@
/*
* 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;
/**
*
*
*/
public class Contact {
// 联系人缓存,存储电话号码与联系人姓名的映射关系
private static HashMap<String, String> sContactCache;
// 日志标签
private static final String TAG = "Contact";
// 来电者ID查询条件
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 = '+')";
/**
*
* @param context
* @param phoneNumber
* @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);
}
// 构建查询条件替换min_match的值
String selection = CALLER_ID_SELECTION.replace("+",
PhoneNumberUtils.toCallerIDMinMatch(phoneNumber));
// 查询联系人数据
Cursor cursor = context.getContentResolver().query(
Data.CONTENT_URI,
new String [] { Phone.DISPLAY_NAME },
selection,
new String[] { phoneNumber },
null);
// 处理查询结果
if (cursor != null && cursor.moveToFirst()) {
try {
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 {
Log.d(TAG, "No contact matched with number:" + phoneNumber);
return null;
}
}
}

@ -0,0 +1,305 @@
/*
* 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;
/**
*
* IDUri
*/
public class Notes {
// 内容提供者的授权者
public static final String AUTHORITY = "micode_notes";
// 日志标签
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; // 系统类型
/**
*
* {@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; // 根文件夹
public static final int ID_TEMPARAY_FOLDER = -1; // 临时文件夹
public static final int ID_CALL_RECORD_FOLDER = -2; // 通话记录文件夹
public static final int ID_TRASH_FOLER = -3; // 回收站文件夹
// Intent额外数据常量
public static final String INTENT_EXTRA_ALERT_DATE = "net.micode.notes.alert_date"; // 提醒日期
public static final String INTENT_EXTRA_BACKGROUND_ID = "net.micode.notes.background_color_id"; // 背景颜色ID
public static final String INTENT_EXTRA_WIDGET_ID = "net.micode.notes.widget_id"; // 小组件ID
public static final String INTENT_EXTRA_WIDGET_TYPE = "net.micode.notes.widget_type"; // 小组件类型
public static final String INTENT_EXTRA_FOLDER_ID = "net.micode.notes.folder_id"; // 文件夹ID
public static final String INTENT_EXTRA_CALL_DATE = "net.micode.notes.call_date"; // 通话日期
// 小组件类型常量
public static final int TYPE_WIDGET_INVALIDE = -1; // 无效小组件
public static final int TYPE_WIDGET_2X = 0; // 2X大小的小组件
public static final int TYPE_WIDGET_4X = 1; // 4X大小的小组件
/**
*
*/
public static class DataConstants {
public static final String NOTE = TextNote.CONTENT_ITEM_TYPE; // 文本笔记类型
public static final String CALL_NOTE = CallNote.CONTENT_ITEM_TYPE; // 通话笔记类型
}
/**
* Uri
*/
public static final Uri CONTENT_NOTE_URI = Uri.parse("content://" + AUTHORITY + "/note");
/**
* Uri
*/
public static final Uri CONTENT_DATA_URI = Uri.parse("content://" + AUTHORITY + "/data");
/**
*
*/
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";
/**
* ID
* <P> : INTEGER (long) </P>
*/
public static final String WIDGET_ID = "widget_id";
/**
*
* <P> : INTEGER (long) </P>
*/
public static final String WIDGET_TYPE = "widget_type";
/**
* ID
* <P> : INTEGER (long) </P>
*/
public static final String BG_COLOR_ID = "bg_color_id";
/**
* ID
* 0:
* 1:
* 2:
* 3:
* <P> : INTEGER </P>
*/
public static final String CATEGORY_ID = "category_id";
/**
*
* <P> : INTEGER </P>
*/
public static final String HAS_ATTACHMENT = "has_attachment";
/**
*
* <P> : INTEGER (long) </P>
*/
public static final String NOTES_COUNT = "notes_count";
/**
*
* <P> : INTEGER </P>
*/
public static final String TYPE = "type";
/**
* ID
* <P> : INTEGER (long) </P>
*/
public static final String SYNC_ID = "sync_id";
/**
*
* <P> : INTEGER </P>
*/
public static final String LOCAL_MODIFIED = "local_modified";
/**
* ID
* <P> : INTEGER </P>
*/
public static final String ORIGIN_PARENT_ID = "origin_parent_id";
/**
* gtask ID
* <P> : TEXT </P>
*/
public static final String GTASK_ID = "gtask_id";
/**
*
* <P> : INTEGER (long) </P>
*/
public static final String VERSION = "version";
}
/**
*
*/
public interface DataColumns {
/**
* ID
* <P> : INTEGER (long) </P>
*/
public static final String ID = "_id";
/**
* MIME
* <P> : Text </P>
*/
public static final String MIME_TYPE = "mime_type";
/**
* ID
* <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";
/**
*
* <P> : INTEGER </P>
*/
public static final String DATA1 = "data1";
/**
*
* <P> : INTEGER </P>
*/
public static final String DATA2 = "data2";
/**
*
* <P> : TEXT </P>
*/
public static final String DATA3 = "data3";
/**
*
* <P> : TEXT </P>
*/
public static final String DATA4 = "data4";
/**
*
* <P> : TEXT </P>
*/
public static final String DATA5 = "data5";
}
/**
* DataColumns
*/
public static final class TextNote implements DataColumns {
/**
* checklist
* <P> : Integer 1:check list 0: </P>
*/
public static final String MODE = DATA1;
public static final int MODE_CHECK_LIST = 1; // checklist模式
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/text_note"; // 文本笔记集合的MIME类型
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/text_note"; // 单个文本笔记的MIME类型
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/text_note"); // 文本笔记的Uri
}
/**
* DataColumns
*/
public static final class CallNote implements DataColumns {
/**
*
* <P> : INTEGER (long) </P>
*/
public static final String CALL_DATE = DATA1;
/**
*
* <P> : TEXT </P>
*/
public static final String PHONE_NUMBER = DATA3;
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/call_note"; // 通话笔记集合的MIME类型
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/call_note"; // 单个通话笔记的MIME类型
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/call_note"); // 通话笔记的Uri
}
}

@ -26,57 +26,62 @@ import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.DataConstants;
import net.micode.notes.data.Notes.NoteColumns;
/**
* SQLiteOpenHelper
*
*/
public class NotesDatabaseHelper extends SQLiteOpenHelper {
// 数据库名称
private static final String DB_NAME = "note.db";
//表示数据库的名称为 "note.db"。这个常量用于指定应用程序中使用的数据库的名称。
private static final int DB_VERSION = 4;
//表示数据库的版本号为 4。
// 数据库版本
private static final int DB_VERSION = 5;
public interface TABLE { //接口
public static final String NOTE = "note";
//表示数据库中的一个表名为 "note"。这个常量用于指定应用程序中使用的表名。
public static final String DATA = "data";
//表示数据库中的一个表名为 "data"。这个常量也用于指定应用程序中使用的表名。
/**
*
*/
public interface TABLE {
public static final String NOTE = "note"; // 笔记表
public static final String DATA = "data"; // 数据表
}
private static final String TAG = "NotesDatabaseHelper"; //日志标签
// 日志标签
private static final String TAG = "NotesDatabaseHelper";
private static NotesDatabaseHelper mInstance;//类静态变量
// 单例实例
private static NotesDatabaseHelper mInstance;
//创建一个名为"NOTE"的数据库表的SQL语句
// 创建笔记表的SQL语句
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.WIDGET_ID + " INTEGER NOT NULL DEFAULT 0," +//小部件ID
NoteColumns.WIDGET_TYPE + " INTEGER NOT NULL DEFAULT -1," +//小部件类型
NoteColumns.SYNC_ID + " INTEGER NOT NULL DEFAULT 0," +//同步ID
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 ''," +//文本类型
NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0" +//版本
NoteColumns.ID + " INTEGER PRIMARY KEY," +
NoteColumns.PARENT_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.ALERTED_DATE + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.BG_COLOR_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.CATEGORY_ID + " INTEGER NOT NULL DEFAULT 0," +
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.WIDGET_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.WIDGET_TYPE + " INTEGER NOT NULL DEFAULT -1," +
NoteColumns.SYNC_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.LOCAL_MODIFIED + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.ORIGIN_PARENT_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''," +
NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0" +
")";
//创建一个名为"DATA"的数据表的SQL语句
// 创建数据表的SQL语句
private static final String CREATE_DATA_TABLE_SQL =
"CREATE TABLE " + TABLE.DATA + "(" +
DataColumns.ID + " INTEGER PRIMARY KEY," +//ID,主键
DataColumns.MIME_TYPE + " TEXT NOT NULL," +//MIME类型
DataColumns.NOTE_ID + " INTEGER NOT NULL DEFAULT 0," +//笔记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.ID + " INTEGER PRIMARY KEY," +
DataColumns.MIME_TYPE + " TEXT NOT NULL," +
DataColumns.NOTE_ID + " INTEGER NOT NULL DEFAULT 0," +
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," +
DataColumns.DATA2 + " INTEGER," +
DataColumns.DATA3 + " TEXT NOT NULL DEFAULT ''," +
@ -84,16 +89,14 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
DataColumns.DATA5 + " TEXT NOT NULL DEFAULT ''" +
")";
//在"DATA"的数据库表上创建一个名为"note_id_index"的索引的SQL语句。
// 该索引是基于"NOTE_ID"字段的旨在提高根据笔记ID进行查询时的检索效率。
// 为data表的note_id列创建索引的SQL语句
private static final String CREATE_DATA_NOTE_ID_INDEX_SQL =
"CREATE INDEX IF NOT EXISTS note_id_index ON " +
TABLE.DATA + "(" + DataColumns.NOTE_ID + ");";
/**
* Increase folder's note count when move note to the folder
*
*/
//创建一个触发器,在"NOTE"表中的"parent_id"字段更新之后执行目的是在更新父文件夹ID后自动增加相应文件夹的笔记计数notes_count
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 +
@ -104,9 +107,8 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
" END";
/**
* Decrease folder's note count when move note from folder
*
*/
//创建一个触发器,在"NOTE"表中的"parent_id"字段更新之后执行目的是在更新父文件夹ID后自动减少相应文件夹的笔记计数notes_count
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 +
@ -118,9 +120,8 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
" END";
/**
* Increase folder's note count when insert new note to the folder
*
*/
//在"NOTE"表中插入新记录之后执行目的是在新笔记被添加到特定文件夹时自动增加该文件夹的笔记计数notes_count
private static final String NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER =
"CREATE TRIGGER increase_folder_count_on_insert " +
" AFTER INSERT ON " + TABLE.NOTE +
@ -131,9 +132,8 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
" END";
/**
* Decrease folder's note count when delete note from the folder
*
*/
//在删除笔记时自动减少相应父文件夹的笔记计数notes_count
private static final String NOTE_DECREASE_FOLDER_COUNT_ON_DELETE_TRIGGER =
"CREATE TRIGGER decrease_folder_count_on_delete " +
" AFTER DELETE ON " + TABLE.NOTE +
@ -145,9 +145,8 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
" END";
/**
* Update note's content when insert data with type {@link DataConstants#NOTE}
* {@link DataConstants#NOTE}
*/
//向"DATA"表中插入记录之后执行当插入数据的MIME类型为"NOTE"时会更新"NOTE"表中相应笔记的摘要snippet字段。
private static final String DATA_UPDATE_NOTE_CONTENT_ON_INSERT_TRIGGER =
"CREATE TRIGGER update_note_content_on_insert " +
" AFTER INSERT ON " + TABLE.DATA +
@ -159,9 +158,8 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
" END";
/**
* Update note's content when data with {@link DataConstants#NOTE} type has changed
* {@link DataConstants#NOTE}
*/
//在更新"DATA"表中的记录之后执行当更新数据的MIME类型为"NOTE"时,会更新"NOTE"表中相应笔记的摘要snippet字段。
private static final String DATA_UPDATE_NOTE_CONTENT_ON_UPDATE_TRIGGER =
"CREATE TRIGGER update_note_content_on_update " +
" AFTER UPDATE ON " + TABLE.DATA +
@ -173,9 +171,8 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
" END";
/**
* Update note's content when data with {@link DataConstants#NOTE} type has deleted
* {@link DataConstants#NOTE}
*/
//在删除"DATA"表中的记录之后执行当被删除数据的MIME类型为"NOTE"时会将对应笔记的摘要snippet字段置空。
private static final String DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER =
"CREATE TRIGGER update_note_content_on_delete " +
" AFTER delete ON " + TABLE.DATA +
@ -187,9 +184,8 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
" END";
/**
* Delete datas belong to note which has been deleted
*
*/
//在删除"NOTE"表中的记录之后执行,会同时删除"DATA"表中与被删除笔记相关联的记录。
private static final String NOTE_DELETE_DATA_ON_DELETE_TRIGGER =
"CREATE TRIGGER delete_data_on_delete " +
" AFTER DELETE ON " + TABLE.NOTE +
@ -199,9 +195,8 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
" END";
/**
* Delete notes belong to folder which has been deleted
*
*/
//触发器在删除"NOTE"表中的记录之后执行,会同时删除"NOTE"表中与被删除文件夹相关联的所有笔记记录。
private static final String FOLDER_DELETE_NOTES_ON_DELETE_TRIGGER =
"CREATE TRIGGER folder_delete_notes_on_delete " +
" AFTER DELETE ON " + TABLE.NOTE +
@ -211,9 +206,8 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
" END";
/**
* Move notes belong to folder which has been moved to trash folder
*
*/
//更新"NOTE"表中的记录之后执行当将笔记移动到垃圾箱Trash Folder会将相关笔记的父级IDParent ID更新为垃圾箱的ID。
private static final String FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER =
"CREATE TRIGGER folder_move_notes_on_trash " +
" AFTER UPDATE ON " + TABLE.NOTE +
@ -224,21 +218,30 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
" WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" +
" END";
/**
*
* @param context
*/
public NotesDatabaseHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}//调用了父类 SQLiteOpenHelper 的构造函数
}
//数据库中创建笔记表
/**
*
* @param db
*/
public void createNoteTable(SQLiteDatabase db) {
db.execSQL(CREATE_NOTE_TABLE_SQL);
reCreateNoteTableTriggers(db);//重新创建与笔记表相关的触发器
createSystemFolder(db);//在数据库中创建系统文件夹
reCreateNoteTableTriggers(db);
createSystemFolder(db);
Log.d(TAG, "note table has been created");
}
//重新创建与笔记表相关的触发器
/**
*
* @param db
*/
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");
@ -246,7 +249,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
db.execSQL("DROP TRIGGER IF EXISTS increase_folder_count_on_insert");
db.execSQL("DROP TRIGGER IF EXISTS folder_delete_notes_on_delete");
db.execSQL("DROP TRIGGER IF EXISTS folder_move_notes_on_trash");
//重新创建与笔记表相关的触发器
db.execSQL(NOTE_INCREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER);
db.execSQL(NOTE_DECREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER);
db.execSQL(NOTE_DECREASE_FOLDER_COUNT_ON_DELETE_TRIGGER);
@ -256,23 +259,22 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
db.execSQL(FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER);
}
//在数据库中创建系统文件夹
/**
*
* @param db
*/
private void createSystemFolder(SQLiteDatabase db) {
ContentValues values = new ContentValues();
// 创建通话记录文件夹:向 Note 表插入一条记录,指定 ID 为 Notes.ID_CALL_RECORD_FOLDER类型为系统类型。
// 创建根文件夹:向 Note 表插入一条记录,指定 ID 为 Notes.ID_ROOT_FOLDER类型为系统类型。
// 创建临时文件夹:向 Note 表插入一条记录,指定 ID 为 Notes.ID_TEMPARAY_FOLDER类型为系统类型。
// 创建垃圾箱文件夹:向 Note 表插入一条记录,指定 ID 为 Notes.ID_TRASH_FOLER类型为系统类型。
/**
* call record foler for call notes
*
*/
values.put(NoteColumns.ID, Notes.ID_CALL_RECORD_FOLDER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
/**
* root folder which is default folder
*
*/
values.clear();
values.put(NoteColumns.ID, Notes.ID_ROOT_FOLDER);
@ -280,7 +282,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
db.insert(TABLE.NOTE, null, values);
/**
* temporary folder which is used for moving note
*
*/
values.clear();
values.put(NoteColumns.ID, Notes.ID_TEMPARAY_FOLDER);
@ -288,7 +290,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
db.insert(TABLE.NOTE, null, values);
/**
* create trash folder
*
*/
values.clear();
values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER);
@ -296,15 +298,21 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
db.insert(TABLE.NOTE, null, values);
}
//在数据库中创建DATA表
/**
*
* @param db
*/
public void createDataTable(SQLiteDatabase db) {
db.execSQL(CREATE_DATA_TABLE_SQL);//创建数据表
reCreateDataTableTriggers(db);//重新创建与DATA表相关的触发器
db.execSQL(CREATE_DATA_NOTE_ID_INDEX_SQL);//索引
Log.d(TAG, "data table has been created");//日志
db.execSQL(CREATE_DATA_TABLE_SQL);
reCreateDataTableTriggers(db);
db.execSQL(CREATE_DATA_NOTE_ID_INDEX_SQL);
Log.d(TAG, "data table has been created");
}
//重新创建与DATA表相关的触发器
/**
*
* @param db
*/
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");
@ -315,7 +323,11 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER);
}
//用于获取 NotesDatabaseHelper 的单例实例
/**
*
* @param context
* @return NotesDatabaseHelper
*/
static synchronized NotesDatabaseHelper getInstance(Context context) {
if (mInstance == null) {
mInstance = new NotesDatabaseHelper(context);
@ -323,22 +335,30 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
return mInstance;
}
@Override//为什么重载
//创建数据库
/**
*
* @param db
*/
@Override
public void onCreate(SQLiteDatabase db) {
createNoteTable(db);
createDataTable(db);
}
/**
*
* @param db
* @param oldVersion
* @param newVersion
*/
@Override
//处理数据库的升级操作
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
boolean reCreateTriggers = false;
boolean skipV2 = false;
if (oldVersion == 1) {
upgradeToV2(db);
skipV2 = true; // this upgrade including the upgrade from v2 to v3
skipV2 = true; // 此升级包含从v2到v3的升级
oldVersion++;
}
@ -353,18 +373,26 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
oldVersion++;
}
if (reCreateTriggers) { //如果需要重新创建触发器
if (oldVersion == 4) {
upgradeToV5(db);
oldVersion++;
}
if (reCreateTriggers) {
reCreateNoteTableTriggers(db);
reCreateDataTableTriggers(db);
}
if (oldVersion != newVersion) { //如果数据库升级失败
if (oldVersion != newVersion) {
throw new IllegalStateException("Upgrade notes database to version " + newVersion
+ "fails");
}
}
//将数据库升级到版本 2
/**
* 2
* @param db
*/
private void upgradeToV2(SQLiteDatabase db) {
db.execSQL("DROP TABLE IF EXISTS " + TABLE.NOTE);
db.execSQL("DROP TABLE IF EXISTS " + TABLE.DATA);
@ -372,25 +400,40 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
createDataTable(db);
}
//将数据库升级到版本 3
/**
* 3
* @param db
*/
private void upgradeToV3(SQLiteDatabase db) {
// drop unused triggers
// 删除未使用的触发器
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");
// add a column for gtask id
// 添加gtask id列
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.GTASK_ID
+ " TEXT NOT NULL DEFAULT ''");
// add a trash system folder
// 添加回收站系统文件夹
ContentValues values = new ContentValues();
values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
}
//将数据库升级到版本 4
/**
* 4
* @param db
*/
private void upgradeToV4(SQLiteDatabase db) {
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.VERSION
+ " INTEGER NOT NULL DEFAULT 0");
}
}
/**
* 5
* @param db
*/
private void upgradeToV5(SQLiteDatabase db) {
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.CATEGORY_ID
+ " INTEGER NOT NULL DEFAULT 0");
}
}

@ -16,7 +16,6 @@
package net.micode.notes.data;
import android.app.SearchManager;
import android.content.ContentProvider;
import android.content.ContentUris;
@ -33,36 +32,32 @@ import net.micode.notes.R;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.NotesDatabaseHelper.TABLE;
//为存储和获取数据提供接口。可以在不同的应用程序之间共享数据
//ContentProvider提供的方法
//query查询
//insert插入
//update更新
//delete删除
//getType得到数据类型
/**
* ContentProvider
* CRUDContentProvider访
*/
public class NotesProvider extends ContentProvider {
// 用于将URI与相应代码匹配的UriMatcher
// Uri匹配器用于匹配不同的Uri请求
private static final UriMatcher mMatcher;
// 数据库助手,用于管理数据库操作
// 数据库帮助类实例
private NotesDatabaseHelper mHelper;
// 用于记录日志标签
// 日志标签
private static final String TAG = "NotesProvider";
// 用于定义不同URI模式的常量
private static final int URI_NOTE = 1;
private static final int URI_NOTE_ITEM = 2;
private static final int URI_DATA = 3;
private static final int URI_DATA_ITEM = 4;
private static final int URI_SEARCH = 5;
private static final int URI_SEARCH_SUGGEST = 6;
// Uri匹配器对应的匹配码
private static final int URI_NOTE = 1; // 匹配note集合的Uri
private static final int URI_NOTE_ITEM = 2; // 匹配单个note的Uri
private static final int URI_DATA = 3; // 匹配data集合的Uri
private static final int URI_DATA_ITEM = 4; // 匹配单个data的Uri
private static final int URI_SEARCH = 5; // 匹配搜索的Uri
private static final int URI_SEARCH_SUGGEST = 6; // 匹配搜索建议的Uri
// 静态初始化器用于填充UriMatcher
// 静态代码块,初始化UriMatcher并添加Uri匹配规则
static {
// 创建UriMatcher时调用UriMatcher(UriMatcher.NO_MATCH)表示不匹配任何路径的返回码
mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
// 把需要匹配Uri路径全部给注册上
mMatcher.addURI(Notes.AUTHORITY, "note", URI_NOTE);
mMatcher.addURI(Notes.AUTHORITY, "note/#", URI_NOTE_ITEM);
mMatcher.addURI(Notes.AUTHORITY, "data", URI_DATA);
@ -73,10 +68,10 @@ public class NotesProvider extends ContentProvider {
}
/**
* x'0A' represents the '\n' character in sqlite. For title and content in the search result,
* we will trim '\n' and white space in order to show more information.
* x'0A'sqlite'\n'
* '\n'
*/
// 声明 NOTES_SEARCH_PROJECTION
// 笔记搜索的投影列,定义了搜索结果需要返回的列
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 + ","
@ -84,57 +79,67 @@ public class NotesProvider extends ContentProvider {
+ 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;
// 声明NOTES_SNIPPET_SEARCH_QUERY
// 笔记片段搜索查询语句
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 true
*/
@Override
// Context只有在onCreate()中才被初始化
// 对mHelper进行实例化
public boolean onCreate() {
mHelper = NotesDatabaseHelper.getInstance(getContext());
return true;
}
/**
*
* @param uri Uri
* @param projection
* @param selection
* @param selectionArgs
* @param sortOrder
* @return Cursor
*/
@Override
// 查询uri在数据库中对应的位置
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
Cursor c = null;
// 获取可读数据库
SQLiteDatabase db = mHelper.getReadableDatabase();
String id = null;
// 匹配查找uri
// 根据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:
// 查询note表中指定id的数据
id = uri.getPathSegments().get(1);
// 根据提供的ID查询特定的笔记项
c = db.query(TABLE.NOTE, projection, NoteColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs, null, null, sortOrder);
break;
case URI_DATA:
// 在数据库中查询 "data"
// 查询data表的所有数据
c = db.query(TABLE.DATA, projection, selection, selectionArgs, null, null,
sortOrder);
break;
case URI_DATA_ITEM:
// 查询data表中指定id的数据
id = uri.getPathSegments().get(1);
// 根据提供的ID查询特定的数据项
c = db.query(TABLE.DATA, projection, DataColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs, null, null, sortOrder);
break;
case URI_SEARCH:
case URI_SEARCH_SUGGEST:
// 处理搜索和搜索建议查询
if (sortOrder != null || projection != null) {
// 不合法的参数异常
throw new IllegalArgumentException(
"do not specify sortOrder, selection, selectionArgs, or projection" + "with this query");
}
@ -142,8 +147,6 @@ public class NotesProvider extends ContentProvider {
String searchString = null;
if (mMatcher.match(uri) == URI_SEARCH_SUGGEST) {
if (uri.getPathSegments().size() > 1) {
// getPathSegments()方法得到一个String的List
// 在uri.getPathSegments().get(1)为第2个元素
searchString = uri.getPathSegments().get(1);
}
} else {
@ -155,6 +158,7 @@ public class NotesProvider extends ContentProvider {
}
try {
// 构建模糊查询字符串
searchString = String.format("%%%s%%", searchString);
c = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY,
new String[] { searchString });
@ -163,31 +167,33 @@ public class NotesProvider extends ContentProvider {
}
break;
default:
// 抛出异常
throw new IllegalArgumentException("Unknown URI " + uri);
}
// 设置通知Uri当数据变化时通知观察者
if (c != null) {
c.setNotificationUri(getContext().getContentResolver(), uri);
}
return c;
}
/**
*
* @param uri Uri
* @param values
* @return Uri
*/
@Override
// 插入一个uri
public Uri insert(Uri uri, ContentValues values) {
// 获得可写的数据库
SQLiteDatabase db = mHelper.getWritableDatabase();
long dataId = 0, noteId = 0, insertedId = 0;
// 根据Uri匹配结果执行不同的插入操作
switch (mMatcher.match(uri)) {
// 新增一个条目
case URI_NOTE:
// 如果 URI 匹配通用的 note URI
// 修改选择条件以确保仅删除 ID 大于 0 的笔记
// 插入数据到note表
insertedId = noteId = db.insert(TABLE.NOTE, null, values);
break;
// 如果存在查找NOTE_ID
case URI_DATA:
// 插入数据到data表
if (values.containsKey(DataColumns.NOTE_ID)) {
noteId = values.getAsLong(DataColumns.NOTE_ID);
} else {
@ -198,112 +204,114 @@ public class NotesProvider extends ContentProvider {
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
// Notify the note uri
// notifyChange获得一个ContextResolver对象并且更新里面的内容
// 通知数据变化
if (noteId > 0) {
getContext().getContentResolver().notifyChange(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null);
}
// Notify the data uri
if (dataId > 0) {
getContext().getContentResolver().notifyChange(
ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), null);
}
// 返回插入的uri的路径
return ContentUris.withAppendedId(uri, insertedId);
}
/**
*
* @param uri Uri
* @param selection
* @param selectionArgs
* @return
*/
@Override
// 删除一个uri
public int delete(Uri uri, String selection, String[] selectionArgs) {
//Uri代表要操作的数据Android上可用的每种资源 -包括 图像、视频片段、音频资源等都可以用Uri来表示。
int count = 0;
String id = null;
// 获得可写的数据库
SQLiteDatabase db = mHelper.getWritableDatabase();
boolean deleteData = false;
// 根据Uri匹配结果执行不同的删除操作
switch (mMatcher.match(uri)) {
case URI_NOTE:
// 删除note表中的数据
selection = "(" + selection + ") AND " + NoteColumns.ID + ">0 ";
count = db.delete(TABLE.NOTE, selection, selectionArgs);
break;
case URI_NOTE_ITEM:
// 如果 URI 匹配特定的 note 项,
// 从 URI 路径段中提取 ID
// 删除note表中指定id的数据
id = uri.getPathSegments().get(1);
/**
* ID that smaller than 0 is system folder which is not allowed to
* trash
* 0ID
*/
long noteId = Long.valueOf(id);
// 如果笔记 ID 小于或等于 0系统文件夹则退出 switch 语句
if (noteId <= 0) {
break;
}
// 否则,删除指定 ID 的笔记并应用选择条件
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 匹配特定的 data 项,从 URI 路径段中提取 ID
// 删除data表中指定id的数据
id = uri.getPathSegments().get(1);
// 删除指定 ID 的数据并应用选择条件
count = db.delete(TABLE.DATA,
DataColumns.ID + "=" + id + parseSelection(selection), selectionArgs);
deleteData = true;
break;
default:
// 如果 URI 不匹配任何已知模式,则抛出 IllegalArgumentException
throw new IllegalArgumentException("Unknown URI " + uri);
}
// 如果删除了任何行,则通知已注册的 ContentObservers
// 通知数据变化
if (count > 0) {
if (deleteData) {
getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null);
}
getContext().getContentResolver().notifyChange(uri, null);
}
// 返回删除的行数
return count;
}
/**
*
* @param uri Uri
* @param values
* @param selection
* @param selectionArgs
* @return
*/
@Override
// 更新一个uri
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
int count = 0;
String id = null;
SQLiteDatabase db = mHelper.getWritableDatabase();
boolean updateData = false;
// 根据Uri匹配结果执行不同的更新操作
switch (mMatcher.match(uri)) {
case URI_NOTE:
// 减少笔记版本号
// 更新note表中的数据先增加笔记版本号
increaseNoteVersion(-1, selection, selectionArgs);
// 更新 "note" 表中的数据
count = db.update(TABLE.NOTE, values, selection, selectionArgs);
break;
case URI_NOTE_ITEM:
// 更新note表中指定id的数据先增加笔记版本号
id = uri.getPathSegments().get(1);
// 减少特定笔记项的版本号
increaseNoteVersion(Long.valueOf(id), selection, selectionArgs);
// 更新 "note" 表中特定ID的数据
count = db.update(TABLE.NOTE, values, NoteColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs);
break;
case URI_DATA:
// 更新 "data" 表中的数据
// 更新data表中的数据
count = db.update(TABLE.DATA, values, selection, selectionArgs);
updateData = true;
break;
case URI_DATA_ITEM:
// 更新data表中指定id的数据
id = uri.getPathSegments().get(1);
// 更新 "data" 表中特定ID的数据
count = db.update(TABLE.DATA, values, DataColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs);
updateData = true;
@ -312,23 +320,31 @@ public class NotesProvider extends ContentProvider {
throw new IllegalArgumentException("Unknown URI " + uri);
}
// 通知数据变化
if (count > 0) {
if (updateData) {
// 通知数据内容观察者数据发生变化
getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null);
}
// 通知URI的观察者数据发生变化
getContext().getContentResolver().notifyChange(uri, null);
}
return count;
}
// 将字符串解析成规定格式
/**
*
* @param selection
* @return
*/
private String parseSelection(String selection) {
return (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : "");
}
//增加一个noteVersion
/**
*
* @param id id-1
* @param selection
* @param selectionArgs
*/
private void increaseNoteVersion(long id, String selection, String[] selectionArgs) {
StringBuilder sql = new StringBuilder(120);
sql.append("UPDATE ");
@ -351,14 +367,18 @@ public class NotesProvider extends ContentProvider {
sql.append(selectString);
}
// execSQL()方法可以执行insert、delete、update和CREATE TABLE之类有更改行为的SQL语句
mHelper.getWritableDatabase().execSQL(sql.toString());
}
/**
* UriMIME
* @param uri MIMEUri
* @return MIME
*/
@Override
public String getType(Uri uri) {
// TODO Auto-generated method stub
return null;
}
}
}

@ -24,41 +24,40 @@ import net.micode.notes.tool.GTaskStringUtils;
import org.json.JSONException;
import org.json.JSONObject;
//MetaData类继承自Task类专门用于处理Google任务相关的元数据
public class MetaData extends Task {
private final static String TAG = MetaData.class.getSimpleName();// 日志输出时使用的TAG标识
private final static String TAG = MetaData.class.getSimpleName();
private String mRelatedGid = null;// 存储与本元数据相关的GTask全局ID
// 设置元数据方法将gid添加到metaInfo中并将整个metaInfo以字符串形式保存到notes同时设置名称
private String mRelatedGid = null;
public void setMeta(String gid, JSONObject metaInfo) {
try {
metaInfo.put(GTaskStringUtils.META_HEAD_GTASK_ID, gid);
} catch (JSONException e) {
Log.e(TAG, "failed to put related gid");// 添加gid失败时的日志输出
Log.e(TAG, "failed to put related gid");
}
setNotes(metaInfo.toString());
setName(GTaskStringUtils.META_NOTE_NAME);
}
// 获取与本元数据相关的GTask全局ID
public String getRelatedGid() {
return mRelatedGid;
}
// 重写父类方法判断当前元数据是否值得保存根据notes是否为空
@Override
public boolean isWorthSaving() {
return getNotes() != null;
}
// 从远程JSON数据设置内容并提取gid信息
@Override
public void setContentByRemoteJSON(JSONObject js) {
super.setContentByRemoteJSON(js);// 调用父类的setContentByRemoteJSON方法
if (getNotes() != null) { // 检查notes字段是否为空
super.setContentByRemoteJSON(js);
if (getNotes() != null) {
try {
JSONObject metaInfo = new JSONObject(getNotes().trim());// 将notes字段转换为JSONObject对象
mRelatedGid = metaInfo.getString(GTaskStringUtils.META_HEAD_GTASK_ID);// 从JSONObject中获取特定字段并赋值给mRelatedGid
JSONObject metaInfo = new JSONObject(getNotes().trim());
mRelatedGid = metaInfo.getString(GTaskStringUtils.META_HEAD_GTASK_ID);
} catch (JSONException e) {
Log.w(TAG, "failed to get related gid");// 记录日志提示无法获取相关的gid
Log.w(TAG, "failed to get related gid");
mRelatedGid = null;
}
}
@ -67,17 +66,17 @@ public class MetaData extends Task {
@Override
public void setContentByLocalJSON(JSONObject js) {
// this function should not be called
throw new IllegalAccessError("MetaData:setContentByLocalJSON should not be called");// 抛出异常,指示不应调用此函数
throw new IllegalAccessError("MetaData:setContentByLocalJSON should not be called");
}
@Override
public JSONObject getLocalJSONFromContent() {
throw new IllegalAccessError("MetaData:getLocalJSONFromContent should not be called");// 抛出异常,指示不应调用此函数
throw new IllegalAccessError("MetaData:getLocalJSONFromContent should not be called");
}
@Override
public int getSyncAction(Cursor c) {
throw new IllegalAccessError("MetaData:getSyncAction should not be called");// 抛出异常,指示不应调用此函数
throw new IllegalAccessError("MetaData:getSyncAction should not be called");
}
}

@ -21,7 +21,6 @@ import android.database.Cursor;
import org.json.JSONObject;
public abstract class Node {
// 定义同步操作类型常量
public static final int SYNC_ACTION_NONE = 0;
public static final int SYNC_ACTION_ADD_REMOTE = 1;
@ -40,63 +39,63 @@ public abstract class Node {
public static final int SYNC_ACTION_ERROR = 8;
// 定义节点属性
private String mGid; // 节点ID
private String mGid;
private String mName;// 节点名称
private String mName;
private long mLastModified; // 最后修改时间
private long mLastModified;
private boolean mDeleted;// 是否已删除
private boolean mDeleted;
public Node() {
mGid = null;
mName = "";
mLastModified = 0;
mDeleted = false;
}// 构造函数初始化节点属性
}
public abstract JSONObject getCreateAction(int actionId);// 抽象方法,用于创建特定类型的同步操作
public abstract JSONObject getCreateAction(int actionId);
public abstract JSONObject getUpdateAction(int actionId); // 抽象方法,用于更新特定类型的同步操作
public abstract JSONObject getUpdateAction(int actionId);
public abstract void setContentByRemoteJSON(JSONObject js); // 抽象方法根据远程JSON数据设置节点内容
public abstract void setContentByRemoteJSON(JSONObject js);
public abstract void setContentByLocalJSON(JSONObject js); // 抽象方法根据本地JSON数据设置节点内容
public abstract void setContentByLocalJSON(JSONObject js);
public abstract JSONObject getLocalJSONFromContent();// 抽象方法从节点内容获取本地JSON数据
public abstract JSONObject getLocalJSONFromContent();
public abstract int getSyncAction(Cursor c); // 抽象方法,根据游标获取同步操作类型
public abstract int getSyncAction(Cursor c);
public void setGid(String gid) {
this.mGid = gid;
}// 设置节点ID
}
public void setName(String name) {
this.mName = name;
}// 设置节点名称
}
public void setLastModified(long lastModified) {
this.mLastModified = lastModified;
}// 设置最后修改时间
}
public void setDeleted(boolean deleted) {
this.mDeleted = deleted;
}// 设置是否已删除
}
public String getGid() {
return this.mGid;
}// 获取节点ID
}
public String getName() {
return this.mName;
}// 获取节点名称
}
public long getLastModified() {
return this.mLastModified;
}// 获取最后修改时间
}
public boolean getDeleted() {
return this.mDeleted;
} // 获取是否已删除
}
}

@ -34,65 +34,61 @@ import net.micode.notes.gtask.exception.ActionFailureException;
import org.json.JSONException;
import org.json.JSONObject;
//SqlData 类是一个数据对象用于封装数据表中的一行数据。SqlData 类提供了一些方法,用于从 Cursor 对象中加载数据、从 JSONObject 对象中加载数据、将数据提交到数据库等操作。
public class SqlData {
private static final String TAG = SqlData.class.getSimpleName();
private static final int INVALID_ID = -99999;
//通常情况下,将一个特殊数值(比如负数)作为无效 ID 的标识,这样在程序中可以通过判断是否等于 INVALID_ID 来表示某个 ID 是否有效。
//定义了一个名为 PROJECTION_DATA 的公共静态常量数组
public static final String[] PROJECTION_DATA = new String[] {
DataColumns.ID, DataColumns.MIME_TYPE, DataColumns.CONTENT, DataColumns.DATA1,
DataColumns.DATA3
};
public static final int DATA_ID_COLUMN = 0;//数据表ID列索引
public static final int DATA_ID_COLUMN = 0;
public static final int DATA_MIME_TYPE_COLUMN = 1;//数据表MIME_TYPE列索引
public static final int DATA_MIME_TYPE_COLUMN = 1;
public static final int DATA_CONTENT_COLUMN = 2;//数据表CONTENT列索引
public static final int DATA_CONTENT_COLUMN = 2;
public static final int DATA_CONTENT_DATA_1_COLUMN = 3;//数据表DATA1列索引
public static final int DATA_CONTENT_DATA_1_COLUMN = 3;
public static final int DATA_CONTENT_DATA_3_COLUMN = 4;//数据表DATA3列索引
public static final int DATA_CONTENT_DATA_3_COLUMN = 4;
private ContentResolver mContentResolver;//访问内容提供者的接口,可以进行数据库操作
private ContentResolver mContentResolver;
private boolean mIsCreate;//标识当前数据是否创建
private boolean mIsCreate;
private long mDataId;//数据ID
private long mDataId;
private String mDataMimeType;//数据MIME类型
private String mDataMimeType;
private String mDataContent;//数据内容
private String mDataContent;
private long mDataContentData1;//数据DATA1
private long mDataContentData1;
private String mDataContentData3;//数据DATA3
private String mDataContentData3;
private ContentValues mDiffDataValues;
//ContentValues 是一个键值对存储容器,通常用于封装一组待插入或更新到数据库的数据。
//mDiffDataValues 主要用来存储数据对象与数据库中实际数据的差异。通过比较 mDiffDataValues 中的数据与数据库中对应的数据,
// 可以确定数据对象是否需要进行更新操作,并且可以方便地获取到数据的变化部分。
public SqlData(Context context) {//构造函数
public SqlData(Context context) {
mContentResolver = context.getContentResolver();
mIsCreate = true;//新创建的数据
mDataId = INVALID_ID;//数据ID初始值为无效ID
mDataMimeType = DataConstants.NOTE;//数据MIME类型
mIsCreate = true;
mDataId = INVALID_ID;
mDataMimeType = DataConstants.NOTE;
mDataContent = "";
mDataContentData1 = 0;
mDataContentData3 = "";
mDiffDataValues = new ContentValues();
}
public SqlData(Context context, Cursor c) {//构造函数2
public SqlData(Context context, Cursor c) {
mContentResolver = context.getContentResolver();
mIsCreate = false;//不是新创建的数据
mIsCreate = false;
loadFromCursor(c);
mDiffDataValues = new ContentValues();
}
//用于从传入的 Cursor 对象中加载数据到当前 SqlData 对象中
private void loadFromCursor(Cursor c) {
mDataId = c.getLong(DATA_ID_COLUMN);
mDataMimeType = c.getString(DATA_MIME_TYPE_COLUMN);
@ -100,13 +96,13 @@ public class SqlData {
mDataContentData1 = c.getLong(DATA_CONTENT_DATA_1_COLUMN);
mDataContentData3 = c.getString(DATA_CONTENT_DATA_3_COLUMN);
}
//用于将传入的 JSONObject 对象中的数据加载到当前 SqlData 对象中
public void setContent(JSONObject js) throws JSONException {
long dataId = js.has(DataColumns.ID) ? js.getLong(DataColumns.ID) : INVALID_ID;//如果JSONObject对象中包含ID则获取ID否则ID为无效ID
if (mIsCreate || mDataId != dataId) {//如果是新创建的数据或者数据ID不等于传入的数据ID
mDiffDataValues.put(DataColumns.ID, dataId);//将数据ID放入mDiffDataValues中
long dataId = js.has(DataColumns.ID) ? js.getLong(DataColumns.ID) : INVALID_ID;
if (mIsCreate || mDataId != dataId) {
mDiffDataValues.put(DataColumns.ID, dataId);
}
mDataId = dataId;//更新数据ID
mDataId = dataId;
String dataMimeType = js.has(DataColumns.MIME_TYPE) ? js.getString(DataColumns.MIME_TYPE)
: DataConstants.NOTE;
@ -133,9 +129,9 @@ public class SqlData {
}
mDataContentData3 = dataContentData3;
}
//用于获取对象的内容并返回一个 JSON 对象
public JSONObject getContent() throws JSONException {
if (mIsCreate) {//若数据未创建
if (mIsCreate) {
Log.e(TAG, "it seems that we haven't created this in database yet");
return null;
}
@ -147,21 +143,9 @@ public class SqlData {
js.put(DataColumns.DATA3, mDataContentData3);
return js;
}
//用于提交数据更改到数据库
public void commit(long noteId, boolean validateVersion, long version) {
// 在 if (mIsCreate) 分支中:
//
// 首先检查条件如果数据ID无效且包含ID字段则将ID字段从差异数据值中移除。
// 将指定的笔记IDnoteId放入差异数据值中。
// 调用内容解析器ContentResolver的insert()方法向数据库中插入数据并获取返回的URI。
// 解析URI以获取新插入数据的ID并将其赋值给 mDataId 变量。
// 如果出现异常NumberFormatException则记录错误日志并抛出 ActionFailureException 异常。
// 在 else 分支中:
//
// 如果差异数据值的大小大于0则继续执行更新操作。
// 根据是否需要验证版本调用内容解析器的update()方法更新数据库中的数据。
// 如果更新结果为0则记录警告日志表示可能在同步期间用户更新了笔记。
if (mIsCreate) {
if (mDataId == INVALID_ID && mDiffDataValues.containsKey(DataColumns.ID)) {
mDiffDataValues.remove(DataColumns.ID);
@ -195,10 +179,10 @@ public class SqlData {
}
}
mDiffDataValues.clear();//清空mDiffDataValues
mIsCreate = false;//数据已经创建
mDiffDataValues.clear();
mIsCreate = false;
}
//返回数据ID
public long getId() {
return mDataId;
}

@ -1,4 +1,3 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
@ -38,23 +37,12 @@ import org.json.JSONObject;
import java.util.ArrayList;
//Sql类的作用是用于处理笔记相关的数据库操作其中包含了对笔记的增删改查以及与笔记相关的数据操作。
//具体来说,这个类实现了以下功能:
//
// 提供了构造函数,可以根据上下文对象初始化一个 SqlNote 对象,并设置默认的属性。
// 可以根据传入的 Cursor 对象或笔记 ID 加载数据到当前 SqlNote 对象中。
// 可以根据传入的 JSON 对象设置笔记对象的内容。
// 可以获取当前对象的内容并返回一个 JSON 对象。
// 提供了设置父ID、Google任务ID、同步ID等属性的方法。
// 包含了提交笔记对象更改的方法,可以将笔记对象的数据插入到数据库中或更新数据库中已有的笔记数据。
// 提供了获取数据ID、父ID、摘要等属性的方法。
// 包含了加载数据内容、设置内容、提交更改等私有方法,用于具体的数据处理和操作。
public class SqlNote {
private static final String TAG = SqlNote.class.getSimpleName();//日志
private static final String TAG = SqlNote.class.getSimpleName();
private static final int INVALID_ID = -99999;//无效ID标识
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,
@ -64,7 +52,6 @@ public class SqlNote {
NoteColumns.VERSION
};
//定义了一组列索引
public static final int ID_COLUMN = 0;
public static final int ALERTED_DATE_COLUMN = 1;
@ -99,43 +86,42 @@ public class SqlNote {
public static final int VERSION_COLUMN = 16;
private Context mContext;//上下文对象,获取应用程序上下文信息
private Context mContext;
private ContentResolver mContentResolver;// 访问内容提供者的接口,可以进行数据库操作
private ContentResolver mContentResolver;
private boolean mIsCreate;//标识当前数据是否创建
private boolean mIsCreate;
private long mId;//数据ID
private long mId;
private long mAlertDate;//提醒日期
private long mAlertDate;
private int mBgColorId;//背景颜色ID
private int mBgColorId;
private long mCreatedDate;//创建日期
private long mCreatedDate;
private int mHasAttachment;//是否有附件
private int mHasAttachment;
private long mModifiedDate;//修改日期
private long mModifiedDate;
private long mParentId;//父ID
private long mParentId;
private String mSnippet;//摘要
private String mSnippet;
private int mType;//类型
private int mType;
private int mWidgetId;//小部件ID
private int mWidgetId;
private int mWidgetType;//小部件类型
private int mWidgetType;
private long mOriginParent;//原始父ID
private long mOriginParent;
private long mVersion;//版本
private long mVersion;
private ContentValues mDiffNoteValues;//ContentValues 是一个键值对存储容器,通常用于封装一组待插入或更新到数据库的数据
private ContentValues mDiffNoteValues;
private ArrayList<SqlData> mDataList;//数据列表
private ArrayList<SqlData> mDataList;
//构造函数
public SqlNote(Context context) {
mContext = context;
mContentResolver = context.getContentResolver();
@ -157,7 +143,6 @@ public class SqlNote {
mDataList = new ArrayList<SqlData>();
}
//构造函数2,根据Cursor对象初始化数据
public SqlNote(Context context, Cursor c) {
mContext = context;
mContentResolver = context.getContentResolver();
@ -169,7 +154,6 @@ public class SqlNote {
mDiffNoteValues = new ContentValues();
}
//构造函数3,根据ID初始化数据
public SqlNote(Context context, long id) {
mContext = context;
mContentResolver = context.getContentResolver();
@ -182,27 +166,25 @@ public class SqlNote {
}
//根据ID加载数据,利用loadFromCursor(Cursor c)方法加载到当前 SqlNote 对象中
private void loadFromCursor(long id) {
Cursor c = null;
try { //通过mContentResolver查询获取Cursor对象
try {
c = mContentResolver.query(Notes.CONTENT_NOTE_URI, PROJECTION_NOTE, "(_id=?)",
new String[] {
String.valueOf(id)
String.valueOf(id)
}, null);
if (c != null) {
c.moveToNext();//将光标移动到第一条记录
c.moveToNext();
loadFromCursor(c);
} else {
Log.w(TAG, "loadFromCursor: cursor = null");
}
} finally { //无论是否发生异常都会执行finally块中的代码
} finally {
if (c != null)
c.close();
}
}
//从传入的 Cursor 对象中加载数据到当前 SqlNote 对象中
private void loadFromCursor(Cursor c) {
mId = c.getLong(ID_COLUMN);
mAlertDate = c.getLong(ALERTED_DATE_COLUMN);
@ -218,23 +200,21 @@ public class SqlNote {
mVersion = c.getLong(VERSION_COLUMN);
}
//从数据库中加载指定笔记ID相关的数据并将这些数据转化为SqlData对象并存储在mDataList中供后续使用
private void loadDataContent() {
Cursor c = null;
mDataList.clear();
try {
//通过内容解析器(ContentResolver)执行查询操作从笔记内容的URI(Notes.CONTENT_DATA_URI)中获取与当前笔记ID(mId)相关联的数据
c = mContentResolver.query(Notes.CONTENT_DATA_URI, SqlData.PROJECTION_DATA,
"(note_id=?)", new String[] {
String.valueOf(mId)
String.valueOf(mId)
}, null);
if (c != null) {
if (c.getCount() == 0) { //如果Cursor对象中没有数据
if (c.getCount() == 0) {
Log.w(TAG, "it seems that the note has not data");
return;
}
while (c.moveToNext()) { //遍历Cursor对象
SqlData data = new SqlData(mContext, c); //对于每一行数据创建一个SqlData对象
while (c.moveToNext()) {
SqlData data = new SqlData(mContext, c);
mDataList.add(data);
}
} else {
@ -246,21 +226,20 @@ public class SqlNote {
}
}
//将传入的 JSONObject 对象中的数据加载到当前 SqlNote 对象中
public boolean setContent(JSONObject js) {
try {
JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_SYSTEM) { //系统文件夹不可操作
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 //如果JSONObject对象中包含摘要则获取摘要否则摘要为空字符串
String snippet = note.has(NoteColumns.SNIPPET) ? note
.getString(NoteColumns.SNIPPET) : "";
if (mIsCreate || !mSnippet.equals(snippet)) { //如果是新创建的数据或者摘要不等于传入的摘要
mDiffNoteValues.put(NoteColumns.SNIPPET, snippet); //将摘要放入mDiffNoteValues中
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) {
@ -380,7 +359,6 @@ public class SqlNote {
return true;
}
//获取对象的内容并返回一个 JSON 对象
public JSONObject getContent() {
try {
JSONObject js = new JSONObject();
@ -407,7 +385,6 @@ public class SqlNote {
js.put(GTaskStringUtils.META_HEAD_NOTE, note);
JSONArray dataArray = new JSONArray();
//将数据列表中的数据加载到JSONArray对象中
for (SqlData sqlData : mDataList) {
JSONObject data = sqlData.getContent();
if (data != null) {
@ -430,52 +407,47 @@ public class SqlNote {
return null;
}
//设置父ID
public void setParentId(long id) {
mParentId = id;
mDiffNoteValues.put(NoteColumns.PARENT_ID, id);
}
//设置Google任务ID
public void setGtaskId(String gid) {
mDiffNoteValues.put(NoteColumns.GTASK_ID, gid);
}
//设置同步ID
public void setSyncId(long syncId) {
mDiffNoteValues.put(NoteColumns.SYNC_ID, syncId);
}
//设置本地修改标识
public void resetLocalModified() {
mDiffNoteValues.put(NoteColumns.LOCAL_MODIFIED, 0);
}
//返回数据ID
public long getId() {
return mId;
}
//返回父ID
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) {
//如果数据未创建,下面创建新笔记
//首先检查条件如果数据ID无效且mDiffNoteValues中包含ID字段则将ID字段从差异数据值中移除。
if (mId == INVALID_ID && mDiffNoteValues.containsKey(NoteColumns.ID)) {
mDiffNoteValues.remove(NoteColumns.ID);
}
//通过内容解析器ContentResolver的insert方法将mDiffNoteValues中的数据插入到Notes.CONTENT_NOTE_URI所代表的位置并获取返回的URI。
Uri uri = mContentResolver.insert(Notes.CONTENT_NOTE_URI, mDiffNoteValues);
try {
//解析URI以获取新创建数据的ID并将其赋值给 mId 变量。
mId = Long.valueOf(uri.getPathSegments().get(1));
} catch (NumberFormatException e) {
Log.e(TAG, "Get note id error :" + e.toString());
@ -485,56 +457,49 @@ public class SqlNote {
throw new IllegalStateException("Create thread id failed");
}
if (mType == Notes.TYPE_NOTE) { //如果是笔记类型
if (mType == Notes.TYPE_NOTE) {
for (SqlData sqlData : mDataList) {
sqlData.commit(mId, false, -1);//提交数据更改到数据库
sqlData.commit(mId, false, -1);
}
}
} else {
if (mId <= 0 && mId != Notes.ID_ROOT_FOLDER && mId != Notes.ID_CALL_RECORD_FOLDER) { //检查ID是否有效
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) { //如果差异数据值的大小大于0表示有数据需要更新
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)
String.valueOf(mId)
});
} else {
//根据笔记的IDmId将mDiffNoteValues中的数据更新到指定的Notes.CONTENT_NOTE_URI所代表的位置
result = mContentResolver.update(Notes.CONTENT_NOTE_URI, mDiffNoteValues, "("
+ NoteColumns.ID + "=?) AND (" + NoteColumns.VERSION + "<=?)",
+ NoteColumns.ID + "=?) AND (" + NoteColumns.VERSION + "<=?)",
new String[] {
String.valueOf(mId), String.valueOf(mVersion)
});
// Notes.CONTENT_NOTE_URI表示要操作的数据的URI可能是笔记数据的地址。
// mDiffNoteValues包含了需要更新的字段和对应数值的键值对。
// ("(" + NoteColumns.ID + "=?)")这是用于指定更新条件的部分其中NoteColumns.ID是笔记的ID字段"=?"表示该位置需要传入一个值。
// new String[] {String.valueOf(mId)}:这是要替换"=?"的具体数值即要更新的笔记的ID。
}
if (result == 0) {
Log.w(TAG, "there is no update. maybe user updates note when syncing");
}
}
if (mType == Notes.TYPE_NOTE) { //如果是笔记类型
if (mType == Notes.TYPE_NOTE) {
for (SqlData sqlData : mDataList) {
sqlData.commit(mId, validateVersion, mVersion); //提交数据更改到数据库
sqlData.commit(mId, validateVersion, mVersion);
}
}
}
// refresh local info
loadFromCursor(mId); //根据ID加载数据将数据加载到当前 SqlNote 对象中
loadFromCursor(mId);
if (mType == Notes.TYPE_NOTE)
loadDataContent(); //从数据库中加载指定笔记ID相关的数据并将这些数据转化为SqlData对象并存储在mDataList中供后续使用
loadDataContent();
mDiffNoteValues.clear();//清空待提交的数据
mDiffNoteValues.clear();
mIsCreate = false;
}
}

@ -1,4 +1,3 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
@ -34,20 +33,20 @@ import org.json.JSONObject;
public class Task extends Node {
private static final String TAG = Task.class.getSimpleName();//日志
private static final String TAG = Task.class.getSimpleName();
private boolean mCompleted; //标识任务是否完成
private boolean mCompleted;
private String mNotes;//任务的备注
private String mNotes;
private JSONObject mMetaInfo;//任务的元数据
private JSONObject mMetaInfo;
private Task mPriorSibling;//任务列表的前一个任务
private Task mPriorSibling;
private TaskList mParent;//任务的父任务列表
private TaskList mParent;
public Task() {//构造方法
super();//调用父类Node的构造方法
public Task() {
super();
mCompleted = false;
mNotes = null;
mPriorSibling = null;
@ -55,15 +54,14 @@ public class Task extends Node {
mMetaInfo = null;
}
//获取创建任务的json对象
public JSONObject getCreateAction(int actionId) {
JSONObject js = new JSONObject();//创建json对象
JSONObject js = new JSONObject();
try {
//设置各种属性
// action_type
//将键值对放入json对象中
js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE);
// action_id
js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId);
@ -76,8 +74,7 @@ public class Task extends Node {
entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null");
entity.put(GTaskStringUtils.GTASK_JSON_ENTITY_TYPE,
GTaskStringUtils.GTASK_JSON_TYPE_TASK);
if (getNotes() != null) { //如果任务的备注不为空
//将备注信息放入json对象中
if (getNotes() != null) {
entity.put(GTaskStringUtils.GTASK_JSON_NOTES, getNotes());
}
js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity);
@ -98,15 +95,14 @@ public class Task extends Node {
}
} catch (JSONException e) {
Log.e(TAG, e.toString());//打印日志
e.printStackTrace();//打印异常堆栈跟踪信息,将异常信息输出到标准错误流
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to generate task-create jsonobject");
}
return js;
}
//生成更新任务的json对象
public JSONObject getUpdateAction(int actionId) {
JSONObject js = new JSONObject();
@ -139,7 +135,6 @@ public class Task extends Node {
return js;
}
//根据远程传入的json对象设置任务的内容
public void setContentByRemoteJSON(JSONObject js) {
if (js != null) {
try {
@ -180,9 +175,7 @@ public class Task extends Node {
}
}
//根据本地的json对象设置任务的内容
public void setContentByLocalJSON(JSONObject js) {
//若传入的json对象为空或者没包含特定键
if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE)
|| !js.has(GTaskStringUtils.META_HEAD_DATA)) {
Log.w(TAG, "setContentByLocalJSON: nothing is avaiable");
@ -198,9 +191,6 @@ public class Task extends Node {
}
for (int i = 0; i < dataArray.length(); i++) {
// 获取数组中索引为 i 的元素,该元素是一个 JSON 对象。
// 检查该对象中的 MIME_TYPE 字段是否等于 DataConstants.NOTE。
// 如果上述条件成立,则将该对象中的 CONTENT 字段的值设置为本地对象的 name 属性。
JSONObject data = dataArray.getJSONObject(i);
if (TextUtils.equals(data.getString(DataColumns.MIME_TYPE), DataConstants.NOTE)) {
setName(data.getString(DataColumns.CONTENT));
@ -214,11 +204,10 @@ public class Task extends Node {
}
}
//根据当前对象的状态生成本地的json对象
public JSONObject getLocalJSONFromContent() {
String name = getName();
try {
if (mMetaInfo == null) { //如果元数据为空,表示是一个新创建的任务
if (mMetaInfo == null) {
// new task created from web
if (name == null) {
Log.w(TAG, "the note seems to be an empty one");
@ -235,7 +224,7 @@ public class Task extends Node {
note.put(NoteColumns.TYPE, Notes.TYPE_NOTE);
js.put(GTaskStringUtils.META_HEAD_NOTE, note);
return js;
} else { //如果元数据不为空,表示是一个同步的任务
} else {
// synced task
JSONObject note = mMetaInfo.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
JSONArray dataArray = mMetaInfo.getJSONArray(GTaskStringUtils.META_HEAD_DATA);
@ -258,11 +247,9 @@ public class Task extends Node {
}
}
//设置任务的元数据
public void setMetaInfo(MetaData metaData) {
if (metaData != null && metaData.getNotes() != null) {
try {
//将notes字符串的值转换为json对象
mMetaInfo = new JSONObject(metaData.getNotes());
} catch (JSONException e) {
Log.w(TAG, e.toString());
@ -271,16 +258,7 @@ public class Task extends Node {
}
}
//根据传入Cursor对象和当前对象的mMetainfo属性判断当前对象的同步状态
public int getSyncAction(Cursor c) {
// 首先尝试获取当前对象的mMetaInfo中的note信息并进行一些基本的验证。
// 如果note信息为空则输出警告日志并返回SYNC_ACTION_UPDATE_REMOTE表示需要更新远程数据。
// 如果note信息中没有ID字段则输出警告日志并返回SYNC_ACTION_UPDATE_LOCAL表示需要更新本地数据。
// 验证note的ID与传入Cursor的ID是否匹配如果不匹配则返回SYNC_ACTION_UPDATE_LOCAL。
// 检查本地是否有更新如果没有则比较最后修改时间如果远程数据有更新则返回SYNC_ACTION_UPDATE_LOCAL否则返回SYNC_ACTION_NONE。
// 如果本地有更新则验证gtask id如果不匹配则返回SYNC_ACTION_ERROR。
// 最后根据本地修改时间和最后同步时间的比较,确定具体的同步操作类型,返回相应的同步操作常量。
// 在处理过程中如果出现异常则会捕获该异常并打印错误日志最终返回SYNC_ACTION_ERROR。
try {
JSONObject noteInfo = null;
if (mMetaInfo != null && mMetaInfo.has(GTaskStringUtils.META_HEAD_NOTE)) {
@ -333,46 +311,41 @@ public class Task extends Node {
return SYNC_ACTION_ERROR;
}
//判断任务是否值得保存
public boolean isWorthSaving() {
// 首先检查当前对象的mMetaInfo属性是否不为null。
// 如果mMetaInfo属性为null则继续检查当前对象的name属性是否不为null且去除首尾空格后长度大于0。
// 接着检查当前对象的notes属性是否不为null且去除首尾空格后长度大于0。
// 如果上述条件有一个成立则返回true否则返回false。
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() { //获取任务的备注
public String getNotes() {
return this.mNotes;
}
public Task getPriorSibling() {
return this.mPriorSibling;
}//获取任务列表的前一个任务
}
public TaskList getParent() {
return this.mParent;
}//获取任务的父任务列表
}
}

@ -31,22 +31,18 @@ import java.util.ArrayList;
public class TaskList extends Node {
private static final String TAG = TaskList.class.getSimpleName();//日志
private static final String TAG = TaskList.class.getSimpleName();
private int mIndex;//可能代表列表中的索引或者用于排序的新任务添加顺序。
private int mIndex;
private ArrayList<Task> mChildren;
//ArrayList<Task> 类型的数据成员,用来存储属于该任务列表的所有子任务实例。
//构造函数
public TaskList() {
super();
mChildren = new ArrayList<Task>();
mIndex = 1;
}
//生成一个表示创建任务列表操作的JSON对象以便在网络传输或者持久化存储中使用。
public JSONObject getCreateAction(int actionId) {
JSONObject js = new JSONObject();
@ -78,8 +74,6 @@ public class TaskList extends Node {
return js;
}
//该Java方法用于构建并返回一个表示更新动作的JSONObject对象该动作与给定的actionId相关联。
// 这个JSON对象遵循预定义的结构其中键值由GTaskStringUtils类提供。
public JSONObject getUpdateAction(int actionId) {
JSONObject js = new JSONObject();
@ -109,7 +103,6 @@ public class TaskList extends Node {
return js;
}
//setContentByRemoteJSON的作用是从传入的远程JSON对象JSONObject中提取并设置相关的属性值。
public void setContentByRemoteJSON(JSONObject js) {
if (js != null) {
try {
@ -136,24 +129,18 @@ public class TaskList extends Node {
}
}
//接受一个 JSONObject 参数 js该方法用于从给定的 JSON 对象中解析并设置本地内容
public void setContentByLocalJSON(JSONObject js) {
if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE)) { //如果传入的JSON对象为空或者不包含指定的键值对
if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE)) {
Log.w(TAG, "setContentByLocalJSON: nothing is avaiable");
}
try {
//获取名为 META_HEAD_NOTE 的键所对应的 JSON 对象,并将其赋值给 folder 变量。
JSONObject folder = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
//检查 folder 对象中键为 NoteColumns.TYPE 所对应的值是否等于 Notes.TYPE_FOLDER即判断是否为文件夹类型的笔记。
//如果是文件夹类型的笔记,则获取文件夹的名称,并设置为当前任务列表的名称。
if (folder.getInt(NoteColumns.TYPE) == Notes.TYPE_FOLDER) {
String name = folder.getString(NoteColumns.SNIPPET);
setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + name);
}
//如果不是文件夹类型的笔记,则检查是否为系统类型的笔记。
//如果是系统类型的笔记则根据笔记的ID来设置当前任务列表的名称。
else if (folder.getInt(NoteColumns.TYPE) == Notes.TYPE_SYSTEM) {
} 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)
@ -169,28 +156,23 @@ public class TaskList extends Node {
e.printStackTrace();
}
}
//用于将当前任务列表的内容转换为本地的 JSON 对象。
public JSONObject getLocalJSONFromContent() {
try {
JSONObject js = new JSONObject();
JSONObject folder = new JSONObject();
String folderName = getName();//获取当前任务列表的名称,并将其赋值给 folderName 变量。
//检查任务列表的名称是否以 GTaskStringUtils.MIUI_FOLDER_PREFFIX 开头。
//如果是,则截取名称中 GTaskStringUtils.MIUI_FOLDER_PREFFIX 后面的部分作为文件夹名称。
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);//将文件夹名称放入 folder 对象中的 NoteColumns.SNIPPET 键下。
//检查文件夹名称是否等于默认文件夹名称或通话笔记文件夹名称。
//如果是,则将文件夹类型设置为系统类型 (Notes.TYPE_SYSTEM)。
folder.put(NoteColumns.SNIPPET, folderName);
if (folderName.equals(GTaskStringUtils.FOLDER_DEFAULT)
|| folderName.equals(GTaskStringUtils.FOLDER_CALL_NOTE))
folder.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
//如果文件夹名称不是默认文件夹名称或通话笔记文件夹名称,则将文件夹类型设置为普通文件夹类型 (Notes.TYPE_FOLDER)。
else
folder.put(NoteColumns.TYPE, Notes.TYPE_FOLDER);
//将存储了文件夹信息的 folder 对象放入主 JSON 对象 js 中的 GTaskStringUtils.META_HEAD_NOTE 键下。
js.put(GTaskStringUtils.META_HEAD_NOTE, folder);
return js;
@ -200,13 +182,9 @@ public class TaskList extends Node {
return null;
}
}
//用于确定与同步相关的操作。它接收一个 Cursor 对象作为参数,该 Cursor 对象包含了任务列表的数据。
public int getSyncAction(Cursor c) {
try {
//检查是否存在本地修改。
//如果没有本地修改:
//如果任务的最后修改时间与本地最后修改时间相同,则表示没有任何更新,返回 SYNC_ACTION_NONE。
//如果任务的最后修改时间与本地最后修改时间不同,则表示需要将远程更新应用到本地,返回 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()) {
@ -216,13 +194,7 @@ public class TaskList extends Node {
// apply remote to local
return SYNC_ACTION_UPDATE_LOCAL;
}
}
//else { ... }
//如果存在本地修改:
//验证任务的 GTasks ID 是否与当前任务的 GTasks ID 匹配,如果不匹配则记录错误并返回 SYNC_ACTION_ERROR。
//如果任务的最后修改时间与本地最后修改时间相同,则表示只有本地修改,需要将本地修改应用到远程,返回 SYNC_ACTION_UPDATE_REMOTE。
//如果任务的最后修改时间与本地最后修改时间不同,目前代码中注释为针对文件夹冲突的情况,仍然将本地修改应用到远程,返回 SYNC_ACTION_UPDATE_REMOTE。
else {
} else {
// validate gtask id
if (!c.getString(SqlNote.GTASK_ID_COLUMN).equals(getGid())) {
Log.e(TAG, "gtask id doesn't match");
@ -243,21 +215,16 @@ public class TaskList extends Node {
return SYNC_ACTION_ERROR;
}
//返回当前任务的子任务数量
public int getChildTaskCount() {
return mChildren.size();
}
//添加一个子任务
public boolean addChildTask(Task task) {
boolean ret = false;//用于标记是否添加成功
//检查传入的子任务对象 task 不为空,并且当前任务的子任务列表 mChildren 中不包含该子任务。
boolean ret = false;
if (task != null && !mChildren.contains(task)) {
//如果满足上述条件,将子任务 task 添加到当前任务的子任务列表 mChildren 中,并将返回值赋给 ret表示添加操作是否成功。
ret = mChildren.add(task);
if (ret) {
//如果添加操作成功:
//设置子任务的前一个兄弟任务prior sibling如果当前任务的子任务列表为空则前一个兄弟任务为 null否则为列表中最后一个子任务。
//设置子任务的父任务为当前任务。
// need to set prior sibling and parent
task.setPriorSibling(mChildren.isEmpty() ? null : mChildren
.get(mChildren.size() - 1));
@ -266,39 +233,33 @@ public class TaskList extends Node {
}
return ret;
}
//另一个版本的添加子任务方法,与之前的版本相比,这个方法允许在指定索引处添加子任务
public boolean addChildTask(Task task, int index) {
//首先检查传入的索引 index 是否有效,即确保它在合理的范围内。如果索引小于 0 或大于子任务列表的大小
if (index < 0 || index > mChildren.size()) {
Log.e(TAG, "add child task: invalid index");
return false;
}
//使用 indexOf() 方法检查子任务列表中是否已经存在传入的子任务 task并将其索引存储在变量 pos 中。
int pos = mChildren.indexOf(task);
if (task != null && pos == -1) {
//将传入的子任务 task 添加到子任务列表 mChildren 的指定索引 index 处。
mChildren.add(index, task);
// update the task list
//初始化变量 preTask 和 afterTask用于存储新添加的子任务的前一个兄弟任务和后一个兄弟任务。
Task preTask = null;
Task afterTask = null;
if (index != 0)
//如果新添加的子任务不是列表中的第一个任务,则将其前一个兄弟任务设置为索引为 index - 1 的任务。
preTask = mChildren.get(index - 1);
if (index != mChildren.size() - 1)
//如果新添加的子任务不是列表中的最后一个任务,则将其后一个兄弟任务设置为索引为 index + 1 的任务。
afterTask = mChildren.get(index + 1);
//将新添加的子任务的前一个兄弟任务设置为 preTask。
task.setPriorSibling(preTask);
if (afterTask != null)
//如果存在后一个兄弟任务,则将其前一个兄弟任务设置为新添加的子任务 task。
afterTask.setPriorSibling(task);
}
return true;
}
//移除子任务
public boolean removeChildTask(Task task) {
boolean ret = false;
int index = mChildren.indexOf(task);
@ -307,12 +268,11 @@ public class TaskList extends Node {
if (ret) {
// reset prior sibling and parent
task.setPriorSibling(null);//将移除的子任务的前一个兄弟任务设置为 null。
task.setParent(null);//将移除的子任务的父任务设置为 null。
task.setPriorSibling(null);
task.setParent(null);
// update the task list
if (index != mChildren.size()) { //如果移除的子任务不是列表中的最后一个任务,则执行以下操作:
//将下一个任务的前一个兄弟任务设置为移除任务的前一个兄弟任务,即更新列表中移除任务后的任务的前一个兄弟关系。
if (index != mChildren.size()) {
mChildren.get(index).setPriorSibling(
index == 0 ? null : mChildren.get(index - 1));
}
@ -320,7 +280,7 @@ public class TaskList extends Node {
}
return ret;
}
//移动子任务到指定索引位置的方法
public boolean moveChildTask(Task task, int index) {
if (index < 0 || index >= mChildren.size()) {
@ -339,7 +299,6 @@ public class TaskList extends Node {
return (removeChildTask(task) && addChildTask(task, index));
}
//根据子任务的 gid在子任务列表中查找特定子任务的方法
public Task findChildTaskByGid(String gid) {
for (int i = 0; i < mChildren.size(); i++) {
Task t = mChildren.get(i);
@ -349,11 +308,11 @@ public class TaskList extends Node {
}
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");
@ -361,7 +320,7 @@ public class TaskList extends Node {
}
return mChildren.get(index);
}
//根据子任务的 gid 查找特定子任务
public Task getChilTaskByGid(String gid) {
for (Task task : mChildren) {
if (task.getGid().equals(gid))
@ -369,15 +328,15 @@ public class TaskList extends Node {
}
return null;
}
//获取子任务列表
public ArrayList<Task> getChildTaskList() {
return this.mChildren;
}
//设置索引
public void setIndex(int index) {
this.mIndex = index;
}
//获取索引
public int getIndex() {
return this.mIndex;
}

@ -14,20 +14,20 @@
* limitations under the License.
*/
package net.micode.notes.gtask.exception;// 导入包声明,这个类属于小米便签应用中的异常处理模块
package net.micode.notes.gtask.exception;
public class ActionFailureException extends RuntimeException {//ActionFailureException 类是小米便签应用中定义的一个运行时异常类,
private static final long serialVersionUID = 4425249765923293627L;//这是一个固定的序列化版本号,用于确保在序列化和反序列化过程中,类的兼容性和一致性。
public class ActionFailureException extends RuntimeException {
private static final long serialVersionUID = 4425249765923293627L;
public ActionFailureException() {
super();
}
public ActionFailureException(String paramString) {//创建一个携带失败原因信息的 ActionFailureException 异常实例。
public ActionFailureException(String paramString) {
super(paramString);
}
public ActionFailureException(String paramString, Throwable paramThrowable) {//同时还接收一个 Throwable 对象paramThrowable该对象通常是指引发当前异常的实际底层异常。
public ActionFailureException(String paramString, Throwable paramThrowable) {
super(paramString, paramThrowable);
}
}

@ -15,19 +15,19 @@
*/
package net.micode.notes.gtask.exception;
//网络异常检测检测和处理
public class NetworkFailureException extends Exception {
private static final long serialVersionUID = 2107610287180234136L; //异常版本ID
private static final long serialVersionUID = 2107610287180234136L;
public NetworkFailureException() { //重写并调用超类方法,
super(); //当网络操作失败且没有特定的错误信息需要传达时,可以使用这个构造函数抛出异常。
public NetworkFailureException() {
super();
}
public NetworkFailureException(String paramString) {
super(paramString); //网络发生异常,需要传递相关排除异常的详细信息,有助于了解和解决异常
super(paramString);
}
public NetworkFailureException(String paramString, Throwable paramThrowable) {
super(paramString, paramThrowable); //获得易于理解的错误描述,还能跟踪到原始的异常堆栈信息,有利于问题排查
super(paramString, paramThrowable);
}
}

@ -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.gtask.remote;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
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;
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();
}
public void cancelSync() {
mTaskManager.cancelSync();
}
public void publishProgess(String message) {
publishProgress(new String[] {
message
});
}
// private void showNotification(int tickerId, String content) {
// Notification notification = new Notification(R.drawable.notification, mContext
// .getString(tickerId), System.currentTimeMillis());
// notification.defaults = Notification.DEFAULT_LIGHTS;
// notification.flags = Notification.FLAG_AUTO_CANCEL;
// PendingIntent pendingIntent;
// if (tickerId != R.string.ticker_success) {
// pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext,
// NotesPreferenceActivity.class), 0);
//
// } else {
// pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext,
// NotesListActivity.class), 0);
// }
// notification.setLatestEventInfo(mContext, mContext.getString(R.string.app_name), content,
// pendingIntent);
// mNotifiManager.notify(GTASK_SYNC_NOTIFICATION_ID, notification);
// }
private void showNotification(int tickerId, String content) {
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);
}
Notification.Builder builder = new Notification.Builder(mContext)
.setAutoCancel(true)
.setContentTitle(mContext.getString(R.string.app_name))
.setContentText(content)
.setContentIntent(pendingIntent)
.setWhen(System.currentTimeMillis())
.setOngoing(true);
Notification notification=builder.getNotification();
mNotifiManager.notify(GTASK_SYNC_NOTIFICATION_ID, notification);
}
@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();
}
}
}

@ -62,39 +62,35 @@ import java.util.zip.InflaterInputStream;
public class GTaskClient {
// 定义日志TAG方便调试输出
private static final String TAG = GTaskClient.class.getSimpleName();
// 定义Google Tasks服务的基本URL
private static final String GTASK_URL = "https://mail.google.com/tasks/";
// 定义获取任务信息的GET请求URL
private static final String GTASK_GET_URL = "https://mail.google.com/tasks/ig";
// 定义提交任务更新等POST请求URL
private static final String GTASK_POST_URL = "https://mail.google.com/tasks/r/ig";
// 单例模式确保全局只有一个GTaskClient实例
private static GTaskClient mInstance = null;
// HTTP客户端实例用于发起网络请求
private DefaultHttpClient mHttpClient;
// 存储GET请求的实际URL
private String mGetUrl;
// 存储POST请求的实际URL
private String mPostUrl;
// 客户端版本号
private long mClientVersion;
// 登录状态标志位,标识是否已登录
private boolean mLoggedin;
// 最后登录时间戳
private long mLastLoginTime;
// 行动操作ID用于跟踪每个请求的唯一性
private int mActionId;
// Google账户对象当前正在使用的同步账户
private Account mAccount;
// 用于缓存待更新的任务数据的JSONArray
private JSONArray mUpdateArray;
// 私有构造函数,防止外部直接实例化
private GTaskClient() {
// 初始化各个成员变量
mHttpClient = null;
mGetUrl = GTASK_GET_URL;
mPostUrl = GTASK_POST_URL;
@ -106,45 +102,40 @@ public class GTaskClient {
mUpdateArray = null;
}
// 静态工厂方法获取GTaskClient的单例
public static synchronized GTaskClient getInstance() {
// 如果实例尚未创建则创建并返回一个新的GTaskClient实例
if (mInstance == null) {
mInstance = new GTaskClient();
}
return mInstance;
}
// 登录方法尝试登录Google账户并验证GTask服务访问权限
public boolean login(Activity activity) {
// we suppose that the cookie would expire after 5 minutes
// then we need to re-login
// 判断上次登录时间是否超过5分钟若超过则需要重新登录
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))) {
.getSyncAccountName(activity))) {
mLoggedin = false;
}
// 如果已登录则直接返回true
if (mLoggedin) {
Log.d(TAG, "already logged in");
return true;
}
// 更新最后登录时间
mLastLoginTime = System.currentTimeMillis();
// 获取Google账户授权令牌
String authToken = loginGoogleAccount(activity, false);
if (authToken == null) {
Log.e(TAG, "login google account failed");
return false;
}
// 对于非Gmail或Googlemail域的账户尝试使用自定义域名登录GTask服务
// login with custom domain if necessary
if (!(mAccount.name.toLowerCase().endsWith("gmail.com") || mAccount.name.toLowerCase()
.endsWith("googlemail.com"))) {
@ -160,7 +151,6 @@ public class GTaskClient {
}
}
// 若未登录成功则尝试使用官方URL登录GTask服务
// try to login with google official url
if (!mLoggedin) {
mGetUrl = GTASK_GET_URL;
@ -169,56 +159,47 @@ public class GTaskClient {
return false;
}
}
// 登录成功,设置登录状态为已登录
mLoggedin = true;
return true;
}
// 辅助方法从Google账户获取授权令牌
private String loginGoogleAccount(Activity activity, boolean invalidateToken) {
String authToken;
// 获取AccountManager实例用于管理账户认证
AccountManager accountManager = AccountManager.get(activity);
// 获取所有类型为"com.google"的账户
Account[] accounts = accountManager.getAccountsByType("com.google");
// 检查是否存在可用的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 {
} 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) {
} catch (Exception e) {
Log.e(TAG, "get auth token failed");
authToken = null;
}
@ -226,69 +207,54 @@ public class GTaskClient {
return authToken;
}
// 这个方法尝试登录GTask服务。首先使用提供的authToken进行登录如果失败则尝试刷新并重新登录。
// 登录成功后返回true否则返回false。
private boolean tryToLoginGtask(Activity activity, String authToken) {
// 使用给定的authToken尝试登录GTask
if (!loginGtask(authToken)) {
// 如果登录失败可能是authToken过期则尝试通过Google账号刷新并获取新的authToken
// maybe the auth token is out of date, now let's invalidate the
// token and try again
authToken = loginGoogleAccount(activity, true);
// 如果刷新并获取新的authToken失败
if (authToken == null) {
Log.e(TAG, "login google account failed");
return false;// 返回登录失败
return false;
}
// 使用新的authToken再次尝试登录GTask
if (!loginGtask(authToken)) {
Log.e(TAG, "login gtask failed");// 记录错误日志
return false;// 返回登录失败
Log.e(TAG, "login gtask failed");
return false;
}
}
// 登录成功
return true;
}
// 这个私有方法用于执行实际的GTask登录过程根据提供的authToken发送HTTP请求并检查响应中的认证Cookie是否存在。
private boolean loginGtask(String authToken) {
// 设置HTTP连接和读取超时时间
int timeoutConnection = 10000;
int timeoutSocket = 15000;
HttpParams httpParameters = new BasicHttpParams();
HttpConnectionParams.setConnectionTimeout(httpParameters, timeoutConnection);
HttpConnectionParams.setSoTimeout(httpParameters, timeoutSocket);
mHttpClient = new DefaultHttpClient(httpParameters); // 创建一个HTTP客户端实例
// 创建并设置Cookie存储对象
mHttpClient = new DefaultHttpClient(httpParameters);
BasicCookieStore localBasicCookieStore = new BasicCookieStore();
mHttpClient.setCookieStore(localBasicCookieStore);
// 设置HTTP协议参数
HttpProtocolParams.setUseExpectContinue(mHttpClient.getParams(), false);
// login gtask
try {
// 构建登录URL并发起GET请求
String loginUrl = mGetUrl + "?auth=" + authToken;
HttpGet httpGet = new HttpGet(loginUrl);
// 执行HTTP请求并获取响应
HttpResponse response = null;
response = mHttpClient.execute(httpGet);
// 检查响应中是否包含认证Cookie
// get the cookie now
List<Cookie> cookies = mHttpClient.getCookieStore().getCookies();
boolean hasAuthCookie = false;
for (Cookie cookie : cookies) {
if (cookie.getName().contains("GTL")) {
hasAuthCookie = true;// 如果找到名为"GTL"的Cookie则表示存在认证Cookie
hasAuthCookie = true;
}
}
if (!hasAuthCookie) {
Log.w(TAG, "it seems that there is no auth cookie");// 若未发现认证Cookie则记录警告日志
Log.w(TAG, "it seems that there is no auth cookie");
}
// 从响应内容中解析客户端版本信息
// get the client version
String resString = getResponseContent(response.getEntity());
String jsBegin = "_setup(";
@ -300,216 +266,166 @@ public class GTaskClient {
jsString = resString.substring(begin + jsBegin.length(), end);
}
JSONObject js = new JSONObject(jsString);
mClientVersion = js.getLong("v");// 获取并保存客户端版本号
}
catch (JSONException e) {
Log.e(TAG, e.toString());// 记录JSON解析异常的日志
mClientVersion = js.getLong("v");
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
return false;// JSON解析出错返回登录失败
}
catch (Exception e) {// 捕获所有其他可能的异常
return false;
} catch (Exception e) {
// simply catch all exceptions
Log.e(TAG, "httpget gtask_url failed");// 记录HTTP GET请求失败的日志
return false;// HTTP请求出错返回登录失败
Log.e(TAG, "httpget gtask_url failed");
return false;
}
// 如果执行到此处,说明登录过程顺利完成
return true;
}
// 这是一个辅助方法用于生成并返回唯一的动作ID。
private int getActionId() {
return mActionId++;// 自增并返回mActionId的当前值
return mActionId++;
}
// 这个私有方法用于创建并配置一个HttpPost对象指定目标URL和相关HTTP头部信息。
private HttpPost createHttpPost() {
// 创建一个新的HttpPost对象指向预定义的POST请求URL
HttpPost httpPost = new HttpPost(mPostUrl);
// 设置HTTP头部Content-Type字段表明发送的数据格式为application/x-www-form-urlencoded并采用UTF-8编码
httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8");
// 添加自定义HTTP头部信息这里设定了一个名为"AT"的字段,其值为"1"
httpPost.setHeader("AT", "1");
// 返回已配置好的HttpPost对象
return httpPost;
}
// 这个私有方法用于从HttpEntity中获取并解析响应内容支持gzip和deflate压缩格式。
private String getResponseContent(HttpEntity entity) throws IOException {
// 获取实体的Content-Encoding属性即数据的压缩编码方式
String contentEncoding = null;
if (entity.getContentEncoding() != null) {
contentEncoding = entity.getContentEncoding().getValue();
Log.d(TAG, "encoding: " + contentEncoding);// 输出编码方式至日志
Log.d(TAG, "encoding: " + contentEncoding);
}
// 获取实体内容的输入流
InputStream input = entity.getContent();
// 根据Content-Encoding选择合适的解压方式
if (contentEncoding != null && contentEncoding.equalsIgnoreCase("gzip")) {
// 如果数据被gzip压缩使用GZIPInputStream解压
input = new GZIPInputStream(entity.getContent());
}
else if (contentEncoding != null && contentEncoding.equalsIgnoreCase("deflate")) {
// 如果数据被deflate压缩使用InflaterInputStream解压注意这里的Inflater构造函数传入true启用ZLIB RFC1950格式
} else if (contentEncoding != null && contentEncoding.equalsIgnoreCase("deflate")) {
Inflater inflater = new Inflater(true);
input = new InflaterInputStream(entity.getContent(), inflater);
}
// 使用Reader和BufferedReader逐行读取并拼接响应内容
try {
InputStreamReader isr = new InputStreamReader(input);// 使用UTF-8编码创建StreamReader
InputStreamReader isr = new InputStreamReader(input);
BufferedReader br = new BufferedReader(isr);
StringBuilder sb = new StringBuilder();
// 循环读取每一行内容直到结束
while (true) {
String buff = br.readLine();
if (buff == null) {
// 当读取到最后一行null返回累积的字符串内容
return sb.toString();
}
sb = sb.append(buff);// 将读取到的一行追加到StringBuilder中
sb = sb.append(buff);
}
}
finally {
// 在所有操作结束后确保关闭输入流
} finally {
input.close();
}
}
// 这个私有方法用于执行一个POST请求并将JSON对象作为请求体发送。若发生网络错误或无法将响应转换为JSON对象将抛出异常
private JSONObject postRequest(JSONObject js) throws NetworkFailureException {
// 验证用户是否已登录,未登录则抛出自定义异常
if (!mLoggedin) {
Log.e(TAG, "please login first");
throw new ActionFailureException("not logged in");
}
// 创建并初始化HttpPost对象
HttpPost httpPost = createHttpPost();
try {
// 创建一个包含请求参数的链表,参数名为'r'值为传入的JSON对象的字符串形式
LinkedList<BasicNameValuePair> list = new LinkedList<BasicNameValuePair>();
list.add(new BasicNameValuePair("r", js.toString()));
// 创建一个UrlEncodedFormEntity对象编码格式为UTF-8用于封装请求参数
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(list, "UTF-8");
httpPost.setEntity(entity);
// execute the post
// 执行POST请求
HttpResponse response = mHttpClient.execute(httpPost);
// 从响应中获取内容并转换为JSON对象
String jsString = getResponseContent(response.getEntity());
return new JSONObject(jsString);
}
catch (ClientProtocolException e) {
// 记录并抛出网络协议错误异常
} catch (ClientProtocolException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new NetworkFailureException("postRequest failed");
}
catch (IOException e) {
// 记录并抛出JSON解析错误异常
} catch (IOException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new NetworkFailureException("postRequest failed");
}
catch (JSONException e) {
// 记录并抛出任何未知错误异常
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("unable to convert response content to jsonobject");
}
catch (Exception e) {
} 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 {
// 创建一个用于发送的JSON对象
JSONObject jsPost = new JSONObject();
JSONArray actionList = new JSONArray();// 创建一个JSONArray存放动作列表
JSONArray actionList = new JSONArray();
// action_list
// 将新任务的动作信息添加到action_list中
actionList.put(task.getCreateAction(getActionId()));
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
// 添加客户端版本信息到JSON对象
// client_version
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
// 调用postRequest方法执行POST请求
// post
JSONObject jsResponse = postRequest(jsPost);
// 从响应的JSON对象中提取结果数组的第一个元素
JSONObject jsResult = (JSONObject)jsResponse.getJSONArray(
JSONObject jsResult = (JSONObject) jsResponse.getJSONArray(
GTaskStringUtils.GTASK_JSON_RESULTS).get(0);
// 从响应结果中获取新创建的任务ID并将其设置到task对象中
task.setGid(jsResult.getString(GTaskStringUtils.GTASK_JSON_NEW_ID));
}
catch (JSONException e) {
// 记录并抛出JSON处理异常
} 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 {
// 创建用于发送请求的JSON对象
JSONObject jsPost = new JSONObject();
JSONArray actionList = new JSONArray();// 创建一个JSONArray来存储动作列表
JSONArray actionList = new JSONArray();
// 将新建任务列表的动作信息加入到动作列表中
// action_list
actionList.put(tasklist.getCreateAction(getActionId()));
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);// 将动作列表放入JSON对象
// 添加客户端版本信息到JSON对象
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
// client version
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
// 发送POST请求
// post
JSONObject jsResponse = postRequest(jsPost);
// 从响应的JSON对象中获取结果数组的第一个元素
JSONObject jsResult = (JSONObject)jsResponse.getJSONArray(
JSONObject jsResult = (JSONObject) jsResponse.getJSONArray(
GTaskStringUtils.GTASK_JSON_RESULTS).get(0);
// 从响应结果中取出新创建的任务列表ID并设置到tasklist对象中
tasklist.setGid(jsResult.getString(GTaskStringUtils.GTASK_JSON_NEW_ID));
}
catch (JSONException e) {
// 处理JSON异常并抛出操作失败异常
} 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();// 创建用于发送请求的JSON对象
JSONObject jsPost = new JSONObject();
// action_list
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, mUpdateArray);// 将待提交的动作列表放入JSON对象
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, mUpdateArray);
// client_version
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); // 添加客户端版本信息到JSON对象
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
postRequest(jsPost);// 发送POST请求
mUpdateArray = null; // 清空已提交的更新数组
}
catch (JSONException e) {// 处理JSON异常并抛出操作失败异常
postRequest(jsPost);
mUpdateArray = null;
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("commit update: handing jsonobject failed");
@ -517,102 +433,94 @@ public class GTaskClient {
}
}
public void addUpdateNode(Node node) throws NetworkFailureException {// 此公共方法用于向待更新的动作列表中添加一个节点的更新动作
if (node != null) {// 如果节点非空
// 避免一次性更新过多导致错误限制最多允许10个更新项
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();// 超过10个则先提交现有的更新
commitUpdate();
}
if (mUpdateArray == null)
mUpdateArray = new JSONArray(); // 如果待更新数组为空则创建一个新的JSONArray
mUpdateArray.put(node.getUpdateAction(getActionId()));// 将节点的更新动作添加到待更新数组中
mUpdateArray = new JSONArray();
mUpdateArray.put(node.getUpdateAction(getActionId()));
}
}
public void moveTask(Task task, TaskList preParent, TaskList curParent)// 此方法用于移动一个任务到另一个任务列表中,包括在同一任务列表内调整位置和跨任务列表移动
throws NetworkFailureException{
commitUpdate();// 在移动任务之前先提交任何待更新的操作
try {// 创建用于发送请求的JSON对象
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());// 动作ID
action.put(GTaskStringUtils.GTASK_JSON_ID, task.getGid());// 目标任务ID
if (preParent == curParent && task.getPriorSibling() != null) {// 如果当前任务列表与原父任务列表相同并且该任务不是第一个子任务则添加前置兄弟任务ID
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());// 添加原父任务列表ID
action.put(GTaskStringUtils.GTASK_JSON_DEST_PARENT, curParent.getGid());// 添加目标父任务列表ID
if (preParent != curParent) {// 如果任务要从一个任务列表移动到另一个任务列表则添加目标任务列表ID
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);// 将移动动作添加到动作列表中
actionList.put(action);
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
// client_version
// 添加客户端版本信息
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
// 发送POST请求
postRequest(jsPost);
}
catch (JSONException e) {
} 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 {// 创建用于发送请求的JSON对象
commitUpdate();
try {
JSONObject jsPost = new JSONObject();
JSONArray actionList = new JSONArray();
// action_list
node.setDeleted(true);// 将节点标记为已删除,并将其更新动作添加到动作列表中
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);// 添加客户端版本信息
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
postRequest(jsPost);// 发送POST请求
mUpdateArray = null;// 清空已提交的更新数组
}
catch (JSONException e) {
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) {// 检查用户是否已登录,未登录则抛出异常
public JSONArray getTaskLists() throws NetworkFailureException {
if (!mLoggedin) {
Log.e(TAG, "please login first");
throw new ActionFailureException("not logged in");
}
try {// 创建并执行HttpGet请求
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>";
@ -622,67 +530,56 @@ public class GTaskClient {
if (begin != -1 && end != -1 && begin < end) {
jsString = resString.substring(begin + jsBegin.length(), end);
}
// 将解析出的JSON字符串转换为JSONObject并从中提取任务列表数据
JSONObject js = new JSONObject(jsString);
return js.getJSONObject("t").getJSONArray(GTaskStringUtils.GTASK_JSON_LISTS);
}
catch (ClientProtocolException e) {
} catch (ClientProtocolException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new NetworkFailureException("gettasklists: httpget failed");
}
catch (IOException e) {
} catch (IOException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new NetworkFailureException("gettasklists: httpget failed");
}
catch (JSONException e) {
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("get task lists: handing jasonobject failed");
}
}
// 此方法用于获取指定任务列表ID下的所有任务
public JSONArray getTaskList(String listGid) throws NetworkFailureException {
// 在获取任务列表前先提交任何待更新的操作
commitUpdate();
try {// 创建用于发送请求的JSON对象及动作列表
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());// 设置动作ID
action.put(GTaskStringUtils.GTASK_JSON_LIST_ID, listGid);// 设置要获取的任务列表ID
action.put(GTaskStringUtils.GTASK_JSON_GET_DELETED, false);// 不获取已删除的任务
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); // 将动作列表放入JSON请求体
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
// client_version
// 添加客户端版本信息到JSON请求体
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
// 发送POST请求并获取响应
JSONObject jsResponse = postRequest(jsPost);
// 从响应的JSON对象中提取任务列表tasks并返回
return jsResponse.getJSONArray(GTaskStringUtils.GTASK_JSON_TASKS);
}
catch (JSONException e) {
Log.e(TAG, e.toString());// 记录并打印异常信息
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("get task list: handing jsonobject failed");// 抛出操作失败异常
throw new ActionFailureException("get task list: handing jsonobject failed");
}
}
public Account getSyncAccount() {// 此方法用于获取同步账户对象
return mAccount;// 直接返回当前持有的同步账户对象
public Account getSyncAccount() {
return mAccount;
}
public void resetUpdateArray() {// 此方法用于重置待更新动作数组
mUpdateArray = null;// 将待更新动作数组设为null表示清空所有待更新项
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;
}
}

@ -23,92 +23,69 @@ import android.content.Intent;
import android.os.Bundle;
import android.os.IBinder;
//定义一个服务类GTaskSyncService用于实现Google Tasks的同步服务
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 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"; // 同步进度消息
// 定义静态成员变量,用于保存异步同步任务实例
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) {
// 创建新的GTaskASyncTask实例
mSyncTask = new GTaskASyncTask(this, new GTaskASyncTask.OnCompleteListener() {
@Override
public void onComplete() {
// 当同步完成时,取消引用并停止服务
mSyncTask = null;
sendBroadcast(""); // 发送广播通知同步已完成
stopSelf(); // 停止当前服务
sendBroadcast("");
stopSelf();
}
});
// 发送初始广播通知开始同步
sendBroadcast("");
// 执行异步同步任务
mSyncTask.execute();
}
}
// 取消同步任务的方法
private void cancelSync() {
// 如果当前有同步任务正在进行
if (mSyncTask != null) {
// 取消同步任务
mSyncTask.cancelSync();
}
}
// 服务创建时重置同步任务
@Override
public void onCreate() {
mSyncTask = null;
}
// 服务启动时接收并处理Intent
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Bundle bundle = intent.getExtras();
// 如果Intent携带了ACTION_STRING_NAME键的数据
if (bundle != null && bundle.containsKey(ACTION_STRING_NAME)) {
// 获取并判断动作类型
switch (bundle.getInt(ACTION_STRING_NAME, ACTION_INVALID)) {
case ACTION_START_SYNC:
startSync(); // 开始同步
startSync();
break;
case ACTION_CANCEL_SYNC:
cancelSync(); // 取消同步
cancelSync();
break;
default:
break;
}
// 返回START_STICKY使得服务在被系统杀死后自动重启
return START_STICKY;
}
// 若Intent不满足条件调用父类方法
return super.onStartCommand(intent, flags, startId);
}
// 当系统内存不足时,尝试取消同步任务
@Override
public void onLowMemory() {
if (mSyncTask != null) {
@ -116,42 +93,35 @@ public class GTaskSyncService extends Service {
}
}
// 绑定服务时返回null表明此服务不支持绑定
@Override
public IBinder onBind(Intent intent) {
return null;
}
// 发送广播的方法,用于传递同步状态和进度信息
public void sendBroadcast(String msg) {
mSyncProgress = 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); // 发送广播
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(ACTION_STRING_NAME, ACTION_START_SYNC);
activity.startService(intent); // 由Activity启动服务并开始同步
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(ACTION_STRING_NAME, ACTION_CANCEL_SYNC);
context.startService(intent); // 由Context启动服务并取消同步
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;
}

@ -15,15 +15,15 @@
*/
package net.micode.notes.model;
import android.content.ContentProviderOperation;//批量的更新、插入、删除数据。
import android.content.ContentProviderResult;//操作的结果
import android.content.ContentUris;//用于添加和获取Uri后面的ID
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 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;
@ -49,18 +49,16 @@ public class Note {
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);//将数据写入数据库表格
values.put(NoteColumns.PARENT_ID, folderId);
Uri uri = context.getContentResolver().insert(Notes.CONTENT_NOTE_URI, values);
//ContentResolver()主要是实现外部应用对ContentProvider中的数据
//进行添加、删除、修改和查询操作
long noteId = 0;
try {
noteId = Long.valueOf(uri.getPathSegments().get(1));
}
catch (NumberFormatException e) {
} catch (NumberFormatException e) {
Log.e(TAG, "Get note id error :" + e.toString());
noteId = 0;
}//try-catch异常处理
}
if (noteId == -1) {
throw new IllegalStateException("Wrong note id:" + noteId);
}
@ -70,37 +68,37 @@ public class Note {
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);
}//设置文本数据的ID
}
public long getTextDataId() {
return mNoteData.mTextDataId;
}//得到文本数据的ID
}
public void setCallDataId(long id) {
mNoteData.setCallDataId(id);
}//设置电话号码数据的ID
}
public void setCallData(String key, String value) {
mNoteData.setCallData(key, value);
}//得到电话号码数据的ID
}
public boolean isLocalModified() {
return mNoteDiffValues.size() > 0 || mNoteData.isLocalModified();
}//判断是否是本地修改
}
public boolean syncNote(Context context, long noteId) {
if (noteId <= 0) {
@ -130,16 +128,16 @@ public class Note {
}
return true;
}//判断数据是否同步
}
private class NoteData {//定义一个基本的便签内容的数据类,主要包含文本数据和电话号码数据
private class NoteData {
private long mTextDataId;
private ContentValues mTextDataValues;//文本数据
private ContentValues mTextDataValues;
private long mCallDataId;
private ContentValues mCallDataValues;//电话号码数据
private ContentValues mCallDataValues;
private static final String TAG = "NoteData";
@ -149,13 +147,13 @@ public class Note {
mTextDataId = 0;
mCallDataId = 0;
}
//下面是上述几个函数的具体实现
boolean isLocalModified() {
return mTextDataValues.size() > 0 || mCallDataValues.size() > 0;
}
void setTextDataId(long id) {
if (id <= 0) {
if(id <= 0) {
throw new IllegalArgumentException("Text data id should larger than 0");
}
mTextDataId = id;
@ -179,19 +177,19 @@ public class Note {
mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1);
mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis());
}
//下面函数的作用是将新的数据通过Uri的操作存储到数据库
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;//数据库的操作列表
ContentProviderOperation.Builder builder = null;
if (mTextDataValues.size() > 0) {
if(mTextDataValues.size() > 0) {
mTextDataValues.put(DataColumns.NOTE_ID, noteId);
if (mTextDataId == 0) {
mTextDataValues.put(DataColumns.MIME_TYPE, TextNote.CONTENT_ITEM_TYPE);
@ -199,23 +197,21 @@ public class Note {
mTextDataValues);
try {
setTextDataId(Long.valueOf(uri.getPathSegments().get(1)));
}
catch (NumberFormatException e) {
} catch (NumberFormatException e) {
Log.e(TAG, "Insert new text data fail with noteId" + noteId);
mTextDataValues.clear();
return null;
}
}
else {
} else {
builder = ContentProviderOperation.newUpdate(ContentUris.withAppendedId(
Notes.CONTENT_DATA_URI, mTextDataId));
builder.withValues(mTextDataValues);
operationList.add(builder.build());
}
mTextDataValues.clear();
}//把文本数据存入DataColumns
}
if (mCallDataValues.size() > 0) {
if(mCallDataValues.size() > 0) {
mCallDataValues.put(DataColumns.NOTE_ID, noteId);
if (mCallDataId == 0) {
mCallDataValues.put(DataColumns.MIME_TYPE, CallNote.CONTENT_ITEM_TYPE);
@ -223,21 +219,19 @@ public class Note {
mCallDataValues);
try {
setCallDataId(Long.valueOf(uri.getPathSegments().get(1)));
}
catch (NumberFormatException e) {
} catch (NumberFormatException e) {
Log.e(TAG, "Insert new call data fail with noteId" + noteId);
mCallDataValues.clear();
return null;
}
}
else {
} else {
builder = ContentProviderOperation.newUpdate(ContentUris.withAppendedId(
Notes.CONTENT_DATA_URI, mCallDataId));
builder.withValues(mCallDataValues);
operationList.add(builder.build());
}
mCallDataValues.clear();
}//把电话号码数据存入DataColumns
}
if (operationList.size() > 0) {
try {
@ -245,16 +239,14 @@ public class Note {
Notes.AUTHORITY, operationList);
return (results == null || results.length == 0 || results[0] == null) ? null
: ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId);
}
catch (RemoteException e) {
} catch (RemoteException e) {
Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage()));
return null;
}
catch (OperationApplicationException e) {
} catch (OperationApplicationException e) {
Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage()));
return null;
}
}//存储过程中的异常处理
}
return null;
}
}

@ -31,6 +31,7 @@ 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;
@ -42,11 +43,19 @@ public class WorkingNote {
private int mMode;
private long mAlertDate;
private long mModifiedDate;
private int mBgColorId;
private int mCategoryId;
private int mWidgetId;
private int mWidgetType;
private long mFolderId;
private Context mContext;
private static final String TAG = "WorkingNote";
@ -55,7 +64,6 @@ public class WorkingNote {
private NoteSettingChangedListener mNoteSettingStatusListener;
// 声明 DATA_PROJECTION字符串数组
public static final String[] DATA_PROJECTION = new String[] {
DataColumns.ID,
DataColumns.CONTENT,
@ -66,29 +74,40 @@ public class WorkingNote {
DataColumns.DATA4,
};
// 声明 NOTE_PROJECTION字符串数组
public static final String[] NOTE_PROJECTION = new String[] {
NoteColumns.PARENT_ID,
NoteColumns.ALERTED_DATE,
NoteColumns.BG_COLOR_ID,
NoteColumns.CATEGORY_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;
private static final int NOTE_CATEGORY_ID_COLUMN = 3;
private static final int NOTE_WIDGET_ID_COLUMN = 4;
private static final int NOTE_WIDGET_TYPE_COLUMN = 5;
private static final int NOTE_MODIFIED_DATE_COLUMN = 6;
// New note construct
public WorkingNote(Context context, long folderId) {
private WorkingNote(Context context, long folderId) {
mContext = context;
mAlertDate = 0;
mModifiedDate = System.currentTimeMillis();
@ -100,7 +119,6 @@ public class WorkingNote {
mWidgetType = Notes.TYPE_WIDGET_INVALIDE;
}
// WorkingNote的构造函数
// Existing note construct
private WorkingNote(Context context, long noteId, long folderId) {
mContext = context;
@ -111,25 +129,22 @@ public class WorkingNote {
loadNote();
}
// 加载Note
// 通过数据库调用query函数找到第一个条目
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);
mCategoryId = cursor.getInt(NOTE_CATEGORY_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);
@ -137,16 +152,14 @@ public class WorkingNote {
loadNoteData();
}
// 加载NoteData
private void loadNoteData() {
Cursor cursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI, DATA_PROJECTION,
DataColumns.NOTE_ID + "=?", new String[] {
String.valueOf(mNoteId)
String.valueOf(mNoteId)
}, null);
if (cursor != null) {
// 查到信息不为空
if (cursor.moveToFirst()) { // 查看第一项是否存在
if (cursor.moveToFirst()) {
do {
String type = cursor.getString(DATA_MIME_TYPE_COLUMN);
if (DataConstants.NOTE.equals(type)) {
@ -158,7 +171,7 @@ public class WorkingNote {
} else {
Log.d(TAG, "Wrong note type with type:" + type);
}
} while (cursor.moveToNext());//查阅所有项,直到为空
} while (cursor.moveToNext());
}
cursor.close();
} else {
@ -167,12 +180,9 @@ public class WorkingNote {
}
}
// 创建空的Note
// 传参context文件夹idwidget背景颜色
public static WorkingNote createEmptyNote(Context context, long folderId, int widgetId,
int widgetType, int defaultBgColorId) {
int widgetType, int defaultBgColorId) {
WorkingNote note = new WorkingNote(context, folderId);
// 设定相关属性
note.setBgColorId(defaultBgColorId);
note.setWidgetId(widgetId);
note.setWidgetType(widgetType);
@ -183,10 +193,9 @@ public class WorkingNote {
return new WorkingNote(context, id, 0);
}
// 保存Note
public synchronized boolean saveNote() {
if (isWorthSaving()) { //是否值得保存
if (!existInDatabase()) { // 是否存在数据库中
if (isWorthSaving()) {
if (!existInDatabase()) {
if ((mNoteId = Note.getNewNoteId(mContext, mFolderId)) == 0) {
Log.e(TAG, "Create new note fail with id:" + mNoteId);
return false;
@ -209,14 +218,11 @@ public class WorkingNote {
}
}
// 是否在数据库中存在
public boolean existInDatabase() {
return mNoteId > 0;
}
// 是否值得保存
private boolean isWorthSaving() {
// 被删除,或(不在数据库中 内容为空),或 本地已保存过
if (mIsDeleted || (!existInDatabase() && TextUtils.isEmpty(mContent))
|| (existInDatabase() && !mNote.isLocalModified())) {
return false;
@ -225,14 +231,10 @@ public class WorkingNote {
}
}
// 设置mNoteSettingStatusListener
public void setOnSettingStatusChangedListener(NoteSettingChangedListener l) {
mNoteSettingStatusListener = l;
}
// 设置AlertDate
// 若 mAlertDate与data不同则更改mAlertDate并设定NoteValue
public void setAlertDate(long date, boolean set) {
if (date != mAlertDate) {
mAlertDate = date;
@ -243,20 +245,16 @@ public class WorkingNote {
}
}
// 设定删除标记
public void markDeleted(boolean mark) {
// 设定标记
mIsDeleted = mark;
if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID
&& mWidgetType != Notes.TYPE_WIDGET_INVALIDE && mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onWidgetChanged();
// 调用mNoteSettingStatusListener的 onWidgetChanged方法
mNoteSettingStatusListener.onWidgetChanged();
}
}
// 设定背景颜色
public void setBgColorId(int id) {
if (id != mBgColorId) { //设定条件 id != mBgColorId
if (id != mBgColorId) {
mBgColorId = id;
if (mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onBackgroundColorChanged();
@ -265,10 +263,19 @@ public class WorkingNote {
}
}
// 设定检查列表模式
// 参数mode
public void setCategoryId(int id) {
if (id != mCategoryId) {
mCategoryId = id;
mNote.setNoteValue(NoteColumns.CATEGORY_ID, String.valueOf(id));
}
}
public int getCategoryId() {
return mCategoryId;
}
public void setCheckListMode(int mode) {
if (mMode != mode) { //设定条件 mMode != mode
if (mMode != mode) {
if (mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onCheckListModeChanged(mMode, mode);
}
@ -277,108 +284,81 @@ public class WorkingNote {
}
}
// 设定WidgetType
// 参数type
public void setWidgetType(int type) {
if (type != mWidgetType) {//设定条件 type != mWidgetType
if (type != mWidgetType) {
mWidgetType = type;
mNote.setNoteValue(NoteColumns.WIDGET_TYPE, String.valueOf(mWidgetType));
// 调用Note的setNoteValue方法更改WidgetType
}
}
// 设定WidgetId
// 参数id
public void setWidgetId(int id) {
if (id != mWidgetId) {//设定条件 id != mWidgetId
if (id != mWidgetId) {
mWidgetId = id;
mNote.setNoteValue(NoteColumns.WIDGET_ID, String.valueOf(mWidgetId));
// 调用Note的setNoteValue方法更改WidgetId
}
}
// 设定WorkingTex
// 参数更改的text
public void setWorkingText(String text) {
if (!TextUtils.equals(mContent, text)) {//设定条件 mContent, text内容不同
if (!TextUtils.equals(mContent, text)) {
mContent = text;
mNote.setTextData(DataColumns.CONTENT, mContent);
// 调用Note的setTextData方法更改WorkingText
}
}
// 转变mNote的CallData及CallNote信息
// 参数String phoneNumber, long callDate
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);
}
// 获取Content
public String getContent() {
return mContent;
}
// 获取AlertDate
public long getAlertDate() {
return mAlertDate;
}
// 获取ModifiedDate
public long getModifiedDate() {
return mModifiedDate;
}
// 获取背景颜色来源id
public int getBgColorResId() {
return NoteBgResources.getNoteBgResource(mBgColorId);
}
// 获取背景颜色id
public int getBgColorId() {
return mBgColorId;
}
// 获取标题背景颜色id
public int getTitleBgResId() {
return NoteBgResources.getNoteTitleBgResource(mBgColorId);
}
// 获取CheckListMode
public int getCheckListMode() {
return mMode;
}
// 获取便签id
public long getNoteId() {
return mNoteId;
}
// 获取文件夹id
public long getFolderId() {
return mFolderId;
}
// 获取WidgetId
public int getWidgetId() {
return mWidgetId;
}
// 获取WidgetType
public int getWidgetType() {
return mWidgetType;
}
// 创建接口 NoteSettingChangedListener,便签更新监视
// 为NoteEditActivity提供接口
// 提供函数有
public interface NoteSettingChangedListener {
/**
* Called when the background color of current note has just changed
@ -403,4 +383,3 @@ public class WorkingNote {
void onCheckListModeChanged(int oldMode, int newMode);
}
}

@ -37,6 +37,39 @@ import java.util.HashSet;
//处理数据的转换和格式化
public class DataUtils {
public static final String TAG = "DataUtils";
//置顶笔记,通过更新修改时间为当前时间来实现
public static boolean pinNote(ContentResolver resolver, HashSet<Long> ids) {
if (ids == null || ids.size() == 0) {
Log.d(TAG, "No notes to pin");
return true;
}
ArrayList<ContentProviderOperation> operationList = new ArrayList<ContentProviderOperation>();
long currentTime = System.currentTimeMillis();
for (long id : ids) {
ContentProviderOperation.Builder builder = ContentProviderOperation
.newUpdate(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id));
builder.withValue(NoteColumns.MODIFIED_DATE, currentTime);
builder.withValue(NoteColumns.LOCAL_MODIFIED, 1);
operationList.add(builder.build());
}
try {
ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operationList);
if (results == null || results.length == 0) {
Log.d(TAG, "Pin 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;
}
public static boolean batchDeleteNotes(ContentResolver resolver, HashSet<Long> ids) { //批量删除笔记
if (ids == null) { //为空返回处理处理完成
Log.d(TAG, "the ids is null");

@ -34,7 +34,13 @@ public class ResourceParser {
// 默认背景颜色为黄色
public static final int BG_DEFAULT_COLOR = YELLOW;
// 定义不同字体大小的常量
// 定义四象限分类常量
public static final int CATEGORY_IMPORTANT_URGENT = 0; // 重要且紧急 - 红色
public static final int CATEGORY_IMPORTANT_NOT_URGENT = 1; // 重要但不紧急 - 黄色
public static final int CATEGORY_NOT_IMPORTANT_URGENT = 2; // 不重要但紧急 - 蓝色
public static final int CATEGORY_NOT_IMPORTANT_NOT_URGENT = 3; // 不重要且不紧急 - 绿色
// 定义字体大小的常量
public static final int TEXT_SMALL = 0;
public static final int TEXT_MEDIUM = 1;
public static final int TEXT_LARGE = 2;

@ -47,6 +47,13 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD
@Override
protected void onCreate(Bundle savedInstanceState) {
// 应用深色模式主题
if (NotesPreferenceActivity.isDarkMode(this)) {
setTheme(R.style.NoteTheme_Dark);
} else {
setTheme(R.style.NoteTheme);
}
super.onCreate(savedInstanceState);
//Bundle类型的数据与Map类型的数据相似都是以key-value的形式存储数据的
//onsaveInstanceState方法是用来保存Activity的状态的

@ -11,6 +11,7 @@ import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Bundle;
import android.preference.PreferenceManager;
@ -26,13 +27,15 @@ import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
@ -45,6 +48,7 @@ import net.micode.notes.tool.DataUtils;
import net.micode.notes.tool.ResourceParser;
import net.micode.notes.tool.ResourceParser.TextAppearanceResources;
import net.micode.notes.ui.DateTimePickerDialog.OnDateTimeSetListener;
import net.micode.notes.ui.NoteEditText;
import net.micode.notes.ui.NoteEditText.OnTextViewChangeListener;
import net.micode.notes.widget.NoteWidgetProvider_2x;
import net.micode.notes.widget.NoteWidgetProvider_4x;
@ -55,11 +59,13 @@ import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 便
*
*/
public class NoteEditActivity extends Activity implements OnClickListener,
NoteSettingChangedListener, OnTextViewChangeListener {
//该类主要是针对标签的编辑
//继承了系统内部许多和监听有关的类
private class HeadViewHolder {
public TextView tvModified;
@ -67,6 +73,8 @@ public class NoteEditActivity extends Activity implements OnClickListener,
public TextView tvAlertDate;
public ImageButton btnSaveExit;
public ImageView ibSetBgColor;
}
//使用Map实现数据存储
@ -118,7 +126,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
//私有化一个界面操作mNoteBgColorSelector对背景颜色的操作
private View mFontSizeSelector;
//私有化一个界面操作mFontSizeSelector对标签字体的操作
private EditText mNoteEditor;
private NoteEditText mNoteEditor;
//声明编辑控件,对文本操作
private View mNoteEditorPanel;
//私有化一个界面操作mNoteEditorPanel文本编辑的控制板
@ -144,6 +152,13 @@ public class NoteEditActivity extends Activity implements OnClickListener,
@Override
protected void onCreate(Bundle savedInstanceState) {
// 应用深色模式主题
if (NotesPreferenceActivity.isDarkMode(this)) {
setTheme(R.style.NoteTheme_Dark);
} else {
setTheme(R.style.NoteTheme);
}
super.onCreate(savedInstanceState);
this.setContentView(R.layout.note_edit);
//对数据库的访问操作
@ -176,6 +191,9 @@ public class NoteEditActivity extends Activity implements OnClickListener,
/**
* If the user specified the {@link Intent#ACTION_VIEW} but not provided with id,
* then jump to the NotesListActivity
* 便
* @param intent NoteEditActivity
* @return boolean truefalse
*/
mWorkingNote = null;
if (TextUtils.equals(Intent.ACTION_VIEW, intent.getAction())) {
@ -212,7 +230,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
//setSoftInputMode——软键盘输入模式
getWindow().setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN
| WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
| WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN);
} else if(TextUtils.equals(Intent.ACTION_INSERT_OR_EDIT, intent.getAction())) {
// intent.getAction()
// 大多用于broadcast发送广播时给机制intent设置一个action就是一个字符串
@ -254,7 +272,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
}//创建一个新的WorkingNote
getWindow().setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN
| WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
} else {
Log.e(TAG, "Intent not specified action, should not support");
@ -285,8 +303,47 @@ public class NoteEditActivity extends Activity implements OnClickListener,
for (Integer id : sBgSelectorSelectionMap.keySet()) {
findViewById(sBgSelectorSelectionMap.get(id)).setVisibility(View.GONE);
}
mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId());
mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId());
// 深色模式下设置黑色背景
if (NotesPreferenceActivity.isDarkMode(this)) {
// 设置头部面板背景为黑色
mHeadViewPanel.setBackgroundColor(Color.BLACK);
// 设置编辑面板背景为黑色
mNoteEditorPanel.setBackgroundColor(Color.BLACK);
// 设置文本颜色为白色
mNoteEditor.setTextColor(Color.WHITE);
// 遍历编辑面板的所有子元素,确保它们的背景都为黑色
if (mNoteEditorPanel instanceof ViewGroup) {
ViewGroup noteEditorPanel = (ViewGroup) mNoteEditorPanel;
for (int i = 0; i < noteEditorPanel.getChildCount(); i++) {
View child = noteEditorPanel.getChildAt(i);
if (child != null) {
child.setBackgroundColor(Color.BLACK);
// 如果子元素是ViewGroup递归设置其所有子元素的背景
if (child instanceof ViewGroup) {
ViewGroup childGroup = (ViewGroup) child;
for (int j = 0; j < childGroup.getChildCount(); j++) {
View grandChild = childGroup.getChildAt(j);
if (grandChild != null) {
grandChild.setBackgroundColor(Color.BLACK);
}
}
}
}
}
}
// 确保整个Activity的根视图背景也为黑色
getWindow().getDecorView().setBackgroundColor(Color.BLACK);
} else {
mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId());
mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId());
mNoteEditor.setTextColor(Color.BLACK);
}
mNoteHeaderHolder.tvModified.setText(DateUtils.formatDateTime(this,
mWorkingNote.getModifiedDate(), DateUtils.FORMAT_SHOW_DATE
@ -303,6 +360,9 @@ public class NoteEditActivity extends Activity implements OnClickListener,
private void showAlertHeader() {
if (mWorkingNote.hasClockAlert()) {
long time = System.currentTimeMillis();
/**便便
*
*/
if (time > mWorkingNote.getAlertDate()) {
mNoteHeaderHolder.tvAlertDate.setText(R.string.note_alert_expired);
}
@ -382,9 +442,11 @@ public class NoteEditActivity extends Activity implements OnClickListener,
mNoteHeaderHolder.tvModified = (TextView) findViewById(R.id.tv_modified_date);
mNoteHeaderHolder.ivAlertIcon = (ImageView) findViewById(R.id.iv_alert_icon);
mNoteHeaderHolder.tvAlertDate = (TextView) findViewById(R.id.tv_alert_date);
mNoteHeaderHolder.btnSaveExit = (ImageButton) findViewById(R.id.btn_save_exit);
mNoteHeaderHolder.btnSaveExit.setOnClickListener(this);
mNoteHeaderHolder.ibSetBgColor = (ImageView) findViewById(R.id.btn_set_bg_color);
mNoteHeaderHolder.ibSetBgColor.setOnClickListener(this);
mNoteEditor = (EditText) findViewById(R.id.note_edit_view);
mNoteEditor = (NoteEditText) findViewById(R.id.note_edit_view);
mNoteEditorPanel = findViewById(R.id.sv_note_edit);
mNoteBgColorSelector = findViewById(R.id.note_bg_color_selector);
for (int id : sBgSelectorBtnsMap.keySet()) {
@ -397,6 +459,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
View view = findViewById(id);
view.setOnClickListener(this);
};//对字体大小的选择
mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
mFontSizeId = mSharedPrefs.getInt(PREFERENCE_FONT_SIZE, ResourceParser.BG_DEFAULT_FONT_SIZE);
/**
@ -408,6 +471,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
mFontSizeId = ResourceParser.BG_DEFAULT_FONT_SIZE;
}
mEditTextList = (LinearLayout) findViewById(R.id.note_edit_list);
mNoteEditor.setOnTextViewChangeListener(this);
}
@Override
@ -434,13 +498,16 @@ public class NoteEditActivity extends Activity implements OnClickListener,
mWorkingNote.getWidgetId()
});
sendBroadcast(intent);
sendBroadcast(intent);//广播发送Intent对象以启动相应的Activity
setResult(RESULT_OK, intent);
}
public void onClick(View v) {
int id = v.getId();
if (id == R.id.btn_set_bg_color) {
if (id == R.id.btn_save_exit) {
saveNote();
finish();
} else if (id == R.id.btn_set_bg_color) {
mNoteBgColorSelector.setVisibility(View.VISIBLE);
findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility(
- View.VISIBLE);
@ -489,8 +556,17 @@ public class NoteEditActivity extends Activity implements OnClickListener,
public void onBackgroundColorChanged() {
findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility(
View.VISIBLE);
mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId());
mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId());
// 深色模式下保持黑色背景
if (NotesPreferenceActivity.isDarkMode(this)) {
mHeadViewPanel.setBackgroundColor(Color.BLACK);
mNoteEditorPanel.setBackgroundColor(Color.BLACK);
mNoteEditor.setTextColor(Color.WHITE);
} else {
mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId());
mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId());
mNoteEditor.setTextColor(Color.BLACK);
}
}
@Override
@ -528,6 +604,11 @@ public class NoteEditActivity extends Activity implements OnClickListener,
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
//根据菜单的id来编剧相关项目
case R.id.menu_save_exit:
//保存并退出
saveNote();
finish();
break;
case R.id.menu_new_note:
//创建一个新的便签
createNewNote();
@ -565,6 +646,10 @@ public class NoteEditActivity extends Activity implements OnClickListener,
findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.VISIBLE);
// 通过id找到相应的大小
break;
case R.id.menu_category:
//分类选择
showCategoryDialog();
break;
case R.id.menu_list_mode:
//选择列表模式
mWorkingNote.setCheckListMode(mWorkingNote.getCheckListMode() == 0 ?
@ -612,6 +697,59 @@ public class NoteEditActivity extends Activity implements OnClickListener,
//显示对话框
}
/*
*
*
*/
private void showCategoryDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.alert_title_select_category);
final CharSequence[] items = new CharSequence[] {
getString(R.string.category_important_urgent),
getString(R.string.category_important_not_urgent),
getString(R.string.category_not_important_urgent),
getString(R.string.category_not_important_not_urgent)
};
int currentCategory = mWorkingNote.getCategoryId();
builder.setSingleChoiceItems(items, currentCategory,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
mWorkingNote.setCategoryId(which);
updateNoteBackgroundColorByCategory(which);
dialog.dismiss();
}
});
builder.show();
}
/*
* 便
*
*/
private void updateNoteBackgroundColorByCategory(int categoryId) {
int bgColorId;
switch (categoryId) {
case ResourceParser.CATEGORY_IMPORTANT_URGENT:
bgColorId = ResourceParser.RED;
break;
case ResourceParser.CATEGORY_IMPORTANT_NOT_URGENT:
bgColorId = ResourceParser.YELLOW;
break;
case ResourceParser.CATEGORY_NOT_IMPORTANT_URGENT:
bgColorId = ResourceParser.BLUE;
break;
case ResourceParser.CATEGORY_NOT_IMPORTANT_NOT_URGENT:
bgColorId = ResourceParser.GREEN;
break;
default:
bgColorId = ResourceParser.YELLOW;
break;
}
mWorkingNote.setBgColorId(bgColorId);
}
/**
* Share note to apps that support {@link Intent#ACTION_SEND} action
* and {@text/plain} type
@ -668,18 +806,9 @@ public class NoteEditActivity extends Activity implements OnClickListener,
Log.d(TAG, "Wrong note id, should not happen");
//否则报错
}
if (!isSyncMode()) {
//在非同步模式情况下
//删除操作
if (!DataUtils.batchDeleteNotes(getContentResolver(), ids)) {
Log.e(TAG, "Delete Note error");
}
} else {
//同步模式
//移动至垃圾文件夹的操作
if (!DataUtils.batchMoveToFolder(getContentResolver(), ids, Notes.ID_TRASH_FOLER)) {
Log.e(TAG, "Move notes to trash folder error, should not happens");
}
// 所有模式下都将便签移动到回收站
if (!DataUtils.batchMoveToFolder(getContentResolver(), ids, Notes.ID_TRASH_FOLER)) {
Log.e(TAG, "Move notes to trash folder error, should not happens");
}
}
mWorkingNote.markDeleted(true);

@ -95,14 +95,12 @@ public class NoteEditText extends androidx.appcompat.widget.AppCompatEditText {
}
@Override
//view里的函数处理手机屏幕的所有事件
/*event
*/
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
//重写了需要处理屏幕被按下的事件
case MotionEvent.ACTION_DOWN:
//跟新当前坐标值
if (isInEditMode()) {
break;
}
int x = (int) event.getX();
int y = (int) event.getY();
x -= getTotalPaddingLeft();
@ -110,16 +108,14 @@ public class NoteEditText extends androidx.appcompat.widget.AppCompatEditText {
x += getScrollX();
y += getScrollY();
//用布局控件layout根据x,y的新值设置新的位置
Layout layout = getLayout();
int line = layout.getLineForVertical(y);
int off = layout.getOffsetForHorizontal(line, x);
//更新光标新的位置
Selection.setSelection(getText(), off);
if (layout != null) {
int line = layout.getLineForVertical(y);
int off = layout.getOffsetForHorizontal(line, x);
Selection.setSelection(getText(), off);
}
break;
}
return super.onTouchEvent(event);
}
@ -133,8 +129,12 @@ public class NoteEditText extends androidx.appcompat.widget.AppCompatEditText {
//根据按键的 Unicode 编码值来处理
case KeyEvent.KEYCODE_ENTER:
//“进入”按键
// 检查是否是输入法的确认操作,如果是则不拦截
if (mOnTextViewChangeListener != null) {
return false;
if ((event.getFlags() & KeyEvent.FLAG_EDITOR_ACTION) == 0 && !isInEditMode()) {
// 允许事件正常传递给父类
return super.onKeyDown(keyCode, event);
}
}
break;
case KeyEvent.KEYCODE_DEL:
@ -173,14 +173,22 @@ public class NoteEditText extends androidx.appcompat.widget.AppCompatEditText {
case KeyEvent.KEYCODE_ENTER:
//同上也是分为监听器是否建立2种情况
if (mOnTextViewChangeListener != null) {
int selectionStart = getSelectionStart();
//获取当前位置
String text = getText().subSequence(selectionStart, length()).toString();
//获取当前文本
setText(getText().subSequence(0, selectionStart));
//根据获取的文本设置当前文本
mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, text);
//当{@link KeyEvent#KEYCODE_ENTER}添加新文本
// 检查是否是输入法的确认操作,如果是则不拦截
// 同时检查是否有文本输入,避免拦截中文输入法选择候选词的操作
if ((event.getFlags() & KeyEvent.FLAG_EDITOR_ACTION) == 0 && !isInEditMode() && event.getAction() == KeyEvent.ACTION_UP) {
// 只有当没有输入文本时才执行换行操作
if (TextUtils.isEmpty(getText())) {
int selectionStart = getSelectionStart();
//获取当前位置
String text = getText().subSequence(selectionStart, length()).toString();
//获取当前文本
setText(getText().subSequence(0, selectionStart));
//根据获取的文本设置当前文本
mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, text);
//当{@link KeyEvent#KEYCODE_ENTER}添加新文本
return true;
}
}
} else {
Log.d(TAG, "OnTextViewChangeListener was not seted");
//其他情况报错,文档的改动监听器并没有建立

@ -15,6 +15,7 @@ public class NoteItemData {
NoteColumns.ID,
NoteColumns.ALERTED_DATE,
NoteColumns.BG_COLOR_ID,
NoteColumns.CATEGORY_ID,
NoteColumns.CREATED_DATE,
NoteColumns.HAS_ATTACHMENT,
NoteColumns.MODIFIED_DATE,
@ -29,19 +30,21 @@ public class NoteItemData {
private static final int ID_COLUMN = 0;
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;
private static final int SNIPPET_COLUMN = 8;
private static final int TYPE_COLUMN = 9;
private static final int WIDGET_ID_COLUMN = 10;
private static final int WIDGET_TYPE_COLUMN = 11;
private static final int CATEGORY_ID_COLUMN = 3;
private static final int CREATED_DATE_COLUMN = 4;
private static final int HAS_ATTACHMENT_COLUMN = 5;
private static final int MODIFIED_DATE_COLUMN = 6;
private static final int NOTES_COUNT_COLUMN = 7;
private static final int PARENT_ID_COLUMN = 8;
private static final int SNIPPET_COLUMN = 9;
private static final int TYPE_COLUMN = 10;
private static final int WIDGET_ID_COLUMN = 11;
private static final int WIDGET_TYPE_COLUMN = 12;
private long mId;
private long mAlertDate;
private int mBgColorId;
private int mCategoryId;
private long mCreatedDate;
private boolean mHasAttachment;
private long mModifiedDate;
@ -65,6 +68,7 @@ public class NoteItemData {
mId = cursor.getLong(ID_COLUMN);
mAlertDate = cursor.getLong(ALERTED_DATE_COLUMN);
mBgColorId = cursor.getInt(BG_COLOR_ID_COLUMN);
mCategoryId = cursor.getInt(CATEGORY_ID_COLUMN);
mCreatedDate = cursor.getLong(CREATED_DATE_COLUMN);
mHasAttachment = (cursor.getInt(HAS_ATTACHMENT_COLUMN) > 0) ? true : false;
mModifiedDate = cursor.getLong(MODIFIED_DATE_COLUMN);
@ -171,6 +175,10 @@ public class NoteItemData {
return mBgColorId;
}
public int getCategoryId() {
return mCategoryId;
}
public long getParentId() {
return mParentId;
}

@ -39,6 +39,7 @@ import android.widget.AdapterView.OnItemClickListener;
import android.widget.AdapterView.OnItemLongClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.GridView;
import android.widget.ListView;
import android.widget.PopupMenu;
import android.widget.TextView;
@ -80,7 +81,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
private static final String PREFERENCE_ADD_INTRODUCTION = "net.micode.notes.introduction"; //单行超过80个字符
private enum ListEditState {
NOTE_LIST, SUB_FOLDER, CALL_RECORD_FOLDER
NOTE_LIST, SUB_FOLDER, CALL_RECORD_FOLDER, TRASH_FOLDER
};
private ListEditState mState;
@ -91,6 +92,8 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
private ListView mNotesListView;
private GridView mNotesGridView;
private Button mAddNewNote;
private boolean mDispatch;
@ -103,6 +106,8 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
private long mCurrentFolderId;
private int mCurrentCategoryFilter = -1; // -1表示显示所有分类
private ContentResolver mContentResolver;
private ModeCallback mModeCallBack;
@ -113,6 +118,8 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
private NoteItemData mFocusNoteDataItem;
private boolean mIsGridMode = false; // 是否为九宫格模式
private static final String NORMAL_SELECTION = NoteColumns.PARENT_ID + "=?";
private static final String ROOT_FOLDER_SELECTION = "(" + NoteColumns.TYPE + "<>"
@ -130,6 +137,14 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
//final方法不能被子类的方法覆盖但可以被继承。
//final成员变量表示常量只能被赋值一次赋值后值不再改变。
//final不能用于修饰构造方法。
// 应用深色模式主题
if (NotesPreferenceActivity.isDarkMode(this)) {
setTheme(R.style.NoteTheme_Dark);
} else {
setTheme(R.style.NoteTheme);
}
super.onCreate(savedInstanceState); // 调用父类的onCreate函数
setContentView(R.layout.note_list);
initResources();
@ -224,6 +239,13 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
mNotesListView.setOnItemLongClickListener(this);
mNotesListAdapter = new NotesListAdapter(this);
mNotesListView.setAdapter(mNotesListAdapter);
// 初始化GridView
mNotesGridView = (GridView) findViewById(R.id.notes_grid);
mNotesGridView.setOnItemClickListener(new OnListItemClickListener());
mNotesGridView.setOnItemLongClickListener(this);
mNotesGridView.setAdapter(mNotesListAdapter);
mAddNewNote = (Button) findViewById(R.id.btn_new_note);// 在activity中要获取该按钮
mAddNewNote.setOnClickListener(this);
mAddNewNote.setOnTouchListener(new NewNoteOnTouchListener());
@ -242,15 +264,24 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
private MenuItem mMoveMenu;
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
getMenuInflater().inflate(R.menu.note_list_options, menu);
menu.findItem(R.id.delete).setOnMenuItemClickListener(this);
mMoveMenu = menu.findItem(R.id.move);
if (mFocusNoteDataItem.getParentId() == Notes.ID_CALL_RECORD_FOLDER
|| DataUtils.getUserFolderCount(mContentResolver) == 0) {
mMoveMenu.setVisible(false);
// 检查是否在回收站中
if (mState == ListEditState.TRASH_FOLDER) {
// 在回收站中,显示恢复菜单
getMenuInflater().inflate(R.menu.trash_list_options, menu);
menu.findItem(R.id.restore).setOnMenuItemClickListener(this);
} else {
mMoveMenu.setVisible(true);
mMoveMenu.setOnMenuItemClickListener(this);
// 不在回收站中,显示普通菜单
getMenuInflater().inflate(R.menu.note_list_options, menu);
menu.findItem(R.id.delete).setOnMenuItemClickListener(this);
menu.findItem(R.id.pin).setOnMenuItemClickListener(this);
mMoveMenu = menu.findItem(R.id.move);
if (mFocusNoteDataItem.getParentId() == Notes.ID_CALL_RECORD_FOLDER
|| DataUtils.getUserFolderCount(mContentResolver) == 0) {
mMoveMenu.setVisible(false);
} else {
mMoveMenu.setVisible(true);
mMoveMenu.setOnMenuItemClickListener(this);
}
}
mActionMode = mode;
mNotesListAdapter.setChoiceMode(true);
@ -326,6 +357,15 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
}
switch (item.getItemId()) {
case R.id.pin:
if (DataUtils.pinNote(mContentResolver, mNotesListAdapter.getSelectedItemIds())) {
Toast.makeText(NotesListActivity.this,
getString(R.string.format_pin_notes, mNotesListAdapter.getSelectedCount()),
Toast.LENGTH_SHORT).show();
startAsyncNotesListQuery();
}
mModeCallBack.finishActionMode();
break;
case R.id.delete:
AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this);
builder.setTitle(getString(R.string.alert_title_delete));
@ -345,6 +385,25 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
case R.id.move:
startQueryDestinationFolders();
break;
case R.id.restore:
// 恢复选中的便签
AlertDialog.Builder restoreBuilder = new AlertDialog.Builder(NotesListActivity.this);
restoreBuilder.setTitle("恢复便签");
restoreBuilder.setMessage("确认要恢复选中的 " + mNotesListAdapter.getSelectedCount() + " 条便签吗?");
restoreBuilder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
if (DataUtils.batchMoveToFolder(mContentResolver, mNotesListAdapter.getSelectedItemIds(), Notes.ID_ROOT_FOLDER)) {
Toast.makeText(NotesListActivity.this, getString(R.string.format_restore_notes, mNotesListAdapter.getSelectedCount()), Toast.LENGTH_SHORT).show();
startAsyncNotesListQuery();
} else {
Log.e(TAG, "Restore notes error");
}
mModeCallBack.finishActionMode();
}
});
restoreBuilder.setNegativeButton(android.R.string.cancel, null);
restoreBuilder.show();
break;
default:
return false;
}
@ -417,10 +476,27 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
private void startAsyncNotesListQuery() {
String selection = (mCurrentFolderId == Notes.ID_ROOT_FOLDER) ? ROOT_FOLDER_SELECTION
: NORMAL_SELECTION;
// 添加分类筛选条件
if (mCurrentCategoryFilter >= 0) {
selection = "(" + selection + ") AND " + NoteColumns.CATEGORY_ID + "=?";
}
String[] selectionArgs;
if (mCurrentCategoryFilter >= 0) {
selectionArgs = new String[] {
String.valueOf(mCurrentFolderId),
String.valueOf(mCurrentCategoryFilter)
};
} else {
selectionArgs = new String[] {
String.valueOf(mCurrentFolderId)
};
}
mBackgroundQueryHandler.startQuery(FOLDER_NOTE_LIST_QUERY_TOKEN, null,
Notes.CONTENT_NOTE_URI, NoteItemData.PROJECTION, selection, new String[] {
String.valueOf(mCurrentFolderId)
}, NoteColumns.TYPE + " DESC," + NoteColumns.MODIFIED_DATE + " DESC");
Notes.CONTENT_NOTE_URI, NoteItemData.PROJECTION, selection, selectionArgs,
NoteColumns.TYPE + " DESC," + NoteColumns.MODIFIED_DATE + " DESC");
}
private final class BackgroundQueryHandler extends AsyncQueryHandler {
@ -479,20 +555,10 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
new AsyncTask<Void, Void, HashSet<AppWidgetAttribute>>() {
protected HashSet<AppWidgetAttribute> doInBackground(Void... unused) {
HashSet<AppWidgetAttribute> widgets = mNotesListAdapter.getSelectedWidget();
if (!isSyncMode()) {
// if not synced, delete notes directly
if (DataUtils.batchDeleteNotes(mContentResolver, mNotesListAdapter
.getSelectedItemIds())) {
} else {
Log.e(TAG, "Delete notes error, should not happens");
}
} else {
// in sync mode, we'll move the deleted note into the trash
// folder
if (!DataUtils.batchMoveToFolder(mContentResolver, mNotesListAdapter
.getSelectedItemIds(), Notes.ID_TRASH_FOLER)) {
Log.e(TAG, "Move notes to trash folder error, should not happens");
}
// 所有模式下都将便签移动到回收站
if (!DataUtils.batchMoveToFolder(mContentResolver, mNotesListAdapter
.getSelectedItemIds(), Notes.ID_TRASH_FOLER)) {
Log.e(TAG, "Move notes to trash folder error, should not happens");
}
return widgets;
}
@ -522,13 +588,8 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
ids.add(folderId);
HashSet<AppWidgetAttribute> widgets = DataUtils.getFolderNoteWidget(mContentResolver,
folderId);
if (!isSyncMode()) {
// if not synced, delete folder directly
DataUtils.batchDeleteNotes(mContentResolver, ids);
} else {
// in sync mode, we'll move the deleted folder into the trash folder
DataUtils.batchMoveToFolder(mContentResolver, ids, Notes.ID_TRASH_FOLER);
}
// 所有模式下都将文件夹移动到回收站
DataUtils.batchMoveToFolder(mContentResolver, ids, Notes.ID_TRASH_FOLER);
if (widgets != null) {
for (AppWidgetAttribute widget : widgets) {
if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID
@ -563,6 +624,15 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
mTitleBar.setVisibility(View.VISIBLE);
}
private void openTrashFolder() {
mCurrentFolderId = Notes.ID_TRASH_FOLER;
mState = ListEditState.TRASH_FOLDER;
mAddNewNote.setVisibility(View.GONE);
mTitleBar.setText("回收站");
mTitleBar.setVisibility(View.VISIBLE);
startAsyncNotesListQuery();
}
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_new_note:
@ -677,10 +747,12 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
@Override
public void onBackPressed() { switch (mState) {
case SUB_FOLDER:
case TRASH_FOLDER:
mCurrentFolderId = Notes.ID_ROOT_FOLDER;
mState = ListEditState.NOTE_LIST;
startAsyncNotesListQuery();
mAddNewNote.setVisibility(View.VISIBLE);
mTitleBar.setVisibility(View.GONE);
startAsyncNotesListQuery();
break;
case CALL_RECORD_FOLDER:
mCurrentFolderId = Notes.ID_ROOT_FOLDER;
@ -793,6 +865,8 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
getMenuInflater().inflate(R.menu.sub_folder, menu);
} else if (mState == ListEditState.CALL_RECORD_FOLDER) {
getMenuInflater().inflate(R.menu.call_record_folder, menu);
} else if (mState == ListEditState.TRASH_FOLDER) {
getMenuInflater().inflate(R.menu.trash_folder, menu);
} else {
Log.e(TAG, "Wrong state:" + mState);
}
@ -810,6 +884,18 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
exportNoteToText();
break;
}
case R.id.menu_filter_category: {
showCategoryFilterDialog();
break;
}
case R.id.menu_dark_mode: {
toggleDarkMode();
break;
}
case R.id.menu_layout_mode: {
toggleLayoutMode();
break;
}
case R.id.menu_sync: {
if (isSyncMode()) {
if (TextUtils.equals(item.getTitle(), getString(R.string.menu_sync))) {
@ -826,10 +912,30 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
startPreferenceActivity();
break;
}
case R.id.menu_trash:
// 打开回收站
openTrashFolder();
break;
case R.id.menu_new_note: {
createNewNote();
break;
}
case R.id.menu_exit_folder:
// 退至上一级文件夹,执行与返回键相同的逻辑
mCurrentFolderId = Notes.ID_ROOT_FOLDER;
mState = ListEditState.NOTE_LIST;
mAddNewNote.setVisibility(View.VISIBLE);
mTitleBar.setVisibility(View.GONE);
startAsyncNotesListQuery();
break;
case R.id.menu_restore:
// 恢复选中的便签
restoreSelectedNotes();
break;
case R.id.menu_empty_trash:
// 清空回收站
emptyTrash();
break;
case R.id.menu_search:
onSearchRequested();
break;
@ -894,6 +1000,39 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
}.execute();
}
/*
*
*
*/
private void showCategoryFilterDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.alert_title_select_category);
final CharSequence[] items = new CharSequence[] {
getString(R.string.category_important_urgent),
getString(R.string.category_important_not_urgent),
getString(R.string.category_not_important_urgent),
getString(R.string.category_not_important_not_urgent),
getString(R.string.menu_select_all)
};
int checkedItem = (mCurrentCategoryFilter >= 0) ? mCurrentCategoryFilter : 4;
builder.setSingleChoiceItems(items, checkedItem,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
if (which == 4) {
// 显示所有分类
mCurrentCategoryFilter = -1;
} else {
mCurrentCategoryFilter = which;
}
startAsyncNotesListQuery();
dialog.dismiss();
}
});
builder.show();
}
/**
* @return
*
@ -911,6 +1050,110 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
from.startActivityIfNeeded(intent, -1);
}
/*
*
*
*/
private void toggleDarkMode() {
boolean currentDarkMode = NotesPreferenceActivity.isDarkMode(this);
boolean newDarkMode = !currentDarkMode;
NotesPreferenceActivity.setDarkMode(this, newDarkMode);
// 重新启动Activity以应用新主题
Intent intent = getIntent();
finish();
startActivity(intent);
String message = newDarkMode ? "已开启深色模式" : "已关闭深色模式";
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
}
private void toggleLayoutMode() {
mIsGridMode = !mIsGridMode;
if (mIsGridMode) {
// 切换到九宫格模式
mNotesListView.setVisibility(View.GONE);
mNotesGridView.setVisibility(View.VISIBLE);
Toast.makeText(this, "已切换到九宫格模式", Toast.LENGTH_SHORT).show();
} else {
// 切换到列表模式
mNotesGridView.setVisibility(View.GONE);
mNotesListView.setVisibility(View.VISIBLE);
Toast.makeText(this, "已切换到列表模式", Toast.LENGTH_SHORT).show();
}
}
private void restoreSelectedNotes() {
if (mNotesListAdapter.getSelectedCount() == 0) {
Toast.makeText(this, "请先选择要恢复的便签", Toast.LENGTH_SHORT).show();
// 进入选择模式
if (mNotesListView != null && mModeCallBack != null) {
mNotesListView.setLongClickable(false);
mModeCallBack.finishActionMode();
}
return;
}
// 显示确认对话框
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("恢复便签");
builder.setMessage("确认要恢复选中的 " + mNotesListAdapter.getSelectedCount() + " 条便签吗?");
builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
// 将选中的便签恢复到根文件夹
if (DataUtils.batchMoveToFolder(mContentResolver, mNotesListAdapter.getSelectedItemIds(), Notes.ID_ROOT_FOLDER)) {
Toast.makeText(NotesListActivity.this, getString(R.string.format_restore_notes, mNotesListAdapter.getSelectedCount()), Toast.LENGTH_SHORT).show();
startAsyncNotesListQuery();
} else {
Log.e(TAG, "Restore notes error");
}
if (mModeCallBack != null) {
mModeCallBack.finishActionMode();
}
}
});
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
}
private void emptyTrash() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.alert_title_empty_trash);
builder.setIcon(android.R.drawable.ic_dialog_alert);
builder.setMessage(R.string.alert_message_empty_trash);
builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
// 直接删除回收站中的所有便签
Cursor cursor = mContentResolver.query(Notes.CONTENT_NOTE_URI,
new String[] { NoteColumns.ID },
NoteColumns.PARENT_ID + "=?",
new String[] { String.valueOf(Notes.ID_TRASH_FOLER) },
null);
if (cursor != null && cursor.moveToFirst()) {
HashSet<Long> ids = new HashSet<Long>();
do {
ids.add(cursor.getLong(0));
} while (cursor.moveToNext());
if (!ids.isEmpty()) {
if (DataUtils.batchDeleteNotes(mContentResolver, ids)) {
Toast.makeText(NotesListActivity.this, R.string.toast_empty_trash_success, Toast.LENGTH_SHORT).show();
startAsyncNotesListQuery();
} else {
Log.e(TAG, "Empty trash error");
}
}
cursor.close();
}
}
});
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
}
/**
* @author k
* 便

@ -11,6 +11,7 @@ import android.widget.TextView;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.tool.DataUtils;
import net.micode.notes.tool.ResourceParser;
import net.micode.notes.tool.ResourceParser.NoteItemBgResources;
@ -93,6 +94,26 @@ public class NotesListItem extends LinearLayout {
//根据data的文件属性来设置背景
private void setBackground(NoteItemData data) {
int id = data.getBgColorId();
//如果设置了分类,根据分类自动设置背景颜色
if (data.getType() == Notes.TYPE_NOTE && data.getCategoryId() >= 0) {
int categoryId = data.getCategoryId();
switch (categoryId) {
case 0: // 重要且紧急 - 红色
id = ResourceParser.RED;
break;
case 1: // 重要但不紧急 - 黄色
id = ResourceParser.YELLOW;
break;
case 2: // 不重要但紧急 - 蓝色
id = ResourceParser.BLUE;
break;
case 3: // 不重要且不紧急 - 绿色
id = ResourceParser.GREEN;
break;
}
}
//若是note型文件则4种情况对于4种不同情况的背景来源
if (data.getType() == Notes.TYPE_NOTE) {
//单个数据并且只有一个子文件夹

@ -44,6 +44,8 @@ public class NotesPreferenceActivity extends PreferenceActivity {
public static final String PREFERENCE_LAST_SYNC_TIME = "pref_last_sync_time";
//同步时间
public static final String PREFERENCE_SET_BG_COLOR_KEY = "pref_key_bg_random_appear";
//深色模式
public static final String PREFERENCE_DARK_MODE_KEY = "pref_key_dark_mode";
private static final String PREFERENCE_SYNC_ACCOUNT_KEY = "pref_sync_account_key";
//同步密码
@ -66,6 +68,13 @@ public class NotesPreferenceActivity extends PreferenceActivity {
*
*/
protected void onCreate(Bundle icicle) {
// 应用深色模式主题
if (isDarkMode(this)) {
setTheme(R.style.NoteTheme_Dark);
} else {
setTheme(R.style.NoteTheme);
}
//先执行父类的创建函数
super.onCreate(icicle);
@ -474,6 +483,28 @@ public class NotesPreferenceActivity extends PreferenceActivity {
return settings.getLong(PREFERENCE_LAST_SYNC_TIME, 0);
}
/*
*
*
*/
public static void setDarkMode(Context context, boolean isDarkMode) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
editor.putBoolean(PREFERENCE_DARK_MODE_KEY, isDarkMode);
editor.commit();
}
/*
*
*
*/
public static boolean isDarkMode(Context context) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
return settings.getBoolean(PREFERENCE_DARK_MODE_KEY, false);
}
/*
*
* BroadcastReceiver

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Before

Width:  |  Height:  |  Size: 245 B

After

Width:  |  Height:  |  Size: 245 B

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Before

Width:  |  Height:  |  Size: 443 B

After

Width:  |  Height:  |  Size: 443 B

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Before

Width:  |  Height:  |  Size: 554 KiB

After

Width:  |  Height:  |  Size: 554 KiB

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

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

Loading…
Cancel
Save