diff --git a/doc/02_小米便签开源代码阅读-精读报告_刘骐瑞.docx b/doc/02_小米便签开源代码阅读-精读报告_刘骐瑞.docx new file mode 100644 index 0000000..2304d8e Binary files /dev/null and b/doc/02_小米便签开源代码阅读-精读报告_刘骐瑞.docx differ diff --git a/doc/02_小米便签开源代码阅读-精读报告_刘骐瑞.pdf b/doc/02_小米便签开源代码阅读-精读报告_刘骐瑞.pdf new file mode 100644 index 0000000..05a6fdf Binary files /dev/null and b/doc/02_小米便签开源代码阅读-精读报告_刘骐瑞.pdf differ diff --git a/other/01_小米便签开源代码阅读-泛读报告_刘骐瑞.docx b/other/01_小米便签开源代码阅读-泛读报告_刘骐瑞.docx deleted file mode 100644 index 84d8f4d..0000000 Binary files a/other/01_小米便签开源代码阅读-泛读报告_刘骐瑞.docx and /dev/null differ diff --git a/other/01_小米便签开源代码阅读-泛读报告_刘骐瑞.pdf b/other/01_小米便签开源代码阅读-泛读报告_刘骐瑞.pdf deleted file mode 100644 index 6d19260..0000000 Binary files a/other/01_小米便签开源代码阅读-泛读报告_刘骐瑞.pdf and /dev/null differ diff --git a/other/03_小米便签开源代码-质量分析报告_刘骐瑞(更正).docx b/other/03_小米便签开源代码-质量分析报告_刘骐瑞(更正).docx deleted file mode 100644 index 098c712..0000000 Binary files a/other/03_小米便签开源代码-质量分析报告_刘骐瑞(更正).docx and /dev/null differ diff --git a/other/03_小米便签开源代码-质量分析报告_刘骐瑞(更正).pdf b/other/03_小米便签开源代码-质量分析报告_刘骐瑞(更正).pdf deleted file mode 100644 index a172ae9..0000000 Binary files a/other/03_小米便签开源代码-质量分析报告_刘骐瑞(更正).pdf and /dev/null differ diff --git a/src/刘骐瑞代码注释/GTaskClient.java b/src/刘骐瑞代码注释/GTaskClient.java new file mode 100644 index 0000000..55b839c --- /dev/null +++ b/src/刘骐瑞代码注释/GTaskClient.java @@ -0,0 +1,440 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.gtask.remote; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.AccountManagerFuture; +import android.app.Activity; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; + +import net.micode.notes.gtask.data.Node; +import net.micode.notes.gtask.data.Task; +import net.micode.notes.gtask.data.TaskList; +import net.micode.notes.gtask.exception.ActionFailureException; +import net.micode.notes.gtask.exception.NetworkFailureException; +import net.micode.notes.tool.GTaskStringUtils; +import net.micode.notes.ui.NotesPreferenceActivity; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.cookie.Cookie; +import org.apache.http.impl.client.BasicCookieStore; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.HttpConnectionParams; +import org.apache.http.params.HttpParams; +import org.apache.http.params.HttpProtocolParams; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.LinkedList; +import java.util.List; +import java.util.zip.GZIPInputStream; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; + +// GTaskClient 类用于与 Google Tasks 服务进行交互,包括登录、创建任务、创建任务列表等操作 +public class GTaskClient { + // 日志标签,用于在日志中标识该类的信息 + private static final String TAG = GTaskClient.class.getSimpleName(); + // Google Tasks 的基础 URL + private static final String GTASK_URL = "https://mail.google.com/tasks/"; + // 用于获取数据的 URL + private static final String GTASK_GET_URL = "https://mail.google.com/tasks/ig"; + // 用于提交数据的 URL + private static final String GTASK_POST_URL = "https://mail.google.com/tasks/r/ig"; + // 单例模式的实例 + private static GTaskClient mInstance = null; + // HTTP 客户端,用于发送 HTTP 请求 + private DefaultHttpClient mHttpClient; + // 当前使用的获取数据的 URL + private String mGetUrl; + // 当前使用的提交数据的 URL + private String mPostUrl; + // 客户端版本号 + private long mClientVersion; + // 登录状态标识 + private boolean mLoggedin; + // 上次登录时间 + private long mLastLoginTime; + // 操作 ID,用于标识每个操作 + private int mActionId; + // 当前使用的 Google 账户 + private Account mAccount; + // 待更新的操作数组 + private JSONArray mUpdateArray; + + // 私有构造函数,确保单例模式 + private GTaskClient() { + // 初始化 HTTP 客户端为 null + mHttpClient = null; + // 初始化获取数据的 URL + mGetUrl = GTASK_GET_URL; + // 初始化提交数据的 URL + mPostUrl = GTASK_POST_URL; + // 初始化客户端版本号为 -1 + mClientVersion = -1; + // 初始化登录状态为未登录 + mLoggedin = false; + // 初始化上次登录时间为 0 + mLastLoginTime = 0; + // 初始化操作 ID 为 1 + mActionId = 1; + // 初始化账户为 null + mAccount = null; + // 初始化待更新操作数组为 null + mUpdateArray = null; + } + + // 获取单例实例的静态方法 + public static synchronized GTaskClient getInstance() { + // 如果实例为空,则创建一个新的实例 + if (mInstance == null) { + mInstance = new GTaskClient(); + } + // 返回实例 + return mInstance; + } + + // 登录 Google Tasks 服务的方法 + public boolean login(Activity activity) { + // 假设 cookie 有效期为 5 分钟,超过该时间需要重新登录 + final long interval = 1000 * 60 * 5; + // 如果距离上次登录超过 5 分钟,将登录状态设置为未登录 + if (mLastLoginTime + interval < System.currentTimeMillis()) { + mLoggedin = false; + } + + // 如果已登录,但账户发生切换,需要重新登录 + if (mLoggedin + && !TextUtils.equals(getSyncAccount().name, NotesPreferenceActivity + .getSyncAccountName(activity))) { + mLoggedin = false; + } + + // 如果已经登录,打印日志并返回 true + if (mLoggedin) { + Log.d(TAG, "already logged in"); + return true; + } + + // 更新上次登录时间 + mLastLoginTime = System.currentTimeMillis(); + // 登录 Google 账户并获取认证令牌 + String authToken = loginGoogleAccount(activity, false); + // 如果认证令牌为空,登录失败,打印错误日志并返回 false + if (authToken == null) { + Log.e(TAG, "login google account failed"); + return false; + } + + // 如果账户不是 gmail.com 或 googlemail.com 结尾,使用自定义域名登录 + if (!(mAccount.name.toLowerCase().endsWith("gmail.com") || mAccount.name.toLowerCase() + .endsWith("googlemail.com"))) { + // 构建自定义域名的 URL + StringBuilder url = new StringBuilder(GTASK_URL).append("a/"); + int index = mAccount.name.indexOf('@') + 1; + String suffix = mAccount.name.substring(index); + url.append(suffix + "/"); + mGetUrl = url.toString() + "ig"; + mPostUrl = url.toString() + "r/ig"; + + // 尝试使用自定义域名登录 Google Tasks + if (tryToLoginGtask(activity, authToken)) { + mLoggedin = true; + } + } + + // 如果使用自定义域名登录失败,尝试使用 Google 官方 URL 登录 + if (!mLoggedin) { + mGetUrl = GTASK_GET_URL; + mPostUrl = GTASK_POST_URL; + // 如果登录失败,打印错误日志并返回 false + if (!tryToLoginGtask(activity, authToken)) { + return false; + } + } + + // 登录成功,设置登录状态为已登录并返回 true + mLoggedin = true; + return true; + } + + // 登录 Google 账户并获取认证令牌的方法 + private String loginGoogleAccount(Activity activity, boolean invalidateToken) { + String authToken; + // 获取账户管理器 + AccountManager accountManager = AccountManager.get(activity); + // 获取所有 Google 账户 + Account[] accounts = accountManager.getAccountsByType("com.google"); + + // 如果没有可用的 Google 账户,打印错误日志并返回 null + if (accounts.length == 0) { + Log.e(TAG, "there is no available google account"); + return null; + } + + // 获取设置中同步的账户名 + String accountName = NotesPreferenceActivity.getSyncAccountName(activity); + Account account = null; + // 查找与设置中账户名相同的账户 + for (Account a : accounts) { + if (a.name.equals(accountName)) { + account = a; + break; + } + } + // 如果找到账户,将其赋值给 mAccount + if (account != null) { + mAccount = account; + } else { + // 如果未找到账户,打印错误日志并返回 null + Log.e(TAG, "unable to get an account with the same name in the settings"); + return null; + } + + // 获取认证令牌 + AccountManagerFuture accountManagerFuture = accountManager.getAuthToken(account, + "goanna_mobile", null, activity, null, null); + try { + // 获取认证令牌的结果 + Bundle authTokenBundle = accountManagerFuture.getResult(); + // 从结果中获取认证令牌 + authToken = authTokenBundle.getString(AccountManager.KEY_AUTHTOKEN); + // 如果需要使令牌失效,调用 invalidateAuthToken 方法并重新获取令牌 + if (invalidateToken) { + accountManager.invalidateAuthToken("com.google", authToken); + loginGoogleAccount(activity, false); + } + } catch (Exception e) { + // 如果获取认证令牌失败,打印错误日志并将 authToken 设置为 null + Log.e(TAG, "get auth token failed"); + authToken = null; + } + + // 返回认证令牌 + return authToken; + } + + // 尝试登录 Google Tasks 服务的方法 + private boolean tryToLoginGtask(Activity activity, String authToken) { + // 首先尝试使用当前认证令牌登录 + if (!loginGtask(authToken)) { + // 如果登录失败,可能是认证令牌过期,使令牌失效并重新获取 + authToken = loginGoogleAccount(activity, true); + // 如果重新获取令牌失败,打印错误日志并返回 false + if (authToken == null) { + Log.e(TAG, "login google account failed"); + return false; + } + + // 再次尝试使用新的认证令牌登录 + if (!loginGtask(authToken)) { + // 如果仍然登录失败,打印错误日志并返回 false + Log.e(TAG, "login gtask failed"); + return false; + } + } + // 登录成功,返回 true + return true; + } + + // 使用认证令牌登录 Google Tasks 服务的方法 + private boolean loginGtask(String authToken) { + // 设置连接超时时间为 10 秒 + int timeoutConnection = 10000; + // 设置套接字超时时间为 15 秒 + int timeoutSocket = 15000; + // 创建 HTTP 参数对象 + HttpParams httpParameters = new BasicHttpParams(); + // 设置连接超时时间 + HttpConnectionParams.setConnectionTimeout(httpParameters, timeoutConnection); + // 设置套接字超时时间 + HttpConnectionParams.setSoTimeout(httpParameters, timeoutSocket); + // 创建 HTTP 客户端 + mHttpClient = new DefaultHttpClient(httpParameters); + // 创建 cookie 存储对象 + BasicCookieStore localBasicCookieStore = new BasicCookieStore(); + // 设置 HTTP 客户端的 cookie 存储 + mHttpClient.setCookieStore(localBasicCookieStore); + // 设置不使用 Expect: 100-continue 机制 + HttpProtocolParams.setUseExpectContinue(mHttpClient.getParams(), false); + + // 构建登录 URL + try { + String loginUrl = mGetUrl + "?auth=" + authToken; + // 创建 HTTP GET 请求 + HttpGet httpGet = new HttpGet(loginUrl); + HttpResponse response = null; + // 执行 HTTP GET 请求 + response = mHttpClient.execute(httpGet); + + // 获取 cookie + List cookies = mHttpClient.getCookieStore().getCookies(); + boolean hasAuthCookie = false; + // 检查是否存在包含 "GTL" 的 cookie + for (Cookie cookie : cookies) { + if (cookie.getName().contains("GTL")) { + hasAuthCookie = true; + } + } + // 如果没有找到认证 cookie,打印警告日志 + if (!hasAuthCookie) { + Log.w(TAG, "it seems that there is no auth cookie"); + } + + // 获取响应内容 + String resString = getResponseContent(response.getEntity()); + // 定义 JSON 字符串的开始和结束标记 + String jsBegin = "_setup("; + String jsEnd = ")}"; + // 查找 JSON 字符串的开始和结束位置 + int begin = resString.indexOf(jsBegin); + int end = resString.lastIndexOf(jsEnd); + String jsString = null; + // 如果找到开始和结束位置,提取 JSON 字符串 + if (begin != -1 && end != -1 && begin < end) { + jsString = resString.substring(begin + jsBegin.length(), end); + } + // 将 JSON 字符串转换为 JSONObject + JSONObject js = new JSONObject(jsString); + // 获取客户端版本号 + mClientVersion = js.getLong("v"); + } catch (JSONException e) { + // 如果处理 JSON 数据时发生异常,打印错误日志并返回 false + Log.e(TAG, e.toString()); + e.printStackTrace(); + return false; + } catch (Exception e) { + // 如果执行 HTTP 请求时发生异常,打印错误日志并返回 false + Log.e(TAG, "httpget gtask_url failed"); + return false; + } + + // 登录成功,返回 true + return true; + } + + // 获取操作 ID 的方法,每次调用操作 ID 加 1 + private int getActionId() { + return mActionId++; + } + + // 创建 HTTP POST 请求的方法 + private HttpPost createHttpPost() { + // 创建 HTTP POST 请求对象 + HttpPost httpPost = new HttpPost(mPostUrl); + // 设置请求头的 Content-Type + httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8"); + // 设置请求头的 AT 字段 + httpPost.setHeader("AT", "1"); + // 返回 HTTP POST 请求对象 + return httpPost; + } + + // 获取 HTTP 响应内容的方法 + private String getResponseContent(HttpEntity entity) throws IOException { + String contentEncoding = null; + // 获取响应内容的编码 + if (entity.getContentEncoding() != null) { + contentEncoding = entity.getContentEncoding().getValue(); + Log.d(TAG, "encoding: " + contentEncoding); + } + + // 获取响应内容的输入流 + InputStream input = entity.getContent(); + // 如果响应内容使用 gzip 压缩,使用 GZIPInputStream 解压 + if (contentEncoding != null && contentEncoding.equalsIgnoreCase("gzip")) { + input = new GZIPInputStream(entity.getContent()); + } else if (contentEncoding != null && contentEncoding.equalsIgnoreCase("deflate")) { + // 如果响应内容使用 deflate 压缩,使用 InflaterInputStream 解压 + Inflater inflater = new Inflater(true); + input = new InflaterInputStream(entity.getContent(), inflater); + } + + try { + // 创建输入流读取器 + InputStreamReader isr = new InputStreamReader(input); + // 创建缓冲读取器 + BufferedReader br = new BufferedReader(isr); + // 创建字符串构建器 + StringBuilder sb = new StringBuilder(); + + while (true) { + // 读取一行内容 + String buff = br.readLine(); + // 如果读取到文件末尾,返回字符串构建器的内容 + if (buff == null) { + return sb.toString(); + } + // 将读取的内容追加到字符串构建器中 + sb = sb.append(buff); + } + } finally { + // 关闭输入流 + input.close(); + } + } + + // 发送 POST 请求的方法 + private JSONObject postRequest(JSONObject js) throws NetworkFailureException { + // 如果未登录,抛出异常并打印错误日志 + if (!mLoggedin) { + Log.e(TAG, "please login first"); + throw new ActionFailureException("not logged in"); + } + + // 创建 HTTP POST 请求 + HttpPost httpPost = createHttpPost(); + try { + // 创建参数列表 + LinkedList list = new LinkedList(); + // 将 JSON 对象转换为字符串并添加到参数列表中 + list.add(new BasicNameValuePair("r", js.toString())); + // 创建 URL 编码的表单实体 + UrlEncodedFormEntity entity = new UrlEncodedFormEntity(list, "UTF-8"); + // 设置请求实体 + httpPost.setEntity(entity); + + // 执行 HTTP POST 请求 + HttpResponse response = mHttpClient.execute(httpPost); + // 获取响应内容 + String jsString = getResponseContent(response.getEntity()); + // 将响应内容转换为 JSONObject 并返回 + return new JSONObject(jsString); + + } catch (ClientProtocolException e) { + // 如果发生客户端协议异常,抛出网络失败异常并打印错误日志 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new NetworkFailureException("postRequest failed"); + } catch (IOException e) { + // 如果发生 IO 异常,抛出网络失败异常并打印错误日志 + Log.e(TAG, \ No newline at end of file diff --git a/src/刘骐瑞代码注释/GTaskManager.java b/src/刘骐瑞代码注释/GTaskManager.java new file mode 100644 index 0000000..4d6f13b --- /dev/null +++ b/src/刘骐瑞代码注释/GTaskManager.java @@ -0,0 +1,439 @@ +/* + * 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 任务的同步操作。 + * 它使用单例模式确保只有一个实例存在,并提供同步方法来处理本地和远程任务数据的同步。 + */ +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; + + // 用于获取认证令牌的 Activity + private Activity mActivity; + // 应用上下文 + private Context mContext; + // 内容解析器,用于与内容提供者交互 + private ContentResolver mContentResolver; + // 标识同步是否正在进行 + private boolean mSyncing; + // 标识同步是否被取消 + private boolean mCancelled; + // 存储从 Google 获取的任务列表,键为任务列表的全局 ID + private HashMap mGTaskListHashMap; + // 存储从 Google 获取的所有任务节点,键为任务的全局 ID + private HashMap mGTaskHashMap; + // 存储元数据,键为相关任务的全局 ID + private HashMap mMetaHashMap; + // 元数据任务列表 + private TaskList mMetaList; + // 存储本地已删除的笔记 ID + private HashSet mLocalDeleteIdMap; + // 用于将 Google 任务 ID 映射到本地笔记 ID + private HashMap mGidToNid; + // 用于将本地笔记 ID 映射到 Google 任务 ID + private HashMap mNidToGid; + + /** + * 私有构造函数,确保单例模式 + */ + private GTaskManager() { + // 初始化同步状态 + mSyncing = false; + mCancelled = false; + // 初始化存储数据的集合 + mGTaskListHashMap = new HashMap(); + mGTaskHashMap = new HashMap(); + mMetaHashMap = new HashMap(); + mMetaList = null; + mLocalDeleteIdMap = new HashSet(); + mGidToNid = new HashMap(); + mNidToGid = new HashMap(); + } + + /** + * 获取 GTaskManager 的单例实例 + * @return GTaskManager 的单例实例 + */ + public static synchronized GTaskManager getInstance() { + if (mInstance == null) { + // 如果实例不存在,则创建一个新的实例 + mInstance = new GTaskManager(); + } + return mInstance; + } + + /** + * 设置用于获取认证令牌的 Activity 上下文 + * @param activity 用于获取认证令牌的 Activity + */ + public synchronized void setActivityContext(Activity activity) { + mActivity = activity; + } + + /** + * 执行本地和 Google 任务之间的同步操作 + * @param context 应用上下文 + * @param asyncTask 异步任务,用于发布同步进度 + * @return 同步结果的状态码 + */ + 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 实例 + 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; + } + + /** + * 初始化从 Google 获取的任务列表 + * @throws NetworkFailureException 网络异常 + */ + private void initGTaskList() throws NetworkFailureException { + if (mCancelled) + return; + // 获取 GTaskClient 实例 + GTaskClient client = GTaskClient.getInstance(); + try { + // 从 Google 获取任务列表的 JSON 数组 + JSONArray jsTaskLists = client.getTaskLists(); + + // 初始化元数据列表 + mMetaList = null; + for (int i = 0; i < jsTaskLists.length(); i++) { + // 获取每个任务列表的 JSON 对象 + JSONObject object = jsTaskLists.getJSONObject(i); + // 获取任务列表的全局 ID + 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(); + // 根据远程 JSON 数据设置任务列表内容 + mMetaList.setContentByRemoteJSON(object); + + // 加载元数据 + JSONArray jsMetas = client.getTaskList(gid); + for (int j = 0; j < jsMetas.length(); j++) { + // 获取每个元数据的 JSON 对象 + object = (JSONObject) jsMetas.getJSONObject(j); + MetaData metaData = new MetaData(); + // 根据远程 JSON 数据设置元数据内容 + metaData.setContentByRemoteJSON(object); + if (metaData.isWorthSaving()) { + // 如果元数据值得保存,添加到元数据列表 + mMetaList.addChildTask(metaData); + if (metaData.getGid() != null) { + // 将元数据添加到元数据哈希表 + 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++) { + // 获取每个任务列表的 JSON 对象 + JSONObject object = jsTaskLists.getJSONObject(i); + // 获取任务列表的全局 ID + 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(); + // 根据远程 JSON 数据设置任务列表内容 + tasklist.setContentByRemoteJSON(object); + // 将任务列表添加到任务列表哈希表 + mGTaskListHashMap.put(gid, tasklist); + // 将任务列表添加到任务哈希表 + mGTaskHashMap.put(gid, tasklist); + + // 加载任务 + JSONArray jsTasks = client.getTaskList(gid); + for (int j = 0; j < jsTasks.length(); j++) { + // 获取每个任务的 JSON 对象 + object = (JSONObject) jsTasks.getJSONObject(j); + // 获取任务的全局 ID + gid = object.getString(GTaskStringUtils.GTASK_JSON_ID); + Task task = new Task(); + // 根据远程 JSON 数据设置任务内容 + task.setContentByRemoteJSON(object); + if (task.isWorthSaving()) { + // 如果任务值得保存,设置元信息并添加到任务列表 + task.setMetaInfo(mMetaHashMap.get(gid)); + tasklist.addChildTask(task); + // 将任务添加到任务哈希表 + mGTaskHashMap.put(gid, task); + } + } + } + } + } catch (JSONException e) { + // 捕获 JSON 解析异常,记录日志并抛出操作失败异常 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("initGTaskList: handing JSONObject failed"); + } + } + + /** + * 执行本地和远程任务内容的同步操作 + * @throws NetworkFailureException 网络异常 + */ + private void syncContent() throws NetworkFailureException { + int syncType; + Cursor c = null; + String gid; + Node node; + + // 清空本地已删除笔记 ID 集合 + mLocalDeleteIdMap.clear(); + + 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()) { + // 获取笔记的全局 ID + 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); + } + + // 将本地已删除的笔记 ID 添加到集合中 + mLocalDeleteIdMap.add(c.getLong(SqlNote.ID_COLUMN)); + } + } 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()) { + // 获取笔记的全局 ID + gid = c.getString(SqlNote.GTASK_ID_COLUMN); + // 从任务哈希表中获取对应的任务节点 + node = mGTaskHashMap.get(gid); + if (node != null) { + // 如果节点存在,从任务哈希表中移除该节点 + mGTaskHashMap.remove(gid); + // 建立全局 ID 到本地笔记 ID 的映射 + mGidToNid.put(gid, c.getLong(SqlNote.ID_COLUMN)); + // 建立本地笔记 ID 到全局 ID 的映射 + mNidToGid.put(c.getLong(SqlNote.ID_COLUMN), gid); + // 获取同步类型 + syncType = node.getSyncAction(c); + } else { + if (c.getString(SqlNote.GTASK_ID_COLUMN).trim().length() == 0) { + // 如果全局 ID 为空,说明是本地新增的笔记 + 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> iter = mGTaskHashMap.entrySet().iterator(); + while (iter.hasNext()) { + Map.Entry entry = iter.next(); + node = entry.getValue(); + // 执行添加本地任务的同步操作 + doContentSync(Node.SYNC_ACTION_ADD_LOCAL, node, null); + } + + // 检查是否被取消 + if (!mCancelled) { + if (!DataUtils.batchDeleteNotes(mContentResolver, mLocalDeleteIdMap)) { + // 批量删除本地已删除的笔记失败,抛出操作失败异常 + throw new ActionFailureException("failed to batch-delete local deleted notes"); + } + } + + // 检查是否被取消 + if (!mCancelled) { + // 提交更新 + GTaskClient.getInstance().commitUpdate(); \ No newline at end of file diff --git a/src/聂方凯小米便签注释/AlarmAlertActivity.java b/src/聂方凯小米便签注释/AlarmAlertActivity.java new file mode 100644 index 0000000..7dac50d --- /dev/null +++ b/src/聂方凯小米便签注释/AlarmAlertActivity.java @@ -0,0 +1,211 @@ +/* + * 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.ui; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.DialogInterface.OnDismissListener; +import android.content.Intent; +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.media.RingtoneManager; +import android.net.Uri; +import android.os.Bundle; +import android.os.PowerManager; +import android.provider.Settings; +import android.view.Window; +import android.view.WindowManager; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.tool.DataUtils; + +import java.io.IOException; + + +public class AlarmAlertActivity extends Activity implements OnClickListener, OnDismissListener { + private long mNoteId; + private String mSnippet; + private static final int SNIPPET_PREW_MAX_LEN = 60; + MediaPlayer mPlayer; + + @Override + protected void onCreate(Bundle savedInstanceState) { + // 调用父类的onCreate方法,初始化Activity + super.onCreate(savedInstanceState); + // 请求无标题窗口特性 + requestWindowFeature(Window.FEATURE_NO_TITLE); + + // 获取当前Activity的Window对象 + final Window win = getWindow(); + // 设置窗口标志,使窗口在锁屏时仍然显示 + win.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); + + // 检查屏幕是否处于关闭状态 + if (!isScreenOn()) { + // 如果屏幕关闭,则添加多个标志以保持屏幕常亮、解锁屏幕并允许在屏幕上显示内容 + win.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON + | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON + | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON + | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR); + } + + // 获取当前Activity的Intent对象 + Intent intent = getIntent(); + + try { + // 从Intent的数据中获取Note的ID + mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1)); + // 根据Note的ID从内容解析器中获取对应的摘要信息 + mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId); + // 如果摘要信息长度超过预设的最大长度,则截取并添加省略号 + mSnippet = mSnippet.length() > SNIPPET_PREW_MAX_LEN ? mSnippet.substring(0, + SNIPPET_PREW_MAX_LEN) + getResources().getString(R.string.notelist_string_info) + : mSnippet; + } catch (IllegalArgumentException e) { + // 捕获并打印非法参数异常 + e.printStackTrace(); + // 发生异常时,直接返回,不继续执行后续代码 + return; + } + + // 创建一个新的MediaPlayer对象 + mPlayer = new MediaPlayer(); + // 检查Note是否在Note数据库中可见 + if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) { + // 如果可见,则显示操作对话框并播放警报声音 + showActionDialog(); + playAlarmSound(); + } else { + // 如果不可见,则结束当前Activity + finish(); + } + } + + // 定义一个私有方法isScreenOn,用于检查设备屏幕是否处于开启状态 + private boolean isScreenOn() { + // 获取PowerManager系统服务,用于管理电源相关的功能 + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + // 调用PowerManager的isScreenOn方法,返回屏幕是否开启的布尔值 + return pm.isScreenOn(); + } + + // 定义一个私有方法用于播放闹钟声音 + private void playAlarmSound() { + // 获取系统默认的闹钟铃声URI + Uri url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM); + + // 获取当前系统设置中静音模式影响的音频流类型 + int silentModeStreams = Settings.System.getInt(getContentResolver(), + Settings.System.MODE_RINGER_STREAMS_AFFECTED, 0); + + // 检查当前静音模式是否影响闹钟音频流 + if ((silentModeStreams & (1 << AudioManager.STREAM_ALARM)) != 0) { + // 如果影响,则设置MediaPlayer的音频流类型为静音模式影响的类型 + mPlayer.setAudioStreamType(silentModeStreams); + } else { + // 如果不影响,则设置MediaPlayer的音频流类型为闹钟类型 + mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM); + } + try { + // 设置MediaPlayer的数据源为闹钟铃声的URI + mPlayer.setDataSource(this, url); + // 准备MediaPlayer + mPlayer.prepare(); + // 设置MediaPlayer为循环播放 + mPlayer.setLooping(true); + mPlayer.start(); + } catch (IllegalArgumentException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (SecurityException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IllegalStateException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + // 定义一个私有方法showActionDialog,用于显示操作对话框 + private void showActionDialog() { + // 创建一个AlertDialog.Builder对象,用于构建对话框 + AlertDialog.Builder dialog = new AlertDialog.Builder(this); + // 设置对话框的标题,使用资源文件中的字符串 + dialog.setTitle(R.string.app_name); + // 设置对话框的消息内容,使用成员变量mSnippet的值 + dialog.setMessage(mSnippet); + // 设置对话框的确定按钮,并指定点击事件监听器为当前对象(this) + dialog.setPositiveButton(R.string.notealert_ok, this); + // 检查屏幕是否处于开启状态 + if (isScreenOn()) { + // 如果屏幕开启,则设置对话框的取消按钮,并指定点击事件监听器为当前对象(this) + dialog.setNegativeButton(R.string.notealert_enter, this); + } + // 显示对话框,并设置对话框关闭时的监听器为当前对象(this) + dialog.show().setOnDismissListener(this); + } + + // 定义一个方法,当对话框中的按钮被点击时调用 + public void onClick(DialogInterface dialog, int which) { + // 使用switch语句根据点击的按钮类型进行不同的处理 + switch (which) { + // 如果点击的是对话框的否定按钮(通常是“取消”或“否”) + case DialogInterface.BUTTON_NEGATIVE: + // 创建一个新的Intent对象,用于启动NoteEditActivity + Intent intent = new Intent(this, NoteEditActivity.class); + // 设置Intent的动作类型为查看(ACTION_VIEW) + intent.setAction(Intent.ACTION_VIEW); + // 将笔记的ID作为额外数据添加到Intent中,键为Intent.EXTRA_UID + intent.putExtra(Intent.EXTRA_UID, mNoteId); + // 启动NoteEditActivity,传递Intent对象 + startActivity(intent); + break; + // 默认情况,不做任何处理 + default: + break; + } + } + + // 定义一个方法,当对话框被取消或关闭时调用 + public void onDismiss(DialogInterface dialog) { + // 当对话框被取消或关闭时,执行以下操作 + // 调用stopAlarmSound方法,停止闹钟声音 + stopAlarmSound(); + // 调用finish方法,结束当前Activity + finish(); + } + + // 定义一个私有方法用于停止闹钟声音 + private void stopAlarmSound() { + // 检查mPlayer对象是否不为空,以避免空指针异常 + if (mPlayer != null) { + // 调用mPlayer的stop方法停止播放声音 + mPlayer.stop(); + // 调用mPlayer的release方法释放资源 + mPlayer.release(); + // 将mPlayer对象设置为null,表示不再持有该对象 + mPlayer = null; + } + } +} diff --git a/src/聂方凯小米便签注释/AlarmInitReceiver.java b/src/聂方凯小米便签注释/AlarmInitReceiver.java new file mode 100644 index 0000000..af69a85 --- /dev/null +++ b/src/聂方凯小米便签注释/AlarmInitReceiver.java @@ -0,0 +1,79 @@ +/* + * 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.ui; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.ContentUris; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; + +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; + + +public class AlarmInitReceiver extends BroadcastReceiver { + + // 定义查询所需的列 + private static final String [] PROJECTION = new String [] { + NoteColumns.ID, // 笔记的ID + NoteColumns.ALERTED_DATE // 笔记的提醒日期 + }; + + + + // 定义列的索引 + private static final int COLUMN_ID = 0; // ID列的索引 + private static final int COLUMN_ALERTED_DATE = 1; // 提醒日期列的索引 + + @Override + public void onReceive(Context context, Intent intent) { + // 获取当前时间 + long currentDate = System.currentTimeMillis(); + // 查询提醒日期大于当前时间且类型为笔记的记录 + Cursor c = context.getContentResolver().query(Notes.CONTENT_NOTE_URI, + PROJECTION, + NoteColumns.ALERTED_DATE + ">? AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE, + new String[] { String.valueOf(currentDate) }, + null); + + if (c != null) { + // 如果查询结果非空,遍历结果集 + if (c.moveToFirst()) { + do { + // 获取提醒日期 + long alertDate = c.getLong(COLUMN_ALERTED_DATE); + // 创建Intent,指定接收器为AlarmReceiver + Intent sender = new Intent(context, AlarmReceiver.class); + // 设置Intent的数据,包含笔记的ID + sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, c.getLong(COLUMN_ID))); + // 创建PendingIntent,用于延迟发送广播 + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, 0); + // 获取AlarmManager服务 + AlarmManager alermManager = (AlarmManager) context + .getSystemService(Context.ALARM_SERVICE); + // 设置闹钟,在提醒日期触发广播 + alermManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent); + } while (c.moveToNext()); + } + // 关闭Cursor + c.close(); + } + } +} diff --git a/src/聂方凯小米便签注释/AlarmReceiver.java b/src/聂方凯小米便签注释/AlarmReceiver.java new file mode 100644 index 0000000..5a65098 --- /dev/null +++ b/src/聂方凯小米便签注释/AlarmReceiver.java @@ -0,0 +1,35 @@ +/* + * 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.ui; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +// 定义一个名为AlarmReceiver的类,继承自BroadcastReceiver,用于接收广播消息 +public class AlarmReceiver extends BroadcastReceiver { + // 重写onReceive方法,该方法会在接收到广播时被调用 + @Override + public void onReceive(Context context, Intent intent) { + // 将Intent的类设置为AlarmAlertActivity,即指定要启动的Activity + intent.setClass(context, AlarmAlertActivity.class); + // 为Intent添加FLAG_ACTIVITY_NEW_TASK标志,表示启动一个新的任务来承载该Activity + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + // 使用上下文context启动Activity,传入修改后的Intent + context.startActivity(intent); + } +} diff --git a/src/聂方凯小米便签注释/DateTimePicker.java b/src/聂方凯小米便签注释/DateTimePicker.java new file mode 100644 index 0000000..20d6d2a --- /dev/null +++ b/src/聂方凯小米便签注释/DateTimePicker.java @@ -0,0 +1,655 @@ +/* + * 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.ui; + +import java.text.DateFormatSymbols; +import java.util.Calendar; + +import net.micode.notes.R; + + +import android.content.Context; +import android.text.format.DateFormat; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.NumberPicker; + +public class DateTimePicker extends FrameLayout { + + // 默认启用状态 + private static final boolean DEFAULT_ENABLE_STATE = true; + + // 一半天的小时数 + private static final int HOURS_IN_HALF_DAY = 12; + // 一天的小时数 + private static final int HOURS_IN_ALL_DAY = 24; + // 一周的天数 + private static final int DAYS_IN_ALL_WEEK = 7; + // 日期选择器的最小值 + private static final int DATE_SPINNER_MIN_VAL = 0; + // 日期选择器的最大值 + private static final int DATE_SPINNER_MAX_VAL = DAYS_IN_ALL_WEEK - 1; + // 24小时制小时选择器的最小值 + private static final int HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW = 0; + // 24小时制小时选择器的最大值 + private static final int HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW = 23; + // 12小时制小时选择器的最小值 + private static final int HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW = 1; + // 12小时制小时选择器的最大值 + private static final int HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW = 12; + // 分钟选择器的最小值 + private static final int MINUT_SPINNER_MIN_VAL = 0; + // 分钟选择器的最大值 + private static final int MINUT_SPINNER_MAX_VAL = 59; + // AM/PM选择器的最小值 + private static final int AMPM_SPINNER_MIN_VAL = 0; + // AM/PM选择器的最大值 + private static final int AMPM_SPINNER_MAX_VAL = 1; + + // 日期选择器 + private final NumberPicker mDateSpinner; + // 小时选择器 + private final NumberPicker mHourSpinner; + // 分钟选择器 + private final NumberPicker mMinuteSpinner; + // AM/PM选择器 + private final NumberPicker mAmPmSpinner; + // 日期对象 + private Calendar mDate; + + // 日期显示值数组 + private String[] mDateDisplayValues = new String[DAYS_IN_ALL_WEEK]; + + // 是否为AM + private boolean mIsAm; + + // 是否为24小时制 + private boolean mIs24HourView; + + // 是否启用 + private boolean mIsEnabled = DEFAULT_ENABLE_STATE; + + private boolean mInitialising; + + private OnDateTimeChangedListener mOnDateTimeChangedListener; + + // 日期选择器值变化监听器 + private NumberPicker.OnValueChangeListener mOnDateChangedListener = new NumberPicker.OnValueChangeListener() { + @Override + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + mDate.add(Calendar.DAY_OF_YEAR, newVal - oldVal); + updateDateControl(); + onDateTimeChanged(); + } + }; + + // 小时选择器值变化监听器 + private NumberPicker.OnValueChangeListener mOnHourChangedListener = new NumberPicker.OnValueChangeListener() { + @Override + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + boolean isDateChanged = false; + Calendar cal = Calendar.getInstance(); + if (!mIs24HourView) { + if (!mIsAm && oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) { + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, 1); + isDateChanged = true; + } else if (mIsAm && oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) { + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, -1); + isDateChanged = true; + } + if (oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY || + oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) { + mIsAm = !mIsAm; + updateAmPmControl(); + } + } else { + if (oldVal == HOURS_IN_ALL_DAY - 1 && newVal == 0) { + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, 1); + isDateChanged = true; + } else if (oldVal == 0 && newVal == HOURS_IN_ALL_DAY - 1) { + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, -1); + isDateChanged = true; + } + } + int newHour = mHourSpinner.getValue() % HOURS_IN_HALF_DAY + (mIsAm ? 0 : HOURS_IN_HALF_DAY); + mDate.set(Calendar.HOUR_OF_DAY, newHour); + onDateTimeChanged(); + if (isDateChanged) { + setCurrentYear(cal.get(Calendar.YEAR)); + setCurrentMonth(cal.get(Calendar.MONTH)); + setCurrentDay(cal.get(Calendar.DAY_OF_MONTH)); + } + } + }; + + // 分钟选择器值变化监听器 + private NumberPicker.OnValueChangeListener mOnMinuteChangedListener = new NumberPicker.OnValueChangeListener() { + @Override + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + int minValue = mMinuteSpinner.getMinValue(); + int maxValue = mMinuteSpinner.getMaxValue(); + int offset = 0; + if (oldVal == maxValue && newVal == minValue) { + offset += 1; + } else if (oldVal == minValue && newVal == maxValue) { + offset -= 1; + } + if (offset != 0) { + mDate.add(Calendar.HOUR_OF_DAY, offset); + mHourSpinner.setValue(getCurrentHour()); + updateDateControl(); + int newHour = getCurrentHourOfDay(); + if (newHour >= HOURS_IN_HALF_DAY) { + mIsAm = false; + updateAmPmControl(); + } else { + mIsAm = true; + updateAmPmControl(); + } + } + mDate.set(Calendar.MINUTE, newVal); + onDateTimeChanged(); + } + }; + + // AM/PM选择器值变化监听器 + private NumberPicker.OnValueChangeListener mOnAmPmChangedListener = new NumberPicker.OnValueChangeListener() { + @Override + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + mIsAm = !mIsAm; + if (mIsAm) { + mDate.add(Calendar.HOUR_OF_DAY, -HOURS_IN_HALF_DAY); + } else { + mDate.add(Calendar.HOUR_OF_DAY, HOURS_IN_HALF_DAY); + } + updateAmPmControl(); + onDateTimeChanged(); + } + }; + + // 日期时间变化监听器接口 + public interface OnDateTimeChangedListener { + void onDateTimeChanged(DateTimePicker view, int year, int month, + int dayOfMonth, int hourOfDay, int minute); + } + + // 构造函数 + // 构造函数,用于创建DateTimePicker对象 + // 参数context:上下文对象,用于获取应用环境和资源 + public DateTimePicker(Context context) { + // 调用另一个构造函数,传入当前系统时间作为默认值 + // System.currentTimeMillis():获取当前系统时间的毫秒值 + this(context, System.currentTimeMillis()); + } + + // 构造函数,用于创建DateTimePicker对象 + // 参数context:上下文对象,用于获取系统资源和配置 + // 参数date:初始日期时间,以毫秒为单位的长整型数值 + public DateTimePicker(Context context, long date) { + // 调用另一个构造函数,传入当前上下文、初始日期时间和是否使用24小时制 + // DateFormat.is24HourFormat(context)方法用于判断当前系统是否使用24小时制 + this(context, date, DateFormat.is24HourFormat(context)); + } + + // 构造函数,用于初始化DateTimePicker对象 + public DateTimePicker(Context context, long date, boolean is24HourView) { + // 调用父类的构造函数,传入上下文 + super(context); + // 初始化Calendar对象,用于存储日期和时间 + mDate = Calendar.getInstance(); + // 设置初始化标志为true,表示正在初始化 + mInitialising = true; + // 获取当前小时,判断是否为上午或下午 + mIsAm = getCurrentHourOfDay() >= HOURS_IN_HALF_DAY; + // 加载布局文件datetime_picker.xml,并将其设置为当前视图 + inflate(context, R.layout.datetime_picker, this); + + // 获取日期选择器NumberPicker,并设置其最小值和最大值 + mDateSpinner = (NumberPicker) findViewById(R.id.date); + mDateSpinner.setMinValue(DATE_SPINNER_MIN_VAL); + mDateSpinner.setMaxValue(DATE_SPINNER_MAX_VAL); + // 设置日期选择器的值变化监听器 + mDateSpinner.setOnValueChangedListener(mOnDateChangedListener); + + // 获取小时选择器NumberPicker,并设置其值变化监听器 + mHourSpinner = (NumberPicker) findViewById(R.id.hour); + mHourSpinner.setOnValueChangedListener(mOnHourChangedListener); + // 获取分钟选择器NumberPicker,并设置其最小值和最大值 + mMinuteSpinner = (NumberPicker) findViewById(R.id.minute); + mMinuteSpinner.setMinValue(MINUT_SPINNER_MIN_VAL); + mMinuteSpinner.setMaxValue(MINUT_SPINNER_MAX_VAL); + // 设置分钟选择器的长按更新间隔 + mMinuteSpinner.setOnLongPressUpdateInterval(100); + // 设置分钟选择器的值变化监听器 + mMinuteSpinner.setOnValueChangedListener(mOnMinuteChangedListener); + + // 获取上午/下午字符串数组 + String[] stringsForAmPm = new DateFormatSymbols().getAmPmStrings(); + // 获取上午/下午选择器NumberPicker,并设置其最小值、最大值和显示的值 + mAmPmSpinner = (NumberPicker) findViewById(R.id.amPm); + mAmPmSpinner.setMinValue(AMPM_SPINNER_MIN_VAL); + mAmPmSpinner.setMaxValue(AMPM_SPINNER_MAX_VAL); + mAmPmSpinner.setDisplayedValues(stringsForAmPm); + // 设置上午/下午选择器的值变化监听器 + mAmPmSpinner.setOnValueChangedListener(mOnAmPmChangedListener); + + // update controls to initial state + updateDateControl(); + updateHourControl(); + updateAmPmControl(); + + set24HourView(is24HourView); + + // set to current time + setCurrentDate(date); + + setEnabled(isEnabled()); + + // set the content descriptions + mInitialising = false; + } + + @Override + public void setEnabled(boolean enabled) { + // 检查当前状态是否与要设置的状态相同,如果相同则直接返回,避免重复设置 + if (mIsEnabled == enabled) { + return; + } + // 调用父类的setEnabled方法,设置组件的基本启用状态 + super.setEnabled(enabled); + // 设置日期选择器的启用状态 + mDateSpinner.setEnabled(enabled); + // 设置分钟选择器的启用状态 + mMinuteSpinner.setEnabled(enabled); + // 设置小时选择器的启用状态 + mHourSpinner.setEnabled(enabled); + // 设置上午/下午选择器的启用状态 + mAmPmSpinner.setEnabled(enabled); + // 更新当前组件的启用状态标志 + mIsEnabled = enabled; + } + + // 重写父类或接口中的isEnabled方法 + @Override + public boolean isEnabled() { + // 返回成员变量mIsEnabled的值,表示当前对象是否启用 + return mIsEnabled; + } + + /** + * Get the current date in millis + * + * @return the current date in millis + */ + // 定义一个公共方法,返回当前日期的时间戳(以毫秒为单位) + public long getCurrentDateInTimeMillis() { + // 调用mDate对象的getTimeInMillis方法,获取当前日期的时间戳 + return mDate.getTimeInMillis(); + } + + /** + * Set the current date + * + * @param date The current date in millis + */ + // 定义一个方法用于设置当前日期和时间 + public void setCurrentDate(long date) { + // 获取一个Calendar实例 + Calendar cal = Calendar.getInstance(); + // 将传入的日期(以毫秒为单位)设置到Calendar对象中 + cal.setTimeInMillis(date); + // 调用另一个重载的setCurrentDate方法,传入年、月、日、小时和分钟 + setCurrentDate(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH), + cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE)); + } + + /** + * Set the current date + * + * @param year The current year + * @param month The current month + * @param dayOfMonth The current dayOfMonth + * @param hourOfDay The current hourOfDay + * @param minute The current minute + */ + // 定义一个方法用于设置当前日期和时间 + public void setCurrentDate(int year, int month, + int dayOfMonth, int hourOfDay, int minute) { + // 调用设置年份的方法,传入年份参数 + setCurrentYear(year); + // 调用设置月份的方法,传入月份参数 + setCurrentMonth(month); + // 调用设置日期的方法,传入日期参数 + setCurrentDay(dayOfMonth); + // 调用设置小时的方法,传入小时参数 + setCurrentHour(hourOfDay); + // 调用设置分钟的方法,传入分钟参数 + setCurrentMinute(minute); + } + + /** + * Get current year + * + * @return The current year + */ + // 定义一个公共方法,用于获取当前年份 + public int getCurrentYear() { + // 调用mDate对象的get方法,传入Calendar.YEAR常量,获取当前年份 + return mDate.get(Calendar.YEAR); + } + + /** + * Set current year + * + * @param year The current year + */ + // 设置当前年份的方法 + public void setCurrentYear(int year) { + // 检查是否正在初始化以及年份是否与当前年份相同 + // 如果正在初始化或者年份未改变,则直接返回,不进行后续操作 + if (!mInitialising && year == getCurrentYear()) { + return; + } + // 使用Calendar对象设置日期的年份 + mDate.set(Calendar.YEAR, year); + // 更新日期控件,以反映新的年份 + updateDateControl(); + // 调用方法处理日期时间变化,可能触发相关事件或更新UI + onDateTimeChanged(); + } + + /** + * Get current month in the year + * + * @return The current month in the year + */ + // 定义一个公共方法,用于获取当前月份 + public int getCurrentMonth() { + // 调用mDate对象的get方法,传入Calendar.MONTH常量,获取当前日期中的月份 + // Calendar.MONTH的值是从0开始的,即0代表一月,1代表二月,依此类推 + return mDate.get(Calendar.MONTH); + } + + /** + * Set current month in the year + * + * @param month The month in the year + */ + // 设置当前月份的方法 + public void setCurrentMonth(int month) { + // 检查是否正在初始化以及传入的月份是否与当前月份相同 + // 如果正在初始化或者月份相同,则直接返回,不进行任何操作 + if (!mInitialising && month == getCurrentMonth()) { + return; + } + // 使用Calendar对象设置日期的月份 + mDate.set(Calendar.MONTH, month); + // 更新日期控件,以反映新的月份 + updateDateControl(); + // 调用onDateTimeChanged方法,通知日期时间已更改 + onDateTimeChanged(); + } + + /** + * Get current day of the month + * + * @return The day of the month + */ + // 定义一个公共方法,用于获取当前日期的天数 + public int getCurrentDay() { + // 调用Calendar实例mDate的get方法,传入Calendar.DAY_OF_MONTH常量 + // 该常量表示获取当前日期中的天数 + // 返回当前日期的天数 + return mDate.get(Calendar.DAY_OF_MONTH); + } + + /** + * Set current day of the month + * + * @param dayOfMonth The day of the month + */ + // 设置当前日期的天数 + public void setCurrentDay(int dayOfMonth) { + // 检查是否正在初始化以及传入的天数是否与当前天数相同 + if (!mInitialising && dayOfMonth == getCurrentDay()) { + // 如果相同,则直接返回,不做任何操作 + return; + } + // 使用Calendar对象设置日期的天数 + mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); + // 更新日期控件,以反映新的日期 + updateDateControl(); + // 触发日期时间变化事件,通知相关监听器 + onDateTimeChanged(); + } + + /** + * Get current hour in 24 hour mode, in the range (0~23) + * @return The current hour in 24 hour mode + */ + // 定义一个公共方法,用于获取当前的小时数(24小时制) + public int getCurrentHourOfDay() { + // 调用mDate对象的get方法,传入Calendar.HOUR_OF_DAY常量 + // 该常量表示获取当前时间的小时数(24小时制) + // 返回获取到的小时数 + return mDate.get(Calendar.HOUR_OF_DAY); + } + + // 获取当前小时数的私有方法 + private int getCurrentHour() { + // 检查是否为24小时制视图 + if (mIs24HourView){ + // 如果是24小时制,直接返回当前的小时数 + return getCurrentHourOfDay(); + } else { + // 如果不是24小时制,先获取当前的小时数 + int hour = getCurrentHourOfDay(); + // 检查当前小时数是否超过半天的小时数(12小时) + if (hour > HOURS_IN_HALF_DAY) { + // 如果超过12小时,返回减去半天小时数的结果(转换为下午的小时数) + return hour - HOURS_IN_HALF_DAY; + } else { + // 如果当前小时数为0(即午夜),则返回半天小时数(12) + // 否则,直接返回当前小时数(上午的小时数) + return hour == 0 ? HOURS_IN_HALF_DAY : hour; + } + } + } + + /** + * Set current hour in 24 hour mode, in the range (0~23) + * + * @param hourOfDay + */ + // 设置当前小时的方法 + public void setCurrentHour(int hourOfDay) { + // 检查是否正在初始化且小时数与当前小时相同,如果是则直接返回 + if (!mInitialising && hourOfDay == getCurrentHourOfDay()) { + return; + } + // 使用Calendar对象设置小时数 + mDate.set(Calendar.HOUR_OF_DAY, hourOfDay); + // 检查是否为24小时制 + if (!mIs24HourView) { + // 如果小时数大于等于半天的小时数(12小时),则设置为下午 + if (hourOfDay >= HOURS_IN_HALF_DAY) { + mIsAm = false; + // 如果小时数大于半天的小时数,则减去半天的小时数 + if (hourOfDay > HOURS_IN_HALF_DAY) { + hourOfDay -= HOURS_IN_HALF_DAY; + } + } else { + // 否则设置为上午 + mIsAm = true; + // 如果小时数为0,则设置为半天的小时数 + if (hourOfDay == 0) { + hourOfDay = HOURS_IN_HALF_DAY; + } + } + // 更新上午/下午控件 + updateAmPmControl(); + } + // 设置小时选择器的值 + mHourSpinner.setValue(hourOfDay); + // 触发时间变化事件 + onDateTimeChanged(); + } + + /** + * Get currentMinute + * + * @return The Current Minute + */ + // 定义一个公共方法,用于获取当前分钟数 + public int getCurrentMinute() { + // 调用mDate对象的get方法,传入Calendar.MINUTE常量,获取当前时间的分钟数 + return mDate.get(Calendar.MINUTE); + } + + /** + * Set current minute + */ + // 设置当前分钟的方法 + public void setCurrentMinute(int minute) { + // 检查是否正在初始化并且分钟值是否与当前分钟相同 + // 如果是,则直接返回,不进行任何操作 + if (!mInitialising && minute == getCurrentMinute()) { + return; + } + // 设置分钟选择器的值为传入的分钟值 + mMinuteSpinner.setValue(minute); + // 更新内部日期对象的分钟字段为传入的分钟值 + mDate.set(Calendar.MINUTE, minute); + // 调用方法通知日期时间已更改 + onDateTimeChanged(); + } + + /** + * @return true if this is in 24 hour view else false. + */ + // 定义一个公共的方法,用于判断是否为24小时制视图 + public boolean is24HourView () { + // 返回成员变量mIs24HourView的值,该变量用于存储是否为24小时制的状态 + return mIs24HourView; + } + + /** + * Set whether in 24 hour or AM/PM mode. + * + * @param is24HourView True for 24 hour mode. False for AM/PM mode. + */ + // 设置24小时制视图 + public void set24HourView(boolean is24HourView) { + // 如果当前24小时制视图状态与传入的参数相同,则直接返回,不做任何操作 + if (mIs24HourView == is24HourView) { + return; + } + // 更新24小时制视图状态 + mIs24HourView = is24HourView; + // 根据是否为24小时制视图,设置上午/下午选择器的可见性 + mAmPmSpinner.setVisibility(is24HourView ? View.GONE : View.VISIBLE); + // 获取当前的小时数 + int hour = getCurrentHourOfDay(); + // 更新小时控制部分 + updateHourControl(); + // 设置当前小时数 + setCurrentHour(hour); + // 更新上午/下午控制部分 + updateAmPmControl(); + } + + // 更新日期控件的方法 + private void updateDateControl() { + // 获取当前时间的Calendar实例 + Calendar cal = Calendar.getInstance(); + // 将Calendar实例的时间设置为mDate的时间 + cal.setTimeInMillis(mDate.getTimeInMillis()); + // 将日期向前调整一周的一半再加一天,即调整到一周的起始位置 + cal.add(Calendar.DAY_OF_YEAR, -DAYS_IN_ALL_WEEK / 2 - 1); + // 清除日期选择器的显示值 + mDateSpinner.setDisplayedValues(null); + // 遍历一周的每一天 + for (int i = 0; i < DAYS_IN_ALL_WEEK; ++i) { + // 每次循环将日期加一天 + cal.add(Calendar.DAY_OF_YEAR, 1); + // 将格式化后的日期字符串存储到数组中 + mDateDisplayValues[i] = (String) DateFormat.format("MM.dd EEEE", cal); + } + // 设置日期选择器的显示值为更新后的日期数组 + mDateSpinner.setDisplayedValues(mDateDisplayValues); + // 设置日期选择器的当前值为一周的中间位置 + mDateSpinner.setValue(DAYS_IN_ALL_WEEK / 2); + // 刷新日期选择器,使其显示最新的值 + mDateSpinner.invalidate(); + } + + // 定义一个私有方法,用于更新上午/下午控制组件的显示状态 + private void updateAmPmControl() { + // 检查是否为24小时制视图 + if (mIs24HourView) { + // 如果是24小时制视图,则隐藏上午/下午选择器 + mAmPmSpinner.setVisibility(View.GONE); + } else { + // 如果不是24小时制视图,则根据当前是上午还是下午设置选择器的值 + int index = mIsAm ? Calendar.AM : Calendar.PM; + mAmPmSpinner.setValue(index); + // 显示上午/下午选择器 + mAmPmSpinner.setVisibility(View.VISIBLE); + } + } + + // 定义一个私有方法updateHourControl,用于更新小时选择器的范围 + private void updateHourControl() { + // 检查是否为24小时制视图 + if (mIs24HourView) { + // 如果是24小时制视图,设置小时选择器的最小值为HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW + mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW); + // 设置小时选择器的最大值为HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW + mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW); + } else { + // 如果不是24小时制视图,设置小时选择器的最小值为HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW + mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW); + // 设置小时选择器的最大值为HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW + mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW); + } + } + + /** + * Set the callback that indicates the 'Set' button has been pressed. + * @param callback the callback, if null will do nothing + */ + // 定义一个方法,用于设置日期时间变化监听器 + public void setOnDateTimeChangedListener(OnDateTimeChangedListener callback) { + // 将传入的监听器对象赋值给成员变量mOnDateTimeChangedListener + mOnDateTimeChangedListener = callback; + } + + // 定义一个私有方法,用于处理日期时间变化事件 + private void onDateTimeChanged() { + // 检查是否设置了日期时间变化监听器 + if (mOnDateTimeChangedListener != null) { + // 如果监听器不为空,则调用监听器的onDateTimeChanged方法 + // 传递当前对象以及当前的年、月、日、小时和分钟 + mOnDateTimeChangedListener.onDateTimeChanged(this, getCurrentYear(), + getCurrentMonth(), getCurrentDay(), getCurrentHourOfDay(), getCurrentMinute()); + } + } +} diff --git a/src/聂方凯小米便签注释/DateTimePickerDialog.java b/src/聂方凯小米便签注释/DateTimePickerDialog.java new file mode 100644 index 0000000..f6c097e --- /dev/null +++ b/src/聂方凯小米便签注释/DateTimePickerDialog.java @@ -0,0 +1,124 @@ +/* + * 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.ui; + +import java.util.Calendar; + +import net.micode.notes.R; +import net.micode.notes.ui.DateTimePicker; +import net.micode.notes.ui.DateTimePicker.OnDateTimeChangedListener; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.text.format.DateFormat; +import android.text.format.DateUtils; + +public class DateTimePickerDialog extends AlertDialog implements OnClickListener { + + // 定义一个日历对象,用于存储日期和时间 + private Calendar mDate = Calendar.getInstance(); + // 是否使用24小时制 + private boolean mIs24HourView; + // 日期时间设置监听器 + private OnDateTimeSetListener mOnDateTimeSetListener; + // 日期时间选择器 + private DateTimePicker mDateTimePicker; + + // 定义一个接口,用于监听日期时间设置事件 + public interface OnDateTimeSetListener { + void OnDateTimeSet(AlertDialog dialog, long date); + } + + // 构造函数,初始化日期时间选择器 + // 构造函数,用于创建DateTimePickerDialog对象 + public DateTimePickerDialog(Context context, long date) { + // 调用父类的构造函数,传入上下文 + super(context); + // 创建DateTimePicker对象,用于选择日期和时间 + mDateTimePicker = new DateTimePicker(context); + // 将DateTimePicker对象设置为对话框的视图 + setView(mDateTimePicker); + // 设置日期时间变化监听器,当日期或时间发生变化时触发 + mDateTimePicker.setOnDateTimeChangedListener(new OnDateTimeChangedListener() { + // 监听器回调方法,当日期或时间发生变化时调用 + public void onDateTimeChanged(DateTimePicker view, int year, int month, + int dayOfMonth, int hourOfDay, int minute) { + // 更新日历对象的日期和时间 + mDate.set(Calendar.YEAR, year); + mDate.set(Calendar.MONTH, month); + mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); + mDate.set(Calendar.HOUR_OF_DAY, hourOfDay); + mDate.set(Calendar.MINUTE, minute); + // 更新对话框标题 + updateTitle(mDate.getTimeInMillis()); + } + }); + // 设置初始日期和时间 + mDate.setTimeInMillis(date); + mDate.set(Calendar.SECOND, 0); + mDateTimePicker.setCurrentDate(mDate.getTimeInMillis()); + // 设置对话框的确定和取消按钮 + setButton(context.getString(R.string.datetime_dialog_ok), this); + setButton2(context.getString(R.string.datetime_dialog_cancel), (OnClickListener)null); + // 设置是否使用24小时制 + set24HourView(DateFormat.is24HourFormat(this.getContext())); + // 更新对话框标题 + updateTitle(mDate.getTimeInMillis()); + } + + // 设置是否使用24小时制 + public void set24HourView(boolean is24HourView) { + mIs24HourView = is24HourView; + } + + // 设置日期时间设置监听器 + public void setOnDateTimeSetListener(OnDateTimeSetListener callBack) { + mOnDateTimeSetListener = callBack; + } + + // 更新对话框标题 + // 定义一个私有方法updateTitle,用于更新标题,接收一个长整型参数date表示日期时间 + private void updateTitle(long date) { + // 定义一个整型变量flag,用于存储日期时间的格式标志 + int flag = + // 使用位或运算符将FORMAT_SHOW_YEAR标志添加到flag中,表示显示年份 + DateUtils.FORMAT_SHOW_YEAR | + // 使用位或运算符将FORMAT_SHOW_DATE标志添加到flag中,表示显示日期 + DateUtils.FORMAT_SHOW_DATE | + // 使用位或运算符将FORMAT_SHOW_TIME标志添加到flag中,表示显示时间 + DateUtils.FORMAT_SHOW_TIME; + // 使用位或运算符将FORMAT_24HOUR标志添加到flag中 + // 如果mIs24HourView为true,则添加FORMAT_24HOUR标志,表示使用24小时制 + // 如果mIs24HourView为false,则同样添加FORMAT_24HOUR标志,这里可能是一个逻辑错误,应该根据mIs24HourView的值决定是否添加该标志 + flag |= mIs24HourView ? DateUtils.FORMAT_24HOUR : DateUtils.FORMAT_24HOUR; + // 调用DateUtils的formatDateTime方法,将日期时间格式化为指定格式,并设置为当前上下文的标题 + setTitle(DateUtils.formatDateTime(this.getContext(), date, flag)); + } + + // 处理对话框按钮点击事件 + // 定义一个方法,当对话框被点击时触发 + public void onClick(DialogInterface arg0, int arg1) { + // 检查mOnDateTimeSetListener是否不为空,确保有监听器注册 + if (mOnDateTimeSetListener != null) { + // 调用监听器的OnDateTimeSet方法,传递当前对象和日期的毫秒值 + mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis()); + } + } + +} \ No newline at end of file diff --git a/src/聂方凯小米便签注释/DropdownMenu.java b/src/聂方凯小米便签注释/DropdownMenu.java new file mode 100644 index 0000000..ce43e89 --- /dev/null +++ b/src/聂方凯小米便签注释/DropdownMenu.java @@ -0,0 +1,78 @@ +/* + * 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.ui; + +import android.content.Context; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.PopupMenu; +import android.widget.PopupMenu.OnMenuItemClickListener; + +import net.micode.notes.R; + +public class DropdownMenu { + // 声明一个Button对象,用于显示下拉菜单的按钮 + private Button mButton; + // 声明一个PopupMenu对象,用于创建和管理下拉菜单 + private PopupMenu mPopupMenu; + // 声明一个Menu对象,用于获取下拉菜单的菜单项 + private Menu mMenu; + + // 构造函数,初始化DropdownMenu对象 + public DropdownMenu(Context context, Button button, int menuId) { + // 将传入的Button对象赋值给mButton + mButton = button; + // 设置按钮的背景资源为下拉图标 + mButton.setBackgroundResource(R.drawable.dropdown_icon); + // 创建一个PopupMenu对象,传入上下文和按钮对象 + mPopupMenu = new PopupMenu(context, mButton); + // 获取PopupMenu的Menu对象 + mMenu = mPopupMenu.getMenu(); + // 使用PopupMenu的MenuInflater对象加载菜单布局文件 + mPopupMenu.getMenuInflater().inflate(menuId, mMenu); + // 设置按钮的点击监听器 + mButton.setOnClickListener(new OnClickListener() { + // 点击按钮时显示PopupMenu + public void onClick(View v) { + mPopupMenu.show(); + } + }); + } + + // 设置下拉菜单项的点击监听器 + public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) { + // 如果PopupMenu对象不为空,则设置菜单项点击监听器 + if (mPopupMenu != null) { + mPopupMenu.setOnMenuItemClickListener(listener); + } + } + + // 根据菜单项ID查找菜单项 + public MenuItem findItem(int id) { + // 返回找到的菜单项 + return mMenu.findItem(id); + } + + // 设置按钮的标题 + public void setTitle(CharSequence title) { + // 将传入的标题设置给按钮 + mButton.setText(title); + } +} diff --git a/src/聂方凯小米便签注释/FoldersListAdapter.java b/src/聂方凯小米便签注释/FoldersListAdapter.java new file mode 100644 index 0000000..896ee3b --- /dev/null +++ b/src/聂方凯小米便签注释/FoldersListAdapter.java @@ -0,0 +1,106 @@ +/* + * 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.ui; + +import android.content.Context; +import android.database.Cursor; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CursorAdapter; +import android.widget.LinearLayout; +import android.widget.TextView; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; + + +public class FoldersListAdapter extends CursorAdapter { + // 定义查询的列 + public static final String [] PROJECTION = { + NoteColumns.ID, // 笔记ID + NoteColumns.SNIPPET // 笔记摘要 + }; + + + + // 定义列的索引 + public static final int ID_COLUMN = 0; + public static final int NAME_COLUMN = 1; // ID列的索引 + + // 定义一个名为 FoldersListAdapter 的构造函数,接收两个参数:Context 类型的 context 和 Cursor 类型的 c + public FoldersListAdapter(Context context, Cursor c) { + // 调用父类(可能是 ArrayAdapter 或其他适配器类)的构造函数,传递 context 和 c 参数 + super(context, c); + // TODO Auto-generated constructor stub + } + + // 重写方法,用于创建一个新的视图实例 + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + // 使用传入的上下文对象创建一个新的FolderListItem视图实例 + // FolderListItem是一个自定义的视图类,用于展示文件夹列表中的每一项 + return new FolderListItem(context); + } + + @Override + // 重写父类或接口中的方法 + public void bindView(View view, Context context, Cursor cursor) { + // 绑定视图的方法,用于将数据绑定到视图上 + if (view instanceof FolderListItem) { + // 检查传入的视图是否是FolderListItem的实例 + String folderName = (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context + // 如果当前游标所在的行的ID是根文件夹的ID + .getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN); + // 则获取字符串资源中的"menu_move_parent_folder"作为文件夹名称 + // 否则,从游标中获取NAME_COLUMN列的值作为文件夹名称 + ((FolderListItem) view).bind(folderName); + // 将文件夹名称绑定到FolderListItem视图上 + } + } + + // 定义一个方法,用于获取指定位置的文件夹名称 + public String getFolderName(Context context, int position) { + // 通过位置获取对应的Cursor对象 + Cursor cursor = (Cursor) getItem(position); + return (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context + .getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN); + } + + // 定义一个内部类FolderListItem,继承自LinearLayout,用于表示文件夹列表中的一个条目 + private class FolderListItem extends LinearLayout { + // 声明一个TextView成员变量mName,用于显示文件夹名称 + private TextView mName; + + // 构造方法,接收一个Context对象作为参数 + public FolderListItem(Context context) { + // 调用父类LinearLayout的构造方法,传入context + super(context); + // 调用inflate方法,将布局文件R.layout.folder_list_item填充到当前LinearLayout中 + inflate(context, R.layout.folder_list_item, this); + // 通过findViewById方法找到布局文件中的TextView控件,并赋值给mName + mName = (TextView) findViewById(R.id.tv_folder_name); + } + + // 定义一个bind方法,接收一个String类型的name参数,用于绑定文件夹名称到TextView上 + public void bind(String name) { + // 调用TextView的setText方法,将传入的name设置为TextView的文本内容 + mName.setText(name); + } + } + +}