From 635603cd2fcd91d236b0c4b18d50007bfcb9afc2 Mon Sep 17 00:00:00 2001 From: moneynewmoon <2075698549@qq.com> Date: Thu, 29 May 2025 15:17:36 +0800 Subject: [PATCH 1/7] 001 --- 对对都.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 对对都.txt diff --git a/对对都.txt b/对对都.txt new file mode 100644 index 0000000..e69de29 From 9cbb43a8ca42497dce0ead75faf92ab0331bdbb5 Mon Sep 17 00:00:00 2001 From: moneynewmoon <2075698549@qq.com> Date: Thu, 29 May 2025 15:26:31 +0800 Subject: [PATCH 2/7] remote --- GTaskClient.java | 618 +++++++++++++++++++++++++++++++ GTaskManager.java | 824 ++++++++++++++++++++++++++++++++++++++++++ GTaskSyncService.java | 181 ++++++++++ 3 files changed, 1623 insertions(+) create mode 100644 GTaskClient.java create mode 100644 GTaskManager.java create mode 100644 GTaskSyncService.java diff --git a/GTaskClient.java b/GTaskClient.java new file mode 100644 index 0000000..23464e2 --- /dev/null +++ b/GTaskClient.java @@ -0,0 +1,618 @@ +/* + * 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; +//导入Android账户相关类 +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; +// 导入Apache HttpClient相关类 +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; +// JSON处理类 +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +// Java IO相关类 +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.LinkedList; +import java.util.List; +import java.util.zip.GZIPInputStream; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; + + +public class GTaskClient { + // 日志标签 + private static final String TAG = GTaskClient.class.getSimpleName(); + // Google Tasks API地址 + private static final String GTASK_URL = "https://mail.google.com/tasks/"; + + private static final String GTASK_GET_URL = "https://mail.google.com/tasks/ig"; + + private static final String GTASK_POST_URL = "https://mail.google.com/tasks/r/ig"; + + private static GTaskClient mInstance = null; + // HTTP客户端和相关配置 + private DefaultHttpClient mHttpClient;// HTTP客户端实例 + + private String mGetUrl; + + private String mPostUrl; + + private long mClientVersion; + + private boolean mLoggedin; + + private long mLastLoginTime; + + private int mActionId; + + private Account mAccount; + + private JSONArray mUpdateArray; + // 私有构造函数 + private GTaskClient() { + mHttpClient = null; + mGetUrl = GTASK_GET_URL; + mPostUrl = GTASK_POST_URL; + mClientVersion = -1; + mLoggedin = false; + mLastLoginTime = 0; + mActionId = 1; + mAccount = null; + mUpdateArray = null; + } + // 获取单例实例 + public static synchronized GTaskClient getInstance() { + if (mInstance == null) { + mInstance = new GTaskClient(); + } + return mInstance; + } + + public boolean login(Activity activity) { + // we suppose that the cookie would expire after 5 minutes + // then we need to re-login + // 检查登录有效期(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))) { + mLoggedin = false; + } + + 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; + } + // 处理自定义域名账户 +// login with custom domain if necessary + 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"; + // 尝试登录自定义域名 + if (tryToLoginGtask(activity, authToken)) { + mLoggedin = true; + } + } + + // try to login with google official url + if (!mLoggedin) { + mGetUrl = GTASK_GET_URL; + mPostUrl = GTASK_POST_URL; + if (!tryToLoginGtask(activity, authToken)) { + return false; + } + } + + mLoggedin = true; + return true; + } + + private String loginGoogleAccount(Activity activity, boolean invalidateToken) { + String authToken; +// 账户管理器操作 + AccountManager accountManager = AccountManager.get(activity); + Account[] accounts = accountManager.getAccountsByType("com.google"); +// 无可用账户检查 + if (accounts.length == 0) { + Log.e(TAG, "there is no available google account"); + return null; + } +// 获取配置中的同步账户 + String accountName = NotesPreferenceActivity.getSyncAccountName(activity); + Account account = null; +// 遍历匹配账户名称 + for (Account a : accounts) { + if (a.name.equals(accountName)) { + account = a; + break; + } + } + if (account != null) { + // 设置当前账户 + mAccount = account; + } else { + Log.e(TAG, "unable to get an account with the same name in the settings"); + return null; + } + + // get the token now + AccountManagerFuture accountManagerFuture = accountManager.getAuthToken(account, + "goanna_mobile", null, activity, null, null); +// 获取授权令牌 + try { + Bundle authTokenBundle = accountManagerFuture.getResult(); + authToken = authTokenBundle.getString(AccountManager.KEY_AUTHTOKEN); +// 需要失效旧令牌时重新获取 + if (invalidateToken) { + accountManager.invalidateAuthToken("com.google", authToken); + loginGoogleAccount(activity, false); + } + } catch (Exception e) { + Log.e(TAG, "get auth token failed"); + authToken = null; + } + + return authToken; + } + + private boolean tryToLoginGtask(Activity activity, String authToken) { +// 首次登录尝试 + if (!loginGtask(authToken)) { + // maybe the auth token is out of date, now let's invalidate the + // token and try again + authToken = loginGoogleAccount(activity, true); + if (authToken == null) { + Log.e(TAG, "login google account failed"); + return false; + } +// 使用新令牌再次尝试登录 + if (!loginGtask(authToken)) { + Log.e(TAG, "login gtask failed"); + return false; + } + } + return true; + } + + private boolean loginGtask(String authToken) { +// 设置HTTP连接参数 + int timeoutConnection = 10000; + int timeoutSocket = 15000; + HttpParams httpParameters = new BasicHttpParams(); + HttpConnectionParams.setConnectionTimeout(httpParameters, timeoutConnection); + HttpConnectionParams.setSoTimeout(httpParameters, timeoutSocket); +// 初始化HTTP客户端 + mHttpClient = new DefaultHttpClient(httpParameters); + BasicCookieStore localBasicCookieStore = new BasicCookieStore(); + mHttpClient.setCookieStore(localBasicCookieStore); + HttpProtocolParams.setUseExpectContinue(mHttpClient.getParams(), false); + + // login gtask + try { +// 构建带认证令牌的登录URL + String loginUrl = mGetUrl + "?auth=" + authToken; + HttpGet httpGet = new HttpGet(loginUrl); + HttpResponse response = null; + response = mHttpClient.execute(httpGet); + + // get the cookie now + List cookies = mHttpClient.getCookieStore().getCookies(); + boolean hasAuthCookie = false; + for (Cookie cookie : cookies) { + if (cookie.getName().contains("GTL")) { + hasAuthCookie = true; + } + } + if (!hasAuthCookie) { + Log.w(TAG, "it seems that there is no auth cookie"); + } + + // get the client version +// 解析响应内容获取客户端版本 + String resString = getResponseContent(response.getEntity()); + String jsBegin = "_setup("; + String jsEnd = ")}"; + int begin = resString.indexOf(jsBegin); + int end = resString.lastIndexOf(jsEnd); + String jsString = null; + if (begin != -1 && end != -1 && begin < end) { + jsString = resString.substring(begin + jsBegin.length(), end); + } + JSONObject js = new JSONObject(jsString); + mClientVersion = js.getLong("v"); + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + return false; + } catch (Exception e) { + // simply catch all exceptions + Log.e(TAG, "httpget gtask_url failed"); + return false; + } + + return true; + } + + private int getActionId() { + return mActionId++; + } + + private HttpPost createHttpPost() { + HttpPost httpPost = new HttpPost(mPostUrl); +// 设置请求头信息 + httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8"); + httpPost.setHeader("AT", "1"); + return httpPost; + } + + private String getResponseContent(HttpEntity entity) throws IOException { + String contentEncoding = null; + if (entity.getContentEncoding() != null) { + contentEncoding = entity.getContentEncoding().getValue(); + Log.d(TAG, "encoding: " + contentEncoding); + } + + InputStream input = entity.getContent(); +// 处理压缩内容 + if (contentEncoding != null && contentEncoding.equalsIgnoreCase("gzip")) { + input = new GZIPInputStream(entity.getContent()); + } else if (contentEncoding != null && contentEncoding.equalsIgnoreCase("deflate")) { + Inflater inflater = new Inflater(true); + input = new InflaterInputStream(entity.getContent(), inflater); + } +// 读取响应内容 + try { + InputStreamReader isr = new InputStreamReader(input); + BufferedReader br = new BufferedReader(isr); + StringBuilder sb = new StringBuilder(); + + while (true) { + String buff = br.readLine(); + if (buff == null) { + return sb.toString(); + } + sb = sb.append(buff); + } + } finally { + input.close(); + } + } + + private JSONObject postRequest(JSONObject js) throws NetworkFailureException { +// 登录状态检查 + if (!mLoggedin) { + Log.e(TAG, "please login first"); + throw new ActionFailureException("not logged in"); + } + + HttpPost httpPost = createHttpPost(); + try { +// 构建POST参数 + LinkedList list = new LinkedList(); + list.add(new BasicNameValuePair("r", js.toString())); + UrlEncodedFormEntity entity = new UrlEncodedFormEntity(list, "UTF-8"); + httpPost.setEntity(entity); +// 执行请求并处理响应 + // execute the post + HttpResponse response = mHttpClient.execute(httpPost); + String jsString = getResponseContent(response.getEntity()); + return new JSONObject(jsString); + + } catch (ClientProtocolException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new NetworkFailureException("postRequest failed"); + } catch (IOException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new NetworkFailureException("postRequest failed"); + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("unable to convert response content to jsonobject"); + } catch (Exception e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("error occurs when posting request"); + } + } + + public void createTask(Task task) throws NetworkFailureException { + commitUpdate(); +// 提交暂存的更新 + try { + JSONObject jsPost = new JSONObject(); + JSONArray actionList = new JSONArray(); +// 构建创建动作 + // action_list + actionList.put(task.getCreateAction(getActionId())); + jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); +// 添加客户端版本 + // client_version + jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); +// 发送请求并处理响应 + // post + JSONObject jsResponse = postRequest(jsPost); + JSONObject jsResult = (JSONObject) jsResponse.getJSONArray( + GTaskStringUtils.GTASK_JSON_RESULTS).get(0); + task.setGid(jsResult.getString(GTaskStringUtils.GTASK_JSON_NEW_ID)); +// 设置服务器生成ID + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("create task: handing jsonobject failed"); + } + } + + public void createTaskList(TaskList tasklist) throws NetworkFailureException { + commitUpdate();// 提交暂存的更新 + try { + JSONObject jsPost = new JSONObject(); + JSONArray actionList = new JSONArray(); + // 构建创建动作 + // action_list + actionList.put(tasklist.getCreateAction(getActionId())); + jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); +// 添加客户端版本 + // client version + jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); +// 发送请求并处理响应 + // post + JSONObject jsResponse = postRequest(jsPost); + JSONObject jsResult = (JSONObject) jsResponse.getJSONArray( + GTaskStringUtils.GTASK_JSON_RESULTS).get(0); + tasklist.setGid(jsResult.getString(GTaskStringUtils.GTASK_JSON_NEW_ID));// 设置服务器生成ID + + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("create tasklist: handing jsonobject failed"); + } + } +// 提交暂存的更新操作(批量执行队列中的更新) + public void commitUpdate() throws NetworkFailureException { + if (mUpdateArray != null) { + try { + JSONObject jsPost = new JSONObject(); + // 设置操作列表字段(直接使用暂存的更新数组) + // action_list + jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, mUpdateArray); + // 添加客户端版本号 + // client_version + jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + + postRequest(jsPost);// 发送批量更新请求 + mUpdateArray = null;// 清空更新队列 + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("commit update: handing jsonobject failed"); + } + } + } +// 向更新队列添加单个操作(自动处理批量限制) + public void addUpdateNode(Node node) throws NetworkFailureException { + if (node != null) { + // too many update items may result in an error + // set max to 10 items +// 限制更新队列最大长度为10(超过则先提交当前队列) + if (mUpdateArray != null && mUpdateArray.length() > 10) { + commitUpdate();// 提交当前队列// 初始化更新队列 + } + + if (mUpdateArray == null)// 初始化更新队列 + mUpdateArray = new JSONArray(); + // 添加节点的更新操作(调用Node对象生成操作JSON) + mUpdateArray.put(node.getUpdateAction(getActionId())); + } + } +// 移动任务到新位置(支持任务列表间移动和任务排序) + public void moveTask(Task task, TaskList preParent, TaskList curParent) + throws NetworkFailureException { + commitUpdate();// 先提交之前暂存的更新 + try { + JSONObject jsPost = new JSONObject(); + JSONArray actionList = new JSONArray(); + JSONObject action = new JSONObject();// 创建移动操作JSON对象 +// 设置操作类型为移动 + // action_list + action.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, + GTaskStringUtils.GTASK_JSON_ACTION_TYPE_MOVE); + action.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, getActionId()); + action.put(GTaskStringUtils.GTASK_JSON_ID, task.getGid()); +// 如果在同一任务列表内移动且非第一个任务,设置前置兄弟ID(用于排序) + if (preParent == curParent && task.getPriorSibling() != null) { + // put prioring_sibing_id only if moving within the tasklist and + // it is not the first one + action.put(GTaskStringUtils.GTASK_JSON_PRIOR_SIBLING_ID, task.getPriorSibling()); + } +// 设置源任务列表ID和目标父级ID(目标任务列表ID) + action.put(GTaskStringUtils.GTASK_JSON_SOURCE_LIST, preParent.getGid()); + action.put(GTaskStringUtils.GTASK_JSON_DEST_PARENT, curParent.getGid()); +// 如果跨任务列表移动,额外设置目标列表ID + if (preParent != curParent) { + // put the dest_list only if moving between tasklists + action.put(GTaskStringUtils.GTASK_JSON_DEST_LIST, curParent.getGid()); + } + actionList.put(action); // 添加操作到列表 + jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); + + // client_version + // 添加客户端版本号 + jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + + postRequest(jsPost);// 发送移动请求 + + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("move task: handing jsonobject failed"); + } + } +// 删除任务或任务列表(标记为删除并提交更新) + public void deleteNode(Node node) throws NetworkFailureException { + commitUpdate();// 先提交之前暂存的更新 + try { + JSONObject jsPost = new JSONObject(); + JSONArray actionList = new JSONArray(); +// 标记节点为已删除 + // action_list + node.setDeleted(true); + // 添加删除操作 + actionList.put(node.getUpdateAction(getActionId())); + jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); + // 添加客户端版本号 + // client_version + jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + + postRequest(jsPost); + mUpdateArray = null; + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("delete node: handing jsonobject failed"); + } + } + // 获取所有任务列表(从Google Tasks获取根任务列表数据) + public JSONArray getTaskLists() throws NetworkFailureException { + if (!mLoggedin) {// 未登录检查 + Log.e(TAG, "please login first"); + throw new ActionFailureException("not logged in"); + } + + try { + HttpGet httpGet = new HttpGet(mGetUrl);// 创建GET请求 + HttpResponse response = null; + response = mHttpClient.execute(httpGet); +// 解析响应内容 + // get the task list + String resString = getResponseContent(response.getEntity()); + String jsBegin = "_setup("; + String jsEnd = ")}"; + int begin = resString.indexOf(jsBegin); + int end = resString.lastIndexOf(jsEnd); + String jsString = null; + if (begin != -1 && end != -1 && begin < end) { + jsString = resString.substring(begin + jsBegin.length(), end); + } + JSONObject js = new JSONObject(jsString); +// 提取任务列表数据 + return js.getJSONObject("t").getJSONArray(GTaskStringUtils.GTASK_JSON_LISTS); + } catch (ClientProtocolException e) { +// HTTP协议异常 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new NetworkFailureException("gettasklists: httpget failed"); + } catch (IOException e) { + // IO异常 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new NetworkFailureException("gettasklists: httpget failed"); + } catch (JSONException e) { +// JSON解析异常 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("get task lists: handing jasonobject failed"); + } + } +// 获取指定任务列表中的任务(支持获取未删除任务) + public JSONArray getTaskList(String listGid) throws NetworkFailureException { + commitUpdate(); + try { + JSONObject jsPost = new JSONObject(); + JSONArray actionList = new JSONArray(); + JSONObject action = new JSONObject(); +// 设置操作类型为获取所有任务 + // action_list + action.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, + GTaskStringUtils.GTASK_JSON_ACTION_TYPE_GETALL); + action.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, getActionId()); + action.put(GTaskStringUtils.GTASK_JSON_LIST_ID, listGid); + action.put(GTaskStringUtils.GTASK_JSON_GET_DELETED, false); + actionList.put(action);// 添加操作到列表 + jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); + + // client_version + jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + + JSONObject jsResponse = postRequest(jsPost); + return jsResponse.getJSONArray(GTaskStringUtils.GTASK_JSON_TASKS); + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("get task list: handing jsonobject failed"); + } + } +// 获取当前同步账户对象 + public Account getSyncAccount() { + return mAccount; + } +// 重置更新队列(清空待提交的操作) + public void resetUpdateArray() { + mUpdateArray = null; + } +} diff --git a/GTaskManager.java b/GTaskManager.java new file mode 100644 index 0000000..bb86b16 --- /dev/null +++ b/GTaskManager.java @@ -0,0 +1,824 @@ + +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * 授权协议:Apache License, 版本 2.0("许可证"); + * 除非遵守许可证,否则不得使用本文件。 + * 您可以在以下网址获取许可证副本: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 除非适用法律要求或书面同意,根据许可证分发的软件按"原样"提供, + * 不附带任何明示或暗示的保证或条件。请参阅许可证,了解具体的权限和限制。 + */ + +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; + + // 用于获取认证令牌的Activity + private Activity mActivity; + // 上下文环境 + private Context mContext; + // 内容解析器 + private ContentResolver mContentResolver; + // 同步进行中标志 + private boolean mSyncing; + // 同步取消标志 + private boolean mCancelled; + // 存储从Google获取的任务列表(键为GID) + private HashMap mGTaskListHashMap; + // 存储从Google获取的任务节点(键为GID) + private HashMap mGTaskHashMap; + // 存储元数据(键为关联的GID) + private HashMap mMetaHashMap; + // 元数据列表 + private TaskList mMetaList; + // 本地已删除笔记的ID集合 + private HashSet mLocalDeleteIdMap; + // GID到本地ID的映射 + private HashMap mGidToNid; + // 本地ID到GID的映射 + 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(); + } + + // 获取单例实例 + public static synchronized GTaskManager getInstance() { + if (mInstance == null) { + mInstance = new GTaskManager(); + } + return mInstance; + } + + // 设置Activity上下文(用于获取认证令牌) + 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; + } + + // 初始化Google任务列表 + private void initGTaskList() throws NetworkFailureException { + if (mCancelled) + return; + GTaskClient client = GTaskClient.getInstance(); + try { + 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) { + 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); + + // 过滤系统文件夹并处理用户自定义文件夹 + 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; + String gid; + Node node; + + 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()) { + 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; + } + } + + // 先同步文件夹 + 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> 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"); + } + } + + // 提交更新并刷新本地同步ID + if (!mCancelled) { + GTaskClient.getInstance().commitUpdate(); + refreshLocalSyncId(); + } + + } + + // 同步文件夹(包括系统文件夹和用户自定义文件夹) + private void syncFolder() throws NetworkFailureException { + Cursor c = null; + String gid; + 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); + // 仅在名称变更时更新远程名称 + 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) { + // 本地新增文件夹,需同步到远程 + 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 folder"); + } + } finally { + if (c != null) { + c.close(); + c = null; + } + } + + // 处理远程新增的文件夹(本地不存在的远程文件夹,需新增到本地) + Iterator> iter = mGTaskListHashMap.entrySet().iterator(); + while (iter.hasNext()) { + Map.Entry 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: + // 冲突处理:使用本地修改覆盖远程 + 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 { + // 处理笔记ID冲突 + 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)) { + note.remove(NoteColumns.ID); + } + } + } + + // 处理数据项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)) { + 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); + + // 更新ID映射 + 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 = new SqlNote(mContext, c); + sqlNote.setContent(node.getLocalJSONFromContent()); + + // 获取父文件夹ID + 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); + + // 更新元数据 + updateRemoteMeta(node.getGid(), sqlNote); + } + + // 新增本地任务到远程服务器 + private void addRemoteNode(Node node, Cursor c) throws NetworkFailureException { + if (mCancelled) { + return; + } + + SqlNote sqlNote = new SqlNote(mContext, c); + Node n; + + // 处理普通笔记 + if (sqlNote.isNoteType()) { + Task task = new Task(); + task.setContentByLocalJSON(sqlNote.getContent()); + + // 获取父任务列表GID + 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; + + // 添加元数据 + updateRemoteMeta(task.getGid(), sqlNote); + } else { + // 处理文件夹 + 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> iter = mGTaskListHashMap.entrySet().iterator(); + while (iter.hasNext()) { + Map.Entry 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; + } + } + + // 不存在则创建新文件夹 + if (tasklist == null) { + tasklist = new TaskList(); + tasklist.setContentByLocalJSON(sqlNote.getContent()); + GTaskClient.getInstance().createTaskList(tasklist); + mGTaskListHashMap.put(tasklist.getGid(), tasklist); + } + n = (Node) tasklist; + } + + // 更新本地笔记的GID + sqlNote.setGtaskId(n.getGid()); + sqlNote.commit(false); + sqlNote.resetLocalModified(); + sqlNote.commit(true); + + // 更新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); + + // 更新远程任务内容 + node.setContentByLocalJSON(sqlNote.getContent()); + GTaskClient.getInstance().addUpdateNode(node); + + // 更新元数据 + updateRemoteMeta(node.getGid(), sqlNote); + + // 处理任务移动(如果父文件夹变更) + 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); + } + } + + // 清除本地修改标志 + 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); + } + } + } + + // 刷新本地同步ID(更新笔记的最后同步时间) + private void refreshLocalSyncId() throws NetworkFailureException { + if (mCancelled) { + return; + } + + // 重新获取最新任务列表 + 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()); + // 更新本地同步ID + 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; + } +} \ No newline at end of file diff --git a/GTaskSyncService.java b/GTaskSyncService.java new file mode 100644 index 0000000..387cbb1 --- /dev/null +++ b/GTaskSyncService.java @@ -0,0 +1,181 @@ + +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * 版权声明:本文件由MiCode开源社区开发,遵循Apache License, Version 2.0协议; + * 您仅在遵守协议的前提下使用本文件,完整协议可通过以下链接获取: + * http://www.apache.org/licenses/LICENSE-2.0 + * 注:未书面明确要求时,本软件按"原样"提供,不附带任何明示或暗示的保证。 + */ + +package net.micode.notes.gtask.remote; + +import android.app.Activity; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.IBinder; + +/** + * Google任务同步服务类(后台服务组件) + * 负责管理同步任务的启动、取消,并通过广播通知外部同步状态(如是否同步中、进度信息) + */ +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; + // 同步状态广播的Action名称(用于发送同步状态通知) + public final static String GTASK_SERVICE_BROADCAST_NAME = "net.micode.notes.gtask.remote.gtask_sync_service"; + // 广播中传递的"是否正在同步"键名 + public final static String GTASK_SERVICE_BROADCAST_IS_SYNCING = "isSyncing"; + // 广播中传递的"进度消息"键名 + public final static String GTASK_SERVICE_BROADCAST_PROGRESS_MSG = "progressMsg"; + + // 当前运行的异步同步任务实例(静态变量,全局唯一) + private static GTaskASyncTask mSyncTask = null; + // 当前同步进度的文本描述(静态变量,记录最新进度) + private static String mSyncProgress = ""; + + /** + * 启动同步任务(内部方法) + * 检查是否已有同步任务运行,若无则创建新任务并执行 + */ + private void startSync() { + if (mSyncTask == null) { // 避免重复启动同步任务 + // 创建异步任务实例,传入服务上下文和完成监听器 + mSyncTask = new GTaskASyncTask(this, new GTaskASyncTask.OnCompleteListener() { + @Override + public void onComplete() { + // 同步完成后:清空任务实例 -> 发送广播 -> 停止服务自身 + mSyncTask = null; + sendBroadcast(""); // 发送同步结束的广播(进度消息为空) + stopSelf(); // 停止当前服务 + } + }); + sendBroadcast(""); // 发送同步开始的广播(进度消息为空) + mSyncTask.execute(); // 执行异步同步任务(触发后台同步逻辑) + } + } + + /** + * 取消当前同步任务(内部方法) + * 若存在运行中的同步任务,则调用其取消方法 + */ + private void cancelSync() { + if (mSyncTask != null) { + mSyncTask.cancelSync(); // 触发异步任务的取消逻辑 + } + } + + /** + * 服务创建时的回调方法 + * 初始化成员变量(确保服务启动时无残留任务) + */ + @Override + public void onCreate() { + mSyncTask = null; // 清空同步任务实例 + } + + /** + * 处理服务启动命令的回调方法 + * 根据意图中传递的动作类型(启动/取消同步)执行对应操作 + * @param intent 启动服务的意图(可能携带动作类型参数) + * @param flags 启动标志(如START_FLAG_REDELIVERY) + * @param startId 服务启动的唯一ID + * @return 服务重启策略(此处返回START_STICKY,意外终止后自动重启) + */ + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Bundle bundle = intent.getExtras(); // 获取意图携带的参数包 + if (bundle != null && bundle.containsKey(ACTION_STRING_NAME)) { + // 根据动作类型执行操作 + switch (bundle.getInt(ACTION_STRING_NAME, ACTION_INVALID)) { + case ACTION_START_SYNC: // 启动同步 + startSync(); + break; + case ACTION_CANCEL_SYNC: // 取消同步 + cancelSync(); + break; + default: // 无效动作,不处理 + break; + } + return START_STICKY; // 服务意外终止后自动重启 + } + return super.onStartCommand(intent, flags, startId); + } + + /** + * 系统内存不足时的回调方法 + * 释放资源,取消当前同步任务以避免内存溢出 + */ + @Override + public void onLowMemory() { + if (mSyncTask != null) { + mSyncTask.cancelSync(); // 内存不足时强制取消同步任务 + } + } + + /** + * 绑定服务时的回调方法(本服务不支持绑定) + * @param intent 绑定意图 + * @return 始终返回null(表示不支持绑定) + */ + public IBinder onBind(Intent intent) { + return null; + } + + /** + * 发送同步状态广播(通知外部当前同步状态) + * @param msg 当前同步进度的文本描述(如"正在初始化任务列表...") + */ + public void sendBroadcast(String msg) { + mSyncProgress = msg; // 更新全局进度信息 + Intent intent = new Intent(GTASK_SERVICE_BROADCAST_NAME); // 创建广播意图 + intent.putExtra(GTASK_SERVICE_BROADCAST_IS_SYNCING, mSyncTask != null); // 是否同步中(任务存在则为true) + intent.putExtra(GTASK_SERVICE_BROADCAST_PROGRESS_MSG, msg); // 传递进度消息 + sendBroadcast(intent); // 发送广播 + } + + /** + * 静态方法:外部调用启动同步服务 + * @param activity 启动服务的Activity上下文(用于设置GTaskManager的Activity引用) + */ + public static void startSync(Activity activity) { + GTaskManager.getInstance().setActivityContext(activity); // 设置GTaskManager的Activity(用于获取认证令牌) + Intent intent = new Intent(activity, GTaskSyncService.class); // 创建启动服务的意图 + intent.putExtra(GTaskSyncService.ACTION_STRING_NAME, GTaskSyncService.ACTION_START_SYNC); // 传递启动同步动作 + activity.startService(intent); // 启动服务 + } + + /** + * 静态方法:外部调用取消同步服务 + * @param context 上下文(用于启动服务) + */ + public static void cancelSync(Context context) { + Intent intent = new Intent(context, GTaskSyncService.class); // 创建启动服务的意图 + intent.putExtra(GTaskSyncService.ACTION_STRING_NAME, GTaskSyncService.ACTION_CANCEL_SYNC); // 传递取消同步动作 + context.startService(intent); // 启动服务(触发取消逻辑) + } + + /** + * 静态方法:检查当前是否正在同步 + * @return true表示同步中,false表示未同步 + */ + public static boolean isSyncing() { + return mSyncTask != null; // 通过任务实例是否存在判断 + } + + /** + * 静态方法:获取当前同步进度的文本描述 + * @return 进度消息字符串(如"正在同步笔记...") + */ + public static String getProgressString() { + return mSyncProgress; // 返回全局进度变量 + } +} \ No newline at end of file From 9c07757ab54447c7d1e95657e1db13ebd9a9f793 Mon Sep 17 00:00:00 2001 From: moneynewmoon <2075698549@qq.com> Date: Thu, 29 May 2025 15:33:17 +0800 Subject: [PATCH 3/7] Note --- Note.java | 357 +++++++++++++++++++++++++++++++++++++++++++ WorkingNote.java | 383 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 740 insertions(+) create mode 100644 Note.java create mode 100644 WorkingNote.java diff --git a/Note.java b/Note.java new file mode 100644 index 0000000..57c3d91 --- /dev/null +++ b/Note.java @@ -0,0 +1,357 @@ + +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * 版权声明:本文件由MiCode开源社区开发,遵循Apache License, Version 2.0协议; + * 您仅在遵守协议的前提下使用本文件,完整协议可通过以下链接获取: + * http://www.apache.org/licenses/LICENSE-2.0 + * 注:未书面明确要求时,本软件按"原样"提供,不附带任何明示或暗示的保证。 + */ + +package net.micode.notes.model; + +import android.content.ContentProviderOperation; +import android.content.ContentProviderResult; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.OperationApplicationException; +import android.net.Uri; +import android.os.RemoteException; +import android.util.Log; + +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.CallNote; +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.data.Notes.TextNote; + +import java.util.ArrayList; + +/** + * 笔记模型类 + * 负责管理笔记的创建、数据修改(文本/通话记录)以及与数据库的同步操作 + */ +public class Note { + // 存储笔记主表(note表)的差异值(待更新的字段) + private ContentValues mNoteDiffValues; + // 存储笔记关联数据(文本/通话记录等子表数据)的管理类实例 + private NoteData mNoteData; + private static final String TAG = "Note"; + + /** + * 静态方法:生成新笔记ID并插入数据库 + * @param context 上下文环境 + * @param folderId 笔记所属文件夹ID + * @return 新笔记的数据库ID(唯一标识) + */ + public static synchronized long getNewNoteId(Context context, long folderId) { + // 创建新笔记的初始值:创建时间、修改时间、类型、本地修改标志、父文件夹ID + ContentValues values = new ContentValues(); + long createdTime = System.currentTimeMillis(); + values.put(NoteColumns.CREATED_DATE, createdTime); // 设置创建时间 + values.put(NoteColumns.MODIFIED_DATE, createdTime); // 初始修改时间与创建时间相同 + values.put(NoteColumns.TYPE, Notes.TYPE_NOTE); // 笔记类型为普通笔记 + values.put(NoteColumns.LOCAL_MODIFIED, 1); // 标记为本地已修改(待同步) + values.put(NoteColumns.PARENT_ID, folderId); // 所属文件夹ID + + // 插入到note表,获取返回的Uri(包含新笔记ID) + Uri uri = context.getContentResolver().insert(Notes.CONTENT_NOTE_URI, values); + + long noteId = 0; + try { + // 从Uri路径中解析笔记ID(格式:content://.../notes/[noteId]) + noteId = Long.valueOf(uri.getPathSegments().get(1)); + } catch (NumberFormatException e) { + Log.e(TAG, "获取笔记ID失败 :" + e.toString()); + noteId = 0; + } + if (noteId == -1) { + throw new IllegalStateException("错误的笔记ID:" + noteId); + } + return noteId; + } + + /** + * 构造方法:初始化笔记差异值和数据管理类 + */ + public Note() { + mNoteDiffValues = new ContentValues(); // 初始化主表差异值容器 + mNoteData = new NoteData(); // 初始化子表数据管理实例 + } + + /** + * 设置笔记主表的属性值(如标题、提醒时间等) + * 每次调用会自动更新本地修改标志和修改时间 + * @param key 笔记主表字段名(如NoteColumns.TITLE) + * @param value 字段对应的值 + */ + public void setNoteValue(String key, String value) { + mNoteDiffValues.put(key, value); + // 标记本地修改(用于同步判断) + mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); + // 更新修改时间为当前系统时间 + mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); + } + + /** + * 设置文本类型子表数据(如笔记内容) + * @param key 文本数据字段名(如TextNote.CONTENT) + * @param value 字段对应的值 + */ + public void setTextData(String key, String value) { + mNoteData.setTextData(key, value); + } + + /** + * 设置文本子表的ID(用于更新已有文本数据) + * @param id 文本数据在data表中的ID(需大于0) + */ + public void setTextDataId(long id) { + mNoteData.setTextDataId(id); + } + + /** + * 获取当前文本子表的ID + * @return 文本数据在data表中的ID(0表示未创建) + */ + public long getTextDataId() { + return mNoteData.mTextDataId; + } + + /** + * 设置通话记录子表的ID(用于更新已有通话数据) + * @param id 通话数据在data表中的ID(需大于0) + */ + public void setCallDataId(long id) { + mNoteData.setCallDataId(id); + } + + /** + * 设置通话记录类型子表数据(如通话号码、时长等) + * @param key 通话数据字段名(如CallNote.NUMBER) + * @param value 字段对应的值 + */ + public void setCallData(String key, String value) { + mNoteData.setCallData(key, value); + } + + /** + * 判断笔记是否有本地修改(主表或子表数据被修改) + * @return true-有修改;false-无修改 + */ + public boolean isLocalModified() { + // 主表差异值非空 或 子表数据有修改时,标记为有本地修改 + return mNoteDiffValues.size() > 0 || mNoteData.isLocalModified(); + } + + /** + * 将笔记的修改同步到数据库(主表+子表) + * @param context 上下文环境 + * @param noteId 待同步的笔记ID + * @return 同步成功-true;失败-false + */ + public boolean syncNote(Context context, long noteId) { + if (noteId <= 0) { + throw new IllegalArgumentException("错误的笔记ID:" + noteId); + } + + // 无修改时直接返回成功 + if (!isLocalModified()) { + return true; + } + + /** + * 理论上,数据修改时应更新note表的LOCAL_MODIFIED和MODIFIED_DATE字段。 + * 为数据安全,即使note表更新失败,仍尝试同步子表数据 + */ + // 更新note表的差异值(如标题、修改时间等) + int updateCount = context.getContentResolver().update( + ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), // 目标笔记Uri + mNoteDiffValues, // 待更新的字段 + null, + null + ); + if (updateCount == 0) { + Log.e(TAG, "更新笔记主表失败(不应发生)"); + // 不返回,继续处理子表同步 + } + mNoteDiffValues.clear(); // 清空已处理的差异值 + + // 子表数据有修改时,同步到data表 + if (mNoteData.isLocalModified() + && (mNoteData.pushIntoContentResolver(context, noteId) == null)) { + return false; // 子表同步失败时返回false + } + + return true; + } + + /** + * 内部类:管理笔记的子表数据(文本/通话记录) + */ + private class NoteData { + private long mTextDataId; // 文本数据在data表中的ID(0表示未创建) + private ContentValues mTextDataValues; // 文本数据的差异值(待更新的字段) + private long mCallDataId; // 通话数据在data表中的ID(0表示未创建) + private ContentValues mCallDataValues; // 通话数据的差异值(待更新的字段) + private static final String TAG = "NoteData"; + + /** + * 构造方法:初始化子表数据容器 + */ + public NoteData() { + mTextDataValues = new ContentValues(); + mCallDataValues = new ContentValues(); + mTextDataId = 0; + mCallDataId = 0; + } + + /** + * 判断子表数据是否有本地修改 + * @return true-有修改;false-无修改 + */ + boolean isLocalModified() { + // 文本或通话数据差异值非空时,标记为有修改 + return mTextDataValues.size() > 0 || mCallDataValues.size() > 0; + } + + /** + * 设置文本数据ID(校验ID有效性) + * @param id 文本数据在data表中的ID(需>0) + */ + void setTextDataId(long id) { + if (id <= 0) { + throw new IllegalArgumentException("文本数据ID必须大于0"); + } + mTextDataId = id; + } + + /** + * 设置通话数据ID(校验ID有效性) + * @param id 通话数据在data表中的ID(需>0) + */ + void setCallDataId(long id) { + if (id <= 0) { + throw new IllegalArgumentException("通话数据ID必须大于0"); + } + mCallDataId = id; + } + + /** + * 设置通话数据字段值(自动触发笔记修改标志) + * @param key 通话数据字段名(如CallNote.DURATION) + * @param value 字段值 + */ + void setCallData(String key, String value) { + mCallDataValues.put(key, value); + // 同步更新主表的本地修改标志和修改时间 + mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); + mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); + } + + /** + * 设置文本数据字段值(自动触发笔记修改标志) + * @param key 文本数据字段名(如TextNote.CONTENT) + * @param value 字段值 + */ + void setTextData(String key, String value) { + mTextDataValues.put(key, value); + // 同步更新主表的本地修改标志和修改时间 + mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); + mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); + } + + /** + * 将子表数据(文本/通话)插入或更新到数据库 + * @param context 上下文环境 + * @param noteId 关联的笔记ID + * @return 操作成功返回笔记Uri;失败返回null + */ + Uri pushIntoContentResolver(Context context, long noteId) { + // 校验笔记ID有效性 + if (noteId <= 0) { + throw new IllegalArgumentException("错误的笔记ID:" + noteId); + } + + // 批量操作容器(用于同时执行多个ContentProvider操作) + ArrayList operationList = new ArrayList<>(); + ContentProviderOperation.Builder builder = null; + + // 处理文本数据 + if (mTextDataValues.size() > 0) { + mTextDataValues.put(DataColumns.NOTE_ID, noteId); // 关联笔记ID + + if (mTextDataId == 0) { // ID为0表示新增文本数据 + // 设置MIME类型为文本笔记 + mTextDataValues.put(DataColumns.MIME_TYPE, TextNote.CONTENT_ITEM_TYPE); + // 插入到data表,获取新数据的Uri + Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI, mTextDataValues); + try { + // 解析新数据的ID并保存 + setTextDataId(Long.valueOf(uri.getPathSegments().get(1))); + } catch (NumberFormatException e) { + Log.e(TAG, "插入新文本数据失败(笔记ID:" + noteId + ")"); + mTextDataValues.clear(); // 清空无效数据 + return null; + } + } else { // ID非0表示更新已有文本数据 + // 构建更新操作(指定data表Uri和待更新值) + builder = ContentProviderOperation.newUpdate( + ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, mTextDataId)); + builder.withValues(mTextDataValues); + operationList.add(builder.build()); // 添加到批量操作列表 + } + mTextDataValues.clear(); // 清空已处理的差异值 + } + + // 处理通话数据(逻辑与文本数据类似) + if (mCallDataValues.size() > 0) { + mCallDataValues.put(DataColumns.NOTE_ID, noteId); // 关联笔记ID + + if (mCallDataId == 0) { // ID为0表示新增通话数据 + // 设置MIME类型为通话记录 + mCallDataValues.put(DataColumns.MIME_TYPE, CallNote.CONTENT_ITEM_TYPE); + // 插入到data表,获取新数据的Uri + Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI, mCallDataValues); + try { + // 解析新数据的ID并保存 + setCallDataId(Long.valueOf(uri.getPathSegments().get(1))); + } catch (NumberFormatException e) { + Log.e(TAG, "插入新通话数据失败(笔记ID:" + noteId + ")"); + mCallDataValues.clear(); // 清空无效数据 + return null; + } + } else { // ID非0表示更新已有通话数据 + // 构建更新操作(指定data表Uri和待更新值) + builder = ContentProviderOperation.newUpdate( + ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, mCallDataId)); + builder.withValues(mCallDataValues); + operationList.add(builder.build()); // 添加到批量操作列表 + } + mCallDataValues.clear(); // 清空已处理的差异值 + } + + // 执行批量操作(仅当有操作需要执行时) + if (operationList.size() > 0) { + try { + ContentProviderResult[] results = context.getContentResolver().applyBatch( + Notes.AUTHORITY, // 内容提供者的AUTHORITY + operationList // 批量操作列表 + ); + // 操作成功时返回笔记Uri;否则返回null + return (results == null || results.length == 0 || results[0] == null) + ? null + : ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId); + } catch (RemoteException e) { + Log.e(TAG, "远程调用异常: " + e.toString() + " - " + e.getMessage()); + return null; + } catch (OperationApplicationException e) { + Log.e(TAG, "操作应用异常: " + e.toString() + " - " + e.getMessage()); + return null; + } + } + return null; + } + } +} \ No newline at end of file diff --git a/WorkingNote.java b/WorkingNote.java new file mode 100644 index 0000000..af9b210 --- /dev/null +++ b/WorkingNote.java @@ -0,0 +1,383 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.model; + +import android.appwidget.AppWidgetManager; +import android.content.ContentUris; +import android.content.Context; +import android.database.Cursor; +import android.text.TextUtils; +import android.util.Log; + +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.CallNote; +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.DataConstants; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.data.Notes.TextNote; +import net.micode.notes.tool.ResourceParser.NoteBgResources; + +// 工作笔记类,用于处理笔记的创建、加载、保存和属性管理 +public class WorkingNote { + // 笔记对象 + private Note mNote; + // 笔记ID + private long mNoteId; + // 笔记内容 + private String mContent; + // 笔记模式(普通模式/清单模式) + private int mMode; + + private long mAlertDate; // 提醒日期 + private long mModifiedDate; // 最后修改日期 + private int mBgColorId; // 背景颜色ID + private int mWidgetId; // 关联的小部件ID + private int mWidgetType; // 小部件类型 + private long mFolderId; // 所属文件夹ID + private Context mContext; // 上下文对象 + + private static final String TAG = "WorkingNote"; // 日志标签 + + private boolean mIsDeleted; // 标记是否已删除 + + // 笔记设置变更监听器 + private NoteSettingChangedListener mNoteSettingStatusListener; + + // 数据表查询列 + public static final String[] DATA_PROJECTION = new String[] { + DataColumns.ID, + DataColumns.CONTENT, + DataColumns.MIME_TYPE, + DataColumns.DATA1, + DataColumns.DATA2, + DataColumns.DATA3, + DataColumns.DATA4, + }; + + // 笔记表查询列 + public static final String[] NOTE_PROJECTION = new String[] { + NoteColumns.PARENT_ID, + NoteColumns.ALERTED_DATE, + NoteColumns.BG_COLOR_ID, + NoteColumns.WIDGET_ID, + NoteColumns.WIDGET_TYPE, + NoteColumns.MODIFIED_DATE + }; + + // 数据表列索引常量 + private static final int DATA_ID_COLUMN = 0; + private static final int DATA_CONTENT_COLUMN = 1; + private static final int DATA_MIME_TYPE_COLUMN = 2; + private static final int DATA_MODE_COLUMN = 3; + + // 笔记表列索引常量 + private static final int NOTE_PARENT_ID_COLUMN = 0; + private static final int NOTE_ALERTED_DATE_COLUMN = 1; + private static final int NOTE_BG_COLOR_ID_COLUMN = 2; + private static final int NOTE_WIDGET_ID_COLUMN = 3; + private static final int NOTE_WIDGET_TYPE_COLUMN = 4; + private static final int NOTE_MODIFIED_DATE_COLUMN = 5; + + // 构造函数:创建新笔记 + private WorkingNote(Context context, long folderId) { + mContext = context; + mAlertDate = 0; // 初始无提醒 + mModifiedDate = System.currentTimeMillis(); // 设置当前时间为修改时间 + mFolderId = folderId; // 设置文件夹ID + mNote = new Note(); // 创建空的Note对象 + mNoteId = 0; // 新笔记ID为0 + mIsDeleted = false; // 未删除状态 + mMode = 0; // 默认模式 + mWidgetType = Notes.TYPE_WIDGET_INVALIDE; // 无效小部件类型 + } + + // 构造函数:加载现有笔记 + private WorkingNote(Context context, long noteId, long folderId) { + mContext = context; + mNoteId = noteId; // 设置笔记ID + mFolderId = folderId; // 设置文件夹ID + mIsDeleted = false; // 未删除状态 + mNote = new Note(); // 创建Note对象 + loadNote(); // 加载笔记数据 + } + + // 从数据库加载笔记基本属性 + private void loadNote() { + // 通过内容提供者查询笔记 + Cursor cursor = mContext.getContentResolver().query( + ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mNoteId), NOTE_PROJECTION, null, + null, null); + + if (cursor != null) { + if (cursor.moveToFirst()) { + // 从游标读取数据 + mFolderId = cursor.getLong(NOTE_PARENT_ID_COLUMN); + mBgColorId = cursor.getInt(NOTE_BG_COLOR_ID_COLUMN); + mWidgetId = cursor.getInt(NOTE_WIDGET_ID_COLUMN); + mWidgetType = cursor.getInt(NOTE_WIDGET_TYPE_COLUMN); + mAlertDate = cursor.getLong(NOTE_ALERTED_DATE_COLUMN); + mModifiedDate = cursor.getLong(NOTE_MODIFIED_DATE_COLUMN); + } + cursor.close(); + } else { + Log.e(TAG, "No note with id:" + mNoteId); + throw new IllegalArgumentException("Unable to find note with id " + mNoteId); + } + loadNoteData(); // 加载笔记内容数据 + } + + // 加载笔记内容数据 + private void loadNoteData() { + // 查询笔记关联的数据表 + Cursor cursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI, DATA_PROJECTION, + DataColumns.NOTE_ID + "=?", new String[] { + String.valueOf(mNoteId) + }, null); + + if (cursor != null) { + if (cursor.moveToFirst()) { + do { + String type = cursor.getString(DATA_MIME_TYPE_COLUMN); + if (DataConstants.NOTE.equals(type)) { + // 普通笔记数据 + mContent = cursor.getString(DATA_CONTENT_COLUMN); + mMode = cursor.getInt(DATA_MODE_COLUMN); + mNote.setTextDataId(cursor.getLong(DATA_ID_COLUMN)); + } else if (DataConstants.CALL_NOTE.equals(type)) { + // 通话笔记数据 + mNote.setCallDataId(cursor.getLong(DATA_ID_COLUMN)); + } else { + Log.d(TAG, "Wrong note type with type:" + type); + } + } while (cursor.moveToNext()); + } + cursor.close(); + } else { + Log.e(TAG, "No data with id:" + mNoteId); + throw new IllegalArgumentException("Unable to find note's data with id " + mNoteId); + } + } + + // 创建空笔记的静态方法 + public static WorkingNote createEmptyNote(Context context, long folderId, int widgetId, + int widgetType, int defaultBgColorId) { + WorkingNote note = new WorkingNote(context, folderId); + note.setBgColorId(defaultBgColorId); // 设置默认背景色 + note.setWidgetId(widgetId); // 设置小部件ID + note.setWidgetType(widgetType); // 设置小部件类型 + return note; + } + + // 加载现有笔记的静态方法 + public static WorkingNote load(Context context, long id) { + return new WorkingNote(context, id, 0); + } + + // 保存笔记到数据库 + public synchronized boolean saveNote() { + if (isWorthSaving()) { // 检查是否需要保存 + if (!existInDatabase()) { + // 为新笔记生成ID + if ((mNoteId = Note.getNewNoteId(mContext, mFolderId)) == 0) { + Log.e(TAG, "Create new note fail with id:" + mNoteId); + return false; + } + } + + // 同步笔记数据到数据库 + mNote.syncNote(mContext, mNoteId); + + // 如果有关联小部件,通知更新 + if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID + && mWidgetType != Notes.TYPE_WIDGET_INVALIDE + && mNoteSettingStatusListener != null) { + mNoteSettingStatusListener.onWidgetChanged(); + } + return true; + } else { + return false; + } + } + + // 检查笔记是否已存在数据库中 + public boolean existInDatabase() { + return mNoteId > 0; + } + + // 判断笔记是否需要保存 + private boolean isWorthSaving() { + if (mIsDeleted || // 已删除 + (!existInDatabase() && TextUtils.isEmpty(mContent)) || // 新笔记且内容为空 + (existInDatabase() && !mNote.isLocalModified())) { // 已有笔记但未修改 + return false; + } else { + return true; + } + } + + // 设置笔记状态变更监听器 + public void setOnSettingStatusChangedListener(NoteSettingChangedListener l) { + mNoteSettingStatusListener = l; + } + + // 设置提醒日期 + public void setAlertDate(long date, boolean set) { + if (date != mAlertDate) { + mAlertDate = date; + mNote.setNoteValue(NoteColumns.ALERTED_DATE, String.valueOf(mAlertDate)); + } + // 通知监听器 + if (mNoteSettingStatusListener != null) { + mNoteSettingStatusListener.onClockAlertChanged(date, set); + } + } + + // 标记删除状态 + public void markDeleted(boolean mark) { + mIsDeleted = mark; + // 如果有关联小部件,通知更新 + if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID + && mWidgetType != Notes.TYPE_WIDGET_INVALIDE && mNoteSettingStatusListener != null) { + mNoteSettingStatusListener.onWidgetChanged(); + } + } + + // 设置背景颜色ID + public void setBgColorId(int id) { + if (id != mBgColorId) { + mBgColorId = id; + if (mNoteSettingStatusListener != null) { + mNoteSettingStatusListener.onBackgroundColorChanged(); // 通知颜色变更 + } + mNote.setNoteValue(NoteColumns.BG_COLOR_ID, String.valueOf(id)); // 更新Note对象 + } + } + + // 设置清单模式 + public void setCheckListMode(int mode) { + if (mMode != mode) { + if (mNoteSettingStatusListener != null) { + // 通知模式变更 + mNoteSettingStatusListener.onCheckListModeChanged(mMode, mode); + } + mMode = mode; + // 更新数据表 + mNote.setTextData(TextNote.MODE, String.valueOf(mMode)); + } + } + + // 设置小部件类型 + public void setWidgetType(int type) { + if (type != mWidgetType) { + mWidgetType = type; + mNote.setNoteValue(NoteColumns.WIDGET_TYPE, String.valueOf(mWidgetType)); + } + } + + // 设置小部件ID + public void setWidgetId(int id) { + if (id != mWidgetId) { + mWidgetId = id; + mNote.setNoteValue(NoteColumns.WIDGET_ID, String.valueOf(mWidgetId)); + } + } + + // 设置工作文本内容 + public void setWorkingText(String text) { + if (!TextUtils.equals(mContent, text)) { + mContent = text; + mNote.setTextData(DataColumns.CONTENT, mContent); // 更新内容 + } + } + + // 转换为通话笔记 + public void convertToCallNote(String phoneNumber, long callDate) { + mNote.setCallData(CallNote.CALL_DATE, String.valueOf(callDate)); // 设置通话日期 + mNote.setCallData(CallNote.PHONE_NUMBER, phoneNumber); // 设置电话号码 + // 移动到通话记录文件夹 + mNote.setNoteValue(NoteColumns.PARENT_ID, String.valueOf(Notes.ID_CALL_RECORD_FOLDER)); + } + + // 检查是否有提醒 + public boolean hasClockAlert() { + return (mAlertDate > 0 ? true : false); + } + + // ------------ 获取属性方法 ------------ // + public String getContent() { + return mContent; + } + + public long getAlertDate() { + return mAlertDate; + } + + public long getModifiedDate() { + return mModifiedDate; + } + + public int getBgColorResId() { + return NoteBgResources.getNoteBgResource(mBgColorId); + } + + public int getBgColorId() { + return mBgColorId; + } + + public int getTitleBgResId() { + return NoteBgResources.getNoteTitleBgResource(mBgColorId); + } + + public int getCheckListMode() { + return mMode; + } + + public long getNoteId() { + return mNoteId; + } + + public long getFolderId() { + return mFolderId; + } + + public int getWidgetId() { + return mWidgetId; + } + + public int getWidgetType() { + return mWidgetType; + } + + // 笔记设置变更监听器接口 + public interface NoteSettingChangedListener { + // 背景颜色变更回调 + void onBackgroundColorChanged(); + + // 提醒设置变更回调 + void onClockAlertChanged(long date, boolean set); + + // 小部件变更回调 + void onWidgetChanged(); + + /** + * 清单模式切换回调 + * @param oldMode 切换前的模式 + * @param newMode 切换后的模式 + */ + void onCheckListModeChanged(int oldMode, int newMode); + } +} \ No newline at end of file From acef57cbc010f21035403cae22cbabf3c327a8cc Mon Sep 17 00:00:00 2001 From: moneynewmoon <2075698549@qq.com> Date: Thu, 29 May 2025 15:41:15 +0800 Subject: [PATCH 4/7] tool --- BackupUtils.java | 419 ++++++++++++++++++++++++++++++++++++++++++ DataUtils.java | 407 ++++++++++++++++++++++++++++++++++++++++ GTaskStringUtils.java | 116 ++++++++++++ ResourceParser.java | 223 ++++++++++++++++++++++ 4 files changed, 1165 insertions(+) create mode 100644 BackupUtils.java create mode 100644 DataUtils.java create mode 100644 GTaskStringUtils.java create mode 100644 ResourceParser.java diff --git a/BackupUtils.java b/BackupUtils.java new file mode 100644 index 0000000..2abe310 --- /dev/null +++ b/BackupUtils.java @@ -0,0 +1,419 @@ + +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * 版权声明:本文件由MiCode开源社区开发,遵循Apache License, Version 2.0协议; + * 您仅在遵守协议的前提下使用本文件,完整协议可通过以下链接获取: + * http://www.apache.org/licenses/LICENSE-2.0 + * 注:未书面明确要求时,本软件按"原样"提供,不附带任何明示或暗示的保证。 + */ + +package net.micode.notes.tool; + +import android.content.Context; +import android.database.Cursor; +import android.os.Environment; +import android.text.TextUtils; +import android.text.format.DateFormat; +import android.util.Log; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.DataConstants; +import net.micode.notes.data.Notes.NoteColumns; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintStream; + +/** + * 笔记备份工具类 + * 提供将笔记数据导出为文本文件的功能,支持检查SD卡状态、生成备份文件、格式化导出内容等操作 + */ +public class BackupUtils { + private static final String TAG = "BackupUtils"; + // 单例实例(保证全局唯一) + private static BackupUtils sInstance; + + /** + * 获取备份工具单例实例 + * @param context 上下文环境 + * @return BackupUtils单例对象 + */ + public static synchronized BackupUtils getInstance(Context context) { + if (sInstance == null) { + sInstance = new BackupUtils(context); + } + return sInstance; + } + + /** + * 备份/恢复操作的状态常量(标识当前操作状态) + */ + // SD卡未挂载状态 + public static final int STATE_SD_CARD_UNMOUONTED = 0; + // 备份文件不存在状态 + public static final int STATE_BACKUP_FILE_NOT_EXIST = 1; + // 数据格式损坏(可能被其他程序修改) + public static final int STATE_DATA_DESTROIED = 2; + // 系统运行时异常(导致备份/恢复失败) + public static final int STATE_SYSTEM_ERROR = 3; + // 备份/恢复成功状态 + public static final int STATE_SUCCESS = 4; + + // 文本导出功能模块实例 + private TextExport mTextExport; + + /** + * 私有构造方法(限制外部实例化) + * @param context 上下文环境 + */ + private BackupUtils(Context context) { + mTextExport = new TextExport(context); + } + + /** + * 检查外部存储(SD卡)是否可用 + * @return true-可用;false-不可用 + */ + private static boolean externalStorageAvailable() { + // 判断SD卡状态是否为"已挂载" + return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()); + } + + /** + * 触发文本导出操作 + * @return 导出状态(使用STATE_*常量) + */ + public int exportToText() { + return mTextExport.exportToText(); + } + + /** + * 获取导出的文本文件名 + * @return 文件名(如"NotesBackup_20231025.txt") + */ + public String getExportedTextFileName() { + return mTextExport.mFileName; + } + + /** + * 获取导出文本文件的存储目录 + * @return 文件目录路径(如"/sdcard/NotesBackup/") + */ + public String getExportedTextFileDir() { + return mTextExport.mFileDirectory; + } + + /** + * 内部类:文本导出功能实现 + */ + private static class TextExport { + // 笔记查询投影(指定需要查询的note表字段) + private static final String[] NOTE_PROJECTION = { + NoteColumns.ID, // 笔记ID(索引0) + NoteColumns.MODIFIED_DATE, // 最后修改时间(索引1) + NoteColumns.SNIPPET, // 笔记摘要(索引2) + NoteColumns.TYPE // 笔记类型(索引3) + }; + private static final int NOTE_COLUMN_ID = 0; // 笔记ID列索引 + private static final int NOTE_COLUMN_MODIFIED_DATE = 1; // 修改时间列索引 + private static final int NOTE_COLUMN_SNIPPET = 2; // 摘要列索引 + + // 笔记关联数据查询投影(指定需要查询的data表字段) + private static final String[] DATA_PROJECTION = { + DataColumns.CONTENT, // 内容(索引0) + DataColumns.MIME_TYPE, // MIME类型(索引1) + DataColumns.DATA1, // 数据字段1(通话记录日期)(索引2) + DataColumns.DATA2, // 数据字段2(预留)(索引3) + DataColumns.DATA3, // 数据字段3(预留)(索引4) + DataColumns.DATA4, // 数据字段4(电话号码)(索引5) + }; + private static final int DATA_COLUMN_CONTENT = 0; // 内容列索引 + private static final int DATA_COLUMN_MIME_TYPE = 1; // MIME类型列索引 + private static final int DATA_COLUMN_CALL_DATE = 2; // 通话日期列索引(DATA1) + private static final int DATA_COLUMN_PHONE_NUMBER = 4; // 电话号码列索引(DATA4) + + // 导出文本的格式数组(从资源文件读取,包含文件夹名、笔记日期、笔记内容的格式) + private final String [] TEXT_FORMAT; + private static final int FORMAT_FOLDER_NAME = 0; // 文件夹名格式索引 + private static final int FORMAT_NOTE_DATE = 1; // 笔记日期格式索引 + private static final int FORMAT_NOTE_CONTENT = 2; // 笔记内容格式索引 + + private Context mContext; // 上下文环境 + private String mFileName; // 导出的文件名 + private String mFileDirectory; // 导出文件的存储目录 + + /** + * 构造方法:初始化文本导出模块 + * @param context 上下文环境 + */ + public TextExport(Context context) { + // 从资源文件获取导出格式数组(如R.array.format_for_exported_note) + TEXT_FORMAT = context.getResources().getStringArray(R.array.format_for_exported_note); + mContext = context; + mFileName = ""; + mFileDirectory = ""; + } + + /** + * 获取导出格式字符串(根据索引) + * @param id 格式数组索引(使用FORMAT_*常量) + * @return 格式字符串(如"文件夹: %s") + */ + private String getFormat(int id) { + return TEXT_FORMAT[id]; + } + + /** + * 将指定文件夹下的笔记导出到文本输出流 + * @param folderId 文件夹ID + * @param ps 文本输出流(用于写入文件) + */ + private void exportFolderToText(String folderId, PrintStream ps) { + // 查询属于该文件夹的所有笔记(note表) + Cursor notesCursor = mContext.getContentResolver().query( + Notes.CONTENT_NOTE_URI, // note表的内容Uri + NOTE_PROJECTION, // 需要查询的字段 + NoteColumns.PARENT_ID + "=?", // 查询条件:父ID等于文件夹ID + new String[] { folderId }, // 查询参数 + null // 排序方式(默认) + ); + + if (notesCursor != null) { + if (notesCursor.moveToFirst()) { // 移动到首条记录 + do { + // 打印笔记的最后修改日期(使用指定格式) + ps.println(String.format(getFormat(FORMAT_NOTE_DATE), + DateFormat.format( + mContext.getString(R.string.format_datetime_mdhm), // 日期时间格式(如"10月25日 15:30") + notesCursor.getLong(NOTE_COLUMN_MODIFIED_DATE) // 笔记修改时间戳 + ))); + // 获取当前笔记ID,导出该笔记的详细内容 + String noteId = notesCursor.getString(NOTE_COLUMN_ID); + exportNoteToText(noteId, ps); + } while (notesCursor.moveToNext()); // 遍历所有笔记 + } + notesCursor.close(); // 关闭游标释放资源 + } + } + + /** + * 将指定笔记的详细内容导出到文本输出流 + * @param noteId 笔记ID + * @param ps 文本输出流(用于写入文件) + */ + private void exportNoteToText(String noteId, PrintStream ps) { + // 查询该笔记关联的data表数据(如文本内容、通话记录等) + Cursor dataCursor = mContext.getContentResolver().query( + Notes.CONTENT_DATA_URI, // data表的内容Uri + DATA_PROJECTION, // 需要查询的字段 + DataColumns.NOTE_ID + "=?", // 查询条件:note_id等于当前笔记ID + new String[] { noteId }, // 查询参数 + null // 排序方式(默认) + ); + + if (dataCursor != null) { + if (dataCursor.moveToFirst()) { // 移动到首条记录 + do { + String mimeType = dataCursor.getString(DATA_COLUMN_MIME_TYPE); // 获取数据类型 + if (DataConstants.CALL_NOTE.equals(mimeType)) { // 通话记录类型 + // 提取通话记录信息 + String phoneNumber = dataCursor.getString(DATA_COLUMN_PHONE_NUMBER); // 电话号码 + long callDate = dataCursor.getLong(DATA_COLUMN_CALL_DATE); // 通话时间戳 + String location = dataCursor.getString(DATA_COLUMN_CONTENT); // 通话附件位置(如录音路径) + + // 打印电话号码(非空时) + if (!TextUtils.isEmpty(phoneNumber)) { + ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT), phoneNumber)); + } + // 打印通话时间(格式化后) + ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT), + DateFormat.format(mContext.getString(R.string.format_datetime_mdhm), callDate))); + // 打印通话附件位置(非空时) + if (!TextUtils.isEmpty(location)) { + ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT), location)); + } + } else if (DataConstants.NOTE.equals(mimeType)) { // 普通文本笔记类型 + String content = dataCursor.getString(DATA_COLUMN_CONTENT); // 笔记内容 + if (!TextUtils.isEmpty(content)) { // 内容非空时打印 + ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT), content)); + } + } + } while (dataCursor.moveToNext()); // 遍历所有关联数据 + } + dataCursor.close(); // 关闭游标释放资源 + } + // 在笔记之间插入换行分隔符 + try { + ps.write(new byte[] { Character.LINE_SEPARATOR, Character.LETTER_NUMBER }); + } catch (IOException e) { + Log.e(TAG, "写入换行符失败: " + e.toString()); + } + } + + /** + * 执行文本导出的核心方法(用户可调用的导出入口) + * @return 导出状态(使用STATE_*常量) + */ + public int exportToText() { + // 检查SD卡是否已挂载 + if (!externalStorageAvailable()) { + Log.d(TAG, "SD卡未挂载"); + return STATE_SD_CARD_UNMOUONTED; + } + + // 获取文本输出流(指向生成的备份文件) + PrintStream ps = getExportToTextPrintStream(); + if (ps == null) { + Log.e(TAG, "获取输出流失败"); + return STATE_SYSTEM_ERROR; + } + + // 第一步:导出文件夹及其下的笔记 + // 查询所有有效文件夹(普通文件夹或通话记录文件夹) + Cursor folderCursor = mContext.getContentResolver().query( + Notes.CONTENT_NOTE_URI, // note表的内容Uri + NOTE_PROJECTION, // 需要查询的字段 + "(" + NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER + " AND " // 类型为文件夹 + + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER + ") OR " // 非回收站文件夹 + + NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER, // 或通话记录文件夹 + null, // 查询参数(无) + null // 排序方式(默认) + ); + + if (folderCursor != null) { + if (folderCursor.moveToFirst()) { // 移动到首条记录 + do { + // 获取文件夹名称(通话记录文件夹使用固定名称,其他文件夹使用摘要) + String folderName = ""; + long folderId = folderCursor.getLong(NOTE_COLUMN_ID); + if (folderId == Notes.ID_CALL_RECORD_FOLDER) { + folderName = mContext.getString(R.string.call_record_folder_name); // 通话记录文件夹名称(如"通话录音") + } else { + folderName = folderCursor.getString(NOTE_COLUMN_SNIPPET); // 普通文件夹名称(摘要字段) + } + // 打印文件夹名称(非空时) + if (!TextUtils.isEmpty(folderName)) { + ps.println(String.format(getFormat(FORMAT_FOLDER_NAME), folderName)); + } + // 导出该文件夹下的所有笔记 + exportFolderToText(String.valueOf(folderId), ps); + } while (folderCursor.moveToNext()); // 遍历所有文件夹 + } + folderCursor.close(); // 关闭游标释放资源 + } + + // 第二步:导出根目录下的笔记(父ID为0的普通笔记) + Cursor noteCursor = mContext.getContentResolver().query( + Notes.CONTENT_NOTE_URI, // note表的内容Uri + NOTE_PROJECTION, // 需要查询的字段 + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE + " AND " // 类型为普通笔记 + + NoteColumns.PARENT_ID + "=0", // 父ID为0(根目录) + null, // 查询参数(无) + null // 排序方式(默认) + ); + + if (noteCursor != null) { + if (noteCursor.moveToFirst()) { // 移动到首条记录 + do { + // 打印笔记的最后修改日期 + ps.println(String.format(getFormat(FORMAT_NOTE_DATE), + DateFormat.format( + mContext.getString(R.string.format_datetime_mdhm), // 日期时间格式 + noteCursor.getLong(NOTE_COLUMN_MODIFIED_DATE) // 笔记修改时间戳 + ))); + // 获取当前笔记ID,导出该笔记的详细内容 + String noteId = noteCursor.getString(NOTE_COLUMN_ID); + exportNoteToText(noteId, ps); + } while (noteCursor.moveToNext()); // 遍历所有根目录笔记 + } + noteCursor.close(); // 关闭游标释放资源 + } + + ps.close(); // 关闭输出流 + return STATE_SUCCESS; // 返回导出成功状态 + } + + /** + * 获取指向导出文件的打印流(用于写入文本内容) + * @return PrintStream输出流;失败返回null + */ + private PrintStream getExportToTextPrintStream() { + // 生成SD卡上的导出文件(路径和名称由资源文件定义) + File file = generateFileMountedOnSDcard( + mContext, + R.string.file_path, // 文件存储目录的资源ID(如"/NotesBackup/") + R.string.file_name_txt_format // 文件名格式的资源ID(如"NotesBackup_%s.txt") + ); + if (file == null) { + Log.e(TAG, "创建导出文件失败"); + return null; + } + // 记录文件名和存储目录 + mFileName = file.getName(); + mFileDirectory = mContext.getString(R.string.file_path); + + // 创建文件输出流 + PrintStream ps = null; + try { + FileOutputStream fos = new FileOutputStream(file); + ps = new PrintStream(fos); // 包装为打印流(支持格式化输出) + } catch (FileNotFoundException e) { + e.printStackTrace(); + return null; + } catch (NullPointerException e) { + e.printStackTrace(); + return null; + } + return ps; + } + } + + /** + * 生成SD卡上的导出文件(用于存储备份数据) + * @param context 上下文环境 + * @param filePathResId 存储目录的资源ID(如R.string.file_path) + * @param fileNameFormatResId 文件名格式的资源ID(如R.string.file_name_txt_format) + * @return 生成的File对象;失败返回null + */ + private static File generateFileMountedOnSDcard(Context context, int filePathResId, int fileNameFormatResId) { + // 构建文件路径:SD卡根目录 + 自定义目录 + 带时间戳的文件名 + StringBuilder sb = new StringBuilder(); + sb.append(Environment.getExternalStorageDirectory()); // SD卡根目录(如"/storage/emulated/0") + sb.append(context.getString(filePathResId)); // 追加自定义目录(如"/NotesBackup/") + File filedir = new File(sb.toString()); // 目标目录对象 + + // 构建完整文件路径(目录 + 文件名) + sb.append(context.getString( + fileNameFormatResId, // 文件名格式(如"NotesBackup_%s.txt") + DateFormat.format( + context.getString(R.string.format_date_ymd), // 日期格式(如"20231025") + System.currentTimeMillis() // 当前时间戳 + ) + )); + File file = new File(sb.toString()); // 目标文件对象 + + try { + // 创建目录(若不存在) + if (!filedir.exists()) { + filedir.mkdir(); // mkdir()仅创建单层目录;如需多层用mkdirs() + } + // 创建文件(若不存在) + if (!file.exists()) { + file.createNewFile(); + } + return file; + } catch (SecurityException e) { + // 权限不足异常(如未获取SD卡写入权限) + e.printStackTrace(); + } catch (IOException e) { + // 文件创建失败异常 + e.printStackTrace(); + } + return null; + } +} \ No newline at end of file diff --git a/DataUtils.java b/DataUtils.java new file mode 100644 index 0000000..dceffc6 --- /dev/null +++ b/DataUtils.java @@ -0,0 +1,407 @@ + +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * 版权声明:本文件由MiCode开源社区开发,遵循Apache License, Version 2.0协议; + * 您仅在遵守协议的前提下使用本文件,完整协议可通过以下链接获取: + * http://www.apache.org/licenses/LICENSE-2.0 + * 注:未书面明确要求时,本软件按"原样"提供,不附带任何明示或暗示的保证。 + */ + +package net.micode.notes.tool; + +import android.content.ContentProviderOperation; +import android.content.ContentProviderResult; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.OperationApplicationException; +import android.database.Cursor; +import android.os.RemoteException; +import android.util.Log; + +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.CallNote; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute; + +import java.util.ArrayList; +import java.util.HashSet; + +/** + * 数据操作工具类 + * 提供笔记数据的批量删除、移动、存在性检查、文件夹管理等功能,封装数据库操作细节 + */ +public class DataUtils { + public static final String TAG = "DataUtils"; + + /** + * 批量删除笔记(支持多条记录一次性删除) + * @param resolver ContentResolver实例(用于访问内容提供者) + * @param ids 待删除的笔记ID集合(HashSet类型) + * @return 删除成功返回true,失败返回false + */ + public static boolean batchDeleteNotes(ContentResolver resolver, HashSet ids) { + if (ids == null) { // 空集合校验 + Log.d(TAG, "待删除的笔记ID集合为空"); + return true; + } + if (ids.size() == 0) { // 无有效ID校验 + Log.d(TAG, "笔记ID集合中无元素"); + return true; + } + + // 构建批量操作列表(使用ContentProviderOperation提高效率) + ArrayList operationList = new ArrayList<>(); + for (long id : ids) { + if (id == Notes.ID_ROOT_FOLDER) { // 系统根文件夹保护逻辑 + Log.e(TAG, "禁止删除系统根文件夹"); + continue; + } + // 构建删除操作(针对note表中指定ID的记录) + ContentProviderOperation.Builder builder = ContentProviderOperation + .newDelete(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id)); + operationList.add(builder.build()); + } + + try { + // 执行批量删除操作(通过ContentResolver提交) + ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operationList); + // 结果校验(任意操作失败则整体失败) + if (results == null || results.length == 0 || results[0] == null) { + Log.d(TAG, "笔记删除失败,ID集合:" + ids.toString()); + return false; + } + return true; + } catch (RemoteException e) { // 跨进程调用异常(如内容提供者崩溃) + Log.e(TAG, "远程调用异常: " + e.toString() + " - " + e.getMessage()); + } catch (OperationApplicationException e) { // 操作应用异常(如数据冲突) + Log.e(TAG, "操作应用异常: " + e.toString() + " - " + e.getMessage()); + } + return false; + } + + /** + * 将单个笔记移动到目标文件夹 + * @param resolver ContentResolver实例 + * @param id 待移动的笔记ID + * @param srcFolderId 原文件夹ID + * @param desFolderId 目标文件夹ID + */ + public static void moveNoteToFoler(ContentResolver resolver, long id, long srcFolderId, long desFolderId) { + ContentValues values = new ContentValues(); + values.put(NoteColumns.PARENT_ID, desFolderId); // 更新父文件夹ID为目标文件夹 + values.put(NoteColumns.ORIGIN_PARENT_ID, srcFolderId); // 记录原始父文件夹ID + values.put(NoteColumns.LOCAL_MODIFIED, 1); // 标记本地修改(用于同步) + // 执行更新操作(针对note表中指定ID的记录) + resolver.update(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id), values, null, null); + } + + /** + * 批量移动笔记到目标文件夹 + * @param resolver ContentResolver实例 + * @param ids 待移动的笔记ID集合 + * @param folderId 目标文件夹ID + * @return 移动成功返回true,失败返回false + */ + public static boolean batchMoveToFolder(ContentResolver resolver, HashSet ids, long folderId) { + if (ids == null) { // 空集合校验 + Log.d(TAG, "待移动的笔记ID集合为空"); + return true; + } + + // 构建批量操作列表(使用ContentProviderOperation提高效率) + ArrayList operationList = new ArrayList<>(); + for (long id : ids) { + // 构建更新操作(设置父文件夹ID并标记本地修改) + ContentProviderOperation.Builder builder = ContentProviderOperation + .newUpdate(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id)); + builder.withValue(NoteColumns.PARENT_ID, folderId); // 目标文件夹ID + builder.withValue(NoteColumns.LOCAL_MODIFIED, 1); // 标记本地修改 + operationList.add(builder.build()); + } + + try { + // 执行批量更新操作 + ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operationList); + // 结果校验 + if (results == null || results.length == 0 || results[0] == null) { + Log.d(TAG, "笔记移动失败,ID集合:" + ids.toString()); + return false; + } + return true; + } catch (RemoteException e) { // 跨进程调用异常 + Log.e(TAG, "远程调用异常: " + e.toString() + " - " + e.getMessage()); + } catch (OperationApplicationException e) { // 操作应用异常 + Log.e(TAG, "操作应用异常: " + e.toString() + " - " + e.getMessage()); + } + return false; + } + + /** + * 获取用户创建的文件夹数量(排除系统文件夹和回收站) + * @param resolver ContentResolver实例 + * @return 用户文件夹数量(整数) + */ + public static int getUserFolderCount(ContentResolver resolver) { + // 查询条件:类型为文件夹 且 父ID不等于回收站ID + Cursor cursor = resolver.query( + Notes.CONTENT_NOTE_URI, + new String[]{"COUNT(*)"}, // 查询字段:记录数 + NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>?", + new String[]{String.valueOf(Notes.TYPE_FOLDER), String.valueOf(Notes.ID_TRASH_FOLER)}, + null + ); + + int count = 0; + if (cursor != null) { + if (cursor.moveToFirst()) { + try { + count = cursor.getInt(0); // 获取统计结果 + } catch (IndexOutOfBoundsException e) { + Log.e(TAG, "获取文件夹数量失败: " + e.toString()); + } finally { + cursor.close(); // 关闭游标释放资源 + } + } + } + return count; + } + + /** + * 检查笔记在数据库中是否可见(非回收站且类型匹配) + * @param resolver ContentResolver实例 + * @param noteId 待检查的笔记ID + * @param type 笔记类型(如Notes.TYPE_NOTE、Notes.TYPE_FOLDER) + * @return 可见返回true,不可见返回false + */ + public static boolean visibleInNoteDatabase(ContentResolver resolver, long noteId, int type) { + // 查询条件:指定ID、类型匹配、非回收站 + Cursor cursor = resolver.query( + ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), + null, + NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER, + new String[]{String.valueOf(type)}, + null + ); + + boolean exist = false; + if (cursor != null) { + exist = cursor.getCount() > 0; // 存在记录则可见 + cursor.close(); // 关闭游标 + } + return exist; + } + + /** + * 检查笔记是否存在于数据库中(无论状态) + * @param resolver ContentResolver实例 + * @param noteId 待检查的笔记ID + * @return 存在返回true,不存在返回false + */ + public static boolean existInNoteDatabase(ContentResolver resolver, long noteId) { + // 查询note表中是否存在指定ID的记录 + Cursor cursor = resolver.query( + ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), + null, + null, + null, + null + ); + + boolean exist = false; + if (cursor != null) { + exist = cursor.getCount() > 0; // 存在记录则返回true + cursor.close(); // 关闭游标 + } + return exist; + } + + /** + * 检查关联数据(如文本、通话记录)是否存在于数据库中 + * @param resolver ContentResolver实例 + * @param dataId 待检查的关联数据ID(data表中的记录ID) + * @return 存在返回true,不存在返回false + */ + public static boolean existInDataDatabase(ContentResolver resolver, long dataId) { + // 查询data表中是否存在指定ID的记录 + Cursor cursor = resolver.query( + ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), + null, + null, + null, + null + ); + + boolean exist = false; + if (cursor != null) { + exist = cursor.getCount() > 0; // 存在记录则返回true + cursor.close(); // 关闭游标 + } + return exist; + } + + /** + * 检查可见文件夹名称是否已存在(用于新建文件夹时的重名校验) + * @param resolver ContentResolver实例 + * @param name 待检查的文件夹名称 + * @return 已存在返回true,不存在返回false + */ + public static boolean checkVisibleFolderName(ContentResolver resolver, String name) { + // 查询条件:类型为文件夹、非回收站、摘要(名称)匹配 + Cursor cursor = resolver.query( + Notes.CONTENT_NOTE_URI, + null, + NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER + + " AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER + + " AND " + NoteColumns.SNIPPET + "=?", + new String[]{name}, + null + ); + + boolean exist = false; + if (cursor != null) { + exist = cursor.getCount() > 0; // 存在记录则名称已存在 + cursor.close(); // 关闭游标 + } + return exist; + } + + /** + * 获取指定文件夹下的所有桌面小部件属性(用于小部件管理) + * @param resolver ContentResolver实例 + * @param folderId 目标文件夹ID + * @return 小部件属性集合(HashSet),无数据返回null + */ + public static HashSet getFolderNoteWidget(ContentResolver resolver, long folderId) { + // 查询note表中属于该文件夹的小部件记录(只取widget_id和widget_type字段) + Cursor c = resolver.query( + Notes.CONTENT_NOTE_URI, + new String[]{NoteColumns.WIDGET_ID, NoteColumns.WIDGET_TYPE}, + NoteColumns.PARENT_ID + "=?", + new String[]{String.valueOf(folderId)}, + null + ); + + HashSet set = null; + if (c != null) { + if (c.moveToFirst()) { + set = new HashSet<>(); // 初始化集合 + do { + try { + AppWidgetAttribute widget = new AppWidgetAttribute(); + widget.widgetId = c.getInt(0); // 小部件ID + widget.widgetType = c.getInt(1); // 小部件类型 + set.add(widget); // 添加到集合 + } catch (IndexOutOfBoundsException e) { + Log.e(TAG, "解析小部件属性失败: " + e.toString()); + } + } while (c.moveToNext()); // 遍历所有记录 + } + c.close(); // 关闭游标 + } + return set; + } + + /** + * 根据笔记ID获取通话记录的电话号码(仅适用于通话类型笔记) + * @param resolver ContentResolver实例 + * @param noteId 通话类型笔记的ID + * @return 电话号码字符串(无数据返回空字符串) + */ + public static String getCallNumberByNoteId(ContentResolver resolver, long noteId) { + // 查询data表中关联的通话记录(MIME类型为CallNote.CONTENT_ITEM_TYPE) + Cursor cursor = resolver.query( + Notes.CONTENT_DATA_URI, + new String[]{CallNote.PHONE_NUMBER}, // 只取电话号码字段 + CallNote.NOTE_ID + "=? AND " + CallNote.MIME_TYPE + "=?", + new String[]{String.valueOf(noteId), CallNote.CONTENT_ITEM_TYPE}, + null + ); + + if (cursor != null && cursor.moveToFirst()) { + try { + return cursor.getString(0); // 返回电话号码 + } catch (IndexOutOfBoundsException e) { + Log.e(TAG, "获取通话号码失败: " + e.toString()); + } finally { + cursor.close(); // 关闭游标 + } + } + return ""; // 无数据返回空字符串 + } + + /** + * 根据电话号码和通话时间查找关联的笔记ID(用于通话记录匹配) + * @param resolver ContentResolver实例 + * @param phoneNumber 电话号码 + * @param callDate 通话时间戳(毫秒级) + * @return 匹配的笔记ID(无匹配返回0) + */ + public static long getNoteIdByPhoneNumberAndCallDate(ContentResolver resolver, String phoneNumber, long callDate) { + // 查询条件:通话时间匹配、MIME类型正确、电话号码匹配(使用PHONE_NUMBERS_EQUAL处理号码格式) + Cursor cursor = resolver.query( + Notes.CONTENT_DATA_URI, + new String[]{CallNote.NOTE_ID}, // 只取笔记ID字段 + CallNote.CALL_DATE + "=? AND " + CallNote.MIME_TYPE + "=? AND PHONE_NUMBERS_EQUAL(" + CallNote.PHONE_NUMBER + ",?)", + new String[]{String.valueOf(callDate), CallNote.CONTENT_ITEM_TYPE, phoneNumber}, + null + ); + + if (cursor != null) { + if (cursor.moveToFirst()) { + try { + return cursor.getLong(0); // 返回匹配的笔记ID + } catch (IndexOutOfBoundsException e) { + Log.e(TAG, "获取通话笔记ID失败: " + e.toString()); + } + } + cursor.close(); // 关闭游标 + } + return 0; // 无匹配返回0 + } + + /** + * 根据笔记ID获取摘要内容(用于显示预览) + * @param resolver ContentResolver实例 + * @param noteId 笔记ID + * @return 摘要字符串(无数据抛出异常) + */ + public static String getSnippetById(ContentResolver resolver, long noteId) { + // 查询note表中的摘要字段 + Cursor cursor = resolver.query( + Notes.CONTENT_NOTE_URI, + new String[]{NoteColumns.SNIPPET}, // 只取摘要字段 + NoteColumns.ID + "=?", + new String[]{String.valueOf(noteId)}, + null + ); + + if (cursor != null) { + String snippet = ""; + if (cursor.moveToFirst()) { + snippet = cursor.getString(0); // 获取摘要内容 + } + cursor.close(); // 关闭游标 + return snippet; + } + // 未找到笔记时抛出异常(参数非法) + throw new IllegalArgumentException("未找到ID为" + noteId + "的笔记"); + } + + /** + * 格式化摘要内容(用于显示优化) + * @param snippet 原始摘要字符串 + * @return 格式化后的摘要(去除首尾空格,截断换行符前的内容) + */ + public static String getFormattedSnippet(String snippet) { + if (snippet != null) { + snippet = snippet.trim(); // 去除首尾空格 + int index = snippet.indexOf('\n'); // 查找换行符位置 + if (index != -1) { // 存在换行符时截断 + snippet = snippet.substring(0, index); + } + } + return snippet; + } +} \ No newline at end of file diff --git a/GTaskStringUtils.java b/GTaskStringUtils.java new file mode 100644 index 0000000..15769cc --- /dev/null +++ b/GTaskStringUtils.java @@ -0,0 +1,116 @@ + +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * 版权声明:本文件由MiCode开源社区开发,遵循Apache License, Version 2.0协议; + * 您仅在遵守协议的前提下使用本文件,完整协议可通过以下链接获取: + * http://www.apache.org/licenses/LICENSE-2.0 + * 注:未书面明确要求时,本软件按"原样"提供,不附带任何明示或暗示的保证。 + */ + +package net.micode.notes.tool; + +/** + * Google Tasks(GTask)相关的字符串常量工具类 + * 定义JSON数据字段名、特定标识字符串、文件夹名称等常量,用于统一管理GTask交互中的关键字符串 + */ +public class GTaskStringUtils { + + // ---------------------- JSON 数据字段名常量 ---------------------- + /** JSON中表示"动作ID"的字段名(用于标识具体操作的唯一ID) */ + public final static String GTASK_JSON_ACTION_ID = "action_id"; + /** JSON中表示"动作列表"的字段名(存储多个操作的集合) */ + public final static String GTASK_JSON_ACTION_LIST = "action_list"; + /** JSON中表示"动作类型"的字段名(说明操作的类型,如创建、更新等) */ + public final static String GTASK_JSON_ACTION_TYPE = "action_type"; + /** 动作类型为"创建"的常量值 */ + public final static String GTASK_JSON_ACTION_TYPE_CREATE = "create"; + /** 动作类型为"获取所有"的常量值(用于请求全量数据) */ + public final static String GTASK_JSON_ACTION_TYPE_GETALL = "get_all"; + /** 动作类型为"移动"的常量值(用于移动实体操作) */ + public final static String GTASK_JSON_ACTION_TYPE_MOVE = "move"; + /** 动作类型为"更新"的常量值(用于修改实体信息) */ + public final static String GTASK_JSON_ACTION_TYPE_UPDATE = "update"; + /** JSON中表示"创建者ID"的字段名(标识操作的创建者) */ + public final static String GTASK_JSON_CREATOR_ID = "creator_id"; + /** JSON中表示"子实体"的字段名(用于关联子级数据,如子任务) */ + public final static String GTASK_JSON_CHILD_ENTITY = "child_entity"; + /** JSON中表示"客户端版本"的字段名(记录客户端版本信息,用于兼容性处理) */ + public final static String GTASK_JSON_CLIENT_VERSION = "client_version"; + /** JSON中表示"已完成"的字段名(通常用于任务状态,标记是否完成) */ + public final static String GTASK_JSON_COMPLETED = "completed"; + /** JSON中表示"当前列表ID"的字段名(标识当前操作所在的任务列表) */ + public final static String GTASK_JSON_CURRENT_LIST_ID = "current_list_id"; + /** JSON中表示"默认列表ID"的字段名(指向用户默认的任务列表) */ + public final static String GTASK_JSON_DEFAULT_LIST_ID = "default_list_id"; + /** JSON中表示"已删除"的字段名(标记实体是否为已删除状态) */ + public final static String GTASK_JSON_DELETED = "deleted"; + /** JSON中表示"目标列表"的字段名(用于移动操作中指定目标列表) */ + public final static String GTASK_JSON_DEST_LIST = "dest_list"; + /** JSON中表示"目标父级"的字段名(指向移动后实体的父级对象ID) */ + public final static String GTASK_JSON_DEST_PARENT = "dest_parent"; + /** JSON中表示"目标父级类型"的字段名(说明父级对象的类型,如组或任务) */ + public final static String GTASK_JSON_DEST_PARENT_TYPE = "dest_parent_type"; + /** JSON中表示"实体差异"的字段名(记录实体的变更信息,用于同步) */ + public final static String GTASK_JSON_ENTITY_DELTA = "entity_delta"; + /** JSON中表示"实体类型"的字段名(区分不同类型的实体,如任务、列表、组) */ + public final static String GTASK_JSON_ENTITY_TYPE = "entity_type"; + /** JSON中表示"获取已删除项"的字段名(用于请求获取已删除的实体数据) */ + public final static String GTASK_JSON_GET_DELETED = "get_deleted"; + /** JSON中表示"唯一ID"的字段名(标识实体的唯一标识符) */ + public final static String GTASK_JSON_ID = "id"; + /** JSON中表示"索引"的字段名(用于实体在列表中的排序位置) */ + public final static String GTASK_JSON_INDEX = "index"; + /** JSON中表示"最后修改时间"的字段名(记录实体最后一次修改的时间戳) */ + public final static String GTASK_JSON_LAST_MODIFIED = "last_modified"; + /** JSON中表示"最新同步点"的字段名(记录最近一次同步的标识,用于增量同步) */ + public final static String GTASK_JSON_LATEST_SYNC_POINT = "latest_sync_point"; + /** JSON中表示"列表ID"的字段名(标识任务列表的唯一ID) */ + public final static String GTASK_JSON_LIST_ID = "list_id"; + /** JSON中表示"列表集合"的字段名(存储多个任务列表的数据) */ + public final static String GTASK_JSON_LISTS = "lists"; + /** JSON中表示"名称"的字段名(用于实体的名称属性,如任务名、列表名) */ + public final static String GTASK_JSON_NAME = "name"; + /** JSON中表示"新ID"的字段名(用于创建操作中生成的新实体ID) */ + public final static String GTASK_JSON_NEW_ID = "new_id"; + /** JSON中表示"笔记内容"的字段名(存储任务或列表的备注信息) */ + public final static String GTASK_JSON_NOTES = "notes"; + /** JSON中表示"父级ID"的字段名(指向当前实体的父级对象ID,如任务所属的组) */ + public final static String GTASK_JSON_PARENT_ID = "parent_id"; + /** JSON中表示"前一个兄弟ID"的字段名(用于确定实体在同级中的排序顺序) */ + public final static String GTASK_JSON_PRIOR_SIBLING_ID = "prior_sibling_id"; + /** JSON中表示"结果"的字段名(存储操作返回的结果数据,如查询结果) */ + public final static String GTASK_JSON_RESULTS = "results"; + /** JSON中表示"源列表"的字段名(用于移动操作中指定原列表) */ + public final static String GTASK_JSON_SOURCE_LIST = "source_list"; + /** JSON中表示"任务集合"的字段名(存储多个任务的数据) */ + public final static String GTASK_JSON_TASKS = "tasks"; + /** JSON中表示"类型"的字段名(用于区分实体的类别,如组或任务) */ + public final static String GTASK_JSON_TYPE = "type"; + /** 类型为"组"的常量值(标识组类型实体,可包含子任务) */ + public final static String GTASK_JSON_TYPE_GROUP = "GROUP"; + /** 类型为"任务"的常量值(标识具体的任务实体) */ + public final static String GTASK_JSON_TYPE_TASK = "TASK"; + /** JSON中表示"用户"的字段名(存储用户相关信息,如用户ID) */ + public final static String GTASK_JSON_USER = "user"; + + // ---------------------- 文件夹相关常量 ---------------------- + /** MIUI笔记文件夹的前缀常量(用于标识MIUI笔记特有的文件夹,如"[MIUI_Notes]") */ + public final static String MIUI_FOLDER_PREFFIX = "[MIUI_Notes]"; + /** 默认文件夹名称常量(GTask同步时的默认列表名称,如"Default") */ + public final static String FOLDER_DEFAULT = "Default"; + /** 通话记录文件夹名称常量(用于存储通话类型笔记的文件夹,如"Call_Note") */ + public final static String FOLDER_CALL_NOTE = "Call_Note"; + /** 元数据文件夹名称常量(用于存储同步元信息的特殊文件夹,如"METADATA") */ + public final static String FOLDER_META = "METADATA"; + + // ---------------------- 元数据头部常量 ---------------------- + /** 元数据头部中"GTask ID"的字段名(存储与MIUI笔记关联的GTask唯一ID) */ + public final static String META_HEAD_GTASK_ID = "meta_gid"; + /** 元数据头部中"笔记"的字段名(存储元数据相关的备注信息) */ + public final static String META_HEAD_NOTE = "meta_note"; + /** 元数据头部中"数据"的字段名(存储元数据的具体内容,如同步配置) */ + public final static String META_HEAD_DATA = "meta_data"; + /** 元数据笔记的名称常量(标识该笔记为元信息,提示用户不要修改或删除,如"[META INFO] DON'T UPDATE AND DELETE") */ + public final static String META_NOTE_NAME = "[META INFO] DON'T UPDATE AND DELETE"; +} \ No newline at end of file diff --git a/ResourceParser.java b/ResourceParser.java new file mode 100644 index 0000000..66a44a1 --- /dev/null +++ b/ResourceParser.java @@ -0,0 +1,223 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.tool; + +import android.content.Context; +import android.preference.PreferenceManager; + +import net.micode.notes.R; +import net.micode.notes.ui.NotesPreferenceActivity; + +/** + * 资源解析工具类,用于管理笔记应用的各种资源ID + */ +public class ResourceParser { + // 背景颜色常量定义 + public static final int YELLOW = 0; + public static final int BLUE = 1; + public static final int WHITE = 2; + public static final int GREEN = 3; + public static final int RED = 4; + + public static final int BG_DEFAULT_COLOR = YELLOW; // 默认背景颜色 + + // 字体大小常量定义 + public static final int TEXT_SMALL = 0; + public static final int TEXT_MEDIUM = 1; + public static final int TEXT_LARGE = 2; + public static final int TEXT_SUPER = 3; + + public static final int BG_DEFAULT_FONT_SIZE = TEXT_MEDIUM; // 默认字体大小 + + /** + * 笔记背景资源类,提供编辑界面背景资源获取 + */ + public static class NoteBgResources { + // 编辑区域背景资源数组 + private final static int [] BG_EDIT_RESOURCES = new int [] { + R.drawable.edit_yellow, + R.drawable.edit_blue, + R.drawable.edit_white, + R.drawable.edit_green, + R.drawable.edit_red + }; + + // 编辑区域标题背景资源数组 + private final static int [] BG_EDIT_TITLE_RESOURCES = new int [] { + R.drawable.edit_title_yellow, + R.drawable.edit_title_blue, + R.drawable.edit_title_white, + R.drawable.edit_title_green, + R.drawable.edit_title_red + }; + + // 获取笔记编辑区域背景资源 + public static int getNoteBgResource(int id) { + return BG_EDIT_RESOURCES[id]; + } + + // 获取笔记标题背景资源 + public static int getNoteTitleBgResource(int id) { + return BG_EDIT_TITLE_RESOURCES[id]; + } + } + + /** + * 获取默认背景ID + * @param context 上下文对象 + * @return 背景资源ID + */ + public static int getDefaultBgId(Context context) { + // 检查偏好设置是否启用了随机背景颜色 + if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean( + NotesPreferenceActivity.PREFERENCE_SET_BG_COLOR_KEY, false)) { + return (int) (Math.random() * NoteBgResources.BG_EDIT_RESOURCES.length); // 随机选择背景 + } else { + return BG_DEFAULT_COLOR; // 返回默认背景 + } + } + + /** + * 笔记列表项背景资源类,提供列表项不同位置的背景资源 + */ + public static class NoteItemBgResources { + // 列表第一项背景资源 + private final static int [] BG_FIRST_RESOURCES = new int [] { + R.drawable.list_yellow_up, + R.drawable.list_blue_up, + R.drawable.list_white_up, + R.drawable.list_green_up, + R.drawable.list_red_up + }; + + // 列表中间项背景资源 + private final static int [] BG_NORMAL_RESOURCES = new int [] { + R.drawable.list_yellow_middle, + R.drawable.list_blue_middle, + R.drawable.list_white_middle, + R.drawable.list_green_middle, + R.drawable.list_red_middle + }; + + // 列表最后一项背景资源 + private final static int [] BG_LAST_RESOURCES = new int [] { + R.drawable.list_yellow_down, + R.drawable.list_blue_down, + R.drawable.list_white_down, + R.drawable.list_green_down, + R.drawable.list_red_down, + }; + + // 列表单项背景资源(当列表只有一项时使用) + private final static int [] BG_SINGLE_RESOURCES = new int [] { + R.drawable.list_yellow_single, + R.drawable.list_blue_single, + R.drawable.list_white_single, + R.drawable.list_green_single, + R.drawable.list_red_single + }; + + // 获取列表第一项背景资源 + public static int getNoteBgFirstRes(int id) { + return BG_FIRST_RESOURCES[id]; + } + + // 获取列表最后一项背景资源 + public static int getNoteBgLastRes(int id) { + return BG_LAST_RESOURCES[id]; + } + + // 获取列表单项背景资源 + public static int getNoteBgSingleRes(int id) { + return BG_SINGLE_RESOURCES[id]; + } + + // 获取列表中间项背景资源 + public static int getNoteBgNormalRes(int id) { + return BG_NORMAL_RESOURCES[id]; + } + + // 获取文件夹背景资源 + public static int getFolderBgRes() { + return R.drawable.list_folder; + } + } + + /** + * 小部件背景资源类,提供不同尺寸小部件的背景资源 + */ + public static class WidgetBgResources { + // 2x大小小部件背景资源 + private final static int [] BG_2X_RESOURCES = new int [] { + R.drawable.widget_2x_yellow, + R.drawable.widget_2x_blue, + R.drawable.widget_2x_white, + R.drawable.widget_2x_green, + R.drawable.widget_2x_red, + }; + + // 获取2x大小小部件背景资源 + public static int getWidget2xBgResource(int id) { + return BG_2X_RESOURCES[id]; + } + + // 4x大小小部件背景资源 + private final static int [] BG_4X_RESOURCES = new int [] { + R.drawable.widget_4x_yellow, + R.drawable.widget_4x_blue, + R.drawable.widget_4x_white, + R.drawable.widget_4x_green, + R.drawable.widget_4x_red + }; + + // 获取4x大小小部件背景资源 + public static int getWidget4xBgResource(int id) { + return BG_4X_RESOURCES[id]; + } + } + + /** + * 文本外观资源类,提供不同字体大小的样式资源 + */ + public static class TextAppearanceResources { + // 文本外观样式资源数组 + private final static int [] TEXTAPPEARANCE_RESOURCES = new int [] { + R.style.TextAppearanceNormal, + R.style.TextAppearanceMedium, + R.style.TextAppearanceLarge, + R.style.TextAppearanceSuper + }; + + /** + * 获取文本外观资源 + * @param id 资源索引 + * @return 样式资源ID + * @note 修复了存储资源ID时可能越界的bug,越界时返回默认字体大小 + */ + public static int getTexAppearanceResource(int id) { + if (id >= TEXTAPPEARANCE_RESOURCES.length) { + return BG_DEFAULT_FONT_SIZE; // 越界保护,返回默认值 + } + return TEXTAPPEARANCE_RESOURCES[id]; + } + + // 获取文本外观资源总数 + public static int getResourcesSize() { + return TEXTAPPEARANCE_RESOURCES.length; + } + } +} \ No newline at end of file From 5966d97c9f08841f0966a5a27f0dcd087744fded Mon Sep 17 00:00:00 2001 From: moneynewmoon <2075698549@qq.com> Date: Thu, 29 May 2025 15:41:42 +0800 Subject: [PATCH 5/7] ui --- AlarmAlertActivity.java | 198 ++++++++++++++++++++++++++++++++++++++++ AlarmInitReceiver.java | 84 +++++++++++++++++ 2 files changed, 282 insertions(+) create mode 100644 AlarmAlertActivity.java create mode 100644 AlarmInitReceiver.java diff --git a/AlarmAlertActivity.java b/AlarmAlertActivity.java new file mode 100644 index 0000000..af6eeb2 --- /dev/null +++ b/AlarmAlertActivity.java @@ -0,0 +1,198 @@ + +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * 版权声明:本文件由MiCode开源社区开发,遵循Apache License, Version 2.0协议; + * 您仅在遵守协议的前提下使用本文件,完整协议可通过以下链接获取: + * http://www.apache.org/licenses/LICENSE-2.0 + * 注:未书面明确要求时,本软件按"原样"提供,不附带任何明示或暗示的保证。 + */ + +package net.micode.notes.ui; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +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; // 当前提醒对应的笔记ID + private String mSnippet; // 当前笔记的摘要内容(用于提醒显示) + private static final int SNIPPET_PREW_MAX_LEN = 60; // 摘要预览的最大长度(超过则截断) + MediaPlayer mPlayer; // 媒体播放器实例(用于播放闹钟声音) + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // 请求无标题窗口(提升提醒界面的专注度) + requestWindowFeature(Window.FEATURE_NO_TITLE); + + final Window win = getWindow(); + // 添加窗口标志:锁屏时显示提醒(确保用户能看到闹钟) + win.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); + + // 如果屏幕未点亮,则添加唤醒屏幕相关标志(确保用户被提醒唤醒) + if (!isScreenOn()) { + win.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON // 保持屏幕常亮 + | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON // 点亮屏幕 + | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON // 允许屏幕锁定时显示 + | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR); // 布局插入装饰(适配不同设备) + } + + // 从启动意图中解析笔记ID和摘要内容 + Intent intent = getIntent(); + try { + // 从Uri路径中获取笔记ID(格式示例:content://.../notes/123 → 取路径段的第二个元素) + mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1)); + // 通过DataUtils工具类获取笔记摘要 + mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId); + // 截断过长的摘要(超过60字符时显示前60字符+省略号) + mSnippet = mSnippet.length() > SNIPPET_PREW_MAX_LEN + ? mSnippet.substring(0, SNIPPET_PREW_MAX_LEN) + getResources().getString(R.string.notelist_string_info) + : mSnippet; + } catch (IllegalArgumentException e) { + // 解析失败时打印异常并结束活动(无效的笔记ID) + e.printStackTrace(); + return; + } + + // 初始化媒体播放器 + mPlayer = new MediaPlayer(); + // 检查笔记是否在数据库中可见(非回收站且类型正确) + if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) { + // 显示提醒对话框并播放闹钟声音 + showActionDialog(); + playAlarmSound(); + } else { + // 笔记不可见时直接结束活动(可能已被删除) + finish(); + } + } + + /** + * 检查屏幕是否处于点亮状态 + * @return 屏幕点亮返回true,否则返回false + */ + private boolean isScreenOn() { + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + return pm.isScreenOn(); // 通过PowerManager获取屏幕状态 + } + + /** + * 播放闹钟声音(使用系统默认闹钟铃声) + */ + 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) { + mPlayer.setAudioStreamType(silentModeStreams); + } else { + mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM); + } + + try { + // 设置铃声数据源(系统默认闹钟铃声) + mPlayer.setDataSource(this, url); + // 同步准备播放器(确保资源加载完成) + mPlayer.prepare(); + // 设置循环播放(持续提醒直到用户操作) + mPlayer.setLooping(true); + // 开始播放 + mPlayer.start(); + } catch (IllegalArgumentException e) { + e.printStackTrace(); // 参数错误异常处理 + } catch (SecurityException e) { + e.printStackTrace(); // 安全权限异常处理(如未获取读取铃声权限) + } catch (IllegalStateException e) { + e.printStackTrace(); // 播放器状态异常处理(如重复调用start) + } catch (IOException e) { + e.printStackTrace(); // 输入输出异常处理(铃声文件不存在) + } + } + + /** + * 显示闹钟提醒对话框(包含笔记摘要和操作按钮) + */ + private void showActionDialog() { + AlertDialog.Builder dialog = new AlertDialog.Builder(this); + dialog.setTitle(R.string.app_name); // 对话框标题为应用名称 + dialog.setMessage(mSnippet); // 对话框内容为笔记摘要 + dialog.setPositiveButton(R.string.notealert_ok, this); // 确定按钮(关闭提醒) + + // 如果屏幕已点亮,添加"进入"按钮(跳转到笔记编辑界面) + if (isScreenOn()) { + dialog.setNegativeButton(R.string.notealert_enter, this); + } + + // 显示对话框并设置关闭监听器(对话框关闭时触发后续操作) + dialog.show().setOnDismissListener(this); + } + + /** + * 对话框按钮点击事件处理 + * @param dialog 触发事件的对话框 + * @param which 被点击的按钮类型(如BUTTON_NEGATIVE) + */ + public void onClick(DialogInterface dialog, int which) { + switch (which) { + case DialogInterface.BUTTON_NEGATIVE: // "进入"按钮点击 + // 启动笔记编辑活动(查看或编辑当前提醒的笔记) + Intent intent = new Intent(this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_VIEW); // 设置操作为查看 + intent.putExtra(Intent.EXTRA_UID, mNoteId); // 传递笔记ID + startActivity(intent); + break; + default: // 其他按钮(如确定按钮)不做额外处理 + break; + } + } + + /** + * 对话框关闭时的回调(无论通过按钮还是其他方式关闭) + * @param dialog 关闭的对话框 + */ + public void onDismiss(DialogInterface dialog) { + // 停止闹钟声音并释放资源 + stopAlarmSound(); + // 结束当前活动 + finish(); + } + + /** + * 停止闹钟声音并释放媒体播放器资源 + */ + private void stopAlarmSound() { + if (mPlayer != null) { + mPlayer.stop(); // 停止播放 + mPlayer.release(); // 释放播放器资源(避免内存泄漏) + mPlayer = null; // 置空引用防止重复操作 + } + } +} \ No newline at end of file diff --git a/AlarmInitReceiver.java b/AlarmInitReceiver.java new file mode 100644 index 0000000..4555187 --- /dev/null +++ b/AlarmInitReceiver.java @@ -0,0 +1,84 @@ + +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * 版权声明:本文件由MiCode开源社区开发,遵循Apache License, Version 2.0协议; + * 您仅在遵守协议的前提下使用本文件,完整协议可通过以下链接获取: + * http://www.apache.org/licenses/LICENSE-2.0 + * 注:未书面明确要求时,本软件按"原样"提供,不附带任何明示或暗示的保证。 + */ + +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 { + + // 数据库查询投影:指定查询的字段为笔记ID和提醒时间 + 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; // 提醒时间索引 + + /** + * 接收广播时的回调方法(核心逻辑:初始化未触发的笔记提醒闹钟) + * @param context 上下文环境 + * @param intent 接收到的广播意图(通常由系统或应用触发) + */ + @Override + public void onReceive(Context context, Intent intent) { + long currentDate = System.currentTimeMillis(); // 获取当前系统时间(毫秒级时间戳) + + // 查询数据库:获取所有未触发的普通笔记(提醒时间 > 当前时间且类型为笔记) + Cursor c = context.getContentResolver().query( + Notes.CONTENT_NOTE_URI, // 查询的Uri(笔记表) + PROJECTION, // 需要查询的字段(ID和提醒时间) + NoteColumns.ALERTED_DATE + ">? AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE, + // 查询条件:提醒时间 > 当前时间 且 类型为普通笔记(TYPE_NOTE) + new String[]{String.valueOf(currentDate)}, // 查询参数(当前时间) + null // 排序方式(默认) + ); + + if (c != null) { + if (c.moveToFirst()) { // 移动到查询结果的第一条记录 + do { + // 提取当前笔记的提醒时间和ID + long alertDate = c.getLong(COLUMN_ALERTED_DATE); // 提醒时间戳 + long noteId = c.getLong(COLUMN_ID); // 笔记ID + + // 创建触发闹钟提醒的Intent(指向AlarmReceiver广播接收器) + Intent sender = new Intent(context, AlarmReceiver.class); + // 设置Intent的数据为当前笔记的Uri(格式:content://.../notes/[noteId]) + sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId)); + + // 创建PendingIntent(用于在闹钟触发时发送广播) + // 参数说明:context-上下文;requestCode-请求码(0表示不区分不同请求);sender-目标Intent;flags-标志位(0为默认) + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, 0); + + // 获取系统闹钟服务实例 + AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + // 设置闹钟:使用RTC_WAKEUP模式(唤醒设备),触发时间为alertDate,绑定PendingIntent + alarmManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent); + } while (c.moveToNext()); // 遍历所有符合条件的笔记记录 + } + c.close(); // 关闭游标释放资源 + } + } +} \ No newline at end of file From cab40acef864100ca8c0a48dbfbe24fdd975f432 Mon Sep 17 00:00:00 2001 From: moneynewmoon <2075698549@qq.com> Date: Thu, 29 May 2025 15:45:41 +0800 Subject: [PATCH 6/7] ui --- AlarmReceiver.java | 43 ++++ DateTimePicker.java | 480 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 523 insertions(+) create mode 100644 AlarmReceiver.java create mode 100644 DateTimePicker.java diff --git a/AlarmReceiver.java b/AlarmReceiver.java new file mode 100644 index 0000000..be8b4d4 --- /dev/null +++ b/AlarmReceiver.java @@ -0,0 +1,43 @@ + +/* + * 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; + +/** + * 闹钟提醒广播接收器类 + * 功能:接收系统或应用触发的闹钟广播,启动对应的提醒界面活动(AlarmAlertActivity) + */ +public class AlarmReceiver extends BroadcastReceiver { + /** + * 接收广播时的回调方法(核心逻辑) + * @param context 上下文环境 + * @param intent 接收到的广播意图(包含触发闹钟的相关信息) + */ + @Override + public void onReceive(Context context, Intent intent) { + // 设置要启动的目标活动类为闹钟提醒界面(AlarmAlertActivity) + intent.setClass(context, AlarmAlertActivity.class); + // 添加"新任务"标志(广播接收器中启动Activity需此标志,否则可能无法启动) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + // 通过上下文启动目标活动(触发闹钟提醒界面显示) + context.startActivity(intent); + } +} \ No newline at end of file diff --git a/DateTimePicker.java b/DateTimePicker.java new file mode 100644 index 0000000..71c5b92 --- /dev/null +++ b/DateTimePicker.java @@ -0,0 +1,480 @@ + +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * 版权声明:本文件由MiCode开源社区开发,遵循Apache License, Version 2.0协议; + * 您仅在遵守协议的前提下使用本文件,完整协议可通过以下链接获取: + * http://www.apache.org/licenses/LICENSE-2.0 + * 注:未书面明确要求时,本软件按"原样"提供,不附带任何明示或暗示的保证。 + */ + +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; + +/** + * 日期时间选择器组件(自定义视图) + * 功能:提供日期(星期)、小时、分钟、上下午的选择功能,支持24小时制和12小时制切换 + * 组件构成:包含四个NumberPicker(日期、小时、分钟、上下午)和相应的交互逻辑 + */ +public class DateTimePicker extends FrameLayout { + + private static final boolean DEFAULT_ENABLE_STATE = true; // 默认启用状态 + + // 时间选择相关常量 + private static final int HOURS_IN_HALF_DAY = 12; // 12小时制的半天小时数 + private static final int HOURS_IN_ALL_DAY = 24; // 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; // 日期选择器最大值(索引终点) + private static final int HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW = 0; // 24小时制小时选择器最小值 + private static final int HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW = 23; // 24小时制小时选择器最大值 + private static final int HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW = 1; // 12小时制小时选择器最小值 + private static final int HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW = 12; // 12小时制小时选择器最大值 + private static final int MINUT_SPINNER_MIN_VAL = 0; // 分钟选择器最小值 + private static final int MINUT_SPINNER_MAX_VAL = 59; // 分钟选择器最大值 + private static final int AMPM_SPINNER_MIN_VAL = 0; // 上下午选择器最小值(AM) + private static final int AMPM_SPINNER_MAX_VAL = 1; // 上下午选择器最大值(PM) + + // 组件实例 + private final NumberPicker mDateSpinner; // 日期选择器(显示一周内的日期) + private final NumberPicker mHourSpinner; // 小时选择器 + private final NumberPicker mMinuteSpinner; // 分钟选择器 + private final NumberPicker mAmPmSpinner; // 上下午选择器(仅12小时制可见) + private Calendar mDate; // 当前选择的日期时间(Calendar对象) + + private String[] mDateDisplayValues = new String[DAYS_IN_ALL_WEEK]; // 日期选择器显示的字符串数组(如"MM.dd EEEE"格式) + private boolean mIsAm; // 是否为上午(仅12小时制有效) + private boolean mIs24HourView; // 是否为24小时制模式 + 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) { + // 根据新旧值差调整日期(如从周一滚动到周二,日期加1天) + 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) { // 12小时制处理 + // 处理跨半天的情况(如12点AM→PM或反之) + if (!mIsAm && oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) { + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, 1); // 日期加1天(PM转次日AM) + 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); // 日期减1天(AM转前一日PM) + isDateChanged = true; + } + // 切换上下午状态(12点切换时) + if (oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY || + oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) { + mIsAm = !mIsAm; + updateAmPmControl(); // 更新上下午选择器显示 + } + } else { // 24小时制处理(跨天逻辑) + if (oldVal == HOURS_IN_ALL_DAY - 1 && newVal == 0) { // 23点→0点,日期加1天 + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, 1); + isDateChanged = true; + } else if (oldVal == 0 && newVal == HOURS_IN_ALL_DAY - 1) { // 0点→23点,日期减1天 + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, -1); + isDateChanged = true; + } + } + // 计算实际小时(12小时制转换为24小时制存储) + 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; + // 处理分钟循环(如59→0时,小时加1;0→59时,小时减1) + 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(); + // 根据新小时更新上下午状态(12小时制) + if (newHour >= HOURS_IN_HALF_DAY) { + mIsAm = false; + } 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; // 切换上下午状态 + // 调整小时(12小时制下AM/PM切换时,小时加减12) + 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); + } + + // 构造方法:使用当前时间初始化选择器 + public DateTimePicker(Context context) { + this(context, System.currentTimeMillis()); + } + + // 构造方法:使用指定时间戳初始化选择器 + public DateTimePicker(Context context, long date) { + this(context, date, DateFormat.is24HourFormat(context)); // 自动判断是否为24小时制 + } + + /** + * 完整构造方法 + * @param context 上下文 + * @param date 初始化时间戳 + * @param is24HourView 是否为24小时制模式 + */ + public DateTimePicker(Context context, long date, boolean is24HourView) { + super(context); + mDate = Calendar.getInstance(); // 初始化日历对象 + mInitialising = true; // 设置初始化标志(防止回调触发) + mIsAm = getCurrentHourOfDay() >= HOURS_IN_HALF_DAY; // 初始化上下午状态 + // 加载布局文件(R.layout.datetime_picker) + inflate(context, R.layout.datetime_picker, this); + + // 初始化日期选择器 + mDateSpinner = (NumberPicker) findViewById(R.id.date); + mDateSpinner.setMinValue(DATE_SPINNER_MIN_VAL); + mDateSpinner.setMaxValue(DATE_SPINNER_MAX_VAL); + mDateSpinner.setOnValueChangedListener(mOnDateChangedListener); + + // 初始化小时选择器 + mHourSpinner = (NumberPicker) findViewById(R.id.hour); + mHourSpinner.setOnValueChangedListener(mOnHourChangedListener); + + // 初始化分钟选择器 + mMinuteSpinner = (NumberPicker) findViewById(R.id.minute); + mMinuteSpinner.setMinValue(MINUT_SPINNER_MIN_VAL); + mMinuteSpinner.setMaxValue(MINUT_SPINNER_MAX_VAL); + mMinuteSpinner.setOnLongPressUpdateInterval(100); // 设置长按滚动间隔(毫秒) + mMinuteSpinner.setOnValueChangedListener(mOnMinuteChangedListener); + + // 初始化上下午选择器(加载AM/PM字符串) + String[] stringsForAmPm = new DateFormatSymbols().getAmPmStrings(); + mAmPmSpinner = (NumberPicker) findViewById(R.id.amPm); + mAmPmSpinner.setMinValue(AMPM_SPINNER_MIN_VAL); + mAmPmSpinner.setMaxValue(AMPM_SPINNER_MAX_VAL); + mAmPmSpinner.setDisplayedValues(stringsForAmPm); + mAmPmSpinner.setOnValueChangedListener(mOnAmPmChangedListener); + + // 更新组件到初始状态 + updateDateControl(); + updateHourControl(); + updateAmPmControl(); + + set24HourView(is24HourView); // 设置时间显示模式 + + // 设置当前时间 + setCurrentDate(date); + + setEnabled(isEnabled()); // 应用启用状态 + + // 清除初始化标志 + mInitialising = false; + } + + // 设置组件启用状态(覆盖父类方法) + @Override + public void setEnabled(boolean enabled) { + if (mIsEnabled == enabled) { + return; + } + super.setEnabled(enabled); + // 控制各选择器的启用状态 + mDateSpinner.setEnabled(enabled); + mMinuteSpinner.setEnabled(enabled); + mHourSpinner.setEnabled(enabled); + mAmPmSpinner.setEnabled(enabled); + mIsEnabled = enabled; + } + + // 获取组件启用状态 + @Override + public boolean isEnabled() { + return mIsEnabled; + } + + /** + * 获取当前选择的时间戳 + * @return 时间戳(毫秒) + */ + public long getCurrentDateInTimeMillis() { + return mDate.getTimeInMillis(); + } + + /** + * 设置当前时间(通过时间戳) + * @param date 时间戳(毫秒) + */ + public void setCurrentDate(long date) { + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(date); + // 分解时间组件并设置 + setCurrentYear(cal.get(Calendar.YEAR)); + setCurrentMonth(cal.get(Calendar.MONTH)); + setCurrentDay(cal.get(Calendar.DAY_OF_MONTH)); + setCurrentHour(cal.get(Calendar.HOUR_OF_DAY)); + setCurrentMinute(cal.get(Calendar.MINUTE)); + } + + /** + * 设置当前时间(通过年月日时分) + * @param year 年份 + * @param month 月份(0-11) + * @param dayOfMonth 日期(1-31) + * @param hourOfDay 小时(24小时制,0-23) + * @param minute 分钟(0-59) + */ + public void setCurrentDate(int year, int month, + int dayOfMonth, int hourOfDay, int minute) { + setCurrentYear(year); + setCurrentMonth(month); + setCurrentDay(dayOfMonth); + setCurrentHour(hourOfDay); + setCurrentMinute(minute); + } + + // 获取当前年份 + public int getCurrentYear() { + return mDate.get(Calendar.YEAR); + } + + // 设置当前年份 + public void setCurrentYear(int year) { + if (!mInitialising && year == getCurrentYear()) { + return; + } + mDate.set(Calendar.YEAR, year); + updateDateControl(); // 更新日期选择器显示 + onDateTimeChanged(); // 触发变更回调 + } + + // 获取当前月份(0-11) + public int getCurrentMonth() { + return mDate.get(Calendar.MONTH); + } + + // 设置当前月份(0-11) + public void setCurrentMonth(int month) { + if (!mInitialising && month == getCurrentMonth()) { + return; + } + mDate.set(Calendar.MONTH, month); + updateDateControl(); // 更新日期选择器显示 + onDateTimeChanged(); // 触发变更回调 + } + + // 获取当前日期(1-31) + public int getCurrentDay() { + return mDate.get(Calendar.DAY_OF_MONTH); + } + + // 设置当前日期(1-31) + public void setCurrentDay(int dayOfMonth) { + if (!mInitialising && dayOfMonth == getCurrentDay()) { + return; + } + mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); + updateDateControl(); // 更新日期选择器显示 + onDateTimeChanged(); // 触发变更回调 + } + + // 获取当前小时(24小时制,0-23) + public int getCurrentHourOfDay() { + return mDate.get(Calendar.HOUR_OF_DAY); + } + + // 获取当前小时(根据显示模式转换后的小时值) + private int getCurrentHour() { + if (mIs24HourView) { + return getCurrentHourOfDay(); + } else { + int hour = getCurrentHourOfDay(); + // 12小时制转换:0点→12 AM,13点→1 PM,依此类推 + return hour == 0 ? HOURS_IN_HALF_DAY : (hour > HOURS_IN_HALF_DAY ? hour - HOURS_IN_HALF_DAY : hour); + } + } + + /** + * 设置当前小时(24小时制,0-23) + * @param hourOfDay 小时(24小时制) + */ + public void setCurrentHour(int hourOfDay) { + if (!mInitialising && hourOfDay == getCurrentHourOfDay()) { + return; + } + mDate.set(Calendar.HOUR_OF_DAY, hourOfDay); + if (!mIs24HourView) { // 12小时制下调整显示值 + if (hourOfDay >= HOURS_IN_HALF_DAY) { + mIsAm = false; + if (hourOfDay > HOURS_IN_HALF_DAY) { + hourOfDay -= HOURS_IN_HALF_DAY; // 转换为12小时制显示值 + } + } else { + mIsAm = true; + if (hourOfDay == 0) { + hourOfDay = HOURS_IN_HALF_DAY; // 0点显示为12 AM + } + } + updateAmPmControl(); // 更新上下午选择器显示 + } + mHourSpinner.setValue(hourOfDay); // 设置小时选择器值 + onDateTimeChanged(); // 触发变更回调 + } + + // 获取当前分钟(0-59) + public int getCurrentMinute() { + return mDate.get(Calendar.MINUTE); + } + + // 设置当前分钟(0-59) + public void setCurrentMinute(int minute) { + if (!mInitialising && minute == getCurrentMinute()) { + return; + } + mMinuteSpinner.setValue(minute); // 设置分钟选择器值 + mDate.set(Calendar.MINUTE, minute); // 更新日历对象 + onDateTimeChanged(); // 触发变更回调 + } + + // 获取是否为24小时制模式 + public boolean is24HourView() { + return mIs24HourView; + } + + /** + * 设置时间显示模式(24小时制或12小时制) + * @param is24HourView true为24小时制,false为12小时制 + */ + public void set24HourView(boolean is24HourView) { + if (mIs24HourView == is24HourView) { + return; + } + mIs24HourView = is24HourView; + // 控制上下午选择器的可见性 + mAmPmSpinner.setVisibility(is24HourView ? View.GONE : View.VISIBLE); + int hour = getCurrentHourOfDay(); // 获取当前小时(24小时制) + updateHourControl(); // 更新小时选择器的范围 + setCurrentHour(hour); // 重新设置小时(触发显示更新) + updateAmPmControl(); // 更新上下午选择器状态 + } + + /** + * 更新日期选择器的显示内容(显示一周内的日期) + * 逻辑:以当前日期为中心,显示前3天、当前天、后3天 + */ + private void updateDateControl() { + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(mDate.getTimeInMillis()); + // 计算起始日期(当前日期前3天) + 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); // 逐日递增 + // 格式化为"MM.dd EEEE"(如"10.25 星期二") + mDateDisplayValues[i] = (String) DateFormat.format("MM.dd EEEE", cal); + } + mDateSpinner.setDisplayedValues(mDateDisplayValues); // 设置显示值数组 + mDateSpinner.setValue(DAYS_IN_ALL_WEEK / 2); // 设置当前日期居中显示(索引3) + mDateSpinner.invalidate(); // 刷新选择器显示 + } + + // 更新上下午选择器的状态(同步显示AM/PM) + private void updateAmPmControl() { + if (mIs24HourView) { + mAmPmSpinner.setVisibility(View.GONE); // 隐藏上下午选择器 + } else { + int index = mIsAm ? Calendar.AM : Calendar.PM; // 根据状态获取AM/PM索引 + mAmPmSpinner.setValue(index); // 设置选择器值 + mAmPmSpinner.setVisibility(View.VISIBLE); // 显示上下午选择器 + } + } + + // 更新小时选择器的范围(根据24小时制/12小时制模式) + private void updateHourControl() { + if (mIs24HourView) { + mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW); + mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW); + } else { + mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW); + mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW); + } + } + + // 设置日期时间变更监听器 + public void setOnDateTimeChangedListener(OnDateTimeChangedListener callback) { + mOnDateTimeChangedListener = callback; + } + + // 触发日期时间变更回调(通知监听器) + private void onDateTimeChanged() { + if (mOnDateTimeChangedListener != null) { + mOnDateTimeChangedListener.onDateTimeChanged(this, getCurrentYear(), + getCurrentMonth(), getCurrentDay(), getCurrentHourOfDay(), getCurrentMinute()); + } + } +} \ No newline at end of file From 422e705b705333ec6c68a614ca26c1dd56fe47cc Mon Sep 17 00:00:00 2001 From: moneynewmoon <2075698549@qq.com> Date: Thu, 29 May 2025 16:16:23 +0800 Subject: [PATCH 7/7] all --- GTaskClient.java => gtask/GTaskClient.java | 0 GTaskManager.java => gtask/GTaskManager.java | 0 GTaskSyncService.java => gtask/GTaskSyncService.java | 0 Note.java => model/Note.java | 0 WorkingNote.java => model/WorkingNote.java | 0 BackupUtils.java => tool/BackupUtils.java | 0 DataUtils.java => tool/DataUtils.java | 0 GTaskStringUtils.java => tool/GTaskStringUtils.java | 0 ResourceParser.java => tool/ResourceParser.java | 0 AlarmAlertActivity.java => ui/AlarmAlertActivity.java | 0 AlarmInitReceiver.java => ui/AlarmInitReceiver.java | 0 AlarmReceiver.java => ui/AlarmReceiver.java | 0 DateTimePicker.java => ui/DateTimePicker.java | 0 对对都.txt | 0 14 files changed, 0 insertions(+), 0 deletions(-) rename GTaskClient.java => gtask/GTaskClient.java (100%) rename GTaskManager.java => gtask/GTaskManager.java (100%) rename GTaskSyncService.java => gtask/GTaskSyncService.java (100%) rename Note.java => model/Note.java (100%) rename WorkingNote.java => model/WorkingNote.java (100%) rename BackupUtils.java => tool/BackupUtils.java (100%) rename DataUtils.java => tool/DataUtils.java (100%) rename GTaskStringUtils.java => tool/GTaskStringUtils.java (100%) rename ResourceParser.java => tool/ResourceParser.java (100%) rename AlarmAlertActivity.java => ui/AlarmAlertActivity.java (100%) rename AlarmInitReceiver.java => ui/AlarmInitReceiver.java (100%) rename AlarmReceiver.java => ui/AlarmReceiver.java (100%) rename DateTimePicker.java => ui/DateTimePicker.java (100%) delete mode 100644 对对都.txt diff --git a/GTaskClient.java b/gtask/GTaskClient.java similarity index 100% rename from GTaskClient.java rename to gtask/GTaskClient.java diff --git a/GTaskManager.java b/gtask/GTaskManager.java similarity index 100% rename from GTaskManager.java rename to gtask/GTaskManager.java diff --git a/GTaskSyncService.java b/gtask/GTaskSyncService.java similarity index 100% rename from GTaskSyncService.java rename to gtask/GTaskSyncService.java diff --git a/Note.java b/model/Note.java similarity index 100% rename from Note.java rename to model/Note.java diff --git a/WorkingNote.java b/model/WorkingNote.java similarity index 100% rename from WorkingNote.java rename to model/WorkingNote.java diff --git a/BackupUtils.java b/tool/BackupUtils.java similarity index 100% rename from BackupUtils.java rename to tool/BackupUtils.java diff --git a/DataUtils.java b/tool/DataUtils.java similarity index 100% rename from DataUtils.java rename to tool/DataUtils.java diff --git a/GTaskStringUtils.java b/tool/GTaskStringUtils.java similarity index 100% rename from GTaskStringUtils.java rename to tool/GTaskStringUtils.java diff --git a/ResourceParser.java b/tool/ResourceParser.java similarity index 100% rename from ResourceParser.java rename to tool/ResourceParser.java diff --git a/AlarmAlertActivity.java b/ui/AlarmAlertActivity.java similarity index 100% rename from AlarmAlertActivity.java rename to ui/AlarmAlertActivity.java diff --git a/AlarmInitReceiver.java b/ui/AlarmInitReceiver.java similarity index 100% rename from AlarmInitReceiver.java rename to ui/AlarmInitReceiver.java diff --git a/AlarmReceiver.java b/ui/AlarmReceiver.java similarity index 100% rename from AlarmReceiver.java rename to ui/AlarmReceiver.java diff --git a/DateTimePicker.java b/ui/DateTimePicker.java similarity index 100% rename from DateTimePicker.java rename to ui/DateTimePicker.java diff --git a/对对都.txt b/对对都.txt deleted file mode 100644 index e69de29..0000000