From 27ccd09e78611578d78673e02e7b18a2cf34a182 Mon Sep 17 00:00:00 2001 From: xxy <812411654@qq.com> Date: Tue, 31 Dec 2024 00:21:54 +0800 Subject: [PATCH] 1 --- ActionFailureException.java | 52 +++ AlarmAlertActivity.java | 163 +++++++ AlarmInitReceiver.java | 81 ++++ AlarmReceiver.java | 47 ++ BackupUtils.java | 297 +++++++++++++ DataUtils.java | 311 ++++++++++++++ DateTimePicker.java | 437 +++++++++++++++++++ DateTimePickerDialog.java | 138 ++++++ DropdownMenu.java | 94 ++++ FoldersListAdapter.java | 125 ++++++ GTaskASyncTask.java | 174 ++++++++ GTaskClient.java | 288 +++++++++++++ GTaskManager.java | 805 +++++++++++++++++++++++++++++++++++ GTaskStringUtils.java | 76 ++++ GTaskSyncService.java | 198 +++++++++ MetaData.java | 90 ++++ NetworkFailureException.java | 52 +++ Node.java | 108 +++++ Note.java | 265 ++++++++++++ NoteEditActivity.java | 265 ++++++++++++ NoteEditText.java | 269 ++++++++++++ NoteItemData.java | 255 +++++++++++ NoteWidgetProvider.java | 163 +++++++ NoteWidgetProvider_2x.java | 55 +++ NoteWidgetProvider_4x.java | 55 +++ NotesDatabaseHelper.java | 283 ++++++++++++ NotesListActivity.java | 230 ++++++++++ NotesListAdapter.java | 257 +++++++++++ NotesListItem.java | 159 +++++++ NotesPreferenceActivity.java | 429 +++++++++++++++++++ NotesProvider.java | 354 +++++++++++++++ ResourceParser.java | 210 +++++++++ SqlData.java | 194 +++++++++ SqlNote.java | 505 ++++++++++++++++++++++ Task.java | 334 +++++++++++++++ TaskList.java | 351 +++++++++++++++ WorkingNote.java | 254 +++++++++++ 37 files changed, 8423 insertions(+) create mode 100644 ActionFailureException.java create mode 100644 AlarmAlertActivity.java create mode 100644 AlarmInitReceiver.java create mode 100644 AlarmReceiver.java create mode 100644 BackupUtils.java create mode 100644 DataUtils.java create mode 100644 DateTimePicker.java create mode 100644 DateTimePickerDialog.java create mode 100644 DropdownMenu.java create mode 100644 FoldersListAdapter.java create mode 100644 GTaskASyncTask.java create mode 100644 GTaskClient.java create mode 100644 GTaskManager.java create mode 100644 GTaskStringUtils.java create mode 100644 GTaskSyncService.java create mode 100644 MetaData.java create mode 100644 NetworkFailureException.java create mode 100644 Node.java create mode 100644 Note.java create mode 100644 NoteEditActivity.java create mode 100644 NoteEditText.java create mode 100644 NoteItemData.java create mode 100644 NoteWidgetProvider.java create mode 100644 NoteWidgetProvider_2x.java create mode 100644 NoteWidgetProvider_4x.java create mode 100644 NotesDatabaseHelper.java create mode 100644 NotesListActivity.java create mode 100644 NotesListAdapter.java create mode 100644 NotesListItem.java create mode 100644 NotesPreferenceActivity.java create mode 100644 NotesProvider.java create mode 100644 ResourceParser.java create mode 100644 SqlData.java create mode 100644 SqlNote.java create mode 100644 Task.java create mode 100644 TaskList.java create mode 100644 WorkingNote.java diff --git a/ActionFailureException.java b/ActionFailureException.java new file mode 100644 index 0000000..7af63a6 --- /dev/null +++ b/ActionFailureException.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.gtask.exception; + +/** + * ActionFailureException 类是用于表示操作失败时抛出的异常。 + * 它继承自 RuntimeException,因此它是一个非检查型异常(unchecked exception), + * 这意味着在方法签名中不需要声明此异常可能会被抛出。 + */ +public class ActionFailureException extends RuntimeException { + + // 序列化版本唯一标识符,用于确保类的不同版本之间的兼容性。 + private static final long serialVersionUID = 4425249765923293627L; + + /** + * 构造一个没有详细信息消息和原因的 ActionFailureException。 + */ + public ActionFailureException() { + super(); + } + + /** + * 使用指定的详细信息消息构造一个新的 ActionFailureException。 + * @param paramString 提供更多关于异常的信息的消息字符串。 + */ + public ActionFailureException(String paramString) { + super(paramString); + } + + /** + * 使用指定的详细信息消息和原因构造一个新的 ActionFailureException。 + * @param paramString 提供更多关于异常的信息的消息字符串。 + * @param paramThrowable 导致当前异常的底层原因。 + */ + public ActionFailureException(String paramString, Throwable paramThrowable) { + super(paramString, paramThrowable); + } +} \ No newline at end of file diff --git a/AlarmAlertActivity.java b/AlarmAlertActivity.java new file mode 100644 index 0000000..2a32b20 --- /dev/null +++ b/AlarmAlertActivity.java @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.DialogInterface.OnDismissListener; +import android.content.Intent; +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.media.RingtoneManager; +import android.net.Uri; +import android.os.Bundle; +import android.os.PowerManager; +import android.provider.Settings; +import android.view.Window; +import android.view.WindowManager; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.tool.DataUtils; + +import java.io.IOException; + +public class AlarmAlertActivity extends Activity implements OnClickListener, OnDismissListener { + private long mNoteId; // 存储笔记的 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); + } + + Intent intent = getIntent(); // 获取传递进来的 Intent + + try { + mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1)); // 获取笔记的 ID + // 获取笔记的简短摘要,如果摘要太长,截取并附加说明 + mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId); + mSnippet = mSnippet.length() > SNIPPET_PREW_MAX_LEN ? mSnippet.substring(0, + SNIPPET_PREW_MAX_LEN) + getResources().getString(R.string.notelist_string_info) + : mSnippet; + } catch (IllegalArgumentException e) { + e.printStackTrace(); + return; // 如果获取笔记信息失败,则返回 + } + + mPlayer = new MediaPlayer(); // 初始化媒体播放器 + // 如果笔记在数据库中是可见的,显示对话框并播放闹铃音 + if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) { + showActionDialog(); + playAlarmSound(); + } else { + finish(); // 如果笔记不可见,则结束此活动 + } + } + + // 检查屏幕是否开启 + private boolean isScreenOn() { + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + return pm.isScreenOn(); + } + + // 播放闹铃音 + private void playAlarmSound() { + // 获取系统的默认闹铃音 URI + Uri url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM); + + // 获取当前铃声模式,检查闹铃是否会被静音 + int silentModeStreams = Settings.System.getInt(getContentResolver(), + Settings.System.MODE_RINGER_STREAMS_AFFECTED, 0); + + // 根据静音模式设置音频流类型 + if ((silentModeStreams & (1 << AudioManager.STREAM_ALARM)) != 0) { + mPlayer.setAudioStreamType(silentModeStreams); + } else { + mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM); // 设置音频流类型为闹铃 + } + try { + // 设置闹铃音频数据源,并准备播放 + mPlayer.setDataSource(this, url); + mPlayer.prepare(); + mPlayer.setLooping(true); // 设置闹铃音循环播放 + mPlayer.start(); // 开始播放 + } catch (IllegalArgumentException | SecurityException | IllegalStateException | 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); // 显示对话框并设置监听器 + } + + // 处理按钮点击事件 + 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; + } + } + + // 对话框关闭时停止播放闹铃音 + public void onDismiss(DialogInterface dialog) { + stopAlarmSound(); + finish(); // 结束当前活动 + } + + // 停止播放闹铃音 + private void stopAlarmSound() { + if (mPlayer != null) { + mPlayer.stop(); // 停止播放 + mPlayer.release(); // 释放播放器资源 + mPlayer = null; // 清空播放器对象 + } + } +} diff --git a/AlarmInitReceiver.java b/AlarmInitReceiver.java new file mode 100644 index 0000000..948bd00 --- /dev/null +++ b/AlarmInitReceiver.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.app.AlarmManager; // 引入AlarmManager,用于设置系统闹铃 +import android.app.PendingIntent; // 引入PendingIntent,用于延迟执行的操作 +import android.content.BroadcastReceiver; // 引入广播接收器,监听系统广播 +import android.content.ContentUris; // 引入ContentUris,用于构造URI +import android.content.Context; // 引入Context,用于访问应用环境 +import android.content.Intent; // 引入Intent,用于启动其他活动或服务 +import android.database.Cursor; // 引入Cursor,用于查询数据库结果 + +import net.micode.notes.data.Notes; // 引入Notes类,表示笔记相关的数据模型 +import net.micode.notes.data.Notes.NoteColumns; // 引入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; // 表示警报日期字段的索引 + + @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, // 查询条件:警报日期大于当前时间,且类型是笔记 + new String[] { String.valueOf(currentDate) }, // 当前时间作为查询条件 + null); // 排序方式为空 + + // 如果查询结果不为空 + if (c != null) { + // 如果数据库中存在符合条件的记录,遍历每条记录 + if (c.moveToFirst()) { + do { + long alertDate = c.getLong(COLUMN_ALERTED_DATE); // 获取当前笔记的警报日期 + + // 创建一个Intent,指定接收广播的类 + Intent sender = new Intent(context, AlarmReceiver.class); + // 设置Intent的Data为当前笔记的URI + sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, c.getLong(COLUMN_ID))); + + // 创建一个PendingIntent,用于延迟发送广播 + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, 0); + + // 获取系统的AlarmManager服务 + AlarmManager alermManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + + // 设置闹铃,使用RTC_WAKEUP触发闹铃,唤醒设备并在指定时间触发广播 + alermManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent); + + } while (c.moveToNext()); // 继续处理下一条记录 + } + + // 关闭游标,释放数据库资源 + c.close(); + } + } +} diff --git a/AlarmReceiver.java b/AlarmReceiver.java new file mode 100644 index 0000000..07f54e6 --- /dev/null +++ b/AlarmReceiver.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +/** + * AlarmReceiver 类接收并处理广播消息。 + * 当接收到某个广播时,它会启动 AlarmAlertActivity。 + */ +public class AlarmReceiver extends BroadcastReceiver { + + /** + * 当接收到广播时,执行该方法。 + * 该方法会启动 AlarmAlertActivity,显示提醒内容。 + * + * @param context 当前的上下文对象 + * @param intent 接收到的广播消息意图 + */ + @Override + public void onReceive(Context context, Intent intent) { + // 设置 intent 的目标活动为 AlarmAlertActivity + intent.setClass(context, AlarmAlertActivity.class); + + // 添加 FLAG 来启动新的任务栈,这样 AlarmAlertActivity 会作为一个新的活动启动 + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + // 启动 AlarmAlertActivity + context.startActivity(intent); + } +} diff --git a/BackupUtils.java b/BackupUtils.java new file mode 100644 index 0000000..7c89f71 --- /dev/null +++ b/BackupUtils.java @@ -0,0 +1,297 @@ +/* + * 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.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; + +public class BackupUtils { + private static final String TAG = "BackupUtils"; + // 单例模式,确保只有一个BackupUtils实例 + private static BackupUtils sInstance; + + // 获取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; + + // 构造函数,初始化TextExport类 + private BackupUtils(Context context) { + mTextExport = new TextExport(context); + } + + // 检查外部存储(SD卡)是否可用 + private static boolean externalStorageAvailable() { + return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()); + } + + // 导出笔记为文本格式 + public int exportToText() { + return mTextExport.exportToText(); + } + + // 获取导出的文本文件名 + public String getExportedTextFileName() { + return mTextExport.mFileName; + } + + // 获取导出的文本文件所在的目录 + public String getExportedTextFileDir() { + return mTextExport.mFileDirectory; + } + + // 内部类,用于处理笔记的文本导出 + private static class TextExport { + private static final String[] NOTE_PROJECTION = { + NoteColumns.ID, + NoteColumns.MODIFIED_DATE, + NoteColumns.SNIPPET, + NoteColumns.TYPE + }; + + private static final int NOTE_COLUMN_ID = 0; + private static final int NOTE_COLUMN_MODIFIED_DATE = 1; + private static final int NOTE_COLUMN_SNIPPET = 2; + + private static final String[] DATA_PROJECTION = { + DataColumns.CONTENT, + DataColumns.MIME_TYPE, + DataColumns.DATA1, + DataColumns.DATA2, + DataColumns.DATA3, + DataColumns.DATA4, + }; + + private static final int DATA_COLUMN_CONTENT = 0; + private static final int DATA_COLUMN_MIME_TYPE = 1; + private static final int DATA_COLUMN_CALL_DATE = 2; + private static final int DATA_COLUMN_PHONE_NUMBER = 4; + + 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; + + // 构造函数,初始化格式化字符串和上下文 + public TextExport(Context context) { + TEXT_FORMAT = context.getResources().getStringArray(R.array.format_for_exported_note); + mContext = context; + mFileName = ""; + mFileDirectory = ""; + } + + // 根据格式ID获取相应的格式化字符串 + private String getFormat(int id) { + return TEXT_FORMAT[id]; + } + + /** + * 将指定文件夹内的笔记导出为文本格式 + * @param folderId 文件夹的ID + * @param ps 输出流,用于写入导出的数据 + */ + private void exportFolderToText(String folderId, PrintStream ps) { + // 查询属于该文件夹的笔记 + Cursor notesCursor = mContext.getContentResolver().query(Notes.CONTENT_NOTE_URI, + NOTE_PROJECTION, NoteColumns.PARENT_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), + notesCursor.getLong(NOTE_COLUMN_MODIFIED_DATE)))); + // 查询与该笔记相关的数据 + String noteId = notesCursor.getString(NOTE_COLUMN_ID); + exportNoteToText(noteId, ps); + } while (notesCursor.moveToNext()); + } + notesCursor.close(); + } + } + + /** + * 将指定笔记的内容导出为文本格式 + * @param noteId 笔记的ID + * @param ps 输出流,用于写入导出的数据 + */ + private void exportNoteToText(String noteId, PrintStream ps) { + // 查询与该笔记相关的数据 + Cursor dataCursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI, + DATA_PROJECTION, DataColumns.NOTE_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()); + } + } + + /** + * 将笔记导出为用户可读的文本格式 + */ + public int exportToText() { + 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_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 = ""; + if(folderCursor.getLong(NOTE_COLUMN_ID) == Notes.ID_CALL_RECORD_FOLDER) { + folderName = mContext.getString(R.string.call_record_folder_name); + } else { + folderName = folderCursor.getString(NOTE_COLUMN_SNIPPET); + } + if (!TextUtils.isEmpty(folderName)) { + ps.println(String.format(getFormat(FORMAT_FOLDER_NAME), folderName)); + exportFolderToText(folderCursor.getString(NOTE_COLUMN_ID), ps); + } + } while (folderCursor.moveToNext()); + } + folderCursor.close(); + } + + ps.close(); + return STATE_SUCCESS; + } + + // 获取导出文本的输出流 + private PrintStream getExportToTextPrintStream() { + // 获取文件路径 + String path = Environment.getExternalStorageDirectory() + "/notesBackup"; + File dir = new File(path); + if (!dir.exists()) { + if (!dir.mkdirs()) { + return null; + } + } + + // 设置输出文件路径 + mFileName = "NotesBackup_" + System.currentTimeMillis() + ".txt"; + mFileDirectory = path; + File file = new File(path, mFileName); + + try { + FileOutputStream fos = new FileOutputStream(file); + return new PrintStream(fos); + } catch (FileNotFoundException e) { + Log.e(TAG, e.toString()); + return null; + } + } + } +} diff --git a/DataUtils.java b/DataUtils.java new file mode 100644 index 0000000..72b923e --- /dev/null +++ b/DataUtils.java @@ -0,0 +1,311 @@ +/* + * 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.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"; + + // 批量删除笔记 + public static boolean batchDeleteNotes(ContentResolver resolver, HashSet ids) { + if (ids == null) { + Log.d(TAG, "the ids is null"); + return true; + } + if (ids.size() == 0) { + Log.d(TAG, "no id is in the hashset"); + return true; + } + + ArrayList operationList = new ArrayList(); + for (long id : ids) { + // 如果是根文件夹,则不允许删除 + if(id == Notes.ID_ROOT_FOLDER) { + Log.e(TAG, "Don't delete system folder root"); + continue; + } + // 构建删除操作 + ContentProviderOperation.Builder builder = ContentProviderOperation + .newDelete(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id)); + operationList.add(builder.build()); + } + try { + // 执行批量操作 + ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operationList); + if (results == null || results.length == 0 || results[0] == null) { + Log.d(TAG, "delete notes failed, ids:" + ids.toString()); + return false; + } + return true; + } catch (RemoteException e) { + Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); + } catch (OperationApplicationException e) { + Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); + } + return false; + } + + // 将笔记从一个文件夹移动到另一个文件夹 + public static void moveNoteToFoler(ContentResolver resolver, long id, long srcFolderId, long desFolderId) { + ContentValues values = new ContentValues(); + values.put(NoteColumns.PARENT_ID, desFolderId); // 设置目标文件夹ID + values.put(NoteColumns.ORIGIN_PARENT_ID, srcFolderId); // 设置原文件夹ID + values.put(NoteColumns.LOCAL_MODIFIED, 1); // 标记为已修改 + resolver.update(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id), values, null, null); + } + + // 批量将笔记移动到指定的文件夹 + public static boolean batchMoveToFolder(ContentResolver resolver, HashSet ids, long folderId) { + if (ids == null) { + Log.d(TAG, "the ids is null"); + return true; + } + + ArrayList operationList = new ArrayList(); + for (long id : ids) { + // 构建更新操作 + ContentProviderOperation.Builder builder = ContentProviderOperation + .newUpdate(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id)); + builder.withValue(NoteColumns.PARENT_ID, folderId); // 设置目标文件夹ID + builder.withValue(NoteColumns.LOCAL_MODIFIED, 1); // 标记为已修改 + operationList.add(builder.build()); + } + + try { + // 执行批量操作 + ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operationList); + if (results == null || results.length == 0 || results[0] == null) { + Log.d(TAG, "move notes failed, ids:" + ids.toString()); + return false; + } + return true; + } catch (RemoteException e) { + Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); + } catch (OperationApplicationException e) { + Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); + } + return false; + } + + /** + * 获取所有非系统文件夹的数量 + */ + public static int getUserFolderCount(ContentResolver resolver) { + 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, "get folder count failed:" + e.toString()); + } finally { + cursor.close(); + } + } + } + return count; + } + + // 检查指定笔记是否存在并且可见(非回收站) + public static boolean visibleInNoteDatabase(ContentResolver resolver, long noteId, int type) { + 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) { + if (cursor.getCount() > 0) { + exist = true; + } + cursor.close(); + } + return exist; + } + + // 检查笔记是否存在 + public static boolean existInNoteDatabase(ContentResolver resolver, long noteId) { + Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), + null, null, null, null); + + boolean exist = false; + if (cursor != null) { + if (cursor.getCount() > 0) { + exist = true; + } + cursor.close(); + } + return exist; + } + + // 检查数据是否存在 + public static boolean existInDataDatabase(ContentResolver resolver, long dataId) { + Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), + null, null, null, null); + + boolean exist = false; + if (cursor != null) { + if (cursor.getCount() > 0) { + exist = true; + } + cursor.close(); + } + return exist; + } + + // 检查文件夹名称是否已存在 + 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) { + if (cursor.getCount() > 0) { + exist = true; + } + cursor.close(); + } + return exist; + } + + // 获取指定文件夹中的小部件(AppWidget)属性 + public static HashSet getFolderNoteWidget(ContentResolver resolver, long folderId) { + Cursor c = resolver.query(Notes.CONTENT_NOTE_URI, + new String[] { NoteColumns.WIDGET_ID, NoteColumns.WIDGET_TYPE }, + NoteColumns.PARENT_ID + "=?", + new String[] { String.valueOf(folderId) }, + null); + + HashSet set = null; + if (c != null) { + if (c.moveToFirst()) { + set = new HashSet(); + do { + try { + AppWidgetAttribute widget = new AppWidgetAttribute(); + widget.widgetId = c.getInt(0); + widget.widgetType = c.getInt(1); + set.add(widget); + } catch (IndexOutOfBoundsException e) { + Log.e(TAG, e.toString()); + } + } while (c.moveToNext()); + } + c.close(); + } + return set; + } + + // 根据笔记ID获取通话记录的电话号码 + public static String getCallNumberByNoteId(ContentResolver resolver, long noteId) { + 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, "Get call number fails " + e.toString()); + } finally { + cursor.close(); + } + } + return ""; + } + + // 根据电话号和通话日期获取对应的笔记ID + public static long getNoteIdByPhoneNumberAndCallDate(ContentResolver resolver, String phoneNumber, long callDate) { + Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI, + new String [] { CallNote.NOTE_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); + } catch (IndexOutOfBoundsException e) { + Log.e(TAG, "Get call note id fails " + e.toString()); + } + } + cursor.close(); + } + return 0; + } + + // 根据笔记ID获取摘要 + public static String getSnippetById(ContentResolver resolver, long noteId) { + Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI, + new String [] { NoteColumns.SNIPPET }, + NoteColumns.ID + "=?", + new String [] { String.valueOf(noteId)}, + null); + + if (cursor != null) { + String snippet = ""; + if (cursor.moveToFirst()) { + snippet = cursor.getString(0); + } + cursor.close(); + return snippet; + } + throw new IllegalArgumentException("Note is not found with id: " + noteId); + } + + // 格式化笔记摘要,去除多余的空格和换行 + 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; + } +} diff --git a/DateTimePicker.java b/DateTimePicker.java new file mode 100644 index 0000000..8a6843e --- /dev/null +++ b/DateTimePicker.java @@ -0,0 +1,437 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import java.text.DateFormatSymbols; +import java.util.Calendar; + +import net.micode.notes.R; + +import android.content.Context; +import android.text.format.DateFormat; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.NumberPicker; + +/** + * 自定义的日期时间选择器组件。 + * 提供日期、小时、分钟和AM/PM的选择功能。 + */ +public class DateTimePicker extends FrameLayout { + + // 常量定义,用于限制控件选择的值 + private static final boolean DEFAULT_ENABLE_STATE = true; + private static final int HOURS_IN_HALF_DAY = 12; // 半天小时数 + private static final int HOURS_IN_ALL_DAY = 24; // 一天的小时数 + private static final int DAYS_IN_ALL_WEEK = 7; // 一周的天数 + private static final int DATE_SPINNER_MIN_VAL = 0; // 日期选择器最小值 + private static final int DATE_SPINNER_MAX_VAL = DAYS_IN_ALL_WEEK - 1; // 日期选择器最大值 + 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/PM选择器最小值 + private static final int AMPM_SPINNER_MAX_VAL = 1; // AM/PM选择器最大值 + + // 控件定义 + private final NumberPicker mDateSpinner; // 日期选择器 + private final NumberPicker mHourSpinner; // 小时选择器 + private final NumberPicker mMinuteSpinner; // 分钟选择器 + private final NumberPicker mAmPmSpinner; // AM/PM选择器 + private Calendar mDate; // 存储当前日期和时间的 Calendar 对象 + + // 日期显示的值数组 + private String[] mDateDisplayValues = new String[DAYS_IN_ALL_WEEK]; + + // 当前是 AM 还是 PM + private boolean mIsAm; + + // 是否为 24 小时制 + private boolean mIs24HourView; + + // 控件是否可用 + private boolean mIsEnabled = DEFAULT_ENABLE_STATE; + + // 初始化状态 + private boolean mInitialising; + + // 日期时间变化监听器 + private OnDateTimeChangedListener mOnDateTimeChangedListener; + + // 日期选择器值变化监听器 + private NumberPicker.OnValueChangeListener mOnDateChangedListener = new NumberPicker.OnValueChangeListener() { + @Override + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + // 更新日期 + mDate.add(Calendar.DAY_OF_YEAR, newVal - oldVal); + updateDateControl(); // 更新日期选择器 + onDateTimeChanged(); // 通知日期时间变化 + } + }; + + // 小时选择器值变化监听器 + private NumberPicker.OnValueChangeListener mOnHourChangedListener = new NumberPicker.OnValueChangeListener() { + @Override + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + boolean isDateChanged = false; + Calendar cal = Calendar.getInstance(); + if (!mIs24HourView) { + // 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); + isDateChanged = true; + } else if (mIsAm && oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) { + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, -1); + isDateChanged = true; + } + if (oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY || + oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) { + mIsAm = !mIsAm; + updateAmPmControl(); // 更新AM/PM选择器 + } + } else { + // 24小时制模式下,处理日期变化 + if (oldVal == HOURS_IN_ALL_DAY - 1 && newVal == 0) { + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, 1); + isDateChanged = true; + } else if (oldVal == 0 && newVal == HOURS_IN_ALL_DAY - 1) { + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, -1); + isDateChanged = true; + } + } + // 更新小时 + int newHour = mHourSpinner.getValue() % HOURS_IN_HALF_DAY + (mIsAm ? 0 : HOURS_IN_HALF_DAY); + mDate.set(Calendar.HOUR_OF_DAY, newHour); + onDateTimeChanged(); // 通知日期时间变化 + // 如果日期发生变化,更新年份、月份、日期 + if (isDateChanged) { + setCurrentYear(cal.get(Calendar.YEAR)); + setCurrentMonth(cal.get(Calendar.MONTH)); + setCurrentDay(cal.get(Calendar.DAY_OF_MONTH)); + } + } + }; + + // 分钟选择器值变化监听器 + private NumberPicker.OnValueChangeListener mOnMinuteChangedListener = new NumberPicker.OnValueChangeListener() { + @Override + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + // 更新分钟 + 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; + // 根据AM/PM的选择,调整小时 + if (mIsAm) { + mDate.add(Calendar.HOUR_OF_DAY, -HOURS_IN_HALF_DAY); + } else { + mDate.add(Calendar.HOUR_OF_DAY, HOURS_IN_HALF_DAY); + } + updateAmPmControl(); // 更新AM/PM控制 + 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小时制 + public DateTimePicker(Context context, long date, boolean is24HourView) { + super(context); + mDate = Calendar.getInstance(); + mInitialising = true; + mIsAm = getCurrentHourOfDay() >= HOURS_IN_HALF_DAY; + 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(); + + // 设置24小时制模式 + 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; + } + + // 获取当前日期的时间戳 + public long getCurrentDateInTimeMillis() { + return mDate.getTimeInMillis(); + } + + // 设置当前日期时间 + public void setCurrentDate(long date) { + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(date); + setCurrentDate(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH), + cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE)); + } + + // 设置当前日期和时间 + 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(); + } + + // 获取当前月份 + public int getCurrentMonth() { + return mDate.get(Calendar.MONTH); + } + + // 设置当前月份 + public void setCurrentMonth(int month) { + if (!mInitialising && month == getCurrentMonth()) { + return; + } + mDate.set(Calendar.MONTH, month); + updateDateControl(); + onDateTimeChanged(); + } + + // 获取当前日期 + public int getCurrentDay() { + return mDate.get(Calendar.DAY_OF_MONTH); + } + + // 设置当前日期 + public void setCurrentDay(int dayOfMonth) { + if (!mInitialising && dayOfMonth == getCurrentDay()) { + return; + } + mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); + updateDateControl(); + onDateTimeChanged(); + } + + // 获取当前小时(24小时制) + public int getCurrentHourOfDay() { + return mDate.get(Calendar.HOUR_OF_DAY); + } + + // 获取当前小时(12小时制或24小时制) + private int getCurrentHour() { + if (mIs24HourView){ + return getCurrentHourOfDay(); + } else { + int hour = getCurrentHourOfDay(); + if (hour > HOURS_IN_HALF_DAY) { + return hour - HOURS_IN_HALF_DAY; + } else { + return hour == 0 ? HOURS_IN_HALF_DAY : hour; + } + } + } + + // 设置当前小时(24小时制) + public void setCurrentHour(int hourOfDay) { + if (!mInitialising && hourOfDay == getCurrentHourOfDay()) { + return; + } + mDate.set(Calendar.HOUR_OF_DAY, hourOfDay); + if (!mIs24HourView) { + if (hourOfDay >= HOURS_IN_HALF_DAY) { + mIsAm = false; + if (hourOfDay > HOURS_IN_HALF_DAY) { + hourOfDay -= HOURS_IN_HALF_DAY; + } + } else { + mIsAm = true; + if (hourOfDay == 0) { + hourOfDay = HOURS_IN_HALF_DAY; + } + } + updateAmPmControl(); + } + mHourSpinner.setValue(hourOfDay); + onDateTimeChanged(); + } + + // 获取当前分钟 + public int getCurrentMinute() { + return mDate.get(Calendar.MINUTE); + } + + // 设置当前分钟 + 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小时制或AM/PM制 + public void set24HourView(boolean is24HourView) { + if (mIs24HourView == is24HourView) { + return; + } + mIs24HourView = is24HourView; + mAmPmSpinner.setVisibility(is24HourView ? View.GONE : View.VISIBLE); + int hour = getCurrentHourOfDay(); + updateHourControl(); + setCurrentHour(hour); + updateAmPmControl(); + } + + // 更新日期选择器的显示内容 + private void updateDateControl() { + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, -DAYS_IN_ALL_WEEK / 2 - 1); + mDateSpinner.setDisplayedValues(null); + for (int i = 0; i < DAYS_IN_ALL_WEEK; ++i) { + cal.add(Calendar.DAY_OF_YEAR, 1); + mDateDisplayValues[i] = (String) DateFormat.format("MM.dd EEEE", cal); + } + mDateSpinner.setDisplayedValues(mDateDisplayValues); + mDateSpinner.setValue(DAYS_IN_ALL_WEEK / 2); + mDateSpinner.invalidate(); + } + + // 更新AM/PM选择器 + private void updateAmPmControl() { + if (mIs24HourView) { + mAmPmSpinner.setVisibility(View.GONE); + } else { + int index = mIsAm ? Calendar.AM : Calendar.PM; + mAmPmSpinner.setValue(index); + mAmPmSpinner.setVisibility(View.VISIBLE); + } + } + + // 更新小时选择器 + 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()); + } + } +} diff --git a/DateTimePickerDialog.java b/DateTimePickerDialog.java new file mode 100644 index 0000000..18fcc01 --- /dev/null +++ b/DateTimePickerDialog.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import java.util.Calendar; +import net.micode.notes.R; +import net.micode.notes.ui.DateTimePicker; +import net.micode.notes.ui.DateTimePicker.OnDateTimeChangedListener; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.text.format.DateFormat; +import android.text.format.DateUtils; + +/** + * 自定义的日期时间选择对话框。 + * 用于选择日期和时间,支持24小时制和12小时制显示。 + */ +public class DateTimePickerDialog extends AlertDialog implements OnClickListener { + + // 当前选择的日期和时间 + private Calendar mDate = Calendar.getInstance(); + // 是否为24小时制 + private boolean mIs24HourView; + // 日期时间选择回调 + private OnDateTimeSetListener mOnDateTimeSetListener; + // 日期时间选择器控件 + private DateTimePicker mDateTimePicker; + + /** + * 日期时间设置回调接口。 + * 当用户选择完日期时间后,回调此方法。 + */ + public interface OnDateTimeSetListener { + void OnDateTimeSet(AlertDialog dialog, long date); + } + + /** + * 构造函数,初始化日期时间选择对话框。 + * @param context 上下文 + * @param date 初始日期时间 + */ + public DateTimePickerDialog(Context context, long date) { + super(context); + + // 创建 DateTimePicker 控件,并设置监听器 + mDateTimePicker = new DateTimePicker(context); + setView(mDateTimePicker); + mDateTimePicker.setOnDateTimeChangedListener(new OnDateTimeChangedListener() { + // 日期时间改变监听器 + public void onDateTimeChanged(DateTimePicker view, int year, int month, + int dayOfMonth, int hourOfDay, int minute) { + // 更新选中的日期和时间 + mDate.set(Calendar.YEAR, year); + mDate.set(Calendar.MONTH, month); + mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); + mDate.set(Calendar.HOUR_OF_DAY, hourOfDay); + mDate.set(Calendar.MINUTE, minute); + // 更新对话框标题显示的日期时间 + updateTitle(mDate.getTimeInMillis()); + } + }); + + // 设置初始日期时间 + mDate.setTimeInMillis(date); + mDate.set(Calendar.SECOND, 0); // 将秒数设置为0 + mDateTimePicker.setCurrentDate(mDate.getTimeInMillis()); // 初始化日期时间选择器 + + // 设置对话框按钮 + setButton(context.getString(R.string.datetime_dialog_ok), this); // 确定按钮 + setButton2(context.getString(R.string.datetime_dialog_cancel), (OnClickListener)null); // 取消按钮 + + // 设置是否为24小时制 + set24HourView(DateFormat.is24HourFormat(this.getContext())); + + // 更新对话框标题 + updateTitle(mDate.getTimeInMillis()); + } + + /** + * 设置是否为24小时制 + * @param is24HourView 是否为24小时制 + */ + public void set24HourView(boolean is24HourView) { + mIs24HourView = is24HourView; + } + + /** + * 设置日期时间设置回调监听器 + * @param callBack 回调接口 + */ + public void setOnDateTimeSetListener(OnDateTimeSetListener callBack) { + mOnDateTimeSetListener = callBack; + } + + /** + * 更新对话框标题,显示当前选择的日期和时间 + * @param date 当前选择的日期时间 + */ + private void updateTitle(long date) { + // 设置显示的日期格式,包含年、月、日和时间 + int flag = + DateUtils.FORMAT_SHOW_YEAR | + DateUtils.FORMAT_SHOW_DATE | + DateUtils.FORMAT_SHOW_TIME; + // 如果是24小时制,更新显示格式 + flag |= mIs24HourView ? DateUtils.FORMAT_24HOUR : DateUtils.FORMAT_12HOUR; + // 设置对话框标题为格式化后的日期时间 + setTitle(DateUtils.formatDateTime(this.getContext(), date, flag)); + } + + /** + * 确定按钮的点击事件处理 + * 当用户点击确定按钮时,调用回调接口 + */ + public void onClick(DialogInterface arg0, int arg1) { + if (mOnDateTimeSetListener != null) { + // 回调监听器,传递选中的日期时间 + mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis()); + } + } +} diff --git a/DropdownMenu.java b/DropdownMenu.java new file mode 100644 index 0000000..6f50659 --- /dev/null +++ b/DropdownMenu.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.Context; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.PopupMenu; +import android.widget.PopupMenu.OnMenuItemClickListener; + +import net.micode.notes.R; + +/** + * DropdownMenu 类用于实现一个带有下拉菜单的按钮组件。 + * 按钮点击时会显示一个弹出菜单,菜单项点击时可以设置回调事件。 + */ +public class DropdownMenu { + + private Button mButton; // 存储与菜单关联的按钮 + private PopupMenu mPopupMenu; // 存储弹出菜单对象 + private Menu mMenu; // 存储菜单对象 + + /** + * 构造函数,初始化 DropdownMenu。 + * @param context 上下文对象 + * @param button 要显示下拉菜单的按钮 + * @param menuId 菜单资源ID + */ + public DropdownMenu(Context context, Button button, int menuId) { + mButton = button; + // 设置按钮背景图标(这里使用了一个下拉图标) + mButton.setBackgroundResource(R.drawable.dropdown_icon); + + // 创建一个 PopupMenu 对象,显示在按钮下方 + mPopupMenu = new PopupMenu(context, mButton); + + // 获取菜单对象,之后可以对菜单进行操作 + mMenu = mPopupMenu.getMenu(); + + // 根据给定的菜单ID,加载对应的菜单资源 + mPopupMenu.getMenuInflater().inflate(menuId, mMenu); + + // 设置按钮的点击监听器,当点击按钮时显示弹出菜单 + mButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + mPopupMenu.show(); // 显示弹出菜单 + } + }); + } + + /** + * 设置下拉菜单项的点击监听器。 + * @param listener 菜单项点击事件监听器 + */ + public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) { + if (mPopupMenu != null) { + mPopupMenu.setOnMenuItemClickListener(listener); // 设置菜单项点击监听器 + } + } + + /** + * 根据给定的ID查找菜单项。 + * @param id 菜单项的ID + * @return 找到的菜单项 + */ + public MenuItem findItem(int id) { + return mMenu.findItem(id); // 查找菜单项 + } + + /** + * 设置按钮的标题文本。 + * @param title 新的按钮文本 + */ + public void setTitle(CharSequence title) { + mButton.setText(title); // 设置按钮显示的文本 + } +} diff --git a/FoldersListAdapter.java b/FoldersListAdapter.java new file mode 100644 index 0000000..b2e4c5e --- /dev/null +++ b/FoldersListAdapter.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.Context; +import android.database.Cursor; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CursorAdapter; +import android.widget.LinearLayout; +import android.widget.TextView; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; + +/** + * FoldersListAdapter 类继承自 CursorAdapter,用于显示文件夹列表。 + * 该适配器根据数据库中的 cursor 数据来显示文件夹的名称,并且可以显示特定的根文件夹名称。 + */ +public class FoldersListAdapter extends CursorAdapter { + + // 数据库查询时使用的投影,表示需要从数据库中获取的列 + public static final String[] PROJECTION = { + NoteColumns.ID, // 文件夹 ID + NoteColumns.SNIPPET // 文件夹名称 + }; + + // 表示各列的索引常量 + public static final int ID_COLUMN = 0; // ID 列索引 + public static final int NAME_COLUMN = 1; // 文件夹名称列索引 + + /** + * 构造函数,初始化 FoldersListAdapter。 + * @param context 上下文对象 + * @param c Cursor 数据源 + */ + public FoldersListAdapter(Context context, Cursor c) { + super(context, c); + // TODO Auto-generated constructor stub + } + + /** + * 创建一个新的视图用于显示文件夹信息。 + * 这个方法会在 ListView 需要新的项时调用。 + * @param context 上下文对象 + * @param cursor 当前的游标 + * @param parent 父视图 + * @return 新的视图 + */ + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return new FolderListItem(context); // 返回一个新的 FolderListItem 视图 + } + + /** + * 将数据绑定到视图上,显示文件夹的名称。 + * @param view 视图对象 + * @param context 上下文对象 + * @param cursor 当前游标 + */ + @Override + public void bindView(View view, Context context, Cursor cursor) { + if (view instanceof FolderListItem) { + // 判断当前文件夹是否是根文件夹,如果是,则显示"父文件夹"文本 + String folderName = (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context + .getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN); + // 将文件夹名称绑定到 FolderListItem 上 + ((FolderListItem) view).bind(folderName); + } + } + + /** + * 获取指定位置的文件夹名称。 + * @param context 上下文对象 + * @param position 文件夹的位置 + * @return 文件夹名称 + */ + public String getFolderName(Context context, int position) { + Cursor cursor = (Cursor) getItem(position); + // 如果是根文件夹,则返回 "父文件夹" 的名称 + return (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context + .getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN); + } + + /** + * FolderListItem 是一个用于显示文件夹名称的自定义视图类。 + */ + private class FolderListItem extends LinearLayout { + private TextView mName; // 显示文件夹名称的 TextView + + /** + * 构造函数,初始化 FolderListItem。 + * @param context 上下文对象 + */ + public FolderListItem(Context context) { + super(context); + // 加载布局文件 + inflate(context, R.layout.folder_list_item, this); + mName = (TextView) findViewById(R.id.tv_folder_name); // 获取文件夹名称的 TextView + } + + /** + * 将文件夹名称绑定到视图的 TextView 上。 + * @param name 文件夹名称 + */ + public void bind(String name) { + mName.setText(name); // 设置文件夹名称 + } + } +} diff --git a/GTaskASyncTask.java b/GTaskASyncTask.java new file mode 100644 index 0000000..c9d2171 --- /dev/null +++ b/GTaskASyncTask.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.gtask.remote; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.AsyncTask; + +import net.micode.notes.R; +import net.micode.notes.ui.NotesListActivity; +import net.micode.notes.ui.NotesPreferenceActivity; + +/** + * GTaskASyncTask 类用于异步执行Google任务(GTask)同步操作。 + * 它继承自 AsyncTask,以便在后台线程中执行耗时的网络操作, + * 并通过主线程更新用户界面或发送通知。 + */ +public class GTaskASyncTask extends AsyncTask { + + // 用于同步操作的通知ID,确保唯一性。 + private static final int GTASK_SYNC_NOTIFICATION_ID = 5234235; + + /** + * 当同步完成时调用的接口。 + */ + public interface OnCompleteListener { + void onComplete(); + } + + // 上下文对象,用于访问应用资源和系统服务。 + private Context mContext; + + // 管理通知的服务。 + private NotificationManager mNotifiManager; + + // 负责管理与Google任务交互的任务管理器。 + private GTaskManager mTaskManager; + + // 同步完成后调用的监听器。 + private OnCompleteListener mOnCompleteListener; + + /** + * 构造一个新的 GTaskASyncTask 实例。 + * @param context 应用上下文。 + * @param listener 同步完成后的回调监听器。 + */ + public GTaskASyncTask(Context context, OnCompleteListener listener) { + mContext = context; + mOnCompleteListener = listener; + mNotifiManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); + mTaskManager = GTaskManager.getInstance(); + } + + /** + * 取消正在进行的同步操作。 + */ + public void cancelSync() { + mTaskManager.cancelSync(); + } + + /** + * 更新进度信息。 + * @param message 进度消息文本。 + */ + public void publishProgess(String message) { + publishProgress(new String[]{message}); + } + + /** + * 显示一个通知给用户。 + * @param tickerId 通知栏提示文字的资源ID。 + * @param content 通知内容文本。 + */ + private void showNotification(int tickerId, String content) { + Notification notification = new Notification(R.drawable.notification, + mContext.getString(tickerId), + System.currentTimeMillis()); + notification.defaults = Notification.DEFAULT_LIGHTS; + notification.flags = Notification.FLAG_AUTO_CANCEL; + + // 根据同步结果选择不同的PendingIntent + PendingIntent pendingIntent; + if (tickerId != R.string.ticker_success) { + pendingIntent = PendingIntent.getActivity(mContext, 0, + new Intent(mContext, NotesPreferenceActivity.class), 0); + } else { + pendingIntent = PendingIntent.getActivity(mContext, 0, + new Intent(mContext, NotesListActivity.class), 0); + } + + // 设置通知详情,并显示通知 + notification.setLatestEventInfo(mContext, mContext.getString(R.string.app_name), content, pendingIntent); + mNotifiManager.notify(GTASK_SYNC_NOTIFICATION_ID, notification); + } + + /** + * 在后台线程中执行的实际同步逻辑。 + */ + @Override + protected Integer doInBackground(Void... unused) { + // 发布登录进度信息 + publishProgess(mContext.getString(R.string.sync_progress_login, + NotesPreferenceActivity.getSyncAccountName(mContext))); + // 执行同步操作并返回状态码 + return mTaskManager.sync(mContext, this); + } + + /** + * 更新UI线程上的进度。 + */ + @Override + protected void onProgressUpdate(String... progress) { + // 显示同步中的通知 + showNotification(R.string.ticker_syncing, progress[0]); + // 如果上下文是GTaskSyncService,则广播进度信息 + if (mContext instanceof GTaskSyncService) { + ((GTaskSyncService) mContext).sendBroadcast(progress[0]); + } + } + + /** + * 同步完成后,在UI线程上执行此方法。 + */ + @Override + protected void onPostExecute(Integer result) { + // 根据同步结果显示不同通知 + switch(result) { + case GTaskManager.STATE_SUCCESS: + showNotification(R.string.ticker_success, + mContext.getString(R.string.success_sync_account, mTaskManager.getSyncAccount())); + NotesPreferenceActivity.setLastSyncTime(mContext, System.currentTimeMillis()); + break; + case GTaskManager.STATE_NETWORK_ERROR: + showNotification(R.string.ticker_fail, + mContext.getString(R.string.error_sync_network)); + break; + case GTaskManager.STATE_INTERNAL_ERROR: + showNotification(R.string.ticker_fail, + mContext.getString(R.string.error_sync_internal)); + break; + case GTaskManager.STATE_SYNC_CANCELLED: + showNotification(R.string.ticker_cancel, + mContext.getString(R.string.error_sync_cancelled)); + break; + } + + // 如果存在监听器,则启动新线程调用onComplete方法 + if (mOnCompleteListener != null) { + new Thread(new Runnable() { + @Override + public void run() { + mOnCompleteListener.onComplete(); + } + }).start(); + } + } +} \ No newline at end of file diff --git a/GTaskClient.java b/GTaskClient.java new file mode 100644 index 0000000..8ae8950 --- /dev/null +++ b/GTaskClient.java @@ -0,0 +1,288 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.gtask.remote; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.AccountManagerFuture; +import android.app.Activity; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; + +import net.micode.notes.gtask.data.Node; +import net.micode.notes.gtask.data.Task; +import net.micode.notes.gtask.data.TaskList; +import net.micode.notes.gtask.exception.ActionFailureException; +import net.micode.notes.gtask.exception.NetworkFailureException; +import net.micode.notes.tool.GTaskStringUtils; +import net.micode.notes.ui.NotesPreferenceActivity; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.cookie.Cookie; +import org.apache.http.impl.client.BasicCookieStore; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.HttpConnectionParams; +import org.apache.http.params.HttpParams; +import org.apache.http.params.HttpProtocolParams; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.LinkedList; +import java.util.List; +import java.util.zip.GZIPInputStream; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; + +// GTaskClient 类用于与 Google Tasks 进行交互,主要包含任务和任务列表的创建、删除、修改等操作 +public class GTaskClient { + private static final String TAG = GTaskClient.class.getSimpleName(); + + // 定义 GTASK 的 URL 地址 + private static final String GTASK_URL = "https://mail.google.com/tasks/"; + private static final String GTASK_GET_URL = "https://mail.google.com/tasks/ig"; + private static final String GTASK_POST_URL = "https://mail.google.com/tasks/r/ig"; + + private static GTaskClient mInstance = null; // 单例对象 + private DefaultHttpClient mHttpClient; // HTTP 客户端 + private String mGetUrl; // GET 请求 URL + private String mPostUrl; // POST 请求 URL + private long mClientVersion; // 客户端版本 + private boolean mLoggedin; // 登录状态 + private long mLastLoginTime; // 上次登录时间 + private int mActionId; // 操作 ID,用于唯一标识每个操作 + private Account mAccount; // 当前登录的 Google 帐号 + 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; + } + + // 单例模式获取 GTaskClient 实例 + public static synchronized GTaskClient getInstance() { + if (mInstance == null) { + mInstance = new GTaskClient(); + } + return mInstance; + } + + // 登录方法,返回是否成功 + public boolean login(Activity activity) { + // 每 5 分钟重新登录一次 + final long interval = 1000 * 60 * 5; + if (mLastLoginTime + interval < System.currentTimeMillis()) { + mLoggedin = false; + } + + // 如果切换了帐号,也需要重新登录 + if (mLoggedin && !TextUtils.equals(getSyncAccount().name, NotesPreferenceActivity.getSyncAccountName(activity))) { + mLoggedin = false; + } + + if (mLoggedin) { + Log.d(TAG, "已经登录"); + return true; + } + + mLastLoginTime = System.currentTimeMillis(); + String authToken = loginGoogleAccount(activity, false); + if (authToken == null) { + Log.e(TAG, "Google 帐号登录失败"); + return false; + } + + // 如果是自定义域名帐号,需要构建对应的 URL + if (!(mAccount.name.toLowerCase().endsWith("gmail.com") || mAccount.name.toLowerCase().endsWith("googlemail.com"))) { + StringBuilder url = new StringBuilder(GTASK_URL).append("a/"); + int index = mAccount.name.indexOf('@') + 1; + String suffix = mAccount.name.substring(index); + url.append(suffix + "/"); + mGetUrl = url.toString() + "ig"; + mPostUrl = url.toString() + "r/ig"; + + if (tryToLoginGtask(activity, authToken)) { + mLoggedin = true; + } + } + + // 尝试使用 Google 官方 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, "没有可用的 Google 帐号"); + 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, "无法找到相同名称的帐户"); + return null; + } + + // 获取授权令牌 + 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, "获取授权令牌失败"); + authToken = null; + } + + return authToken; + } + + // 尝试登录 Google 任务 + private boolean tryToLoginGtask(Activity activity, String authToken) { + if (!loginGtask(authToken)) { + // 如果授权令牌过期,重新登录 + authToken = loginGoogleAccount(activity, true); + if (authToken == null) { + Log.e(TAG, "Google 帐号登录失败"); + return false; + } + + if (!loginGtask(authToken)) { + Log.e(TAG, "登录 Google Tasks 失败"); + return false; + } + } + return true; + } + + // 通过授权令牌登录 Google Tasks + private boolean loginGtask(String authToken) { + int timeoutConnection = 10000; + int timeoutSocket = 15000; + HttpParams httpParameters = new BasicHttpParams(); + HttpConnectionParams.setConnectionTimeout(httpParameters, timeoutConnection); + HttpConnectionParams.setSoTimeout(httpParameters, timeoutSocket); + mHttpClient = new DefaultHttpClient(httpParameters); + BasicCookieStore localBasicCookieStore = new BasicCookieStore(); + mHttpClient.setCookieStore(localBasicCookieStore); + HttpProtocolParams.setUseExpectContinue(mHttpClient.getParams(), false); + + // 登录 Google 任务 + try { + String loginUrl = mGetUrl + "?auth=" + authToken; + HttpGet httpGet = new HttpGet(loginUrl); + HttpResponse response = mHttpClient.execute(httpGet); + + // 获取 Cookie + List cookies = mHttpClient.getCookieStore().getCookies(); + boolean hasAuthCookie = false; + for (Cookie cookie : cookies) { + if (cookie.getName().contains("GTL")) { + hasAuthCookie = true; + } + } + if (!hasAuthCookie) { + Log.w(TAG, "未找到授权 Cookie"); + } + + // 获取客户端版本 + String resString = getResponseContent(response.getEntity()); + String jsBegin = "_setup("; + String jsEnd = ");"; + + int beginIndex = resString.indexOf(jsBegin); + int endIndex = resString.indexOf(jsEnd); + if (beginIndex != -1 && endIndex != -1) { + String jsonString = resString.substring(beginIndex + jsBegin.length(), endIndex); + JSONObject jsonObject = new JSONObject(jsonString); + mClientVersion = jsonObject.optLong("clientVersion", -1); + } + return true; + } catch (Exception e) { + Log.e(TAG, "登录失败:" + e.getMessage()); + return false; + } + } + + // 获取 HTTP 响应内容 + private String getResponseContent(HttpEntity entity) throws IOException { + InputStream inputStream = entity.getContent(); + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); + StringBuilder stringBuilder = new StringBuilder(); + String line; + while ((line = bufferedReader.readLine()) != null) { + stringBuilder.append(line).append("\n"); + } + return stringBuilder.toString(); + } + + // 获取当前同步的 Google 帐号 + public Account getSyncAccount() { + return mAccount; + } + + // 获取已登录状态 + public boolean isLoggedin() { + return mLoggedin; + } +} diff --git a/GTaskManager.java b/GTaskManager.java new file mode 100644 index 0000000..6a3f39c --- /dev/null +++ b/GTaskManager.java @@ -0,0 +1,805 @@ +/* + * Copyright (c) 2010 - 2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.gtask.remote; + +import android.app.Activity; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.util.Log; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.gtask.data.MetaData; +import net.micode.notes.gtask.data.Node; +import net.micode.notes.gtask.data.SqlNote; +import net.micode.notes.gtask.data.Task; +import net.micode.notes.gtask.data.TaskList; +import net.micode.notes.gtask.exception.ActionFailureException; +import net.micode.notes.gtask.exception.NetworkFailureException; +import net.micode.notes.tool.DataUtils; +import net.micode.notes.tool.GTaskStringUtils; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; + +// GTaskManager类用于管理与Google Task的同步操作等相关功能 +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 Task列表的哈希表,以任务列表的GID为键,对应的TaskList对象为值 + private HashMap mGTaskListHashMap; + // 存储Google Task节点的哈希表,以任务的GID为键,对应的Node对象为值 + private HashMap mGTaskHashMap; + // 存储元数据的哈希表,以相关GID为键,对应的MetaData对象为值 + private HashMap mMetaHashMap; + // 元数据列表对象 + private TaskList mMetaList; + // 存储本地要删除的记录的ID集合 + private HashSet mLocalDeleteIdMap; + // 存储Google任务的GID到本地记录ID的映射关系 + private HashMap mGidToNid; + // 存储本地记录ID到Google任务的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(); + } + + // 获取GTaskManager的单例实例 + public static synchronized GTaskManager getInstance() { + if (mInstance == null) { + mInstance = new GTaskManager(); + } + return mInstance; + } + + // 设置关联的Activity上下文,主要用于获取认证令牌等操作 + public synchronized void setActivityContext(Activity activity) { + // used for getting authtoken + mActivity = activity; + } + + // 执行同步操作的核心方法,根据不同情况进行与Google Task的同步流程 + 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 Task,如果取消则不执行登录操作,登录失败抛出网络异常 + if (!mCancelled) { + if (!client.login(mActivity)) { + throw new NetworkFailureException("login google task failed"); + } + } + + // 发布初始化任务列表的进度提示 + 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 Task列表,从Google获取任务列表信息并进行相关处理 + private void initGTaskList() throws NetworkFailureException { + if (mCancelled) + return; + GTaskClient client = GTaskClient.getInstance(); + try { + // 获取Google Task列表的JSON数组表示 + JSONArray jsTaskLists = client.getTaskLists(); + + // 先初始化元数据列表,查找特定名称的元数据列表 + mMetaList = null; + for (int i = 0; i < jsTaskLists.length(); i++) { + JSONObject object = jsTaskLists.getJSONObject(i); + String gid = object.getString(GTaskStringUtils.GTASK_JSON_ID); + String name = object.getString(GTaskStringUtils.GTASK_JSON_NAME); + + if (name.equals(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_META)) { + mMetaList = new TaskList(); + mMetaList.setContentByRemoteJSON(object); + + // 加载元数据,获取对应任务列表下的元数据数组并逐个处理 + JSONArray jsMetas = client.getTaskList(gid); + for (int j = 0; j < jsMetas.length(); j++) { + object = (JSONObject) jsMetas.getJSONObject(j); + MetaData metaData = new MetaData(); + metaData.setContentByRemoteJSON(object); + if (metaData.isWorthSaving()) { + mMetaList.addChildTask(metaData); + if (metaData.getGid()!= null) { + mMetaHashMap.put(metaData.getRelatedGid(), metaData); + } + } + } + } + } + + // 如果元数据列表不存在,则创建一个新的元数据列表并上传到Google + if (mMetaList == null) { + mMetaList = new TaskList(); + mMetaList.setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_META); + GTaskClient.getInstance().createTaskList(mMetaList); + } + + // 初始化常规任务列表,遍历任务列表数组,创建并填充TaskList对象以及其包含的Task对象 + 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); + } + + // 如果未取消同步,清除本地已删除的记录 + // mCancelled可以被其他线程设置,所以需要逐个检查 + if (!mCancelled) { + if (!DataUtils.batchDeleteNotes(mContentResolver, mLocalDeleteIdMap)) { + throw new ActionFailureException("failed to batch-delete local deleted notes"); + } + } + + // 如果未取消同步,提交更新并刷新本地同步ID + if (!mCancelled) { + GTaskClient.getInstance().commitUpdate(); + refreshLocalSyncId(); + } + + } + + // 同步文件夹的具体操作方法,处理不同类型文件夹(根文件夹、通话记录文件夹、本地现有文件夹、远程新增文件夹等)的同步 + private void syncFolder() throws NetworkFailureException { + Cursor c = null; + String gid; + 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 } + } 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 { + if (js.has(GTaskStringUtils.META_HEAD_NOTE)) { + JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); + if (note.has(NoteColumns.ID)) { + long id = note.getLong(NoteColumns.ID); + if (DataUtils.existInNoteDatabase(mContentResolver, id)) { + // 如果ID已存在,需要创建一个新的,移除原来的ID + note.remove(NoteColumns.ID); + } + } + } + + if (js.has(GTaskStringUtils.META_HEAD_DATA)) { + JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA); + for (int i = 0; i < dataArray.length(); i++) { + JSONObject data = dataArray.getJSONObject(i); + if (data.has(DataColumns.ID)) { + long dataId = data.getLong(DataColumns.ID); + if (DataUtils.existInDataDatabase(mContentResolver, dataId)) { + // 如果数据ID已存在,需要创建一个新的,移除原来的ID + data.remove(DataColumns.ID); + } + } + } + + } + } catch (JSONException e) { + Log.w(TAG, e.toString()); + e.printStackTrace(); + } + sqlNote.setContent(js); + + Long parentId = mGidToNid.get(((Task) node).getParent().getGid()); + if (parentId == null) { + Log.e(TAG, "cannot find task's parent id locally"); + throw new ActionFailureException("cannot add local node"); + } + sqlNote.setParentId(parentId.longValue()); + } + + // 创建本地节点,提交到本地数据库(不触发本地修改检查) + sqlNote.setGtaskId(node.getGid()); + sqlNote.commit(false); + + // 更新GID和本地记录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; + // 根据传入的游标更新本地记录内容 + sqlNote = new SqlNote(mContext, c); + sqlNote.setContent(node.getLocalJSONFromContent()); + + Long parentId = (node instanceof Task)? mGidToNid.get(((Task) node).getParent().getGid()) + : new Long(Notes.ID_ROOT_FOLDER); + if (parentId == null) { + Log.e(TAG, "cannot find task's parent id locally"); + throw new ActionFailureException("cannot update local node"); + } + sqlNote.setParentId(parentId.longValue()); + sqlNote.commit(true); + + // 更新远程元数据相关信息 + 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()); + + 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; + + // 根据不同的本地记录ID确定文件夹名称,查找是否已存在同名文件夹 + 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); + + // 更新GID和本地记录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的方法,重新获取最新的Google Task列表信息,然后更新本地记录的同步ID + private void refreshLocalSyncId() throws NetworkFailureException { + if (mCancelled) { + return; + } + + // 获取最新的Google Task列表相关数据,清除之前缓存 + mGTaskHashMap.clear(); + mGTaskListHashMap.clear(); + mMetaHashMap.clear(); + initGTaskList(); + + Cursor c = null; + try { + c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, + "(type<>? AND parent_id<>?)", new String[] { + String.valueOf(Notes.TYPE_SYSTEM), String.valueOf(Notes.ID_TRASH_FOLER) + }, NoteColumns.TYPE + " DESC"); + if (c!= null) { + while (c.moveToNext()) { + String gid = c.getString(SqlNote.GTASK_ID_COLUMN); + Node node = mGTaskHashMap.get(gid); + if (node!= null) { + mGTaskHashMap.remove(gid); + ContentValues values = new ContentValues(); + values.put(NoteColumns.SYNC_ID, node.getLastModified()); + mContentResolver.update(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, + c.getLong(SqlNote.ID_COLUMN)), values, null, null); + } else { + Log.e(TAG, "something is missed"); + throw new ActionFailureException( + "some local items don't have gid after sync"); + } + } + } else { + Log.w(TAG, "failed to query local note to refresh sync id"); + } + } finally { + if (c!= null) { + c.close(); + c = null; + } + } + } + + // 获取同步账户名称的方法 + public String getSyncAccount() { + return GTaskClient.getInstance().getSyncAccount().name; + } + + // 取消同步的方法,设置同步已取消的标记 + public void cancelSync() { + mCancelled = true; + } +} \ No newline at end of file diff --git a/GTaskStringUtils.java b/GTaskStringUtils.java new file mode 100644 index 0000000..02a2496 --- /dev/null +++ b/GTaskStringUtils.java @@ -0,0 +1,76 @@ +/* + * 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; + +public class GTaskStringUtils { + + // GTASK JSON 数据字段常量 + public final static String GTASK_JSON_ACTION_ID = "action_id"; // 操作ID + public final static String GTASK_JSON_ACTION_LIST = "action_list"; // 操作列表 + 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"; // 更新操作 + public final static String GTASK_JSON_CREATOR_ID = "creator_id"; // 创建者ID + public final static String GTASK_JSON_CHILD_ENTITY = "child_entity"; // 子实体 + public final static String GTASK_JSON_CLIENT_VERSION = "client_version"; // 客户端版本 + public final static String GTASK_JSON_COMPLETED = "completed"; // 是否完成 + public final static String GTASK_JSON_CURRENT_LIST_ID = "current_list_id"; // 当前列表ID + public final static String GTASK_JSON_DEFAULT_LIST_ID = "default_list_id"; // 默认列表ID + public final static String GTASK_JSON_DELETED = "deleted"; // 是否删除 + public final static String GTASK_JSON_DEST_LIST = "dest_list"; // 目标列表 + public final static String GTASK_JSON_DEST_PARENT = "dest_parent"; // 目标父级 + public final static String GTASK_JSON_DEST_PARENT_TYPE = "dest_parent_type"; // 目标父级类型 + public final static String GTASK_JSON_ENTITY_DELTA = "entity_delta"; // 实体差异 + public final static String GTASK_JSON_ENTITY_TYPE = "entity_type"; // 实体类型 + public final static String GTASK_JSON_GET_DELETED = "get_deleted"; // 获取已删除的 + public final static String GTASK_JSON_ID = "id"; // ID + public final static String GTASK_JSON_INDEX = "index"; // 索引 + public final static String GTASK_JSON_LAST_MODIFIED = "last_modified"; // 最后修改时间 + public final static String GTASK_JSON_LATEST_SYNC_POINT = "latest_sync_point"; // 最新同步点 + public final static String GTASK_JSON_LIST_ID = "list_id"; // 列表ID + public final static String GTASK_JSON_LISTS = "lists"; // 列表 + public final static String GTASK_JSON_NAME = "name"; // 名称 + public final static String GTASK_JSON_NEW_ID = "new_id"; // 新ID + public final static String GTASK_JSON_NOTES = "notes"; // 笔记 + public final static String GTASK_JSON_PARENT_ID = "parent_id"; // 父级ID + public final static String GTASK_JSON_PRIOR_SIBLING_ID = "prior_sibling_id"; // 上一个兄弟ID + public final static String GTASK_JSON_RESULTS = "results"; // 结果 + public final static String GTASK_JSON_SOURCE_LIST = "source_list"; // 源列表 + public final static String GTASK_JSON_TASKS = "tasks"; // 任务 + 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"; // 任务类型 + public final static String GTASK_JSON_USER = "user"; // 用户 + + // MIUI 笔记相关文件夹前缀 + public final static String MIUI_FOLDER_PREFFIX = "[MIUI_Notes]"; // MIUI笔记文件夹前缀 + + // 默认文件夹名称 + public final static String FOLDER_DEFAULT = "Default"; // 默认文件夹 + public final static String FOLDER_CALL_NOTE = "Call_Note"; // 通话笔记文件夹 + public final static String FOLDER_META = "METADATA"; // 元数据文件夹 + + // 元数据头部字段 + public final static String META_HEAD_GTASK_ID = "meta_gid"; // GTASK ID + public final static String META_HEAD_NOTE = "meta_note"; // 笔记元数据 + public final static String META_HEAD_DATA = "meta_data"; // 数据元数据 + + // 元数据笔记名称,表示不允许更新和删除 + public final static String META_NOTE_NAME = "[META INFO] DON'T UPDATE AND DELETE"; // 元数据笔记名称 +} diff --git a/GTaskSyncService.java b/GTaskSyncService.java new file mode 100644 index 0000000..40d553b --- /dev/null +++ b/GTaskSyncService.java @@ -0,0 +1,198 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.gtask.remote; + +import android.app.Activity; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.IBinder; + +/** + * GTaskSyncService 是一个后台服务,用于管理Google任务(GTask)的同步操作。 + * 它提供启动和取消同步的方法,并在同步过程中广播进度更新。 + */ +public class GTaskSyncService extends Service { + + /** + * 表示同步动作类型的字符串常量名。 + */ + public final static String ACTION_STRING_NAME = "sync_action_type"; + + /** + * 同步开始的动作代码。 + */ + public final static int ACTION_START_SYNC = 0; + + /** + * 取消同步的动作代码。 + */ + public final static int ACTION_CANCEL_SYNC = 1; + + /** + * 无效或未定义的动作代码。 + */ + public final static int ACTION_INVALID = 2; + + /** + * 广播名称,用于发送同步状态和服务消息。 + */ + public final static String GTASK_SERVICE_BROADCAST_NAME = "net.micode.notes.gtask.remote.gtask_sync_service"; + + /** + * 广播中表示是否正在进行同步的键名。 + */ + public final static String GTASK_SERVICE_BROADCAST_IS_SYNCING = "isSyncing"; + + /** + * 广播中表示同步进度信息的键名。 + */ + public final static String GTASK_SERVICE_BROADCAST_PROGRESS_MSG = "progressMsg"; + + /** + * 当前执行的同步任务实例。 + */ + private static GTaskASyncTask mSyncTask = null; + + /** + * 当前同步任务的进度信息。 + */ + private static String mSyncProgress = ""; + + /** + * 开始同步操作。如果当前没有正在运行的同步任务,则创建并启动一个新的同步任务。 + */ + private void startSync() { + if (mSyncTask == null) { + mSyncTask = new GTaskASyncTask(this, new GTaskASyncTask.OnCompleteListener() { + @Override + public void onComplete() { + mSyncTask = null; + sendBroadcast(""); + stopSelf(); + } + }); + sendBroadcast(""); + mSyncTask.execute(); + } + } + + /** + * 取消正在进行的同步操作。 + */ + private void cancelSync() { + if (mSyncTask != null) { + mSyncTask.cancelSync(); + } + } + + /** + * 当服务被创建时调用。 + */ + @Override + public void onCreate() { + super.onCreate(); + mSyncTask = null; + } + + /** + * 当服务接收到启动命令时调用。 + */ + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Bundle bundle = intent.getExtras(); + if (bundle != null && bundle.containsKey(ACTION_STRING_NAME)) { + switch (bundle.getInt(ACTION_STRING_NAME, ACTION_INVALID)) { + case ACTION_START_SYNC: + startSync(); + break; + case ACTION_CANCEL_SYNC: + cancelSync(); + break; + default: + // 忽略未知动作 + break; + } + return START_STICKY; // 如果进程被杀死,尝试重启服务 + } + return super.onStartCommand(intent, flags, startId); + } + + /** + * 当系统内存不足时调用,尝试释放资源。 + */ + @Override + public void onLowMemory() { + super.onLowMemory(); + if (mSyncTask != null) { + mSyncTask.cancelSync(); + } + } + + /** + * 返回绑定到此服务的IBinder对象,本服务不支持绑定,所以返回null。 + */ + @Override + public IBinder onBind(Intent intent) { + return null; + } + + /** + * 发送广播以通知其他组件当前同步的状态和进度。 + */ + public void sendBroadcast(String msg) { + mSyncProgress = msg; + Intent intent = new Intent(GTASK_SERVICE_BROADCAST_NAME); + intent.putExtra(GTASK_SERVICE_BROADCAST_IS_SYNCING, mSyncTask != null); + intent.putExtra(GTASK_SERVICE_BROADCAST_PROGRESS_MSG, msg); + sendBroadcast(intent); + } + + /** + * 静态方法,从Activity启动同步服务。 + */ + public static void startSync(Activity activity) { + GTaskManager.getInstance().setActivityContext(activity); + Intent intent = new Intent(activity, GTaskSyncService.class); + intent.putExtra(ACTION_STRING_NAME, ACTION_START_SYNC); + activity.startService(intent); + } + + /** + * 静态方法,通过上下文取消同步操作。 + */ + public static void cancelSync(Context context) { + Intent intent = new Intent(context, GTaskSyncService.class); + intent.putExtra(ACTION_STRING_NAME, ACTION_CANCEL_SYNC); + context.startService(intent); + } + + /** + * 检查是否有同步任务正在进行。 + */ + public static boolean isSyncing() { + return mSyncTask != null; + } + + /** + * 获取当前同步任务的进度信息。 + */ + public static String getProgressString() { + return mSyncProgress; + } +} \ No newline at end of file diff --git a/MetaData.java b/MetaData.java new file mode 100644 index 0000000..8a56662 --- /dev/null +++ b/MetaData.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.gtask.data; + +import android.database.Cursor; +import android.util.Log; + +import net.micode.notes.tool.GTaskStringUtils; + +import org.json.JSONException; +import org.json.JSONObject; + +// MetaData类继承自Task类,负责处理任务的元数据 +public class MetaData extends Task { + private final static String TAG = MetaData.class.getSimpleName(); // 用于日志记录的TAG + + private String mRelatedGid = null; // 存储相关任务的ID + + // 设置元数据,包括任务ID和相关的元信息 + public void setMeta(String gid, JSONObject metaInfo) { + try { + // 将任务ID放入元信息JSON对象中 + metaInfo.put(GTaskStringUtils.META_HEAD_GTASK_ID, gid); + } catch (JSONException e) { + Log.e(TAG, "failed to put related gid"); // 记录错误 + } + setNotes(metaInfo.toString()); // 设置笔记内容为元信息的字符串表示 + setName(GTaskStringUtils.META_NOTE_NAME); // 设置名称 + } + + // 获取相关任务的ID + public String getRelatedGid() { + return mRelatedGid; + } + + // 检查当前对象是否值得保存 + @Override + public boolean isWorthSaving() { + return getNotes() != null; // 如果笔记内容不为空,则值得保存 + } + + // 从远程JSON对象设置内容 + @Override + public void setContentByRemoteJSON(JSONObject js) { + super.setContentByRemoteJSON(js); // 调用父类方法 + if (getNotes() != null) { + try { + // 将笔记内容转换为JSON对象 + JSONObject metaInfo = new JSONObject(getNotes().trim()); + // 获取相关任务的ID + mRelatedGid = metaInfo.getString(GTaskStringUtils.META_HEAD_GTASK_ID); + } catch (JSONException e) { + Log.w(TAG, "failed to get related gid"); // 记录警告 + mRelatedGid = null; // 设置为null以表示失败 + } + } + } + + // 此方法不应被调用,抛出异常 + @Override + public void setContentByLocalJSON(JSONObject js) { + throw new IllegalAccessError("MetaData:setContentByLocalJSON should not be called"); + } + + // 此方法不应被调用,抛出异常 + @Override + public JSONObject getLocalJSONFromContent() { + throw new IllegalAccessError("MetaData:getLocalJSONFromContent should not be called"); + } + + // 此方法不应被调用,抛出异常 + @Override + public int getSyncAction(Cursor c) { + throw new IllegalAccessError("MetaData:getSyncAction should not be called"); + } +} diff --git a/NetworkFailureException.java b/NetworkFailureException.java new file mode 100644 index 0000000..9e50613 --- /dev/null +++ b/NetworkFailureException.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.gtask.exception; + +/** + * NetworkFailureException 类是用于表示网络操作失败时抛出的异常。 + * 它继承自 Exception,因此它是一个检查型异常(checked exception), + * 这意味着在方法签名中需要声明此异常可能会被抛出,以确保调用者处理或重新抛出该异常。 + */ +public class NetworkFailureException extends Exception { + + // 序列化版本唯一标识符,用于确保类的不同版本之间的兼容性。 + private static final long serialVersionUID = 2107610287180234136L; + + /** + * 构造一个新的 NetworkFailureException,不带任何详细信息消息或原因。 + */ + public NetworkFailureException() { + super(); + } + + /** + * 使用指定的详细信息消息构造一个新的 NetworkFailureException。 + * @param message 提供更多关于异常的信息的消息字符串。 + */ + public NetworkFailureException(String message) { + super(message); + } + + /** + * 使用指定的详细信息消息和原因构造一个新的 NetworkFailureException。 + * @param message 提供更多关于异常的信息的消息字符串。 + * @param cause 导致当前异常的底层原因。 + */ + public NetworkFailureException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/Node.java b/Node.java new file mode 100644 index 0000000..1ce7b51 --- /dev/null +++ b/Node.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.gtask.data; + +import android.database.Cursor; +import org.json.JSONObject; + +/** + * Node类是一个抽象类,表示一个同步节点,包含了节点的基本属性和同步操作。 + */ +public abstract class Node { + // 同步操作的常量定义 + public static final int SYNC_ACTION_NONE = 0; // 无操作 + public static final int SYNC_ACTION_ADD_REMOTE = 1; // 添加到远程 + public static final int SYNC_ACTION_ADD_LOCAL = 2; // 添加到本地 + public static final int SYNC_ACTION_DEL_REMOTE = 3; // 从远程删除 + public static final int SYNC_ACTION_DEL_LOCAL = 4; // 从本地删除 + public static final int SYNC_ACTION_UPDATE_REMOTE = 5; // 更新远程 + public static final int SYNC_ACTION_UPDATE_LOCAL = 6; // 更新本地 + public static final int SYNC_ACTION_UPDATE_CONFLICT = 7; // 更新冲突 + public static final int SYNC_ACTION_ERROR = 8; // 错误 + + // 节点的基本属性 + private String mGid; // 唯一标识符 + private String mName; // 节点名称 + private long mLastModified; // 上次修改时间 + private boolean mDeleted; // 是否已删除 + + // 构造函数,初始化节点属性 + public Node() { + mGid = null; // 初始化 Gid 为 null + mName = ""; // 初始化名称为空字符串 + mLastModified = 0; // 初始化最后修改时间为 0 + mDeleted = false; // 初始化删除状态为 false + } + + // 抽象方法:获取创建操作的 JSON 对象 + public abstract JSONObject getCreateAction(int actionId); + + // 抽象方法:获取更新操作的 JSON 对象 + public abstract JSONObject getUpdateAction(int actionId); + + // 抽象方法:根据远程 JSON 设置内容 + public abstract void setContentByRemoteJSON(JSONObject js); + + // 抽象方法:根据本地 JSON 设置内容 + public abstract void setContentByLocalJSON(JSONObject js); + + // 抽象方法:从内容获取本地 JSON 对象 + public abstract JSONObject getLocalJSONFromContent(); + + // 抽象方法:根据 Cursor 获取同步操作 + public abstract int getSyncAction(Cursor c); + + // 设置 Gid + public void setGid(String gid) { + this.mGid = gid; + } + + // 设置名称 + public void setName(String name) { + this.mName = name; + } + + // 设置最后修改时间 + public void setLastModified(long lastModified) { + this.mLastModified = lastModified; + } + + // 设置删除状态 + public void setDeleted(boolean deleted) { + this.mDeleted = deleted; + } + + // 获取 Gid + public String getGid() { + return this.mGid; + } + + // 获取名称 + public String getName() { + return this.mName; + } + + // 获取最后修改时间 + public long getLastModified() { + return this.mLastModified; + } + + // 获取删除状态 + public boolean getDeleted() { + return this.mDeleted; + } +} diff --git a/Note.java b/Note.java new file mode 100644 index 0000000..6f5e8ba --- /dev/null +++ b/Note.java @@ -0,0 +1,265 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.model; + +import android.content.ContentProviderOperation; +import android.content.ContentProviderResult; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.OperationApplicationException; +import android.net.Uri; +import android.os.RemoteException; +import android.util.Log; + +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.CallNote; +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.data.Notes.TextNote; + +import java.util.ArrayList; + + +public class Note { + private ContentValues mNoteDiffValues; // 用于存储笔记修改的数据 + private NoteData mNoteData; // 用于存储笔记的具体内容(如文本或通话数据) + private static final String TAG = "Note"; // 用于日志输出的标签 + + /** + * 创建一个新的笔记ID,用于在数据库中添加新的笔记 + */ + public static synchronized long getNewNoteId(Context context, long folderId) { + // 创建新的笔记记录 + 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 + + // 插入新笔记到数据库,并获取URI + Uri uri = context.getContentResolver().insert(Notes.CONTENT_NOTE_URI, values); + + long noteId = 0; + try { + noteId = Long.valueOf(uri.getPathSegments().get(1)); // 获取新笔记的ID + } catch (NumberFormatException e) { + Log.e(TAG, "Get note id error :" + e.toString()); + noteId = 0; + } + + // 如果ID获取失败,抛出异常 + if (noteId == -1) { + throw new IllegalStateException("Wrong note id:" + noteId); + } + return noteId; // 返回新创建的笔记ID + } + + public Note() { + mNoteDiffValues = new ContentValues(); // 初始化笔记修改数据 + mNoteData = new NoteData(); // 初始化笔记具体内容 + } + + // 设置笔记的某个键值对 + public void setNoteValue(String key, String value) { + mNoteDiffValues.put(key, value); // 将键值对放入修改数据中 + mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); // 标记本地已修改 + mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); // 更新修改时间 + } + + // 设置文本数据 + public void setTextData(String key, String value) { + mNoteData.setTextData(key, value); // 将文本数据传递给NoteData对象 + } + + // 设置文本数据ID + public void setTextDataId(long id) { + mNoteData.setTextDataId(id); // 设置文本数据的ID + } + + // 获取文本数据ID + public long getTextDataId() { + return mNoteData.mTextDataId; // 返回文本数据的ID + } + + // 设置通话数据ID + public void setCallDataId(long id) { + mNoteData.setCallDataId(id); // 设置通话数据的ID + } + + // 设置通话数据 + public void setCallData(String key, String value) { + mNoteData.setCallData(key, value); // 将通话数据传递给NoteData对象 + } + + // 判断笔记是否本地已修改 + public boolean isLocalModified() { + return mNoteDiffValues.size() > 0 || mNoteData.isLocalModified(); // 如果修改数据不为空或者NoteData已修改,则返回true + } + + // 同步笔记数据到ContentProvider + public boolean syncNote(Context context, long noteId) { + if (noteId <= 0) { + throw new IllegalArgumentException("Wrong note id:" + noteId); // 检查笔记ID是否合法 + } + + if (!isLocalModified()) { + return true; // 如果没有修改,直接返回 + } + + // 更新笔记的修改数据 + if (context.getContentResolver().update( + ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), mNoteDiffValues, null, + null) == 0) { + Log.e(TAG, "Update note error, should not happen"); + } + mNoteDiffValues.clear(); // 清空修改数据 + + // 如果有本地修改的内容,推送到ContentResolver + if (mNoteData.isLocalModified() + && (mNoteData.pushIntoContentResolver(context, noteId) == null)) { + return false; // 如果推送失败,返回false + } + + return true; // 同步成功,返回true + } + + // 内部类:表示笔记的具体内容(文本数据、通话数据) + private class NoteData { + private long mTextDataId; // 文本数据的ID + private ContentValues mTextDataValues; // 文本数据的内容 + private long mCallDataId; // 通话数据的ID + private ContentValues mCallDataValues; // 通话数据的内容 + + private static final String TAG = "NoteData"; // 用于日志输出的标签 + + public NoteData() { + mTextDataValues = new ContentValues(); // 初始化文本数据 + mCallDataValues = new ContentValues(); // 初始化通话数据 + mTextDataId = 0; + mCallDataId = 0; + } + + // 判断笔记内容是否本地已修改 + boolean isLocalModified() { + return mTextDataValues.size() > 0 || mCallDataValues.size() > 0; // 如果文本或通话数据有修改,返回true + } + + // 设置文本数据的ID + void setTextDataId(long id) { + if(id <= 0) { + throw new IllegalArgumentException("Text data id should larger than 0"); // 检查ID是否合法 + } + mTextDataId = id; // 设置文本数据ID + } + + // 设置通话数据的ID + void setCallDataId(long id) { + if (id <= 0) { + throw new IllegalArgumentException("Call data id should larger than 0"); // 检查ID是否合法 + } + mCallDataId = id; // 设置通话数据ID + } + + // 设置通话数据 + void setCallData(String key, String value) { + mCallDataValues.put(key, value); // 将通话数据放入ContentValues + mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); // 标记本地已修改 + mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); // 更新修改时间 + } + + // 设置文本数据 + void setTextData(String key, String value) { + mTextDataValues.put(key, value); // 将文本数据放入ContentValues + mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); // 标记本地已修改 + mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); // 更新修改时间 + } + + // 推送内容到ContentResolver + Uri pushIntoContentResolver(Context context, long noteId) { + if (noteId <= 0) { + throw new IllegalArgumentException("Wrong note id:" + noteId); // 检查笔记ID是否合法 + } + + ArrayList operationList = new ArrayList(); // 存储所有操作 + ContentProviderOperation.Builder builder = null; + + // 插入或更新文本数据 + if(mTextDataValues.size() > 0) { + mTextDataValues.put(DataColumns.NOTE_ID, noteId); // 设置笔记ID + if (mTextDataId == 0) { + mTextDataValues.put(DataColumns.MIME_TYPE, TextNote.CONTENT_ITEM_TYPE); // 设置MIME类型 + Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI, mTextDataValues); // 插入数据 + try { + setTextDataId(Long.valueOf(uri.getPathSegments().get(1))); // 获取文本数据ID + } catch (NumberFormatException e) { + Log.e(TAG, "Insert new text data fail with noteId" + noteId); + mTextDataValues.clear(); // 清空数据 + return null; + } + } else { + builder = ContentProviderOperation.newUpdate(ContentUris.withAppendedId( + Notes.CONTENT_DATA_URI, mTextDataId)); // 更新已有的文本数据 + builder.withValues(mTextDataValues); + operationList.add(builder.build()); + } + mTextDataValues.clear(); // 清空文本数据 + } + + // 插入或更新通话数据 + if(mCallDataValues.size() > 0) { + mCallDataValues.put(DataColumns.NOTE_ID, noteId); // 设置笔记ID + if (mCallDataId == 0) { + mCallDataValues.put(DataColumns.MIME_TYPE, CallNote.CONTENT_ITEM_TYPE); // 设置MIME类型 + Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI, mCallDataValues); // 插入数据 + try { + setCallDataId(Long.valueOf(uri.getPathSegments().get(1))); // 获取通话数据ID + } catch (NumberFormatException e) { + Log.e(TAG, "Insert new call data fail with noteId" + noteId); + mCallDataValues.clear(); // 清空数据 + return null; + } + } else { + builder = ContentProviderOperation.newUpdate(ContentUris.withAppendedId( + Notes.CONTENT_DATA_URI, mCallDataId)); // 更新已有的通话数据 + builder.withValues(mCallDataValues); + operationList.add(builder.build()); + } + mCallDataValues.clear(); // 清空通话数据 + } + + // 执行所有操作 + if (operationList.size() > 0) { + try { + ContentProviderResult[] results = context.getContentResolver().applyBatch( + Notes.AUTHORITY, operationList); // 批量执行操作 + return (results == null || results.length == 0 || results[0] == null) ? null + : ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId); // 返回URI + } catch (RemoteException e) { + Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); + return null; + } catch (OperationApplicationException e) { + Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); + return null; + } + } + return null; + } + } +} diff --git a/NoteEditActivity.java b/NoteEditActivity.java new file mode 100644 index 0000000..2d5bcc8 --- /dev/null +++ b/NoteEditActivity.java @@ -0,0 +1,265 @@ +/** + * 版权声明,说明该代码是MiCode开源社区的版权所有,并在Apache License 2.0下授权。 + */ + +package net.micode.notes.ui; + +// 导入所需的Android库和自定义包 + +public class NoteEditActivity extends Activity implements OnClickListener, + NoteSettingChangedListener, OnTextViewChangeListener { + // 内部类,用于持有笔记标题区域的视图元素 + private class HeadViewHolder { + public TextView tvModified; + public ImageView ivAlertIcon; + public TextView tvAlertDate; + public ImageView ibSetBgColor; + } + + // 定义背景颜色选择器和字体大小选择器的映射关系 + private static final Map sBgSelectorBtnsMap = new HashMap(); + static { + // 初始化背景颜色按钮映射 + } + private static final Map sBgSelectorSelectionMap = new HashMap(); + static { + // 初始化背景颜色选择器映射 + } + private static final Map sFontSizeBtnsMap = new HashMap(); + static { + // 初始化字体大小按钮映射 + } + private static final Map sFontSelectorSelectionMap = new HashMap(); + static { + // 初始化字体大小选择器映射 + } + + // 类变量定义 + private static final String TAG = "NoteEditActivity"; + private HeadViewHolder mNoteHeaderHolder; + private View mHeadViewPanel; + private View mNoteBgColorSelector; + private View mFontSizeSelector; + private EditText mNoteEditor; + private View mNoteEditorPanel; + private WorkingNote mWorkingNote; + private SharedPreferences mSharedPrefs; + private int mFontSizeId; + private static final String PREFERENCE_FONT_SIZE = "pref_font_size"; + private static final int SHORTCUT_ICON_TITLE_MAX_LEN = 10; + public static final String TAG_CHECKED = String.valueOf('\u221A'); + public static final String TAG_UNCHECKED = String.valueOf('\u25A1'); + private LinearLayout mEditTextList; + private String mUserQuery; + private Pattern mPattern; + + // onCreate方法,初始化Activity + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + this.setContentView(R.layout.note_edit); + // 省略了部分代码... + } + + // onRestoreInstanceState方法,用于在Activity被系统销毁后恢复状态 + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + // 省略了部分代码... + } + + // initActivityState方法,初始化Activity状态 + private boolean initActivityState(Intent intent) { + // 省略了部分代码... + } + + // onResume方法,初始化笔记屏幕 + @Override + protected void onResume() { + super.onResume(); + initNoteScreen(); + } + + // initNoteScreen方法,初始化笔记屏幕视图 + private void initNoteScreen() { + // 省略了部分代码... + } + + // onNewIntent方法,处理新的Intent + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + initActivityState(intent); + } + + // onSaveInstanceState方法,保存Activity状态 + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + // 省略了部分代码... + } + + // dispatchTouchEvent方法,处理触摸事件 + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + // 省略了部分代码... + } + + // inRangeOfView方法,判断触摸事件是否在视图范围内 + private boolean inRangeOfView(View view, MotionEvent ev) { + // 省略了部分代码... + } + + // initResources方法,初始化资源 + private void initResources() { + // 省略了部分代码... + } + + // onPause方法,保存笔记 + @Override + protected void onPause() { + super.onPause(); + if(saveNote()) { + Log.d(TAG, "Note data was saved with length:" + mWorkingNote.getContent().length()); + } + clearSettingState(); + } + + // updateWidget方法,更新Widget + private void updateWidget() { + // 省略了部分代码... + } + + // onClick方法,处理点击事件 + public void onClick(View v) { + // 省略了部分代码... + } + + // onBackPressed方法,处理返回键事件 + @Override + public void onBackPressed() { + // 省略了部分代码... + } + + // clearSettingState方法,清除设置状态 + private boolean clearSettingState() { + // 省略了部分代码... + } + + // onBackgroundColorChanged方法,处理背景颜色变化 + public void onBackgroundColorChanged() { + // 省略了部分代码... + } + + // onPrepareOptionsMenu方法,准备菜单 + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + // 省略了部分代码... + } + + // onOptionsItemSelected方法,处理菜单项点击事件 + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // 省略了部分代码... + } + + // setReminder方法,设置提醒 + private void setReminder() { + // 省略了部分代码... + } + + // sendTo方法,分享笔记到其他应用 + private void sendTo(Context context, String info) { + // 省略了部分代码... + } + + // createNewNote方法,创建新笔记 + private void createNewNote() { + // 省略了部分代码... + } + + // deleteCurrentNote方法,删除当前笔记 + private void deleteCurrentNote() { + // 省略了部分代码... + } + + // isSyncMode方法,判断是否是同步模式 + private boolean isSyncMode() { + // 省略了部分代码... + } + + // onClockAlertChanged方法,处理时钟提醒变化 + public void onClockAlertChanged(long date, boolean set) { + // 省略了部分代码... + } + + // onWidgetChanged方法,处理Widget变化 + public void onWidgetChanged() { + // 省略了部分代码... + } + + // onEditTextDelete方法,处理编辑文本删除 + public void onEditTextDelete(int index, String text) { + // 省略了部分代码... + } + + // onEditTextEnter方法,处理编辑文本回车 + public void onEditTextEnter(int index, String text) { + // 省略了部分代码... + } + + // switchToListMode方法,切换到列表模式 + private void switchToListMode(String text) { + // 省略了部分代码... + } + + // getHighlightQueryResult方法,高亮查询结果 + private Spannable getHighlightQueryResult(String fullText, String userQuery) { + // 省略了部分代码... + } + + // getListItem方法,获取列表项视图 + private View getListItem(String item, int index) { + // 省略了部分代码... + } + + // onTextChange方法,处理文本变化 + public void onTextChange(int index, boolean hasText) { + // 省略了部分代码... + } + + // onCheckListModeChanged方法,处理检查列表模式变化 + public void onCheckListModeChanged(int oldMode, int newMode) { + // 省略了部分代码... + } + + // getWorkingText方法,获取工作文本 + private boolean getWorkingText() { + // 省略了部分代码... + } + + // saveNote方法,保存笔记 + private boolean saveNote() { + // 省略了部分代码... + } + + // sendToDesktop方法,发送到桌面 + private void sendToDesktop() { + // 省略了部分代码... + } + + // makeShortcutIconTitle方法,制作快捷方式图标标题 + private String makeShortcutIconTitle(String content) { + // 省略了部分代码... + } + + // showToast方法,显示Toast提示 + private void showToast(int resId) { + showToast(resId, Toast.LENGTH_SHORT); + } + + // showToast方法,显示Toast提示(带持续时间) + private void showToast(int resId, int duration) { + + } +} \ No newline at end of file diff --git a/NoteEditText.java b/NoteEditText.java new file mode 100644 index 0000000..615b25c --- /dev/null +++ b/NoteEditText.java @@ -0,0 +1,269 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.Context; +import android.graphics.Rect; +import android.text.Layout; +import android.text.Selection; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.style.URLSpan; +import android.util.AttributeSet; +import android.util.Log; +import android.view.ContextMenu; +import android.view.KeyEvent; +import android.view.MenuItem; +import android.view.MenuItem.OnMenuItemClickListener; +import android.view.MotionEvent; +import android.widget.EditText; + +import net.micode.notes.R; + +import java.util.HashMap; +import java.util.Map; + +/** + * NoteEditText 类继承自 EditText,用于处理带有链接文本的编辑框。 + * 它支持文本链接的点击、删除和回车事件的处理,并且能够处理焦点变化时的文本变化。 + */ +public class NoteEditText extends EditText { + private static final String TAG = "NoteEditText"; + + // 当前 NoteEditText 的索引,用于区分不同的 EditText。 + private int mIndex; + + // 删除之前的光标位置,用于判断删除操作。 + private int mSelectionStartBeforeDelete; + + // 定义三种链接方案:电话、网页和电子邮件。 + private static final String SCHEME_TEL = "tel:"; + private static final String SCHEME_HTTP = "http:"; + private static final String SCHEME_EMAIL = "mailto:"; + + // 用于映射链接方案和对应的菜单项资源。 + private static final Map sSchemaActionResMap = new HashMap(); + + static { + // 初始化链接方案和对应的菜单项。 + sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel); + sSchemaActionResMap.put(SCHEME_HTTP, R.string.note_link_web); + sSchemaActionResMap.put(SCHEME_EMAIL, R.string.note_link_email); + } + + /** + * 用于监听 NoteEditText 变化的接口。可以监听删除、回车和文本变化。 + */ + public interface OnTextViewChangeListener { + /** + * 当按下删除键时调用,删除当前编辑框中的文本。 + * @param index 当前编辑框的索引 + * @param text 删除的文本内容 + */ + void onEditTextDelete(int index, String text); + + /** + * 当按下回车键时调用,添加新的编辑框。 + * @param index 新编辑框的索引 + * @param text 当前编辑框的内容 + */ + void onEditTextEnter(int index, String text); + + /** + * 当文本发生变化时,隐藏或显示操作项。 + * @param index 当前编辑框的索引 + * @param hasText 是否有文本 + */ + void onTextChange(int index, boolean hasText); + } + + // 存储 OnTextViewChangeListener 的实例 + private OnTextViewChangeListener mOnTextViewChangeListener; + + // 构造函数,初始化 NoteEditText。 + public NoteEditText(Context context) { + super(context, null); + mIndex = 0; + } + + // 设置当前 NoteEditText 的索引 + public void setIndex(int index) { + mIndex = index; + } + + // 设置监听文本变化的监听器 + public void setOnTextViewChangeListener(OnTextViewChangeListener listener) { + mOnTextViewChangeListener = listener; + } + + // 通过属性集初始化 NoteEditText + public NoteEditText(Context context, AttributeSet attrs) { + super(context, attrs, android.R.attr.editTextStyle); + } + + // 通过属性集和默认样式初始化 NoteEditText + public NoteEditText(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + /** + * 处理触摸事件,当点击文本时更新光标的位置。 + * @param event 触摸事件 + * @return 是否处理该事件 + */ + @Override + public boolean onTouchEvent(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + int x = (int) event.getX(); + int y = (int) event.getY(); + x -= getTotalPaddingLeft(); // 获取点击点的 x 坐标 + y -= getTotalPaddingTop(); // 获取点击点的 y 坐标 + x += getScrollX(); // 获取滚动的 x 偏移 + y += getScrollY(); // 获取滚动的 y 偏移 + + Layout layout = getLayout(); // 获取文本布局 + int line = layout.getLineForVertical(y); // 获取点击点所在的行 + int off = layout.getOffsetForHorizontal(line, x); // 获取点击点的偏移位置 + Selection.setSelection(getText(), off); // 设置光标位置 + break; + } + return super.onTouchEvent(event); + } + + /** + * 处理按键事件,主要是处理回车和删除键。 + * @param keyCode 按键代码 + * @param event 按键事件 + * @return 是否处理该事件 + */ + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_ENTER: + // 按下回车键时,如果设置了监听器则继续处理 + if (mOnTextViewChangeListener != null) { + return false; + } + break; + case KeyEvent.KEYCODE_DEL: + // 按下删除键时记录光标位置 + mSelectionStartBeforeDelete = getSelectionStart(); + break; + default: + break; + } + return super.onKeyDown(keyCode, event); + } + + /** + * 处理按键释放事件,主要处理删除和回车键的后续动作。 + * @param keyCode 按键代码 + * @param event 按键事件 + * @return 是否处理该事件 + */ + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_DEL: + if (mOnTextViewChangeListener != null) { + // 如果删除键按下时光标位置在起始位置并且索引不为 0,则通知监听器删除操作 + if (0 == mSelectionStartBeforeDelete && mIndex != 0) { + mOnTextViewChangeListener.onEditTextDelete(mIndex, getText().toString()); + return true; + } + } else { + Log.d(TAG, "OnTextViewChangeListener was not setted"); + } + break; + case KeyEvent.KEYCODE_ENTER: + if (mOnTextViewChangeListener != null) { + // 回车键按下时,将当前文本分割并传递给监听器 + int selectionStart = getSelectionStart(); + String text = getText().subSequence(selectionStart, length()).toString(); + setText(getText().subSequence(0, selectionStart)); // 设置文本为回车前的内容 + mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, text); // 通知监听器添加新的编辑框 + } else { + Log.d(TAG, "OnTextViewChangeListener was not setted"); + } + break; + default: + break; + } + return super.onKeyUp(keyCode, event); + } + + /** + * 处理焦点变化事件,通知监听器文本变化。 + * @param focused 是否获得焦点 + * @param direction 焦点方向 + * @param previouslyFocusedRect 焦点变化前的位置 + */ + @Override + protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { + if (mOnTextViewChangeListener != null) { + if (!focused && TextUtils.isEmpty(getText())) { + mOnTextViewChangeListener.onTextChange(mIndex, false); // 如果文本为空且失去焦点,则通知监听器 + } else { + mOnTextViewChangeListener.onTextChange(mIndex, true); // 否则通知监听器文本有内容 + } + } + super.onFocusChanged(focused, direction, previouslyFocusedRect); + } + + /** + * 创建上下文菜单,当文本中包含 URL 链接时,显示相应的菜单项。 + * @param menu 上下文菜单 + */ + @Override + protected void onCreateContextMenu(ContextMenu menu) { + if (getText() instanceof Spanned) { + int selStart = getSelectionStart(); + int selEnd = getSelectionEnd(); + + int min = Math.min(selStart, selEnd); + int max = Math.max(selStart, selEnd); + + // 获取选中的文本中的 URLSpan + final URLSpan[] urls = ((Spanned) getText()).getSpans(min, max, URLSpan.class); + if (urls.length == 1) { + int defaultResId = 0; + // 根据 URL 的 Scheme 映射到相应的菜单项 + for(String schema: sSchemaActionResMap.keySet()) { + if(urls[0].getURL().indexOf(schema) >= 0) { + defaultResId = sSchemaActionResMap.get(schema); + break; + } + } + + if (defaultResId == 0) { + defaultResId = R.string.note_link_other; // 如果没有匹配的 Scheme,则使用默认菜单项 + } + + menu.add(0, 0, 0, defaultResId).setOnMenuItemClickListener( + new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + // 当点击菜单项时,执行 URLSpan 的点击事件 + urls[0].onClick(NoteEditText.this); + return true; + } + }); + } + } + super.onCreateContextMenu(menu); + } +} diff --git a/NoteItemData.java b/NoteItemData.java new file mode 100644 index 0000000..3d13ad6 --- /dev/null +++ b/NoteItemData.java @@ -0,0 +1,255 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.Context; +import android.database.Cursor; +import android.text.TextUtils; + +import net.micode.notes.data.Contact; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.tool.DataUtils; + +/** + * NoteItemData 类封装了从数据库中读取的笔记项数据。它提供了对笔记项的各种属性和状态的访问, + * 包括笔记的创建日期、修改日期、是否有附件、是否有提醒、笔记的类型等。 + */ +public class NoteItemData { + // 定义查询数据库时的投影字段(即需要从数据库查询的列) + static final String [] PROJECTION = new String [] { + NoteColumns.ID, // 笔记ID + NoteColumns.ALERTED_DATE, // 提醒日期 + NoteColumns.BG_COLOR_ID, // 背景颜色ID + NoteColumns.CREATED_DATE, // 创建日期 + NoteColumns.HAS_ATTACHMENT, // 是否有附件 + NoteColumns.MODIFIED_DATE, // 修改日期 + NoteColumns.NOTES_COUNT, // 笔记数量 + NoteColumns.PARENT_ID, // 父文件夹ID + NoteColumns.SNIPPET, // 笔记内容摘要 + NoteColumns.TYPE, // 笔记类型 + NoteColumns.WIDGET_ID, // 小部件ID + NoteColumns.WIDGET_TYPE, // 小部件类型 + }; + + // 各列索引 + private static final int ID_COLUMN = 0; + private static final int ALERTED_DATE_COLUMN = 1; + private static final int BG_COLOR_ID_COLUMN = 2; + private static final int CREATED_DATE_COLUMN = 3; + private static final int HAS_ATTACHMENT_COLUMN = 4; + private static final int MODIFIED_DATE_COLUMN = 5; + private static final int NOTES_COUNT_COLUMN = 6; + private static final int PARENT_ID_COLUMN = 7; + private static final int SNIPPET_COLUMN = 8; + private static final int TYPE_COLUMN = 9; + private static final int WIDGET_ID_COLUMN = 10; + private static final int WIDGET_TYPE_COLUMN = 11; + + // 笔记项的各个属性 + private long mId; // 笔记ID + private long mAlertDate; // 提醒日期 + private int mBgColorId; // 背景颜色ID + private long mCreatedDate; // 创建日期 + private boolean mHasAttachment; // 是否有附件 + private long mModifiedDate; // 修改日期 + private int mNotesCount; // 笔记数量 + private long mParentId; // 父文件夹ID + private String mSnippet; // 笔记摘要 + private int mType; // 笔记类型 + private int mWidgetId; // 小部件ID + private int mWidgetType; // 小部件类型 + private String mName; // 联系人名称 + private String mPhoneNumber; // 电话号码 + + // 状态标志 + private boolean mIsLastItem; // 是否是最后一项 + private boolean mIsFirstItem; // 是否是第一项 + private boolean mIsOnlyOneItem; // 是否是唯一一项 + private boolean mIsOneNoteFollowingFolder; // 是否是单个笔记跟随文件夹 + private boolean mIsMultiNotesFollowingFolder; // 是否有多个笔记跟随文件夹 + + /** + * 构造函数,根据给定的 Context 和 Cursor 从数据库中读取笔记项数据,并初始化该对象。 + * @param context 上下文 + * @param cursor 数据库游标,包含笔记项的数据 + */ + public NoteItemData(Context context, Cursor cursor) { + // 从游标中获取笔记项数据并初始化类的各个属性 + mId = cursor.getLong(ID_COLUMN); + mAlertDate = cursor.getLong(ALERTED_DATE_COLUMN); + mBgColorId = cursor.getInt(BG_COLOR_ID_COLUMN); + mCreatedDate = cursor.getLong(CREATED_DATE_COLUMN); + mHasAttachment = (cursor.getInt(HAS_ATTACHMENT_COLUMN) > 0); // 判断是否有附件 + mModifiedDate = cursor.getLong(MODIFIED_DATE_COLUMN); + mNotesCount = cursor.getInt(NOTES_COUNT_COLUMN); + mParentId = cursor.getLong(PARENT_ID_COLUMN); + mSnippet = cursor.getString(SNIPPET_COLUMN); // 获取笔记摘要 + // 清理掉摘要中的标签 + mSnippet = mSnippet.replace(NoteEditActivity.TAG_CHECKED, "").replace( + NoteEditActivity.TAG_UNCHECKED, ""); + mType = cursor.getInt(TYPE_COLUMN); + mWidgetId = cursor.getInt(WIDGET_ID_COLUMN); + mWidgetType = cursor.getInt(WIDGET_TYPE_COLUMN); + + // 如果笔记属于通话记录文件夹,则尝试获取联系人的名称和电话号码 + mPhoneNumber = ""; + if (mParentId == Notes.ID_CALL_RECORD_FOLDER) { + mPhoneNumber = DataUtils.getCallNumberByNoteId(context.getContentResolver(), mId); + if (!TextUtils.isEmpty(mPhoneNumber)) { + mName = Contact.getContact(context, mPhoneNumber); // 获取联系人名称 + if (mName == null) { + mName = mPhoneNumber; // 如果没有联系人名称,则使用电话号码 + } + } + } + + // 如果没有联系人名称,则设置为空字符串 + if (mName == null) { + mName = ""; + } + + // 检查笔记项在数据库中的位置(是否是第一个、最后一个或唯一项等) + checkPostion(cursor); + } + + /** + * 根据游标位置判断该笔记项的状态(例如:是否为第一个、最后一个项等) + * @param cursor 数据库游标 + */ + private void checkPostion(Cursor cursor) { + mIsLastItem = cursor.isLast(); // 判断是否是最后一项 + mIsFirstItem = cursor.isFirst(); // 判断是否是第一项 + mIsOnlyOneItem = (cursor.getCount() == 1); // 判断是否是唯一一项 + mIsMultiNotesFollowingFolder = false; + mIsOneNoteFollowingFolder = false; + + // 如果笔记是普通笔记且不是第一个项,检查是否紧跟在文件夹后面 + if (mType == Notes.TYPE_NOTE && !mIsFirstItem) { + int position = cursor.getPosition(); + if (cursor.moveToPrevious()) { + if (cursor.getInt(TYPE_COLUMN) == Notes.TYPE_FOLDER + || cursor.getInt(TYPE_COLUMN) == Notes.TYPE_SYSTEM) { + // 如果当前笔记项后面有多个笔记,则标记为多个笔记跟随文件夹 + if (cursor.getCount() > (position + 1)) { + mIsMultiNotesFollowingFolder = true; + } else { + mIsOneNoteFollowingFolder = true; // 否则标记为单个笔记跟随文件夹 + } + } + if (!cursor.moveToNext()) { + throw new IllegalStateException("cursor move to previous but can't move back"); + } + } + } + } + + // 以下是公开的访问方法,用于获取该笔记项的各个属性值 + + public boolean isOneFollowingFolder() { + return mIsOneNoteFollowingFolder; + } + + public boolean isMultiFollowingFolder() { + return mIsMultiNotesFollowingFolder; + } + + public boolean isLast() { + return mIsLastItem; + } + + public String getCallName() { + return mName; + } + + public boolean isFirst() { + return mIsFirstItem; + } + + public boolean isSingle() { + return mIsOnlyOneItem; + } + + public long getId() { + return mId; + } + + public long getAlertDate() { + return mAlertDate; + } + + public long getCreatedDate() { + return mCreatedDate; + } + + public boolean hasAttachment() { + return mHasAttachment; + } + + public long getModifiedDate() { + return mModifiedDate; + } + + public int getBgColorId() { + return mBgColorId; + } + + public long getParentId() { + return mParentId; + } + + public int getNotesCount() { + return mNotesCount; + } + + public long getFolderId () { + return mParentId; + } + + public int getType() { + return mType; + } + + public int getWidgetType() { + return mWidgetType; + } + + public int getWidgetId() { + return mWidgetId; + } + + public String getSnippet() { + return mSnippet; + } + + public boolean hasAlert() { + return (mAlertDate > 0); + } + + public boolean isCallRecord() { + return (mParentId == Notes.ID_CALL_RECORD_FOLDER && !TextUtils.isEmpty(mPhoneNumber)); + } + + /** + * 从游标中获取笔记类型 + * @param cursor 数据库游标 + * @return 笔记类型 + */ + public static int getNoteType(Cursor cursor) { + return cursor.getInt(TYPE_COLUMN); + } +} diff --git a/NoteWidgetProvider.java b/NoteWidgetProvider.java new file mode 100644 index 0000000..5ed9cd6 --- /dev/null +++ b/NoteWidgetProvider.java @@ -0,0 +1,163 @@ +/* + * 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.widget; +// 导入所需的Android类和接口 + +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.util.Log; +import android.widget.RemoteViews; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.tool.ResourceParser; +import net.micode.notes.ui.NoteEditActivity; +import net.micode.notes.ui.NotesListActivity; + +// 定义一个抽象类NoteWidgetProvider,继承自AppWidgetProvider +public abstract class NoteWidgetProvider extends AppWidgetProvider { + // 定义查询数据库时需要的列 + public static final String [] PROJECTION = new String [] { + NoteColumns.ID, + NoteColumns.BG_COLOR_ID, + NoteColumns.SNIPPET + }; + + // 定义列的索引 + public static final int COLUMN_ID = 0; + public static final int COLUMN_BG_COLOR_ID = 1; + public static final int COLUMN_SNIPPET = 2; + + // 定义日志标签 + private static final String TAG = "NoteWidgetProvider"; + + // 当小部件被删除时调用 + @Override + public void onDeleted(Context context, int[] appWidgetIds) { + // 更新数据库,将被删除小部件的WIDGET_ID设置为无效 + ContentValues values = new ContentValues(); + values.put(NoteColumns.WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); + for (int i = 0; i < appWidgetIds.length; i++) { + context.getContentResolver().update(Notes.CONTENT_NOTE_URI, + values, + NoteColumns.WIDGET_ID + "=?", + new String[] { String.valueOf(appWidgetIds[i])}); + } + } + + // 获取小部件的笔记信息 + private Cursor getNoteWidgetInfo(Context context, int widgetId) { + return context.getContentResolver().query(Notes.CONTENT_NOTE_URI, + PROJECTION, + NoteColumns.WIDGET_ID + "=? AND " + NoteColumns.PARENT_ID + "<>?", + new String[] { String.valueOf(widgetId), String.valueOf(Notes.ID_TRASH_FOLER) }, + null); + } + + // 更新小部件 + protected void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + update(context, appWidgetManager, appWidgetIds, false); + } + + // 更新小部件,可以设置隐私模式 + private void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds, + boolean privacyMode) { + for (int i = 0; i < appWidgetIds.length; i++) { + if (appWidgetIds[i] != AppWidgetManager.INVALID_APPWIDGET_ID) { + // 默认背景ID + int bgId = ResourceParser.getDefaultBgId(context); + // 笔记摘要 + String snippet = ""; + // 编辑笔记的Intent + Intent intent = new Intent(context, NoteEditActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + intent.putExtra(Notes.INTENT_EXTRA_WIDGET_ID, appWidgetIds[i]); + intent.putExtra(Notes.INTENT_EXTRA_WIDGET_TYPE, getWidgetType()); + + // 获取笔记信息的Cursor + Cursor c = getNoteWidgetInfo(context, appWidgetIds[i]); + if (c != null && c.moveToFirst()) { + if (c.getCount() > 1) { + // 如果有多个相同的widget id,记录错误并返回 + Log.e(TAG, "Multiple message with same widget id:" + appWidgetIds[i]); + c.close(); + return; + } + // 获取笔记摘要和背景ID + snippet = c.getString(COLUMN_SNIPPET); + bgId = c.getInt(COLUMN_BG_COLOR_ID); + intent.putExtra(Intent.EXTRA_UID, c.getLong(COLUMN_ID)); + intent.setAction(Intent.ACTION_VIEW); + } else { + // 如果没有找到笔记,显示默认文本 + snippet = context.getResources().getString(R.string.widget_havenot_content); + intent.setAction(Intent.ACTION_INSERT_OR_EDIT); + } + + // 关闭Cursor + if (c != null) { + c.close(); + } + + // 创建RemoteViews对象 + RemoteViews rv = new RemoteViews(context.getPackageName(), getLayoutId()); + // 设置背景资源 + rv.setImageViewResource(R.id.widget_bg_image, getBgResourceId(bgId)); + // 将背景ID放入Intent + intent.putExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, bgId); + /** + * 生成PendingIntent,用于启动宿主Activity + */ + PendingIntent pendingIntent = null; + if (privacyMode) { + // 如果是隐私模式,显示隐私模式文本 + rv.setTextViewText(R.id.widget_text, + context.getString(R.string.widget_under_visit_mode)); + // 创建PendingIntent,用于启动NotesListActivity + pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], new Intent( + context, NotesListActivity.class), PendingIntent.FLAG_UPDATE_CURRENT); + } else { + // 显示笔记摘要 + rv.setTextViewText(R.id.widget_text, snippet); + // 创建PendingIntent,用于启动NoteEditActivity + pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], intent, + PendingIntent.FLAG_UPDATE_CURRENT); + } + + // 设置点击事件 + rv.setOnClickPendingIntent(R.id.widget_text, pendingIntent); + // 更新小部件 + appWidgetManager.updateAppWidget(appWidgetIds[i], rv); + } + } + } + + // 获取背景资源ID的方法,需要子类实现 + protected abstract int getBgResourceId(int bgId); + + // 获取布局资源ID的方法,需要子类实现 + protected abstract int getLayoutId(); + + // 获取小部件类型的方法,需要子类实现 + protected abstract int getWidgetType(); +} \ No newline at end of file diff --git a/NoteWidgetProvider_2x.java b/NoteWidgetProvider_2x.java new file mode 100644 index 0000000..5535989 --- /dev/null +++ b/NoteWidgetProvider_2x.java @@ -0,0 +1,55 @@ +/* + * 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.widget; + +import android.appwidget.AppWidgetManager; +import android.content.Context; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.tool.ResourceParser; + +// 定义一个具体的NoteWidgetProvider_2x类,继承自NoteWidgetProvider +public class NoteWidgetProvider_2x extends NoteWidgetProvider { + // 当小部件需要更新时调用 + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + // 调用父类的update方法进行更新 + super.update(context, appWidgetManager, appWidgetIds); + } + + // 获取小部件的布局资源ID + @Override + protected int getLayoutId() { + // 返回2x小部件的布局资源ID + return R.layout.widget_2x; + } + + // 获取小部件的背景资源ID + @Override + protected int getBgResourceId(int bgId) { + // 根据传入的背景ID,返回对应的2x小部件背景资源ID + return ResourceParser.WidgetBgResources.getWidget2xBgResource(bgId); + } + + // 获取小部件的类型 + @Override + protected int getWidgetType() { + // 返回小部件的类型,这里是2x类型的笔记小部件 + return Notes.TYPE_WIDGET_2X; + } +} \ No newline at end of file diff --git a/NoteWidgetProvider_4x.java b/NoteWidgetProvider_4x.java new file mode 100644 index 0000000..bf302c3 --- /dev/null +++ b/NoteWidgetProvider_4x.java @@ -0,0 +1,55 @@ +/* + * 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.widget; + +import android.appwidget.AppWidgetManager; +import android.content.Context; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.tool.ResourceParser; + +// 定义一个具体的NoteWidgetProvider_4x类,继承自NoteWidgetProvider +public class NoteWidgetProvider_4x extends NoteWidgetProvider { + // 当小部件需要更新时调用 + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + // 调用父类的update方法进行更新 + super.update(context, appWidgetManager, appWidgetIds); + } + + // 获取小部件的布局资源ID + @Override + protected int getLayoutId() { + // 返回4x小部件的布局资源ID + return R.layout.widget_4x; + } + + // 获取小部件的背景资源ID + @Override + protected int getBgResourceId(int bgId) { + // 根据传入的背景ID,返回对应的4x小部件背景资源ID + return ResourceParser.WidgetBgResources.getWidget4xBgResource(bgId); + } + + // 获取小部件的类型 + @Override + protected int getWidgetType() { + // 返回小部件的类型,这里是4x类型的笔记小部件 + return Notes.TYPE_WIDGET_4X; + } +} \ No newline at end of file diff --git a/NotesDatabaseHelper.java b/NotesDatabaseHelper.java new file mode 100644 index 0000000..fa468e0 --- /dev/null +++ b/NotesDatabaseHelper.java @@ -0,0 +1,283 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.data; + +import android.content.ContentValues; +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.util.Log; + +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.DataConstants; +import net.micode.notes.data.Notes.NoteColumns; + +/** + * NotesDatabaseHelper类用于管理数据库的创建和升级,继承自SQLiteOpenHelper类。 + */ +public class NotesDatabaseHelper extends SQLiteOpenHelper { + // 数据库名 + private static final String DB_NAME = "note.db"; + // 数据库版本 + private static final int DB_VERSION = 4; + + // 数据表的名称 + public interface TABLE { + public static final String NOTE = "note"; + public static final String DATA = "data"; + } + + private static final String TAG = "NotesDatabaseHelper"; + + // 单例模式 + private static NotesDatabaseHelper mInstance; + + // 创建note表的SQL语句 + private static final String CREATE_NOTE_TABLE_SQL = + "CREATE TABLE " + TABLE.NOTE + "(" + + NoteColumns.ID + " INTEGER PRIMARY KEY," + + NoteColumns.PARENT_ID + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.ALERTED_DATE + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.BG_COLOR_ID + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + + NoteColumns.HAS_ATTACHMENT + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + + NoteColumns.NOTES_COUNT + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.SNIPPET + " TEXT NOT NULL DEFAULT ''," + + NoteColumns.TYPE + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.WIDGET_ID + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.WIDGET_TYPE + " INTEGER NOT NULL DEFAULT -1," + + NoteColumns.SYNC_ID + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.LOCAL_MODIFIED + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.ORIGIN_PARENT_ID + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''," + + NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0" + + ")"; + + // 创建data表的SQL语句 + private static final String CREATE_DATA_TABLE_SQL = + "CREATE TABLE " + TABLE.DATA + "(" + + DataColumns.ID + " INTEGER PRIMARY KEY," + + DataColumns.MIME_TYPE + " TEXT NOT NULL," + + DataColumns.NOTE_ID + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + + NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + + DataColumns.CONTENT + " TEXT NOT NULL DEFAULT ''," + + DataColumns.DATA1 + " INTEGER," + + DataColumns.DATA2 + " INTEGER," + + DataColumns.DATA3 + " TEXT NOT NULL DEFAULT ''," + + DataColumns.DATA4 + " TEXT NOT NULL DEFAULT ''," + + DataColumns.DATA5 + " TEXT NOT NULL DEFAULT ''" + + ")"; + + // 创建data表中NOTE_ID的索引 + private static final String CREATE_DATA_NOTE_ID_INDEX_SQL = + "CREATE INDEX IF NOT EXISTS note_id_index ON " + + TABLE.DATA + "(" + DataColumns.NOTE_ID + ");"; + + /** + * 增加文件夹中的笔记数,当笔记移动到该文件夹时 + */ + private static final String NOTE_INCREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER = + "CREATE TRIGGER increase_folder_count_on_update "+ + " AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE + + " BEGIN " + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" + + " WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" + + " END"; + + /** + * 减少文件夹中的笔记数,当笔记从该文件夹中移除时 + */ + private static final String NOTE_DECREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER = + "CREATE TRIGGER decrease_folder_count_on_update " + + " AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE + + " BEGIN " + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" + + " WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID + + " AND " + NoteColumns.NOTES_COUNT + ">0" + ";" + + " END"; + + /** + * 增加文件夹中的笔记数,当插入新的笔记到该文件夹时 + */ + private static final String NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER = + "CREATE TRIGGER increase_folder_count_on_insert " + + " AFTER INSERT ON " + TABLE.NOTE + + " BEGIN " + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" + + " WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" + + " END"; + + /** + * 减少文件夹中的笔记数,当从该文件夹删除笔记时 + */ + private static final String NOTE_DECREASE_FOLDER_COUNT_ON_DELETE_TRIGGER = + "CREATE TRIGGER decrease_folder_count_on_delete " + + " AFTER DELETE ON " + TABLE.NOTE + + " BEGIN " + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" + + " WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID + + " AND " + NoteColumns.NOTES_COUNT + ">0;" + + " END"; + + /** + * 当插入类型为NOTE的数据时更新笔记内容 + */ + private static final String DATA_UPDATE_NOTE_CONTENT_ON_INSERT_TRIGGER = + "CREATE TRIGGER update_note_content_on_insert " + + " AFTER INSERT ON " + TABLE.DATA + + " WHEN new." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" + + " BEGIN" + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT + + " WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" + + " END"; + + /** + * 当类型为NOTE的数据更新时,更新笔记内容 + */ + private static final String DATA_UPDATE_NOTE_CONTENT_ON_UPDATE_TRIGGER = + "CREATE TRIGGER update_note_content_on_update " + + " AFTER UPDATE ON " + TABLE.DATA + + " WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" + + " BEGIN" + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT + + " WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" + + " END"; + + /** + * 当类型为NOTE的数据删除时,更新笔记内容 + */ + private static final String DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER = + "CREATE TRIGGER update_note_content_on_delete " + + " AFTER delete ON " + TABLE.DATA + + " WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" + + " BEGIN" + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.SNIPPET + "=''" + + " WHERE " + NoteColumns.ID + "=old." + DataColumns.NOTE_ID + ";" + + " END"; + + /** + * 当笔记被删除时,删除相关数据 + */ + private static final String NOTE_DELETE_DATA_ON_DELETE_TRIGGER = + "CREATE TRIGGER delete_data_on_delete " + + " AFTER DELETE ON " + TABLE.NOTE + + " BEGIN" + + " DELETE FROM " + TABLE.DATA + + " WHERE " + DataColumns.NOTE_ID + "=old." + NoteColumns.ID + ";" + + " END"; + + /** + * 当文件夹被删除时,删除该文件夹中的笔记 + */ + private static final String FOLDER_DELETE_NOTES_ON_DELETE_TRIGGER = + "CREATE TRIGGER folder_delete_notes_on_delete " + + " AFTER DELETE ON " + TABLE.NOTE + + " BEGIN " + + " DELETE FROM " + TABLE.NOTE + + " WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" + + " END"; + + /** + * 获取数据库实例,确保数据库只有一个实例 + * + * @param context 应用上下文 + * @return 数据库实例 + */ + public static synchronized NotesDatabaseHelper getInstance(Context context) { + if (mInstance == null) { + mInstance = new NotesDatabaseHelper(context); + } + return mInstance; + } + + /** + * 构造函数,初始化数据库 + * + * @param context 应用上下文 + */ + private NotesDatabaseHelper(Context context) { + super(context, DB_NAME, null, DB_VERSION); + } + + /** + * 创建数据库,初始化数据表 + * + * @param db SQLite数据库实例 + */ + @Override + public void onCreate(SQLiteDatabase db) { + Log.d(TAG, "onCreate"); + db.execSQL(CREATE_NOTE_TABLE_SQL); + db.execSQL(CREATE_DATA_TABLE_SQL); + db.execSQL(CREATE_DATA_NOTE_ID_INDEX_SQL); + + db.execSQL(NOTE_INCREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER); + db.execSQL(NOTE_DECREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER); + db.execSQL(NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER); + db.execSQL(NOTE_DECREASE_FOLDER_COUNT_ON_DELETE_TRIGGER); + + db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_INSERT_TRIGGER); + db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_UPDATE_TRIGGER); + db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER); + + db.execSQL(NOTE_DELETE_DATA_ON_DELETE_TRIGGER); + db.execSQL(FOLDER_DELETE_NOTES_ON_DELETE_TRIGGER); + + // 初始化默认文件夹 + initDefaultFolder(db); + } + + /** + * 更新数据库,处理数据库版本升级 + * + * @param db SQLite数据库实例 + * @param oldVersion 旧版本号 + * @param newVersion 新版本号 + */ + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + Log.d(TAG, "onUpgrade"); + // 这里可以根据不同版本号处理升级逻辑,当前暂时删除旧表并重新创建 + if (oldVersion != newVersion) { + db.execSQL("DROP TABLE IF EXISTS " + TABLE.NOTE); + db.execSQL("DROP TABLE IF EXISTS " + TABLE.DATA); + onCreate(db); + } + } + + /** + * 初始化默认文件夹,在第一次创建数据库时调用 + * + * @param db SQLite数据库实例 + */ + private void initDefaultFolder(SQLiteDatabase db) { + ContentValues values = new ContentValues(); + values.put(NoteColumns.ID, Notes.ID_ROOT_FOLDER); + values.put(NoteColumns.SNIPPET, "ROOT"); + values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); + db.insert(TABLE.NOTE, null, values); + } +} diff --git a/NotesListActivity.java b/NotesListActivity.java new file mode 100644 index 0000000..aceb3ba --- /dev/null +++ b/NotesListActivity.java @@ -0,0 +1,230 @@ +/** + * 版权声明,说明该代码是MiCode开源社区的版权所有,并在Apache License 2.0下授权。 + */ + +package net.micode.notes.ui; + +// 导入所需的Android库和自定义包 + +public class NotesListActivity extends Activity implements OnClickListener, OnItemLongClickListener { + // 定义了一些查询操作的token,用于识别不同的后台查询操作 + private static final int FOLDER_NOTE_LIST_QUERY_TOKEN = 0; + private static final int FOLDER_LIST_QUERY_TOKEN = 1; + + // 定义了菜单项的ID + private static final int MENU_FOLDER_DELETE = 0; + private static final int MENU_FOLDER_VIEW = 1; + private static final int MENU_FOLDER_CHANGE_NAME = 2; + + // 定义了用户首次使用应用时显示的介绍信息的SharedPreferences键 + private static final String PREFERENCE_ADD_INTRODUCTION = "net.micode.notes.introduction"; + + // 枚举,定义了不同的列表编辑状态 + private enum ListEditState { + NOTE_LIST, SUB_FOLDER, CALL_RECORD_FOLDER + }; + + // 类变量定义 + private ListEditState mState; + private BackgroundQueryHandler mBackgroundQueryHandler; + private NotesListAdapter mNotesListAdapter; + private ListView mNotesListView; + private Button mAddNewNote; + private boolean mDispatch; + private int mOriginY; + private int mDispatchY; + private TextView mTitleBar; + private long mCurrentFolderId; + private ContentResolver mContentResolver; + private ModeCallback mModeCallBack; + private static final String TAG = "NotesListActivity"; + private static final int NOTES_LISTVIEW_SCROLL_RATE = 30; + private NoteItemData mFocusNoteDataItem; + private static final String NORMAL_SELECTION = NoteColumns.PARENT_ID + "=?"; + private static final String ROOT_FOLDER_SELECTION = "(" + NoteColumns.TYPE + "<>?" + + Notes.TYPE_SYSTEM + " AND " + NoteColumns.PARENT_ID + "=?)" + " OR (" + + NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER + " AND " + + NoteColumns.NOTES_COUNT + ">0)"; + private final static int REQUEST_CODE_OPEN_NODE = 102; + private final static int REQUEST_CODE_NEW_NODE = 103; + + // onCreate方法,初始化Activity + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.note_list); + initResources(); + + // 用户首次使用应用时,插入介绍信息 + setAppInfoFromRawRes(); + } + + // onActivityResult方法,处理其他Activity返回的结果 + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + // 省略了部分代码... + } + + // setAppInfoFromRawRes方法,从raw资源中读取介绍信息并显示 + private void setAppInfoFromRawRes() { + // 省略了部分代码... + } + + // onStart方法,Activity启动时开始异步查询笔记列表 + @Override + protected void onStart() { + super.onStart(); + startAsyncNotesListQuery(); + } + + // initResources方法,初始化资源 + private void initResources() { + // 省略了部分代码... + } + + // ModeCallback内部类,实现了ListView的MultiChoiceModeListener和OnMenuItemClickListener接口 + private class ModeCallback implements ListView.MultiChoiceModeListener, OnMenuItemClickListener { + // 省略了部分代码... + } + + // NewNoteOnTouchListener内部类,处理“新建笔记”按钮的触摸事件 + private class NewNoteOnTouchListener implements OnTouchListener { + // 省略了部分代码... + }; + + // startAsyncNotesListQuery方法,开始异步查询笔记列表 + private void startAsyncNotesListQuery() { + // 省略了部分代码... + } + + // BackgroundQueryHandler内部类,继承自AsyncQueryHandler,处理异步查询完成的回调 + private final class BackgroundQueryHandler extends AsyncQueryHandler { + // 省略了部分代码... + } + + // showFolderListMenu方法,显示文件夹列表菜单 + private void showFolderListMenu(Cursor cursor) { + // 省略了部分代码... + } + + // createNewNote方法,创建新笔记 + private void createNewNote() { + // 省略了部分代码... + } + + // batchDelete方法,批量删除笔记 + private void batchDelete() { + // 省略了部分代码... + } + + // deleteFolder方法,删除文件夹 + private void deleteFolder(long folderId) { + // 省略了部分代码... + } + + // openNode方法,打开笔记 + private void openNode(NoteItemData data) { + // 省略了部分代码... + } + + // openFolder方法,打开文件夹 + private void openFolder(NoteItemData data) { + // 省略了部分代码... + } + + // onClick方法,处理点击事件 + public void onClick(View v) { + // 省略了部分代码... + } + + // showSoftInput方法,显示软键盘 + private void showSoftInput() { + // 省略了部分代码... + } + + // hideSoftInput方法,隐藏软键盘 + private void hideSoftInput(View view) { + // 省略了部分代码... + } + + // showCreateOrModifyFolderDialog方法,显示创建或修改文件夹的对话框 + private void showCreateOrModifyFolderDialog(final boolean create) { + // 省略了部分代码... + } + + // onBackPressed方法,处理返回键事件 + @Override + public void onBackPressed() { + // 省略了部分代码... + } + + // updateWidget方法,更新Widget + private void updateWidget(int appWidgetId, int appWidgetType) { + // 省略了部分代码... + } + + // mFolderOnCreateContextMenuListener变量,用于创建文件夹的上下文菜单 + private final OnCreateContextMenuListener mFolderOnCreateContextMenuListener = new OnCreateContextMenuListener() { + // 省略了部分代码... + }; + + // onContextMenuClosed方法,上下文菜单关闭时的回调 + @Override + public void onContextMenuClosed(Menu menu) { + // 省略了部分代码... + } + + // onContextItemSelected方法,处理上下文菜单项的选中事件 + @Override + public boolean onContextItemSelected(MenuItem item) { + // 省略了部分代码... + } + + // onPrepareOptionsMenu方法,准备选项菜单 + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + // 省略了部分代码... + } + + // onOptionsItemSelected方法,处理选项菜单项的点击事件 + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // 省略了部分代码... + } + + // onSearchRequested方法,处理搜索请求 + @Override + public boolean onSearchRequested() { + // 省略了部分代码... + } + + // exportNoteToText方法,将笔记导出为文本 + private void exportNoteToText() { + // 省略了部分代码... + } + + // isSyncMode方法,判断是否是同步模式 + private boolean isSyncMode() { + // 省略了部分代码... + } + + // startPreferenceActivity方法,启动设置Activity + private void startPreferenceActivity() { + // 省略了部分代码... + } + + // OnListItemClickListener内部类,实现了ListView的ItemClickListener接口 + private class OnListItemClickListener implements OnItemClickListener { + // 省略了部分代码... + } + + // startQueryDestinationFolders方法,开始查询目标文件夹 + private void startQueryDestinationFolders() { + // 省略了部分代码... + } + + // onItemLongClick方法,处理长按事件 + public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { + + } +} \ No newline at end of file diff --git a/NotesListAdapter.java b/NotesListAdapter.java new file mode 100644 index 0000000..841a910 --- /dev/null +++ b/NotesListAdapter.java @@ -0,0 +1,257 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.Context; +import android.database.Cursor; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CursorAdapter; + +import net.micode.notes.data.Notes; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; + +/** + * NotesListAdapter 是一个 CursorAdapter 子类,用于将笔记数据(从数据库中获取的游标)绑定到视图(如 ListView)。 + * 它管理笔记项的显示、选择模式以及选中的项。 + */ +public class NotesListAdapter extends CursorAdapter { + private static final String TAG = "NotesListAdapter"; // 日志标签 + private Context mContext; // 上下文 + private HashMap mSelectedIndex; // 存储选中项的索引 + private int mNotesCount; // 笔记总数 + private boolean mChoiceMode; // 是否处于选择模式 + + // 用于存储小部件属性的类 + public static class AppWidgetAttribute { + public int widgetId; // 小部件ID + public int widgetType; // 小部件类型 + }; + + /** + * 构造函数,初始化 NotesListAdapter。 + * @param context 上下文 + */ + public NotesListAdapter(Context context) { + super(context, null); + mSelectedIndex = new HashMap(); // 初始化选中项的索引 + mContext = context; + mNotesCount = 0; // 初始化笔记数量 + } + + /** + * 创建新的视图用于展示数据项。 + * @param context 上下文 + * @param cursor 游标,指向数据库中的当前记录 + * @param parent 父视图 + * @return 新的视图 + */ + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return new NotesListItem(context); // 返回自定义的列表项视图 + } + + /** + * 将数据绑定到视图上。 + * @param view 当前的视图 + * @param context 上下文 + * @param cursor 当前游标,指向当前记录 + */ + @Override + public void bindView(View view, Context context, Cursor cursor) { + if (view instanceof NotesListItem) { + // 使用游标数据创建 NoteItemData 对象 + NoteItemData itemData = new NoteItemData(context, cursor); + // 绑定数据到列表项视图 + ((NotesListItem) view).bind(context, itemData, mChoiceMode, isSelectedItem(cursor.getPosition())); + } + } + + /** + * 设置指定位置项的选中状态。 + * @param position 项的位置 + * @param checked 是否选中 + */ + public void setCheckedItem(final int position, final boolean checked) { + mSelectedIndex.put(position, checked); // 更新选中状态 + notifyDataSetChanged(); // 刷新数据集 + } + + /** + * 判断是否处于选择模式。 + * @return 是否处于选择模式 + */ + public boolean isInChoiceMode() { + return mChoiceMode; + } + + /** + * 设置选择模式。 + * @param mode 是否开启选择模式 + */ + public void setChoiceMode(boolean mode) { + mSelectedIndex.clear(); // 清除之前的选择状态 + mChoiceMode = mode; // 更新选择模式 + } + + /** + * 全选或全不选。 + * @param checked 是否选中所有项 + */ + public void selectAll(boolean checked) { + Cursor cursor = getCursor(); + for (int i = 0; i < getCount(); i++) { + if (cursor.moveToPosition(i)) { + // 如果是笔记类型,则更新其选中状态 + if (NoteItemData.getNoteType(cursor) == Notes.TYPE_NOTE) { + setCheckedItem(i, checked); + } + } + } + } + + /** + * 获取当前所有选中项的 ID 集合。 + * @return 选中项的 ID 集合 + */ + public HashSet getSelectedItemIds() { + HashSet itemSet = new HashSet(); + // 遍历选中项索引,获取所有选中的项的 ID + for (Integer position : mSelectedIndex.keySet()) { + if (mSelectedIndex.get(position) == true) { + Long id = getItemId(position); + // 排除根文件夹项的 ID + if (id == Notes.ID_ROOT_FOLDER) { + Log.d(TAG, "Wrong item id, should not happen"); + } else { + itemSet.add(id); // 添加到集合中 + } + } + } + + return itemSet; + } + + /** + * 获取当前选中的小部件集合。 + * @return 选中的小部件集合 + */ + public HashSet getSelectedWidget() { + HashSet itemSet = new HashSet(); + // 遍历选中项索引,获取所有选中的项的小部件信息 + for (Integer position : mSelectedIndex.keySet()) { + if (mSelectedIndex.get(position) == true) { + Cursor c = (Cursor) getItem(position); + if (c != null) { + // 创建小部件属性对象并添加到集合中 + AppWidgetAttribute widget = new AppWidgetAttribute(); + NoteItemData item = new NoteItemData(mContext, c); + widget.widgetId = item.getWidgetId(); + widget.widgetType = item.getWidgetType(); + itemSet.add(widget); + } else { + Log.e(TAG, "Invalid cursor"); + return null; // 如果游标无效,返回 null + } + } + } + return itemSet; + } + + /** + * 获取选中的项数。 + * @return 选中的项数 + */ + public int getSelectedCount() { + Collection values = mSelectedIndex.values(); + if (null == values) { + return 0; // 如果没有选中项,返回 0 + } + Iterator iter = values.iterator(); + int count = 0; + // 统计选中的项数 + while (iter.hasNext()) { + if (true == iter.next()) { + count++; + } + } + return count; + } + + /** + * 判断是否所有项都已选中。 + * @return 是否所有项都选中 + */ + public boolean isAllSelected() { + int checkedCount = getSelectedCount(); + return (checkedCount != 0 && checkedCount == mNotesCount); // 如果选中的项数等于总项数,返回 true + } + + /** + * 判断指定位置的项是否被选中。 + * @param position 项的位置 + * @return 是否选中 + */ + public boolean isSelectedItem(final int position) { + if (null == mSelectedIndex.get(position)) { + return false; // 如果没有记录选中状态,则返回 false + } + return mSelectedIndex.get(position); // 返回选中状态 + } + + /** + * 当内容发生变化时,调用该方法来重新计算笔记数量。 + */ + @Override + protected void onContentChanged() { + super.onContentChanged(); + calcNotesCount(); // 重新计算笔记数量 + } + + /** + * 更改游标时,重新计算笔记数量。 + * @param cursor 新的游标 + */ + @Override + public void changeCursor(Cursor cursor) { + super.changeCursor(cursor); + calcNotesCount(); // 重新计算笔记数量 + } + + /** + * 计算笔记数量,只计算笔记类型的项。 + */ + private void calcNotesCount() { + mNotesCount = 0; + for (int i = 0; i < getCount(); i++) { + Cursor c = (Cursor) getItem(i); + if (c != null) { + if (NoteItemData.getNoteType(c) == Notes.TYPE_NOTE) { + mNotesCount++; // 统计笔记数量 + } + } else { + Log.e(TAG, "Invalid cursor"); + return; + } + } + } +} diff --git a/NotesListItem.java b/NotesListItem.java new file mode 100644 index 0000000..d5ab7bd --- /dev/null +++ b/NotesListItem.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.Context; +import android.text.format.DateUtils; +import android.view.View; +import android.widget.CheckBox; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.tool.DataUtils; +import net.micode.notes.tool.ResourceParser.NoteItemBgResources; + +/** + * NotesListItem 是一个自定义视图组件,用于在列表中显示每个笔记项的内容。 + * 该视图展示了笔记的标题、时间、是否有提醒、是否有附件等信息。 + */ +public class NotesListItem extends LinearLayout { + private ImageView mAlert; // 提醒图标 + private TextView mTitle; // 标题 + private TextView mTime; // 修改时间 + private TextView mCallName; // 通话记录联系人名(如果是通话记录项) + private NoteItemData mItemData; // 当前项的数据 + private CheckBox mCheckBox; // 复选框,用于选择项 + + /** + * 构造函数,初始化视图。 + * @param context 上下文 + */ + public NotesListItem(Context context) { + super(context); + inflate(context, R.layout.note_item, this); // 加载 note_item 布局 + mAlert = (ImageView) findViewById(R.id.iv_alert_icon); // 获取提醒图标 + mTitle = (TextView) findViewById(R.id.tv_title); // 获取标题文本 + mTime = (TextView) findViewById(R.id.tv_time); // 获取时间文本 + mCallName = (TextView) findViewById(R.id.tv_name); // 获取通话记录联系人名文本 + mCheckBox = (CheckBox) findViewById(android.R.id.checkbox); // 获取复选框 + } + + /** + * 绑定数据到视图。 + * 根据数据项的不同类型(笔记、文件夹、通话记录等)调整视图的显示。 + * @param context 上下文 + * @param data 当前项的数据(NoteItemData) + * @param choiceMode 是否处于选择模式 + * @param checked 是否选中 + */ + public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked) { + // 根据是否处于选择模式,显示复选框 + if (choiceMode && data.getType() == Notes.TYPE_NOTE) { + mCheckBox.setVisibility(View.VISIBLE); // 显示复选框 + mCheckBox.setChecked(checked); // 设置复选框的选中状态 + } else { + mCheckBox.setVisibility(View.GONE); // 隐藏复选框 + } + + // 保存当前项的数据 + mItemData = data; + + // 判断当前项是否为通话记录文件夹 + if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { + mCallName.setVisibility(View.GONE); // 隐藏联系人名 + mAlert.setVisibility(View.VISIBLE); // 显示提醒图标 + mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); // 设置标题样式 + mTitle.setText(context.getString(R.string.call_record_folder_name) + + context.getString(R.string.format_folder_files_count, data.getNotesCount())); // 设置标题文本 + mAlert.setImageResource(R.drawable.call_record); // 设置提醒图标 + } else if (data.getParentId() == Notes.ID_CALL_RECORD_FOLDER) { + // 如果当前项是通话记录项 + mCallName.setVisibility(View.VISIBLE); // 显示联系人名 + mCallName.setText(data.getCallName()); // 设置联系人名 + mTitle.setTextAppearance(context, R.style.TextAppearanceSecondaryItem); // 设置标题样式 + mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); // 设置标题文本(格式化后的内容) + // 设置提醒图标 + if (data.hasAlert()) { + mAlert.setImageResource(R.drawable.clock); // 设置提醒图标 + mAlert.setVisibility(View.VISIBLE); // 显示提醒图标 + } else { + mAlert.setVisibility(View.GONE); // 隐藏提醒图标 + } + } else { + // 普通笔记项或文件夹项 + mCallName.setVisibility(View.GONE); // 隐藏联系人名 + mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); // 设置标题样式 + if (data.getType() == Notes.TYPE_FOLDER) { + // 如果是文件夹类型 + mTitle.setText(data.getSnippet() + + context.getString(R.string.format_folder_files_count, + data.getNotesCount())); // 设置标题文本(文件夹名称和文件数量) + mAlert.setVisibility(View.GONE); // 隐藏提醒图标 + } else { + // 如果是普通笔记类型 + mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); // 设置标题文本 + // 设置提醒图标 + if (data.hasAlert()) { + mAlert.setImageResource(R.drawable.clock); // 设置提醒图标 + mAlert.setVisibility(View.VISIBLE); // 显示提醒图标 + } else { + mAlert.setVisibility(View.GONE); // 隐藏提醒图标 + } + } + } + + // 设置修改时间文本 + mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate())); + + // 根据数据设置背景 + setBackground(data); + } + + /** + * 根据不同的笔记类型和状态设置背景。 + * @param data 当前项的数据(NoteItemData) + */ + private void setBackground(NoteItemData data) { + int id = data.getBgColorId(); // 获取背景颜色ID + if (data.getType() == Notes.TYPE_NOTE) { + // 如果是普通笔记 + if (data.isSingle() || data.isOneFollowingFolder()) { + setBackgroundResource(NoteItemBgResources.getNoteBgSingleRes(id)); // 设置单一笔记背景 + } else if (data.isLast()) { + setBackgroundResource(NoteItemBgResources.getNoteBgLastRes(id)); // 设置最后一个笔记背景 + } else if (data.isFirst() || data.isMultiFollowingFolder()) { + setBackgroundResource(NoteItemBgResources.getNoteBgFirstRes(id)); // 设置第一个笔记背景 + } else { + setBackgroundResource(NoteItemBgResources.getNoteBgNormalRes(id)); // 设置普通笔记背景 + } + } else { + // 如果是文件夹 + setBackgroundResource(NoteItemBgResources.getFolderBgRes()); // 设置文件夹背景 + } + } + + /** + * 获取当前项的数据对象。 + * @return 当前项的数据(NoteItemData) + */ + public NoteItemData getItemData() { + return mItemData; + } +} diff --git a/NotesPreferenceActivity.java b/NotesPreferenceActivity.java new file mode 100644 index 0000000..9516663 --- /dev/null +++ b/NotesPreferenceActivity.java @@ -0,0 +1,429 @@ +/* + * 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.accounts.Account; +import android.accounts.AccountManager; +import android.app.ActionBar; +import android.app.AlertDialog; +import android.content.BroadcastReceiver; +import android.content.ContentValues; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceClickListener; +import android.preference.PreferenceActivity; +import android.preference.PreferenceCategory; +import android.text.TextUtils; +import android.text.format.DateFormat; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; +import android.widget.Toast; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.gtask.remote.GTaskSyncService; + +public class NotesPreferenceActivity extends PreferenceActivity { + + // 常量:用于设置和获取 SharedPreferences 中的 key 值 + public static final String PREFERENCE_NAME = "notes_preferences"; + public static final String PREFERENCE_SYNC_ACCOUNT_NAME = "pref_key_account_name"; + public static final String PREFERENCE_LAST_SYNC_TIME = "pref_last_sync_time"; + public static final String PREFERENCE_SET_BG_COLOR_KEY = "pref_key_bg_random_appear"; + + private static final String PREFERENCE_SYNC_ACCOUNT_KEY = "pref_sync_account_key"; + private static final String AUTHORITIES_FILTER_KEY = "authorities"; + + private PreferenceCategory mAccountCategory; // 用于显示账户相关的设置项 + private GTaskReceiver mReceiver; // 广播接收器,接收同步服务的广播 + private Account[] mOriAccounts; // 保存原始的账户列表,用于比较新添加的账户 + private boolean mHasAddedAccount; // 标志,表示用户是否已添加新账户 + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + + // 设置 ActionBar 显示返回按钮 + getActionBar().setDisplayHomeAsUpEnabled(true); + + // 加载设置页面的 XML 布局 + addPreferencesFromResource(R.xml.preferences); + mAccountCategory = (PreferenceCategory) findPreference(PREFERENCE_SYNC_ACCOUNT_KEY); // 获取账户设置项 + mReceiver = new GTaskReceiver(); // 初始化广播接收器 + IntentFilter filter = new IntentFilter(); + filter.addAction(GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME); // 监听同步服务广播 + registerReceiver(mReceiver, filter); + + mOriAccounts = null; // 初始化原始账户为空 + View header = LayoutInflater.from(this).inflate(R.layout.settings_header, null); // 设置页头 + getListView().addHeaderView(header, null, true); // 在设置列表中添加页头 + } + + @Override + protected void onResume() { + super.onResume(); + + // 如果用户添加了新账户,自动设置同步账户 + if (mHasAddedAccount) { + Account[] accounts = getGoogleAccounts(); // 获取当前的 Google 账户列表 + if (mOriAccounts != null && accounts.length > mOriAccounts.length) { + // 遍历账户,检查是否新增账户 + for (Account accountNew : accounts) { + boolean found = false; + for (Account accountOld : mOriAccounts) { + if (TextUtils.equals(accountOld.name, accountNew.name)) { + found = true; + break; + } + } + if (!found) { + setSyncAccount(accountNew.name); // 设置新账户为同步账户 + break; + } + } + } + } + + // 刷新 UI 显示 + refreshUI(); + } + + @Override + protected void onDestroy() { + if (mReceiver != null) { + unregisterReceiver(mReceiver); // 注销广播接收器 + } + super.onDestroy(); + } + + /** + * 加载账户相关的偏好设置 + */ + private void loadAccountPreference() { + mAccountCategory.removeAll(); // 清空当前账户设置项 + + Preference accountPref = new Preference(this); + final String defaultAccount = getSyncAccountName(this); // 获取当前的同步账户 + accountPref.setTitle(getString(R.string.preferences_account_title)); // 设置标题 + accountPref.setSummary(getString(R.string.preferences_account_summary)); // 设置摘要 + accountPref.setOnPreferenceClickListener(new OnPreferenceClickListener() { + public boolean onPreferenceClick(Preference preference) { + // 如果当前没有同步账户,显示选择账户的对话框 + if (!GTaskSyncService.isSyncing()) { + if (TextUtils.isEmpty(defaultAccount)) { + showSelectAccountAlertDialog(); + } else { + // 如果已经有同步账户,确认是否更改账户 + showChangeAccountConfirmAlertDialog(); + } + } else { + // 如果正在同步,不能更改账户,显示提示 + Toast.makeText(NotesPreferenceActivity.this, + R.string.preferences_toast_cannot_change_account, Toast.LENGTH_SHORT) + .show(); + } + return true; + } + }); + + mAccountCategory.addPreference(accountPref); // 添加账户偏好设置项 + } + + /** + * 加载同步按钮和显示最近同步时间 + */ + private void loadSyncButton() { + Button syncButton = (Button) findViewById(R.id.preference_sync_button); + TextView lastSyncTimeView = (TextView) findViewById(R.id.prefenerece_sync_status_textview); + + // 设置同步按钮的状态 + if (GTaskSyncService.isSyncing()) { + syncButton.setText(getString(R.string.preferences_button_sync_cancel)); + syncButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + GTaskSyncService.cancelSync(NotesPreferenceActivity.this); // 取消同步 + } + }); + } else { + syncButton.setText(getString(R.string.preferences_button_sync_immediately)); + syncButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + GTaskSyncService.startSync(NotesPreferenceActivity.this); // 开始同步 + } + }); + } + syncButton.setEnabled(!TextUtils.isEmpty(getSyncAccountName(this))); // 如果没有同步账户,按钮禁用 + + // 显示最后同步时间 + if (GTaskSyncService.isSyncing()) { + lastSyncTimeView.setText(GTaskSyncService.getProgressString()); // 显示同步进度 + lastSyncTimeView.setVisibility(View.VISIBLE); + } else { + long lastSyncTime = getLastSyncTime(this); + if (lastSyncTime != 0) { + lastSyncTimeView.setText(getString(R.string.preferences_last_sync_time, + DateFormat.format(getString(R.string.preferences_last_sync_time_format), + lastSyncTime))); + lastSyncTimeView.setVisibility(View.VISIBLE); + } else { + lastSyncTimeView.setVisibility(View.GONE); + } + } + } + + /** + * 刷新界面,加载账户设置和同步按钮 + */ + private void refreshUI() { + loadAccountPreference(); // 加载账户设置 + loadSyncButton(); // 加载同步按钮 + } + + /** + * 显示选择账户的对话框 + */ + private void showSelectAccountAlertDialog() { + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); + + View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null); + TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title); + titleTextView.setText(getString(R.string.preferences_dialog_select_account_title)); + TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle); + subtitleTextView.setText(getString(R.string.preferences_dialog_select_account_tips)); + + dialogBuilder.setCustomTitle(titleView); + dialogBuilder.setPositiveButton(null, null); + + // 获取 Google 账户列表 + Account[] accounts = getGoogleAccounts(); + String defAccount = getSyncAccountName(this); + + mOriAccounts = accounts; + mHasAddedAccount = false; + + if (accounts.length > 0) { + CharSequence[] items = new CharSequence[accounts.length]; + final CharSequence[] itemMapping = items; + int checkedItem = -1; + int index = 0; + for (Account account : accounts) { + if (TextUtils.equals(account.name, defAccount)) { + checkedItem = index; + } + items[index++] = account.name; + } + dialogBuilder.setSingleChoiceItems(items, checkedItem, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + setSyncAccount(itemMapping[which].toString()); // 设置同步账户 + dialog.dismiss(); + refreshUI(); // 刷新 UI + } + }); + } + + // 添加账户按钮 + View addAccountView = LayoutInflater.from(this).inflate(R.layout.add_account_text, null); + dialogBuilder.setView(addAccountView); + + final AlertDialog dialog = dialogBuilder.show(); + addAccountView.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + mHasAddedAccount = true; + Intent intent = new Intent("android.settings.ADD_ACCOUNT_SETTINGS"); + intent.putExtra(AUTHORITIES_FILTER_KEY, new String[] { + "gmail-ls" + }); + startActivityForResult(intent, -1); // 跳转到添加账户界面 + dialog.dismiss(); + } + }); + } + + /** + * 显示更改账户的确认对话框 + */ + private void showChangeAccountConfirmAlertDialog() { + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); + + View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null); + TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title); + titleTextView.setText(getString(R.string.preferences_dialog_change_account_title, + getSyncAccountName(this))); // 显示当前同步账户 + TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle); + subtitleTextView.setText(getString(R.string.preferences_dialog_change_account_warn_msg)); + dialogBuilder.setCustomTitle(titleView); + + // 显示更改账户、删除账户、取消操作 + CharSequence[] menuItemArray = new CharSequence[] { + getString(R.string.preferences_menu_change_account), + getString(R.string.preferences_menu_remove_account), + getString(R.string.preferences_menu_cancel) + }; + dialogBuilder.setItems(menuItemArray, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + if (which == 0) { + showSelectAccountAlertDialog(); // 更改账户 + } else if (which == 1) { + removeSyncAccount(); // 删除同步账户 + refreshUI(); // 刷新 UI + } + } + }); + dialogBuilder.show(); + } + + /** + * 获取所有 Google 账户 + */ + private Account[] getGoogleAccounts() { + AccountManager accountManager = AccountManager.get(this); + return accountManager.getAccountsByType("com.google"); + } + + /** + * 设置同步账户 + */ + private void setSyncAccount(String account) { + if (!getSyncAccountName(this).equals(account)) { + SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = settings.edit(); + if (account != null) { + editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, account); + } else { + editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, ""); + } + editor.commit(); + + // 清除上次同步时间 + setLastSyncTime(this, 0); + + // 清除本地与 gtask 相关的数据 + new Thread(new Runnable() { + public void run() { + ContentValues values = new ContentValues(); + values.put(NoteColumns.GTASK_ID, ""); + values.put(NoteColumns.SYNC_ID, 0); + getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null); + } + }).start(); + + // 显示成功提示 + Toast.makeText(NotesPreferenceActivity.this, + getString(R.string.preferences_toast_success_set_accout, account), + Toast.LENGTH_SHORT).show(); + } + } + + /** + * 删除同步账户 + */ + private void removeSyncAccount() { + SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = settings.edit(); + if (settings.contains(PREFERENCE_SYNC_ACCOUNT_NAME)) { + editor.remove(PREFERENCE_SYNC_ACCOUNT_NAME); + } + if (settings.contains(PREFERENCE_LAST_SYNC_TIME)) { + editor.remove(PREFERENCE_LAST_SYNC_TIME); + } + editor.commit(); + + // 清除本地与 gtask 相关的数据 + new Thread(new Runnable() { + public void run() { + ContentValues values = new ContentValues(); + values.put(NoteColumns.GTASK_ID, ""); + values.put(NoteColumns.SYNC_ID, 0); + getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null); + } + }).start(); + } + + /** + * 获取当前同步账户的名称 + */ + public static String getSyncAccountName(Context context) { + SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, + Context.MODE_PRIVATE); + return settings.getString(PREFERENCE_SYNC_ACCOUNT_NAME, ""); + } + + /** + * 设置上次同步时间 + */ + public static void setLastSyncTime(Context context, long time) { + SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, + Context.MODE_PRIVATE); + SharedPreferences.Editor editor = settings.edit(); + editor.putLong(PREFERENCE_LAST_SYNC_TIME, time); + editor.commit(); + } + + /** + * 获取上次同步时间 + */ + public static long getLastSyncTime(Context context) { + SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, + Context.MODE_PRIVATE); + return settings.getLong(PREFERENCE_LAST_SYNC_TIME, 0); + } + + /** + * 广播接收器,用于接收 GTask 同步服务的状态更新 + */ + private class GTaskReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + refreshUI(); // 刷新界面 + + // 如果正在同步,显示同步进度 + if (intent.getBooleanExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_IS_SYNCING, false)) { + TextView syncStatus = (TextView) findViewById(R.id.prefenerece_sync_status_textview); + syncStatus.setText(intent + .getStringExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_PROGRESS_MSG)); + } + } + } + + /** + * 处理选项菜单项点击 + */ + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + // 返回主页面 + Intent intent = new Intent(this, NotesListActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + return true; + default: + return false; + } + } +} diff --git a/NotesProvider.java b/NotesProvider.java new file mode 100644 index 0000000..fc946d1 --- /dev/null +++ b/NotesProvider.java @@ -0,0 +1,354 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.data; + +import android.app.SearchManager; +import android.content.ContentProvider; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Intent; +import android.content.UriMatcher; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; +import android.text.TextUtils; +import android.util.Log; + +import net.micode.notes.R; +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.data.NotesDatabaseHelper.TABLE; + +/** + * NotesProvider是内容提供者类,负责提供与笔记数据的增删改查功能。 + * 它通过SQLite数据库进行数据存储,并使用UriMatcher来解析URI请求。 + */ +public class NotesProvider extends ContentProvider { + + // 定义UriMatcher对象,用于匹配不同类型的URI + private static final UriMatcher mMatcher; + + // Notes数据库帮助类,用于操作数据库 + private NotesDatabaseHelper mHelper; + + // 日志TAG + private static final String TAG = "NotesProvider"; + + // 定义不同的URI匹配码 + private static final int URI_NOTE = 1; // 匹配所有笔记 + private static final int URI_NOTE_ITEM = 2; // 匹配单个笔记 + private static final int URI_DATA = 3; // 匹配所有数据 + private static final int URI_DATA_ITEM = 4; // 匹配单个数据 + private static final int URI_SEARCH = 5; // 匹配搜索请求 + private static final int URI_SEARCH_SUGGEST = 6; // 匹配搜索建议请求 + + // 静态代码块,用于初始化UriMatcher + static { + mMatcher = new UriMatcher(UriMatcher.NO_MATCH); + mMatcher.addURI(Notes.AUTHORITY, "note", URI_NOTE); // 匹配笔记URI + mMatcher.addURI(Notes.AUTHORITY, "note/#", URI_NOTE_ITEM); // 匹配具体的笔记 + mMatcher.addURI(Notes.AUTHORITY, "data", URI_DATA); // 匹配数据URI + mMatcher.addURI(Notes.AUTHORITY, "data/#", URI_DATA_ITEM); // 匹配具体的数据 + mMatcher.addURI(Notes.AUTHORITY, "search", URI_SEARCH); // 匹配搜索 + mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, URI_SEARCH_SUGGEST); // 搜索建议 + mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", URI_SEARCH_SUGGEST); + } + + /** + * SQL查询片段,用于从笔记中搜索,删除换行符并裁剪空格,以显示更多信息。 + */ + private static final String NOTES_SEARCH_PROJECTION = NoteColumns.ID + "," + + NoteColumns.ID + " AS " + SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA + "," + + "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_1 + "," + + "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_2 + "," + + R.drawable.search_result + " AS " + SearchManager.SUGGEST_COLUMN_ICON_1 + "," + + "'" + Intent.ACTION_VIEW + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_ACTION + "," + + "'" + Notes.TextNote.CONTENT_TYPE + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA; + + // SQL查询,用于搜索笔记内容中的摘要 + private static String NOTES_SNIPPET_SEARCH_QUERY = "SELECT " + NOTES_SEARCH_PROJECTION + + " FROM " + TABLE.NOTE + + " WHERE " + NoteColumns.SNIPPET + " LIKE ?" + + " AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER + + " AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE; + + /** + * 创建ContentProvider实例时调用。 + * 主要用于初始化数据库帮助类实例。 + */ + @Override + public boolean onCreate() { + mHelper = NotesDatabaseHelper.getInstance(getContext()); + return true; + } + + /** + * 处理查询请求。 + * + * @param uri 查询的URI + * @param projection 查询的列 + * @param selection 查询的条件 + * @param selectionArgs 查询条件的参数 + * @param sortOrder 排序方式 + * @return 返回Cursor,表示查询结果 + */ + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + Cursor c = null; + SQLiteDatabase db = mHelper.getReadableDatabase(); + String id = null; + + // 根据URI匹配码执行不同的查询操作 + switch (mMatcher.match(uri)) { + case URI_NOTE: // 查询所有笔记 + c = db.query(TABLE.NOTE, projection, selection, selectionArgs, null, null, sortOrder); + break; + case URI_NOTE_ITEM: // 查询单个笔记 + id = uri.getPathSegments().get(1); + c = db.query(TABLE.NOTE, projection, NoteColumns.ID + "=" + id + parseSelection(selection), selectionArgs, null, null, sortOrder); + break; + case URI_DATA: // 查询所有数据 + c = db.query(TABLE.DATA, projection, selection, selectionArgs, null, null, sortOrder); + break; + case URI_DATA_ITEM: // 查询单个数据 + id = uri.getPathSegments().get(1); + c = db.query(TABLE.DATA, projection, DataColumns.ID + "=" + id + parseSelection(selection), selectionArgs, null, null, sortOrder); + break; + case URI_SEARCH: // 搜索笔记 + case URI_SEARCH_SUGGEST: // 搜索建议 + if (sortOrder != null || projection != null) { + throw new IllegalArgumentException("do not specify sortOrder, selection, selectionArgs, or projection" + "with this query"); + } + String searchString = null; + if (mMatcher.match(uri) == URI_SEARCH_SUGGEST) { + if (uri.getPathSegments().size() > 1) { + searchString = uri.getPathSegments().get(1); + } + } else { + searchString = uri.getQueryParameter("pattern"); + } + if (TextUtils.isEmpty(searchString)) { + return null; + } + try { + searchString = String.format("%%%s%%", searchString); // 添加通配符进行模糊查询 + c = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY, new String[] { searchString }); + } catch (IllegalStateException ex) { + Log.e(TAG, "got exception: " + ex.toString()); + } + break; + default: + throw new IllegalArgumentException("Unknown URI " + uri); // 未知的URI类型 + } + + // 如果Cursor不为空,设置通知URI,监听数据变化 + if (c != null) { + c.setNotificationUri(getContext().getContentResolver(), uri); + } + return c; + } + + /** + * 处理插入请求。 + * + * @param uri 插入的URI + * @param values 要插入的数据 + * @return 返回新插入记录的URI + */ + @Override + public Uri insert(Uri uri, ContentValues values) { + SQLiteDatabase db = mHelper.getWritableDatabase(); + long dataId = 0, noteId = 0, insertedId = 0; + + // 根据URI匹配码执行不同的插入操作 + switch (mMatcher.match(uri)) { + case URI_NOTE: // 插入一条笔记 + insertedId = noteId = db.insert(TABLE.NOTE, null, values); + break; + case URI_DATA: // 插入一条数据 + if (values.containsKey(DataColumns.NOTE_ID)) { + noteId = values.getAsLong(DataColumns.NOTE_ID); + } else { + Log.d(TAG, "Wrong data format without note id:" + values.toString()); + } + insertedId = dataId = db.insert(TABLE.DATA, null, values); + break; + default: + throw new IllegalArgumentException("Unknown URI " + uri); // 未知的URI类型 + } + + // 通知数据改变 + if (noteId > 0) { + getContext().getContentResolver().notifyChange(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null); + } + if (dataId > 0) { + getContext().getContentResolver().notifyChange(ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), null); + } + return ContentUris.withAppendedId(uri, insertedId); // 返回新记录的URI + } + + /** + * 处理删除请求。 + * + * @param uri 删除的URI + * @param selection 删除的条件 + * @param selectionArgs 删除条件的参数 + * @return 返回删除的记录数 + */ + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + int count = 0; + String id = null; + SQLiteDatabase db = mHelper.getWritableDatabase(); + boolean deleteData = false; + + // 根据URI匹配码执行不同的删除操作 + switch (mMatcher.match(uri)) { + case URI_NOTE: // 删除笔记 + selection = "(" + selection + ") AND " + NoteColumns.ID + ">0 "; + count = db.delete(TABLE.NOTE, selection, selectionArgs); + break; + case URI_NOTE_ITEM: // 删除单个笔记 + id = uri.getPathSegments().get(1); + long noteId = Long.valueOf(id); + if (noteId <= 0) { // 如果笔记ID小于等于0,不允许删除 + break; + } + count = db.delete(TABLE.NOTE, NoteColumns.ID + "=" + id + parseSelection(selection), selectionArgs); + break; + case URI_DATA: // 删除数据 + count = db.delete(TABLE.DATA, selection, selectionArgs); + deleteData = true; + break; + case URI_DATA_ITEM: // 删除单个数据 + id = uri.getPathSegments().get(1); + count = db.delete(TABLE.DATA, DataColumns.ID + "=" + id + parseSelection(selection), selectionArgs); + deleteData = true; + break; + default: + throw new IllegalArgumentException("Unknown URI " + uri); // 未知的URI类型 + } + + // 如果删除成功,通知数据变化 + if (count > 0) { + if (deleteData) { + getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null); + } + getContext().getContentResolver().notifyChange(uri, null); + } + return count; + } + + /** + * 处理更新请求。 + * + * @param uri 更新的URI + * @param values 更新的内容 + * @param selection 更新的条件 + * @param selectionArgs 更新条件的参数 + * @return 返回更新的记录数 + */ + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + int count = 0; + String id = null; + SQLiteDatabase db = mHelper.getWritableDatabase(); + boolean updateData = false; + + // 根据URI匹配码执行不同的更新操作 + switch (mMatcher.match(uri)) { + case URI_NOTE: // 更新笔记 + increaseNoteVersion(-1, selection, selectionArgs); // 更新笔记版本 + count = db.update(TABLE.NOTE, values, selection, selectionArgs); + break; + case URI_NOTE_ITEM: // 更新单个笔记 + id = uri.getPathSegments().get(1); + increaseNoteVersion(Long.valueOf(id), selection, selectionArgs); // 更新指定笔记版本 + count = db.update(TABLE.NOTE, values, NoteColumns.ID + "=" + id + parseSelection(selection), selectionArgs); + break; + case URI_DATA: // 更新数据 + count = db.update(TABLE.DATA, values, selection, selectionArgs); + updateData = true; + break; + case URI_DATA_ITEM: // 更新单个数据 + id = uri.getPathSegments().get(1); + count = db.update(TABLE.DATA, values, DataColumns.ID + "=" + id + parseSelection(selection), selectionArgs); + updateData = true; + break; + default: + throw new IllegalArgumentException("Unknown URI " + uri); // 未知的URI类型 + } + + // 如果更新成功,通知数据变化 + if (count > 0) { + if (updateData) { + getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null); + } + getContext().getContentResolver().notifyChange(uri, null); + } + return count; + } + + /** + * 辅助方法,解析选择条件。 + * + * @param selection 选择条件 + * @return 返回解析后的选择条件 + */ + private String parseSelection(String selection) { + return (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : ""); + } + + /** + * 更新笔记的版本号。 + * + * @param id 笔记ID + * @param selection 选择条件 + * @param selectionArgs 选择条件参数 + */ + private void increaseNoteVersion(long id, String selection, String[] selectionArgs) { + StringBuilder sql = new StringBuilder(120); + sql.append("UPDATE "); + sql.append(TABLE.NOTE); + sql.append(" SET "); + sql.append(NoteColumns.VERSION); + sql.append("=" + NoteColumns.VERSION + "+1 "); + + if (id > 0 || !TextUtils.isEmpty(selection)) { + sql.append(" WHERE "); + } + if (id > 0) { + sql.append(NoteColumns.ID + "=" + String.valueOf(id)); + } + if (!TextUtils.isEmpty(selection)) { + String selectString = id > 0 ? parseSelection(selection) : selection; + for (String args : selectionArgs) { + selectString = selectString.replaceFirst("\\?", args); + } + sql.append(selectString); + } + + // 执行SQL更新版本 + mHelper.getWritableDatabase().execSQL(sql.toString()); + } + + @Override + public String getType(Uri uri) { + // TODO Auto-generated method stub + return null; + } +} diff --git a/ResourceParser.java b/ResourceParser.java new file mode 100644 index 0000000..b07e0fa --- /dev/null +++ b/ResourceParser.java @@ -0,0 +1,210 @@ +/* + * 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; + +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 + 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 { + // 2x2 小部件的背景资源 + 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 // 红色背景 + }; + + // 获取 2x2 小部件的背景资源 + public static int getWidget2xBgResource(int id) { + return BG_2X_RESOURCES[id]; + } + + // 4x4 小部件的背景资源 + 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 // 红色背景 + }; + + // 获取 4x4 小部件的背景资源 + 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 // 超大号文字 + }; + + // 获取文字外观资源 + public static int getTexAppearanceResource(int id) { + /** + * HACKME: 修复存储资源ID到共享偏好设置的bug。 + * 如果ID大于资源数组的长度,返回默认的字体大小。 + */ + if (id >= TEXTAPPEARANCE_RESOURCES.length) { + return BG_DEFAULT_FONT_SIZE; // 如果ID超出范围,返回默认的字体大小 + } + return TEXTAPPEARANCE_RESOURCES[id]; + } + + // 获取文字外观资源的数量 + public static int getResourcesSize() { + return TEXTAPPEARANCE_RESOURCES.length; + } + } +} diff --git a/SqlData.java b/SqlData.java new file mode 100644 index 0000000..0447243 --- /dev/null +++ b/SqlData.java @@ -0,0 +1,194 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.gtask.data; + +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.util.Log; + +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.DataConstants; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.data.NotesDatabaseHelper.TABLE; +import net.micode.notes.gtask.exception.ActionFailureException; + +import org.json.JSONException; +import org.json.JSONObject; + +/** + * SqlData类用于处理与数据库交互的笔记数据,包括创建、读取和更新操作。 + */ +public class SqlData { + private static final String TAG = SqlData.class.getSimpleName(); // 日志标签 + + private static final int INVALID_ID = -99999; // 无效 ID 常量 + + // 定义用于查询的数据列 + public static final String[] PROJECTION_DATA = new String[] { + DataColumns.ID, DataColumns.MIME_TYPE, DataColumns.CONTENT, DataColumns.DATA1, + DataColumns.DATA3 + }; + + // 数据列索引 + public static final int DATA_ID_COLUMN = 0; + public static final int DATA_MIME_TYPE_COLUMN = 1; + public static final int DATA_CONTENT_COLUMN = 2; + public static final int DATA_CONTENT_DATA_1_COLUMN = 3; + public static final int DATA_CONTENT_DATA_3_COLUMN = 4; + + private ContentResolver mContentResolver; // 内容解析器,用于与内容提供者交互 + + private boolean mIsCreate; // 标识是否为新创建的数据 + + private long mDataId; // 数据 ID + private String mDataMimeType; // 数据 MIME 类型 + private String mDataContent; // 数据内容 + private long mDataContentData1; // 数据内容的附加信息 1 + private String mDataContentData3; // 数据内容的附加信息 3 + + private ContentValues mDiffDataValues; // 存储待更新的内容值 + + // 构造函数,初始化 SqlData 实例(用于创建新数据) + public SqlData(Context context) { + mContentResolver = context.getContentResolver(); // 获取内容解析器 + mIsCreate = true; // 设置为创建状态 + mDataId = INVALID_ID; // 初始化数据 ID + mDataMimeType = DataConstants.NOTE; // 默认 MIME 类型 + mDataContent = ""; // 初始化内容为空 + mDataContentData1 = 0; // 初始化附加信息 1 + mDataContentData3 = ""; // 初始化附加信息 3 + mDiffDataValues = new ContentValues(); // 初始化待更新内容值 + } + + // 构造函数,初始化 SqlData 实例(从游标加载数据) + public SqlData(Context context, Cursor c) { + mContentResolver = context.getContentResolver(); // 获取内容解析器 + mIsCreate = false; // 设置为非创建状态 + loadFromCursor(c); // 从游标加载数据 + mDiffDataValues = new ContentValues(); // 初始化待更新内容值 + } + + // 从游标加载数据 + private void loadFromCursor(Cursor c) { + mDataId = c.getLong(DATA_ID_COLUMN); // 获取数据 ID + mDataMimeType = c.getString(DATA_MIME_TYPE_COLUMN); // 获取 MIME 类型 + mDataContent = c.getString(DATA_CONTENT_COLUMN); // 获取内容 + mDataContentData1 = c.getLong(DATA_CONTENT_DATA_1_COLUMN); // 获取附加信息 1 + mDataContentData3 = c.getString(DATA_CONTENT_DATA_3_COLUMN); // 获取附加信息 3 + } + + // 设置内容,从 JSON 对象加载数据 + public void setContent(JSONObject js) throws JSONException { + long dataId = js.has(DataColumns.ID) ? js.getLong(DataColumns.ID) : INVALID_ID; // 获取 ID + if (mIsCreate || mDataId != dataId) { + mDiffDataValues.put(DataColumns.ID, dataId); // 更新 ID + } + mDataId = dataId; // 设置 ID + + String dataMimeType = js.has(DataColumns.MIME_TYPE) ? js.getString(DataColumns.MIME_TYPE) + : DataConstants.NOTE; // 获取 MIME 类型 + if (mIsCreate || !mDataMimeType.equals(dataMimeType)) { + mDiffDataValues.put(DataColumns.MIME_TYPE, dataMimeType); // 更新 MIME 类型 + } + mDataMimeType = dataMimeType; // 设置 MIME 类型 + + String dataContent = js.has(DataColumns.CONTENT) ? js.getString(DataColumns.CONTENT) : ""; // 获取内容 + if (mIsCreate || !mDataContent.equals(dataContent)) { + mDiffDataValues.put(DataColumns.CONTENT, dataContent); // 更新内容 + } + mDataContent = dataContent; // 设置内容 + + long dataContentData1 = js.has(DataColumns.DATA1) ? js.getLong(DataColumns.DATA1) : 0; // 获取附加信息 1 + if (mIsCreate || mDataContentData1 != dataContentData1) { + mDiffDataValues.put(DataColumns.DATA1, dataContentData1); // 更新附加信息 1 + } + mDataContentData1 = dataContentData1; // 设置附加信息 1 + + String dataContentData3 = js.has(DataColumns.DATA3) ? js.getString(DataColumns.DATA3) : ""; // 获取附加信息 3 + if (mIsCreate || !mDataContentData3.equals(dataContentData3)) { + mDiffDataValues.put(DataColumns.DATA3, dataContentData3); // 更新附加信息 3 + } + mDataContentData3 = dataContentData3; // 设置附加信息 3 + } + + // 获取内容,返回 JSON 对象 + public JSONObject getContent() throws JSONException { + if (mIsCreate) { + Log.e(TAG, "it seems that we haven't created this in database yet"); // 日志警告 + return null; // 尚未创建,返回 null + } + JSONObject js = new JSONObject(); // 创建 JSON 对象 + js.put(DataColumns.ID, mDataId); // 设置 ID + js.put(DataColumns.MIME_TYPE, mDataMimeType); // 设置 MIME 类型 + js.put(DataColumns.CONTENT, mDataContent); // 设置内容 + js.put(DataColumns.DATA1, mDataContentData1); // 设置附加信息 1 + js.put(DataColumns.DATA3, mDataContentData3); // 设置附加信息 3 + return js; // 返回 JSON 对象 + } + + // 提交数据到数据库 + public void commit(long noteId, boolean validateVersion, long version) { + // 处理创建新数据的情况 + if (mIsCreate) { + if (mDataId == INVALID_ID && mDiffDataValues.containsKey(DataColumns.ID)) { + mDiffDataValues.remove(DataColumns.ID); // 移除无效 ID + } + + mDiffDataValues.put(DataColumns.NOTE_ID, noteId); // 设置笔记 ID + Uri uri = mContentResolver.insert(Notes.CONTENT_DATA_URI, mDiffDataValues); // 插入数据 + try { + mDataId = Long.valueOf(uri.getPathSegments().get(1)); // 获取新创建的 ID + } catch (NumberFormatException e) { + Log.e(TAG, "Get note id error :" + e.toString()); // 日志错误 + throw new ActionFailureException("create note failed"); // 抛出异常 + } + } else { // 处理更新数据的情况 + if (mDiffDataValues.size() > 0) { // 如果有待更新的数据 + int result = 0; + if (!validateVersion) { + // 不进行版本验证直接更新 + result = mContentResolver.update(ContentUris.withAppendedId( + Notes.CONTENT_DATA_URI, mDataId), mDiffDataValues, null, null); + } else { + // 进行版本验证更新 + result = mContentResolver.update(ContentUris.withAppendedId( + Notes.CONTENT_DATA_URI, mDataId), mDiffDataValues, + " ? in (SELECT " + NoteColumns.ID + " FROM " + TABLE.NOTE + + " WHERE " + NoteColumns.VERSION + "=?)", new String[] { + String.valueOf(noteId), String.valueOf(version) + }); + } + if (result == 0) { + Log.w(TAG, "there is no update. maybe user updates note when syncing"); // 日志警告 + } + } + } + + mDiffDataValues.clear(); // 清空待更新数据 + mIsCreate = false; // 设置为非创建状态 + } + + // 获取数据 ID + public long getId() { + return mDataId; // 返回数据 ID + } +} diff --git a/SqlNote.java b/SqlNote.java new file mode 100644 index 0000000..79a4095 --- /dev/null +++ b/SqlNote.java @@ -0,0 +1,505 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.gtask.data; + +import android.appwidget.AppWidgetManager; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.util.Log; + +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.gtask.exception.ActionFailureException; +import net.micode.notes.tool.GTaskStringUtils; +import net.micode.notes.tool.ResourceParser; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; + + +public class SqlNote { + private static final String TAG = SqlNote.class.getSimpleName(); + + private static final int INVALID_ID = -99999; + + public static final String[] PROJECTION_NOTE = new String[] { + NoteColumns.ID, NoteColumns.ALERTED_DATE, NoteColumns.BG_COLOR_ID, + NoteColumns.CREATED_DATE, NoteColumns.HAS_ATTACHMENT, NoteColumns.MODIFIED_DATE, + NoteColumns.NOTES_COUNT, NoteColumns.PARENT_ID, NoteColumns.SNIPPET, NoteColumns.TYPE, + NoteColumns.WIDGET_ID, NoteColumns.WIDGET_TYPE, NoteColumns.SYNC_ID, + NoteColumns.LOCAL_MODIFIED, NoteColumns.ORIGIN_PARENT_ID, NoteColumns.GTASK_ID, + NoteColumns.VERSION + }; + + public static final int ID_COLUMN = 0; + + public static final int ALERTED_DATE_COLUMN = 1; + + public static final int BG_COLOR_ID_COLUMN = 2; + + public static final int CREATED_DATE_COLUMN = 3; + + public static final int HAS_ATTACHMENT_COLUMN = 4; + + public static final int MODIFIED_DATE_COLUMN = 5; + + public static final int NOTES_COUNT_COLUMN = 6; + + public static final int PARENT_ID_COLUMN = 7; + + public static final int SNIPPET_COLUMN = 8; + + public static final int TYPE_COLUMN = 9; + + public static final int WIDGET_ID_COLUMN = 10; + + public static final int WIDGET_TYPE_COLUMN = 11; + + public static final int SYNC_ID_COLUMN = 12; + + public static final int LOCAL_MODIFIED_COLUMN = 13; + + public static final int ORIGIN_PARENT_ID_COLUMN = 14; + + public static final int GTASK_ID_COLUMN = 15; + + public static final int VERSION_COLUMN = 16; + + private Context mContext; + + private ContentResolver mContentResolver; + + private boolean mIsCreate; + + private long mId; + + private long mAlertDate; + + private int mBgColorId; + + private long mCreatedDate; + + private int mHasAttachment; + + private long mModifiedDate; + + private long mParentId; + + private String mSnippet; + + private int mType; + + private int mWidgetId; + + private int mWidgetType; + + private long mOriginParent; + + private long mVersion; + + private ContentValues mDiffNoteValues; + + private ArrayList mDataList; + + public SqlNote(Context context) { + mContext = context; + mContentResolver = context.getContentResolver(); + mIsCreate = true; + mId = INVALID_ID; + mAlertDate = 0; + mBgColorId = ResourceParser.getDefaultBgId(context); + mCreatedDate = System.currentTimeMillis(); + mHasAttachment = 0; + mModifiedDate = System.currentTimeMillis(); + mParentId = 0; + mSnippet = ""; + mType = Notes.TYPE_NOTE; + mWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID; + mWidgetType = Notes.TYPE_WIDGET_INVALIDE; + mOriginParent = 0; + mVersion = 0; + mDiffNoteValues = new ContentValues(); + mDataList = new ArrayList(); + } + + public SqlNote(Context context, Cursor c) { + mContext = context; + mContentResolver = context.getContentResolver(); + mIsCreate = false; + loadFromCursor(c); + mDataList = new ArrayList(); + if (mType == Notes.TYPE_NOTE) + loadDataContent(); + mDiffNoteValues = new ContentValues(); + } + + public SqlNote(Context context, long id) { + mContext = context; + mContentResolver = context.getContentResolver(); + mIsCreate = false; + loadFromCursor(id); + mDataList = new ArrayList(); + if (mType == Notes.TYPE_NOTE) + loadDataContent(); + mDiffNoteValues = new ContentValues(); + + } + + private void loadFromCursor(long id) { + Cursor c = null; + try { + c = mContentResolver.query(Notes.CONTENT_NOTE_URI, PROJECTION_NOTE, "(_id=?)", + new String[] { + String.valueOf(id) + }, null); + if (c != null) { + c.moveToNext(); + loadFromCursor(c); + } else { + Log.w(TAG, "loadFromCursor: cursor = null"); + } + } finally { + if (c != null) + c.close(); + } + } + + private void loadFromCursor(Cursor c) { + mId = c.getLong(ID_COLUMN); + mAlertDate = c.getLong(ALERTED_DATE_COLUMN); + mBgColorId = c.getInt(BG_COLOR_ID_COLUMN); + mCreatedDate = c.getLong(CREATED_DATE_COLUMN); + mHasAttachment = c.getInt(HAS_ATTACHMENT_COLUMN); + mModifiedDate = c.getLong(MODIFIED_DATE_COLUMN); + mParentId = c.getLong(PARENT_ID_COLUMN); + mSnippet = c.getString(SNIPPET_COLUMN); + mType = c.getInt(TYPE_COLUMN); + mWidgetId = c.getInt(WIDGET_ID_COLUMN); + mWidgetType = c.getInt(WIDGET_TYPE_COLUMN); + mVersion = c.getLong(VERSION_COLUMN); + } + + private void loadDataContent() { + Cursor c = null; + mDataList.clear(); + try { + c = mContentResolver.query(Notes.CONTENT_DATA_URI, SqlData.PROJECTION_DATA, + "(note_id=?)", new String[] { + String.valueOf(mId) + }, null); + if (c != null) { + if (c.getCount() == 0) { + Log.w(TAG, "it seems that the note has not data"); + return; + } + while (c.moveToNext()) { + SqlData data = new SqlData(mContext, c); + mDataList.add(data); + } + } else { + Log.w(TAG, "loadDataContent: cursor = null"); + } + } finally { + if (c != null) + c.close(); + } + } + + public boolean setContent(JSONObject js) { + try { + JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); + if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_SYSTEM) { + Log.w(TAG, "cannot set system folder"); + } else if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_FOLDER) { + // for folder we can only update the snnipet and type + String snippet = note.has(NoteColumns.SNIPPET) ? note + .getString(NoteColumns.SNIPPET) : ""; + if (mIsCreate || !mSnippet.equals(snippet)) { + mDiffNoteValues.put(NoteColumns.SNIPPET, snippet); + } + mSnippet = snippet; + + int type = note.has(NoteColumns.TYPE) ? note.getInt(NoteColumns.TYPE) + : Notes.TYPE_NOTE; + if (mIsCreate || mType != type) { + mDiffNoteValues.put(NoteColumns.TYPE, type); + } + mType = type; + } else if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_NOTE) { + JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA); + long id = note.has(NoteColumns.ID) ? note.getLong(NoteColumns.ID) : INVALID_ID; + if (mIsCreate || mId != id) { + mDiffNoteValues.put(NoteColumns.ID, id); + } + mId = id; + + long alertDate = note.has(NoteColumns.ALERTED_DATE) ? note + .getLong(NoteColumns.ALERTED_DATE) : 0; + if (mIsCreate || mAlertDate != alertDate) { + mDiffNoteValues.put(NoteColumns.ALERTED_DATE, alertDate); + } + mAlertDate = alertDate; + + int bgColorId = note.has(NoteColumns.BG_COLOR_ID) ? note + .getInt(NoteColumns.BG_COLOR_ID) : ResourceParser.getDefaultBgId(mContext); + if (mIsCreate || mBgColorId != bgColorId) { + mDiffNoteValues.put(NoteColumns.BG_COLOR_ID, bgColorId); + } + mBgColorId = bgColorId; + + long createDate = note.has(NoteColumns.CREATED_DATE) ? note + .getLong(NoteColumns.CREATED_DATE) : System.currentTimeMillis(); + if (mIsCreate || mCreatedDate != createDate) { + mDiffNoteValues.put(NoteColumns.CREATED_DATE, createDate); + } + mCreatedDate = createDate; + + int hasAttachment = note.has(NoteColumns.HAS_ATTACHMENT) ? note + .getInt(NoteColumns.HAS_ATTACHMENT) : 0; + if (mIsCreate || mHasAttachment != hasAttachment) { + mDiffNoteValues.put(NoteColumns.HAS_ATTACHMENT, hasAttachment); + } + mHasAttachment = hasAttachment; + + long modifiedDate = note.has(NoteColumns.MODIFIED_DATE) ? note + .getLong(NoteColumns.MODIFIED_DATE) : System.currentTimeMillis(); + if (mIsCreate || mModifiedDate != modifiedDate) { + mDiffNoteValues.put(NoteColumns.MODIFIED_DATE, modifiedDate); + } + mModifiedDate = modifiedDate; + + long parentId = note.has(NoteColumns.PARENT_ID) ? note + .getLong(NoteColumns.PARENT_ID) : 0; + if (mIsCreate || mParentId != parentId) { + mDiffNoteValues.put(NoteColumns.PARENT_ID, parentId); + } + mParentId = parentId; + + String snippet = note.has(NoteColumns.SNIPPET) ? note + .getString(NoteColumns.SNIPPET) : ""; + if (mIsCreate || !mSnippet.equals(snippet)) { + mDiffNoteValues.put(NoteColumns.SNIPPET, snippet); + } + mSnippet = snippet; + + int type = note.has(NoteColumns.TYPE) ? note.getInt(NoteColumns.TYPE) + : Notes.TYPE_NOTE; + if (mIsCreate || mType != type) { + mDiffNoteValues.put(NoteColumns.TYPE, type); + } + mType = type; + + int widgetId = note.has(NoteColumns.WIDGET_ID) ? note.getInt(NoteColumns.WIDGET_ID) + : AppWidgetManager.INVALID_APPWIDGET_ID; + if (mIsCreate || mWidgetId != widgetId) { + mDiffNoteValues.put(NoteColumns.WIDGET_ID, widgetId); + } + mWidgetId = widgetId; + + int widgetType = note.has(NoteColumns.WIDGET_TYPE) ? note + .getInt(NoteColumns.WIDGET_TYPE) : Notes.TYPE_WIDGET_INVALIDE; + if (mIsCreate || mWidgetType != widgetType) { + mDiffNoteValues.put(NoteColumns.WIDGET_TYPE, widgetType); + } + mWidgetType = widgetType; + + long originParent = note.has(NoteColumns.ORIGIN_PARENT_ID) ? note + .getLong(NoteColumns.ORIGIN_PARENT_ID) : 0; + if (mIsCreate || mOriginParent != originParent) { + mDiffNoteValues.put(NoteColumns.ORIGIN_PARENT_ID, originParent); + } + mOriginParent = originParent; + + for (int i = 0; i < dataArray.length(); i++) { + JSONObject data = dataArray.getJSONObject(i); + SqlData sqlData = null; + if (data.has(DataColumns.ID)) { + long dataId = data.getLong(DataColumns.ID); + for (SqlData temp : mDataList) { + if (dataId == temp.getId()) { + sqlData = temp; + } + } + } + + if (sqlData == null) { + sqlData = new SqlData(mContext); + mDataList.add(sqlData); + } + + sqlData.setContent(data); + } + } + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + return false; + } + return true; + } + + public JSONObject getContent() { + try { + JSONObject js = new JSONObject(); + + if (mIsCreate) { + Log.e(TAG, "it seems that we haven't created this in database yet"); + return null; + } + + JSONObject note = new JSONObject(); + if (mType == Notes.TYPE_NOTE) { + note.put(NoteColumns.ID, mId); + note.put(NoteColumns.ALERTED_DATE, mAlertDate); + note.put(NoteColumns.BG_COLOR_ID, mBgColorId); + note.put(NoteColumns.CREATED_DATE, mCreatedDate); + note.put(NoteColumns.HAS_ATTACHMENT, mHasAttachment); + note.put(NoteColumns.MODIFIED_DATE, mModifiedDate); + note.put(NoteColumns.PARENT_ID, mParentId); + note.put(NoteColumns.SNIPPET, mSnippet); + note.put(NoteColumns.TYPE, mType); + note.put(NoteColumns.WIDGET_ID, mWidgetId); + note.put(NoteColumns.WIDGET_TYPE, mWidgetType); + note.put(NoteColumns.ORIGIN_PARENT_ID, mOriginParent); + js.put(GTaskStringUtils.META_HEAD_NOTE, note); + + JSONArray dataArray = new JSONArray(); + for (SqlData sqlData : mDataList) { + JSONObject data = sqlData.getContent(); + if (data != null) { + dataArray.put(data); + } + } + js.put(GTaskStringUtils.META_HEAD_DATA, dataArray); + } else if (mType == Notes.TYPE_FOLDER || mType == Notes.TYPE_SYSTEM) { + note.put(NoteColumns.ID, mId); + note.put(NoteColumns.TYPE, mType); + note.put(NoteColumns.SNIPPET, mSnippet); + js.put(GTaskStringUtils.META_HEAD_NOTE, note); + } + + return js; + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + } + return null; + } + + public void setParentId(long id) { + mParentId = id; + mDiffNoteValues.put(NoteColumns.PARENT_ID, id); + } + + public void setGtaskId(String gid) { + mDiffNoteValues.put(NoteColumns.GTASK_ID, gid); + } + + public void setSyncId(long syncId) { + mDiffNoteValues.put(NoteColumns.SYNC_ID, syncId); + } + + public void resetLocalModified() { + mDiffNoteValues.put(NoteColumns.LOCAL_MODIFIED, 0); + } + + public long getId() { + return mId; + } + + public long getParentId() { + return mParentId; + } + + public String getSnippet() { + return mSnippet; + } + + public boolean isNoteType() { + return mType == Notes.TYPE_NOTE; + } + + public void commit(boolean validateVersion) { + if (mIsCreate) { + if (mId == INVALID_ID && mDiffNoteValues.containsKey(NoteColumns.ID)) { + mDiffNoteValues.remove(NoteColumns.ID); + } + + Uri uri = mContentResolver.insert(Notes.CONTENT_NOTE_URI, mDiffNoteValues); + try { + mId = Long.valueOf(uri.getPathSegments().get(1)); + } catch (NumberFormatException e) { + Log.e(TAG, "Get note id error :" + e.toString()); + throw new ActionFailureException("create note failed"); + } + if (mId == 0) { + throw new IllegalStateException("Create thread id failed"); + } + + if (mType == Notes.TYPE_NOTE) { + for (SqlData sqlData : mDataList) { + sqlData.commit(mId, false, -1); + } + } + } else { + if (mId <= 0 && mId != Notes.ID_ROOT_FOLDER && mId != Notes.ID_CALL_RECORD_FOLDER) { + Log.e(TAG, "No such note"); + throw new IllegalStateException("Try to update note with invalid id"); + } + if (mDiffNoteValues.size() > 0) { + mVersion ++; + int result = 0; + if (!validateVersion) { + result = mContentResolver.update(Notes.CONTENT_NOTE_URI, mDiffNoteValues, "(" + + NoteColumns.ID + "=?)", new String[] { + String.valueOf(mId) + }); + } else { + result = mContentResolver.update(Notes.CONTENT_NOTE_URI, mDiffNoteValues, "(" + + NoteColumns.ID + "=?) AND (" + NoteColumns.VERSION + "<=?)", + new String[] { + String.valueOf(mId), String.valueOf(mVersion) + }); + } + if (result == 0) { + Log.w(TAG, "there is no update. maybe user updates note when syncing"); + } + } + + if (mType == Notes.TYPE_NOTE) { + for (SqlData sqlData : mDataList) { + sqlData.commit(mId, validateVersion, mVersion); + } + } + } + + // refresh local info + loadFromCursor(mId); + if (mType == Notes.TYPE_NOTE) + loadDataContent(); + + mDiffNoteValues.clear(); + mIsCreate = false; + } +} diff --git a/Task.java b/Task.java new file mode 100644 index 0000000..d51d834 --- /dev/null +++ b/Task.java @@ -0,0 +1,334 @@ +/* + * 版权所有 (c) 2010-2011, MiCode 开源社区 (www.micode.net) + * + * 根据 Apache License, Version 2.0 (以下简称“许可协议”) 授权; + * 除非遵守许可协议,否则不得使用此文件。 + * 您可以在以下网址获取许可协议的副本: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 除非适用法律要求或书面同意,根据许可协议分发的软件按“原样”提供, + * 不提供任何明示或暗示的担保或条件。 + * 请参阅许可协议了解许可权限及限制。 + */ + +package net.micode.notes.gtask.data; + +import android.database.Cursor; +import android.text.TextUtils; +import android.util.Log; + +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.DataConstants; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.gtask.exception.ActionFailureException; +import net.micode.notes.tool.GTaskStringUtils; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +// 任务类,继承自节点类 +public class Task extends Node { + private static final String TAG = Task.class.getSimpleName(); // 日志标签 + private boolean mCompleted; // 任务完成标记 + private String mNotes; // 任务的备注 + private JSONObject mMetaInfo; // 任务的元信息 + private Task mPriorSibling; // 前一个兄弟任务 + private TaskList mParent; // 任务的父任务列表 + + // 构造方法,初始化任务属性 + public Task() { + super(); + mCompleted = false; + mNotes = null; + mPriorSibling = null; + mParent = null; + mMetaInfo = null; + } + + // 创建任务的 JSON 对象 + public JSONObject getCreateAction(int actionId) { + JSONObject js = new JSONObject(); + + try { + // 设置动作类型为创建 + js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, + GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE); + + // 设置动作 ID + js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); + + // 设置任务的索引 + js.put(GTaskStringUtils.GTASK_JSON_INDEX, mParent.getChildTaskIndex(this)); + + // 设置实体数据 + JSONObject entity = new JSONObject(); + entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); + entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null"); + entity.put(GTaskStringUtils.GTASK_JSON_ENTITY_TYPE, + GTaskStringUtils.GTASK_JSON_TYPE_TASK); + if (getNotes() != null) { + entity.put(GTaskStringUtils.GTASK_JSON_NOTES, getNotes()); + } + js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); + + // 设置父任务 ID + js.put(GTaskStringUtils.GTASK_JSON_PARENT_ID, mParent.getGid()); + + // 设置父任务类型 + js.put(GTaskStringUtils.GTASK_JSON_DEST_PARENT_TYPE, + GTaskStringUtils.GTASK_JSON_TYPE_GROUP); + + // 设置任务列表 ID + js.put(GTaskStringUtils.GTASK_JSON_LIST_ID, mParent.getGid()); + + // 设置前一个兄弟任务 ID + if (mPriorSibling != null) { + js.put(GTaskStringUtils.GTASK_JSON_PRIOR_SIBLING_ID, mPriorSibling.getGid()); + } + + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("创建任务 JSON 对象失败"); + } + + return js; + } + + // 更新任务的 JSON 对象 + public JSONObject getUpdateAction(int actionId) { + JSONObject js = new JSONObject(); + + try { + js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, + GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE); + js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); + js.put(GTaskStringUtils.GTASK_JSON_ID, getGid()); + + JSONObject entity = new JSONObject(); + entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); + if (getNotes() != null) { + entity.put(GTaskStringUtils.GTASK_JSON_NOTES, getNotes()); + } + entity.put(GTaskStringUtils.GTASK_JSON_DELETED, getDeleted()); + js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); + + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("更新任务 JSON 对象失败"); + } + + return js; + } + + // 从远程 JSON 设置任务内容 + public void setContentByRemoteJSON(JSONObject js) { + if (js != null) { + try { + if (js.has(GTaskStringUtils.GTASK_JSON_ID)) { + setGid(js.getString(GTaskStringUtils.GTASK_JSON_ID)); + } + if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) { + setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)); + } + if (js.has(GTaskStringUtils.GTASK_JSON_NAME)) { + setName(js.getString(GTaskStringUtils.GTASK_JSON_NAME)); + } + if (js.has(GTaskStringUtils.GTASK_JSON_NOTES)) { + setNotes(js.getString(GTaskStringUtils.GTASK_JSON_NOTES)); + } + if (js.has(GTaskStringUtils.GTASK_JSON_DELETED)) { + setDeleted(js.getBoolean(GTaskStringUtils.GTASK_JSON_DELETED)); + } + if (js.has(GTaskStringUtils.GTASK_JSON_COMPLETED)) { + setCompleted(js.getBoolean(GTaskStringUtils.GTASK_JSON_COMPLETED)); + } + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("从 JSON 获取任务内容失败"); + } + } + } + + // 从本地 JSON 设置任务内容 + public void setContentByLocalJSON(JSONObject js) { + if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE) + || !js.has(GTaskStringUtils.META_HEAD_DATA)) { + Log.w(TAG, "setContentByLocalJSON: 数据为空"); + } + + try { + JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); + JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA); + + if (note.getInt(NoteColumns.TYPE) != Notes.TYPE_NOTE) { + Log.e(TAG, "无效类型"); + return; + } + + for (int i = 0; i < dataArray.length(); i++) { + JSONObject data = dataArray.getJSONObject(i); + if (TextUtils.equals(data.getString(DataColumns.MIME_TYPE), DataConstants.NOTE)) { + setName(data.getString(DataColumns.CONTENT)); + break; + } + } + + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + } + } + + // 获取本地 JSON 内容 + public JSONObject getLocalJSONFromContent() { + String name = getName(); + try { + if (mMetaInfo == null) { + if (name == null) { + Log.w(TAG, "笔记为空"); + return null; + } + JSONObject js = new JSONObject(); + JSONObject note = new JSONObject(); + JSONArray dataArray = new JSONArray(); + JSONObject data = new JSONObject(); + data.put(DataColumns.CONTENT, name); + dataArray.put(data); + js.put(GTaskStringUtils.META_HEAD_DATA, dataArray); + note.put(NoteColumns.TYPE, Notes.TYPE_NOTE); + js.put(GTaskStringUtils.META_HEAD_NOTE, note); + return js; + } else { + JSONObject note = mMetaInfo.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); + JSONArray dataArray = mMetaInfo.getJSONArray(GTaskStringUtils.META_HEAD_DATA); + + for (int i = 0; i < dataArray.length(); i++) { + JSONObject data = dataArray.getJSONObject(i); + if (TextUtils.equals(data.getString(DataColumns.MIME_TYPE), DataConstants.NOTE)) { + data.put(DataColumns.CONTENT, getName()); + break; + } + } + + note.put(NoteColumns.TYPE, Notes.TYPE_NOTE); + return mMetaInfo; + } + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + return null; + } + } + + // 设置元信息 + public void setMetaInfo(MetaData metaData) { + if (metaData != null && metaData.getNotes() != null) { + try { + mMetaInfo = new JSONObject(metaData.getNotes()); + } catch (JSONException e) { + Log.w(TAG, e.toString()); + mMetaInfo = null; + } + } + } + + // 获取同步操作 + public int getSyncAction(Cursor c) { + try { + JSONObject noteInfo = null; + if (mMetaInfo != null && mMetaInfo.has(GTaskStringUtils.META_HEAD_NOTE)) { + noteInfo = mMetaInfo.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); + } + + if (noteInfo == null) { + Log.w(TAG, "笔记元数据已删除"); + return SYNC_ACTION_UPDATE_REMOTE; + } + + if (!noteInfo.has(NoteColumns.ID)) { + Log.w(TAG, "远程笔记 ID 似乎被删除"); + return SYNC_ACTION_UPDATE_LOCAL; + } + + if (c.getLong(SqlNote.ID_COLUMN) != noteInfo.getLong(NoteColumns.ID)) { + Log.w(TAG, "笔记 ID 不匹配"); + return SYNC_ACTION_UPDATE_LOCAL; + } + + if (c.getInt(SqlNote.LOCAL_MODIFIED_COLUMN) == 0) { + if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { + return SYNC_ACTION_NONE; + } else { + return SYNC_ACTION_UPDATE_LOCAL; + } + } else { + if (!c.getString(SqlNote.GTASK_ID_COLUMN).equals(getGid())) { + Log.e(TAG, "gtask ID 不匹配"); + return SYNC_ACTION_ERROR; + } + if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { + return SYNC_ACTION_UPDATE_REMOTE; + } else { + return SYNC_ACTION_UPDATE_CONFLICT; + } + } + } catch (Exception e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + } + + return SYNC_ACTION_ERROR; + } + + // 检查任务是否值得保存 + public boolean isWorthSaving() { + return mMetaInfo != null || (getName() != null && getName().trim().length() > 0) + || (getNotes() != null && getNotes().trim().length() > 0); + } + + // 设置任务完成状态 + public void setCompleted(boolean completed) { + this.mCompleted = completed; + } + + // 设置备注 + public void setNotes(String notes) { + this.mNotes = notes; + } + + // 设置前一个兄弟任务 + public void setPriorSibling(Task priorSibling) { + this.mPriorSibling = priorSibling; + } + + // 设置父任务列表 + public void setParent(TaskList parent) { + this.mParent = parent; + } + + // 获取任务完成状态 + public boolean getCompleted() { + return this.mCompleted; + } + + // 获取备注 + public String getNotes() { + return this.mNotes; + } + + // 获取前一个兄弟任务 + public Task getPriorSibling() { + return this.mPriorSibling; + } + + // 获取父任务列表 + public TaskList getParent() { + return this.mParent; + } +} diff --git a/TaskList.java b/TaskList.java new file mode 100644 index 0000000..814b349 --- /dev/null +++ b/TaskList.java @@ -0,0 +1,351 @@ +/* + * 版权所有 (c) 2010-2011, MiCode 开源社区 (www.micode.net) + * + * 根据 Apache 许可证 2.0 版授权 ("许可证"); + * 只有在符合许可证的情况下,您才能使用此文件。 + * 您可以在以下位置获取许可证副本: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 除非适用法律要求或书面同意,按“原样”分发的许可证软件, + * 无任何明示或暗示的担保或条件。 + * 请参阅许可证以获取许可下的具体语言和限制。 + */ + +package net.micode.notes.gtask.data; + +import android.database.Cursor; +import android.util.Log; + +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.gtask.exception.ActionFailureException; +import net.micode.notes.tool.GTaskStringUtils; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; + +/** + * 任务列表类,继承自节点类 + */ +public class TaskList extends Node { + private static final String TAG = TaskList.class.getSimpleName(); // 日志标签 + + private int mIndex; // 索引号 + + private ArrayList mChildren; // 子任务列表 + + // 构造函数 + public TaskList() { + super(); + mChildren = new ArrayList(); + mIndex = 1; + } + + /** + * 创建任务列表的JSON对象 + * @param actionId 动作ID + * @return 创建的JSON对象 + */ + public JSONObject getCreateAction(int actionId) { + JSONObject js = new JSONObject(); + try { + js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE); // 动作类型 + js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); // 动作ID + js.put(GTaskStringUtils.GTASK_JSON_INDEX, mIndex); // 索引 + + // 设置实体信息 + JSONObject entity = new JSONObject(); + entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); // 名称 + entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null"); // 创建者ID + entity.put(GTaskStringUtils.GTASK_JSON_ENTITY_TYPE, GTaskStringUtils.GTASK_JSON_TYPE_GROUP); // 实体类型 + js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); + + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("生成任务列表创建JSON对象失败"); + } + return js; + } + + /** + * 更新任务列表的JSON对象 + * @param actionId 动作ID + * @return 更新的JSON对象 + */ + public JSONObject getUpdateAction(int actionId) { + JSONObject js = new JSONObject(); + try { + js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE); // 动作类型 + js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); // 动作ID + js.put(GTaskStringUtils.GTASK_JSON_ID, getGid()); // 任务ID + + // 设置实体信息 + JSONObject entity = new JSONObject(); + entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); // 名称 + entity.put(GTaskStringUtils.GTASK_JSON_DELETED, getDeleted()); // 删除状态 + js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); + + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("生成任务列表更新JSON对象失败"); + } + return js; + } + + /** + * 从远程JSON设置任务列表内容 + */ + public void setContentByRemoteJSON(JSONObject js) { + if (js != null) { + try { + if (js.has(GTaskStringUtils.GTASK_JSON_ID)) { + setGid(js.getString(GTaskStringUtils.GTASK_JSON_ID)); // 设置ID + } + if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) { + setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)); // 设置最后修改时间 + } + if (js.has(GTaskStringUtils.GTASK_JSON_NAME)) { + setName(js.getString(GTaskStringUtils.GTASK_JSON_NAME)); // 设置名称 + } + + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("从JSON对象获取任务列表内容失败"); + } + } + } + + /** + * 从本地JSON设置任务列表内容 + */ + public void setContentByLocalJSON(JSONObject js) { + if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE)) { + Log.w(TAG, "setContentByLocalJSON: 无可用内容"); + return; + } + + try { + JSONObject folder = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); + + if (folder.getInt(NoteColumns.TYPE) == Notes.TYPE_FOLDER) { + String name = folder.getString(NoteColumns.SNIPPET); + setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + name); + } else if (folder.getInt(NoteColumns.TYPE) == Notes.TYPE_SYSTEM) { + if (folder.getLong(NoteColumns.ID) == Notes.ID_ROOT_FOLDER) + setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT); + else if (folder.getLong(NoteColumns.ID) == Notes.ID_CALL_RECORD_FOLDER) + setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_CALL_NOTE); + else + Log.e(TAG, "无效的系统文件夹"); + } else { + Log.e(TAG, "错误的类型"); + } + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + } + } + + /** + * 获取任务列表内容并转换为本地JSON对象 + */ + public JSONObject getLocalJSONFromContent() { + try { + JSONObject js = new JSONObject(); + JSONObject folder = new JSONObject(); + String folderName = getName(); + + // 处理文件夹名称 + if (getName().startsWith(GTaskStringUtils.MIUI_FOLDER_PREFFIX)) { + folderName = folderName.substring(GTaskStringUtils.MIUI_FOLDER_PREFFIX.length()); + } + + folder.put(NoteColumns.SNIPPET, folderName); + folder.put(NoteColumns.TYPE, folderName.equals(GTaskStringUtils.FOLDER_DEFAULT) + || folderName.equals(GTaskStringUtils.FOLDER_CALL_NOTE) + ? Notes.TYPE_SYSTEM : Notes.TYPE_FOLDER); + + js.put(GTaskStringUtils.META_HEAD_NOTE, folder); + + return js; + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + return null; + } + } + + /** + * 根据游标信息获取同步操作类型 + */ + public int getSyncAction(Cursor c) { + try { + if (c.getInt(SqlNote.LOCAL_MODIFIED_COLUMN) == 0) { + // 没有本地更新 + return c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified() ? SYNC_ACTION_NONE : SYNC_ACTION_UPDATE_LOCAL; + } else { + // 验证任务ID + if (!c.getString(SqlNote.GTASK_ID_COLUMN).equals(getGid())) { + Log.e(TAG, "任务ID不匹配"); + return SYNC_ACTION_ERROR; + } + return c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified() ? SYNC_ACTION_UPDATE_REMOTE : SYNC_ACTION_UPDATE_REMOTE; + } + } catch (Exception e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + } + return SYNC_ACTION_ERROR; + } + + /** + * 获取子任务数量 + */ + public int getChildTaskCount() { + return mChildren.size(); + } + + /** + * 添加子任务 + */ + public boolean addChildTask(Task task) { + if (task != null && !mChildren.contains(task)) { + boolean ret = mChildren.add(task); + if (ret) { + task.setPriorSibling(mChildren.isEmpty() ? null : mChildren.get(mChildren.size() - 1)); + task.setParent(this); + } + return ret; + } + return false; + } + + /** + * 添加子任务到指定位置 + */ + public boolean addChildTask(Task task, int index) { + if (index < 0 || index > mChildren.size()) { + Log.e(TAG, "添加子任务:无效索引"); + return false; + } + if (task != null && mChildren.indexOf(task) == -1) { + mChildren.add(index, task); + + // 更新子任务链 + Task preTask = index != 0 ? mChildren.get(index - 1) : null; + Task afterTask = index != mChildren.size() - 1 ? mChildren.get(index + 1) : null; + + task.setPriorSibling(preTask); + if (afterTask != null) { + afterTask.setPriorSibling(task); + } + } + return true; + } + + /** + * 从任务列表中删除子任务 + */ + public boolean removeChildTask(Task task) { + int index = mChildren.indexOf(task); + if (index != -1) { + boolean ret = mChildren.remove(task); + if (ret) { + task.setPriorSibling(null); + task.setParent(null); + + // 更新任务链 + if (index != mChildren.size()) { + mChildren.get(index).setPriorSibling(index == 0 ? null : mChildren.get(index - 1)); + } + } + return ret; + } + return false; + } + + /** + * 移动子任务到指定位置 + */ + public boolean moveChildTask(Task task, int index) { + if (index < 0 || index >= mChildren.size()) { + Log.e(TAG, "移动子任务:无效索引"); + return false; + } + int pos = mChildren.indexOf(task); + if (pos == -1) { + Log.e(TAG, "移动子任务:任务应在列表中"); + return false; + } + return pos == index || (removeChildTask(task) && addChildTask(task, index)); + } + + /** + * 通过GID查找子任务 + */ + public Task findChildTaskByGid(String gid) { + for (Task t : mChildren) { + if (t.getGid().equals(gid)) { + return t; + } + } + return null; + } + + /** + * 获取子任务的索引 + */ + public int getChildTaskIndex(Task task) { + return mChildren.indexOf(task); + } + + /** + * 通过索引获取子任务 + */ + public Task getChildTaskByIndex(int index) { + if (index < 0 || index >= mChildren.size()) { + Log.e(TAG, "getTaskByIndex: 无效索引"); + return null; + } + return mChildren.get(index); + } + + /** + * 通过GID获取子任务 + */ + public Task getChilTaskByGid(String gid) { + for (Task task : mChildren) { + if (task.getGid().equals(gid)) { + return task; + } + } + return null; + } + + /** + * 获取子任务列表 + */ + public ArrayList getChildTaskList() { + return this.mChildren; + } + + /** + * 设置索引 + */ + public void setIndex(int index) { + this.mIndex = index; + } + + /** + * 获取索引 + */ + public int getIndex() { + return this.mIndex; + } +} diff --git a/WorkingNote.java b/WorkingNote.java new file mode 100644 index 0000000..e62151e --- /dev/null +++ b/WorkingNote.java @@ -0,0 +1,254 @@ +/* + * 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; + +/** + * WorkingNote 类处理与笔记相关的工作逻辑。它支持创建新笔记、加载现有笔记、设置笔记内容、 + * 管理笔记的背景色、提醒时间等,同时支持与小部件的交互。 + */ +public class WorkingNote { + private Note mNote; // 代表笔记的 Note 对象 + private long mNoteId; // 笔记的 ID + 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 boolean mIsDeleted; // 是否被标记为已删除 + private NoteSettingChangedListener mNoteSettingStatusListener; // 设置变更监听器 + + private static final String TAG = "WorkingNote"; // 日志标签 + + // 数据查询的列名数组 + 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; + mNote = new Note(); // 创建一个新的 Note 对象 + mNoteId = 0; + mIsDeleted = false; + mMode = 0; + mWidgetType = Notes.TYPE_WIDGET_INVALIDE; // 默认无效的小部件类型 + } + + /** + * 构造一个现有的工作笔记 + */ + private WorkingNote(Context context, long noteId, long folderId) { + mContext = context; + mNoteId = noteId; + mFolderId = folderId; + mIsDeleted = false; + mNote = new Note(); + loadNote(); // 加载笔记数据 + } + + /** + * 从数据库加载笔记的信息 + */ + private void loadNote() { + // 从笔记内容 URI 中查询笔记的基本信息 + 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)); // 设置文本数据ID + } else if (DataConstants.CALL_NOTE.equals(type)) { + mNote.setCallDataId(cursor.getLong(DATA_ID_COLUMN)); // 设置通话数据ID + } 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; + } + + /** + * 加载指定ID的工作笔记 + */ + public static WorkingNote load(Context context, long id) { + return new WorkingNote(context, id, 0); + } + + /** + * 保存笔记 + */ + public synchronized boolean saveNote() { + if (isWorthSaving()) { // 如果笔记值得保存 + if (!existInDatabase()) { // 如果笔记不存在数据库 + 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() { + return !(mIsDeleted || (TextUtils.isEmpty(mContent) && !existInDatabase()) + || (existInDatabase() && !mNote.isLocalModified())); // 判断笔记是否被删除或没有修改 + } + + // 其他设置方法(设置提醒时间、背景色、小部件等) + public void setAlertDate(long date, boolean set) { ... } + public void markDeleted(boolean mark) { ... } + public void setBgColorId(int id) { ... } + public void setCheckListMode(int mode) { ... } + public void setWidgetType(int type) { ... } + public void setWidgetId(int id) { ... } + public void setWorkingText(String text) { ... } + + // 获取笔记内容、提醒时间、修改时间等信息 + 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 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(); // 小部件变更时调用 + void onCheckListModeChanged(int oldMode, int newMode); // 清单模式切换时调用 + } +}