diff --git a/src/AndroidManifest.xml b/src/AndroidManifest.xml index e061ed5..956cd37 100644 --- a/src/AndroidManifest.xml +++ b/src/AndroidManifest.xml @@ -1,17 +1,15 @@ - + + + - - - - - + - - - + - + - + - + @@ -75,6 +71,15 @@ android:name="net.micode.notes.data.NotesProvider" android:authorities="micode_notes" android:multiprocess="true" /> + + + + android:theme="@style/Theme.NotesMaster" > + android:theme="@style/Theme.NotesMaster" > + + + + - - - { @@ -21,4 +35,44 @@ public class MainActivity extends AppCompatActivity { return insets; }); } + private void checkDatabase() { + new Thread(() -> { + try { + NotesDatabaseHelper dbHelper = NotesDatabaseHelper.getInstance(this); + SQLiteDatabase db = dbHelper.getReadableDatabase(); + + // 检查表是否存在 + Cursor cursor = db.rawQuery( + "SELECT name FROM sqlite_master WHERE type='table' AND name='note_attachment'", + null); + boolean tableExists = cursor != null && cursor.getCount() > 0; + Log.d("Debug", "附件表存在: " + tableExists); + if (cursor != null) cursor.close(); + + // 查询附件数据 + cursor = db.rawQuery("SELECT COUNT(*) FROM note_attachment", null); + if (cursor != null && cursor.moveToFirst()) { + int count = cursor.getInt(0); + Log.d("Debug", "附件表记录数: " + count); + } + if (cursor != null) cursor.close(); + + // 列出所有附件 + cursor = db.query("note_attachment", null, null, null, null, null, null); + if (cursor != null) { + Log.d("Debug", "附件详情 - 总数: " + cursor.getCount()); + while (cursor.moveToNext()) { + long id = cursor.getLong(cursor.getColumnIndexOrThrow("_id")); + long noteId = cursor.getLong(cursor.getColumnIndexOrThrow("note_id")); + String path = cursor.getString(cursor.getColumnIndexOrThrow("file_path")); + Log.d("Debug", String.format("附件: id=%d, noteId=%d, path=%s", id, noteId, path)); + } + cursor.close(); + } + + } catch (Exception e) { + Log.e("Debug", "数据库检查失败", e); + } + }).start(); + } } diff --git a/src/notes/account/AccountManager.java b/src/notes/account/AccountManager.java new file mode 100644 index 0000000..eddf8f2 --- /dev/null +++ b/src/notes/account/AccountManager.java @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may 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.account; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +/** + * 账户管理工具类 + * 负责用户登录、注册、登出等账户管理功能 + * + * 注意: 密码以明文形式存储在 SharedPreferences 中 + * 这仅用于学习目的,实际项目中应使用加密存储 + * 推荐使用 Android Keystore 或其他安全方案 + */ +public class AccountManager { + private static final String TAG = "AccountManager"; + private static final String PREF_NAME = "notes_preferences"; + + // 用户登录状态 + private static final String PREF_USER_LOGGED_IN = "pref_user_logged_in"; + + // 当前登录用户名 + private static final String PREF_CURRENT_USERNAME = "pref_current_username"; + + // 用户数据前缀 (用于存储多个用户) + private static final String PREF_USER_DATA_PREFIX = "pref_user_data_"; + + // 用户密码后缀 (格式: pref_user_data__password) + private static final String PREF_USER_PASSWORD_SUFFIX = "_password"; + + /** + * 检查用户是否已登录 + * @param context 上下文对象 + * @return 如果已登录返回true,否则返回false + */ + public static boolean isUserLoggedIn(Context context) { + try { + SharedPreferences sp = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + return sp.getBoolean(PREF_USER_LOGGED_IN, false); + } catch (Exception e) { + Log.e(TAG, "Error checking login status", e); + return false; + } + } + + /** + * 获取当前登录用户名 + * @param context 上下文对象 + * @return 当前登录用户名,如果未登录则返回空字符串 + */ + public static String getCurrentUser(Context context) { + try { + SharedPreferences sp = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + return sp.getString(PREF_CURRENT_USERNAME, ""); + } catch (Exception e) { + Log.e(TAG, "Error getting current user", e); + return ""; + } + } + + /** + * 用户登录验证 + * @param context 上下文对象 + * @param username 用户名 + * @param password 密码 + * @return 登录成功返回true,失败返回false + */ + public static boolean login(Context context, String username, String password) { + if (username == null || username.isEmpty()) { + Log.w(TAG, "Empty username provided"); + return false; + } + if (password == null || password.isEmpty()) { + Log.w(TAG, "Empty password provided"); + return false; + } + + try { + SharedPreferences sp = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + String storedPassword = sp.getString(getUserPasswordKey(username), ""); + + if (storedPassword.equals(password)) { + SharedPreferences.Editor editor = sp.edit(); + editor.putBoolean(PREF_USER_LOGGED_IN, true); + editor.putString(PREF_CURRENT_USERNAME, username); + boolean result = editor.commit(); + + if (result) { + Log.d(TAG, "User logged in successfully: " + username); + } else { + Log.e(TAG, "Failed to save login status"); + } + + return result; + } else { + Log.w(TAG, "Login failed: incorrect password for user " + username); + return false; + } + } catch (Exception e) { + Log.e(TAG, "Error during login", e); + return false; + } + } + + /** + * 注册新用户 + * @param context 上下文对象 + * @param username 用户名 + * @param password 密码 + * @return 注册成功返回true,失败返回false + */ + public static boolean register(Context context, String username, String password) { + if (username == null || username.isEmpty()) { + Log.w(TAG, "Empty username provided"); + return false; + } + if (password == null || password.isEmpty()) { + Log.w(TAG, "Empty password provided"); + return false; + } + + try { + SharedPreferences sp = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + String userPasswordKey = getUserPasswordKey(username); + + if (sp.contains(userPasswordKey)) { + Log.w(TAG, "Registration failed: user already exists - " + username); + return false; + } + + SharedPreferences.Editor editor = sp.edit(); + editor.putString(userPasswordKey, password); + boolean result = editor.commit(); + + if (result) { + Log.d(TAG, "User registered successfully: " + username); + } else { + Log.e(TAG, "Failed to register user"); + } + + return result; + } catch (Exception e) { + Log.e(TAG, "Error during registration", e); + return false; + } + } + + /** + * 用户登出 + * @param context 上下文对象 + * @return 登出成功返回true,失败返回false + */ + public static boolean logout(Context context) { + try { + SharedPreferences sp = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sp.edit(); + editor.putBoolean(PREF_USER_LOGGED_IN, false); + editor.putString(PREF_CURRENT_USERNAME, ""); + boolean result = editor.commit(); + + if (result) { + Log.d(TAG, "User logged out successfully"); + } else { + Log.e(TAG, "Failed to logout"); + } + + return result; + } catch (Exception e) { + Log.e(TAG, "Error during logout", e); + return false; + } + } + + /** + * 检查用户名是否已存在 + * @param context 上下文对象 + * @param username 用户名 + * @return 如果用户名已存在返回true,否则返回false + */ + public static boolean isUserExists(Context context, String username) { + try { + SharedPreferences sp = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + return sp.contains(getUserPasswordKey(username)); + } catch (Exception e) { + Log.e(TAG, "Error checking if user exists", e); + return false; + } + } + + /** + * 获取用户密码的存储Key + * @param username 用户名 + * @return 存储Key + */ + private static String getUserPasswordKey(String username) { + return PREF_USER_DATA_PREFIX + username + PREF_USER_PASSWORD_SUFFIX; + } +} diff --git a/src/notes/data/AttachmentManager.java b/src/notes/data/AttachmentManager.java new file mode 100644 index 0000000..d92d750 --- /dev/null +++ b/src/notes/data/AttachmentManager.java @@ -0,0 +1,344 @@ +package net.micode.notes.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.os.Environment; +import android.text.TextUtils; +import android.util.Log; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.util.ArrayList; +import java.util.List; + +/** + * 附件管理器,负责附件的数据库操作和文件管理 + */ +public class AttachmentManager { + private static final String TAG = "AttachmentManager"; + + private final Context mContext; + private final ContentResolver mContentResolver; + + /** + * 附件文件存储目录 + */ + private static final String ATTACHMENT_DIR = "attachments"; + + /** + * 构造函数 + * @param context 上下文 + */ + public AttachmentManager(Context context) { + mContext = context.getApplicationContext(); + mContentResolver = mContext.getContentResolver(); + } + + /** + * 添加附件 + * @param noteId 笔记ID + * @param type 附件类型(Notes.ATTACHMENT_TYPE_GALLERY 或 Notes.ATTACHMENT_TYPE_CAMERA) + * @param sourceFile 源文件(来自相册或相机) + * @return 附件ID,失败返回-1 + */ + public long addAttachment(long noteId, int type, File sourceFile) { + if (sourceFile == null || !sourceFile.exists()) { + Log.e(TAG, "Source file does not exist"); + return -1; + } + + // 复制文件到应用私有存储 + File destFile = copyToPrivateStorage(sourceFile, noteId); + if (destFile == null) { + Log.e(TAG, "Failed to copy file to private storage"); + return -1; + } + + // 插入数据库记录 + ContentValues values = new ContentValues(); + values.put(Notes.AttachmentColumns.NOTE_ID, noteId); + values.put(Notes.AttachmentColumns.TYPE, type); + values.put(Notes.AttachmentColumns.FILE_PATH, destFile.getAbsolutePath()); + values.put(Notes.AttachmentColumns.CREATED_TIME, System.currentTimeMillis()); + + try { + Uri uri = mContentResolver.insert(Notes.CONTENT_ATTACHMENT_URI, values); + if (uri != null) { + String idStr = uri.getLastPathSegment(); + if (!TextUtils.isEmpty(idStr)) { + long attachmentId = Long.parseLong(idStr); + // 更新笔记的附件状态 + updateNoteAttachmentStatus(noteId, true); + return attachmentId; + } + } + } catch (Exception e) { + Log.e(TAG, "Failed to insert attachment", e); + // 插入失败时删除已复制的文件 + destFile.delete(); + } + + return -1; + } + + /** + * 删除附件 + * @param attachmentId 附件ID + * @return 是否成功 + */ + public boolean deleteAttachment(long attachmentId) { + // 先获取笔记ID + long noteId = getNoteIdByAttachmentId(attachmentId); + if (noteId <= 0) { + Log.e(TAG, "Failed to get note id for attachment " + attachmentId); + return false; + } + + // 获取文件路径并删除文件 + String filePath = getAttachmentFilePath(attachmentId); + if (filePath != null) { + File file = new File(filePath); + if (file.exists()) { + file.delete(); + } + } + + // 删除数据库记录 + Uri uri = Uri.withAppendedPath(Notes.CONTENT_ATTACHMENT_URI, String.valueOf(attachmentId)); + int count = mContentResolver.delete(uri, null, null); + boolean success = count > 0; + + if (success) { + // 检查笔记是否还有其他附件 + List remainingAttachments = getAttachmentsByNoteId(noteId); + if (remainingAttachments.isEmpty()) { + // 没有附件了,更新笔记状态 + updateNoteAttachmentStatus(noteId, false); + } + } + + return success; + } + + /** + * 删除笔记的所有附件 + * @param noteId 笔记ID + * @return 删除的附件数量 + */ + public int deleteAttachmentsByNoteId(long noteId) { + List attachments = getAttachmentsByNoteId(noteId); + int deletedCount = 0; + for (Attachment attachment : attachments) { + if (deleteAttachment(attachment.id)) { + deletedCount++; + } + } + return deletedCount; + } + + /** + * 获取附件的文件路径 + * @param attachmentId 附件ID + * @return 文件路径,失败返回null + */ + public String getAttachmentFilePath(long attachmentId) { + String[] projection = { Notes.AttachmentColumns.FILE_PATH }; + Uri uri = Uri.withAppendedPath(Notes.CONTENT_ATTACHMENT_URI, String.valueOf(attachmentId)); + Cursor cursor = null; + try { + cursor = mContentResolver.query(uri, projection, null, null, null); + if (cursor != null && cursor.moveToFirst()) { + return cursor.getString(0); + } + } catch (Exception e) { + Log.e(TAG, "Failed to get attachment file path", e); + } finally { + if (cursor != null) { + cursor.close(); + } + } + return null; + } + + /** + * 获取笔记的所有附件 + * @param noteId 笔记ID + * @return 附件列表 + */ + public List getAttachmentsByNoteId(long noteId) { + List attachments = new ArrayList<>(); + String[] projection = { + Notes.AttachmentColumns.ID, + Notes.AttachmentColumns.NOTE_ID, + Notes.AttachmentColumns.TYPE, + Notes.AttachmentColumns.FILE_PATH, + Notes.AttachmentColumns.CREATED_TIME + }; + String selection = Notes.AttachmentColumns.NOTE_ID + "=?"; + String[] selectionArgs = { String.valueOf(noteId) }; + String sortOrder = Notes.AttachmentColumns.CREATED_TIME + " ASC"; + + Cursor cursor = null; + try { + cursor = mContentResolver.query(Notes.CONTENT_ATTACHMENT_URI, + projection, selection, selectionArgs, sortOrder); + if (cursor != null) { + while (cursor.moveToNext()) { + Attachment attachment = new Attachment(); + attachment.id = cursor.getLong(0); + attachment.noteId = cursor.getLong(1); + attachment.type = cursor.getInt(2); + attachment.filePath = cursor.getString(3); + attachment.createdTime = cursor.getLong(4); + attachments.add(attachment); + } + } + } catch (Exception e) { + Log.e(TAG, "Failed to get attachments", e); + } finally { + if (cursor != null) { + cursor.close(); + } + } + return attachments; + } + + /** + * 复制文件到应用私有存储 + * @param sourceFile 源文件 + * @param noteId 笔记ID(用于生成文件名) + * @return 目标文件,失败返回null + */ + private File copyToPrivateStorage(File sourceFile, long noteId) { + File destDir = getAttachmentStorageDir(); + if (destDir == null || !destDir.exists() && !destDir.mkdirs()) { + Log.e(TAG, "Failed to create attachment directory"); + return null; + } + + // 生成唯一文件名:note_{noteId}_{timestamp}_{random}.jpg + String timestamp = String.valueOf(System.currentTimeMillis()); + String random = String.valueOf((int)(Math.random() * 1000)); + String extension = getFileExtension(sourceFile.getName()); + String fileName = "note_" + noteId + "_" + timestamp + "_" + random + extension; + File destFile = new File(destDir, fileName); + + try { + copyFile(sourceFile, destFile); + return destFile; + } catch (IOException e) { + Log.e(TAG, "Failed to copy file", e); + return null; + } + } + + /** + * 获取附件存储目录 + * @return 目录文件 + */ + public File getAttachmentStorageDir() { + File picturesDir = mContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES); + if (picturesDir == null) { + picturesDir = new File(mContext.getFilesDir(), "Pictures"); + } + return new File(picturesDir, ATTACHMENT_DIR); + } + + /** + * 获取文件扩展名 + * @param fileName 文件名 + * @return 扩展名(包含点) + */ + private String getFileExtension(String fileName) { + int dotIndex = fileName.lastIndexOf('.'); + if (dotIndex > 0 && dotIndex < fileName.length() - 1) { + return fileName.substring(dotIndex); + } + return ".jpg"; + } + + /** + * 复制文件 + * @param source 源文件 + * @param dest 目标文件 + * @throws IOException IO异常 + */ + private void copyFile(File source, File dest) throws IOException { + try (FileChannel sourceChannel = new FileInputStream(source).getChannel(); + FileChannel destChannel = new FileOutputStream(dest).getChannel()) { + destChannel.transferFrom(sourceChannel, 0, sourceChannel.size()); + } + } + + /** + * 通过附件ID获取笔记ID + * @param attachmentId 附件ID + * @return 笔记ID,失败返回-1 + */ + private long getNoteIdByAttachmentId(long attachmentId) { + String[] projection = { Notes.AttachmentColumns.NOTE_ID }; + Uri uri = Uri.withAppendedPath(Notes.CONTENT_ATTACHMENT_URI, String.valueOf(attachmentId)); + Cursor cursor = null; + try { + cursor = mContentResolver.query(uri, projection, null, null, null); + if (cursor != null && cursor.moveToFirst()) { + return cursor.getLong(0); + } + } catch (Exception e) { + Log.e(TAG, "Failed to get note id by attachment id", e); + } finally { + if (cursor != null) { + cursor.close(); + } + } + return -1; + } + + /** + * 更新笔记的附件状态 + * @param noteId 笔记ID + * @param hasAttachment 是否有附件 + */ + private void updateNoteAttachmentStatus(long noteId, boolean hasAttachment) { + ContentValues values = new ContentValues(); + values.put(Notes.NoteColumns.HAS_ATTACHMENT, hasAttachment ? 1 : 0); + values.put(Notes.NoteColumns.LOCAL_MODIFIED, 1); + values.put(Notes.NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); + + Uri uri = ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId); + try { + mContentResolver.update(uri, values, null, null); + } catch (Exception e) { + Log.e(TAG, "Failed to update note attachment status", e); + } + } + + /** + * 附件数据模型 + */ + public static class Attachment { + public long id; + public long noteId; + public int type; + public String filePath; + public long createdTime; + + @Override + public String toString() { + return "Attachment{" + + "id=" + id + + ", noteId=" + noteId + + ", type=" + type + + ", filePath='" + filePath + '\'' + + ", createdTime=" + createdTime + + '}'; + } + } +} \ No newline at end of file diff --git a/src/notes/data/Notes.java b/src/notes/data/Notes.java index 0be82f2..05123dc 100644 --- a/src/notes/data/Notes.java +++ b/src/notes/data/Notes.java @@ -140,6 +140,47 @@ public class Notes { public static final String CALL_NOTE = CallNote.CONTENT_ITEM_TYPE; } + /** + * 附件类型常量 + */ + public static final int ATTACHMENT_TYPE_GALLERY = 0; + public static final int ATTACHMENT_TYPE_CAMERA = 1; + + /** + * 附件表列名定义接口 + */ + public interface AttachmentColumns { + /** + * The unique ID for a row + *

Type: INTEGER (long)

+ */ + public static final String ID = "_id"; + + /** + * The note's id this attachment belongs to + *

Type: INTEGER (long)

+ */ + public static final String NOTE_ID = "note_id"; + + /** + * Attachment type (0=gallery, 1=camera) + *

Type: INTEGER

+ */ + public static final String TYPE = "type"; + + /** + * File path in private storage + *

Type: TEXT

+ */ + public static final String FILE_PATH = "file_path"; + + /** + * Created time + *

Type: INTEGER (long)

+ */ + public static final String CREATED_TIME = "created_time"; + } + /** * 用于查询所有笔记和文件夹的Uri */ @@ -150,6 +191,11 @@ public class Notes { */ public static final Uri CONTENT_DATA_URI = Uri.parse("content://" + AUTHORITY + "/data"); + /** + * 用于查询附件的Uri + */ + public static final Uri CONTENT_ATTACHMENT_URI = Uri.parse("content://" + AUTHORITY + "/attachment"); + /** * 笔记表列名定义接口 */ @@ -257,6 +303,12 @@ public class Notes { *

Type : INTEGER (long)

*/ public static final String VERSION = "version"; + + /** + * Whether the note is locked with password + *

Type : INTEGER

+ */ + public static final String IS_LOCKED = "is_locked"; } /** diff --git a/src/notes/data/NotesDatabaseHelper.java b/src/notes/data/NotesDatabaseHelper.java index abda0d7..b42d678 100644 --- a/src/notes/data/NotesDatabaseHelper.java +++ b/src/notes/data/NotesDatabaseHelper.java @@ -18,6 +18,7 @@ package net.micode.notes.data; import android.content.ContentValues; import android.content.Context; +import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.util.Log; @@ -48,7 +49,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { /** * 数据库版本号,版本迭代历史详见 {@link #onUpgrade} 方法 */ - private static final int DB_VERSION = 4; + private static final int DB_VERSION = 8; /** * 数据库表名定义接口 @@ -56,6 +57,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { * 定义两个核心表: * 1. {@link #NOTE}:便签元数据表,存储文件夹、标题、时间、类型等信息。 * 2. {@link #DATA}:便签内容数据表,支持多种 MIME 类型(文本、图片、提醒等)。 + * 3. {@link #ATTACHMENT}:便签附件表,存储图片等附件信息。 */ public interface TABLE { /** @@ -67,6 +69,11 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { * 数据表名 */ public static final String DATA = "data"; + + /** + * 附件表名 + */ + public static final String ATTACHMENT = "note_attachment"; } /** @@ -109,6 +116,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { NoteColumns.ORIGIN_PARENT_ID + " INTEGER NOT NULL DEFAULT 0," + NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''," + NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0" + + NoteColumns.IS_LOCKED + " INTEGER NOT NULL DEFAULT 0" + ")"; /** @@ -136,6 +144,26 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { DataColumns.DATA5 + " TEXT NOT NULL DEFAULT ''" + ")"; + /** + * 创建附件表 (note_attachment) 的 SQL 语句。 + *

+ * 存储便签的图片附件信息,支持本地相册和相机拍照两种来源。 + * 核心字段说明: + * - _id: 主键(与Android ContentProvider标准保持一致) + * - note_id: 关联的笔记ID + * - type: 附件类型(0=本地相册,1=相机拍照) + * - file_path: 图片文件在应用私有存储中的绝对路径 + * - created_time: 创建时间戳 + */ + private static final String CREATE_ATTACHMENT_TABLE_SQL = + "CREATE TABLE " + TABLE.ATTACHMENT + "(" + + "_id INTEGER PRIMARY KEY," + + "note_id INTEGER NOT NULL," + + "type INTEGER NOT NULL DEFAULT 0," + + "file_path TEXT NOT NULL," + + "created_time INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)" + + ")"; + /** * 在 data 表的 NOTE_ID 列上创建索引的 SQL 语句。 *

@@ -403,6 +431,20 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { Log.d(TAG, "data table has been created"); } + /** + * 创建附件表 (note_attachment) 及其索引。 + * + * @param db 可写的数据库实例。 + */ + public void createAttachmentTable(SQLiteDatabase db) { + // 执行创建表SQL + db.execSQL(CREATE_ATTACHMENT_TABLE_SQL); + // 创建note_id索引以提高查询性能 + db.execSQL("CREATE INDEX IF NOT EXISTS attachment_note_id_index ON " + + TABLE.ATTACHMENT + "(note_id);"); + Log.d(TAG, "attachment table has been created"); + } + /** * 重建 data 表的所有 SQL 触发器。 *

@@ -428,7 +470,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { * @param context 应用上下文。 * @return NotesDatabaseHelper 的单例实例。 */ - static synchronized NotesDatabaseHelper getInstance(Context context) { + public static synchronized NotesDatabaseHelper getInstance(Context context) { if (mInstance == null) { mInstance = new NotesDatabaseHelper(context); } @@ -438,7 +480,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { /** * 数据库首次创建时回调。 *

- * 执行顺序:先创建 note 表(含系统文件夹),再创建 data 表。 + * 执行顺序:先创建 note 表(含系统文件夹),再创建 data 表,最后创建附件表。 * * @param db 数据库实例。 */ @@ -448,6 +490,8 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { createNoteTable(db); // 创建数据表 createDataTable(db); + // 创建附件表 + createAttachmentTable(db); } /** @@ -486,13 +530,37 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { oldVersion++; } - // 4. 如果表结构在v3升级中发生较大变更,需要重建触发器以确保兼容性 + // 4. 从版本4升级到版本5(支持图片插入功能) + if (oldVersion == 4) { + upgradeToV5(db); + oldVersion++; + } + + // 5. 从版本5升级到版本6(添加附件表) + if (oldVersion == 5) { + upgradeToV6(db); + oldVersion++; + } + + // 6. 从版本6升级到版本7(修复附件表主键列名) + if (oldVersion == 6) { + upgradeToV7(db); + oldVersion++; + } + + // 8. 从版本7升级到版本8(添加便签加锁功能) + if (oldVersion == 7) { + upgradeToV8(db); + oldVersion++; + } + + // 9. 如果表结构在v3升级中发生较大变更,需要重建触发器以确保兼容性 if (reCreateTriggers) { reCreateNoteTableTriggers(db); reCreateDataTableTriggers(db); } - // 5. 最终版本校验:确保所有升级步骤已执行完毕 + // 8. 最终版本校验:确保所有升级步骤已执行完毕 if (oldVersion != newVersion) { throw new IllegalStateException("Upgrade notes database to version " + newVersion + "fails"); @@ -555,4 +623,148 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0"); } + + /** + * 升级数据库到版本 5。 + *

+ * 升级方式:渐进式迁移,添加图片查询索引。 + * 主要变更:支持图片插入功能。 + *

+ * 设计说明: + * - 图片数据存储在 data 表中,使用现有的 DATA3(图片路径)、DATA4(图片宽度)、DATA5(图片高度)字段 + * - 通过 MIME_TYPE 区分图片数据类型 + * - 为图片查询添加索引以提高性能 + * + * @param db 可写的数据库实例。 + */ + private void upgradeToV5(SQLiteDatabase db) { + // 为图片数据添加查询索引,提高图片查询性能 + db.execSQL("CREATE INDEX IF NOT EXISTS image_data_index ON " + TABLE.DATA + "(" + DataColumns.MIME_TYPE + ") WHERE " + + DataColumns.MIME_TYPE + " LIKE 'image/%'"); + Log.d(TAG, "Database upgraded to version 5 (image support with index)"); + } + + /** + * 升级数据库到版本 6。 + *

+ * 升级方式:渐进式迁移,添加附件表。 + * 主要变更:支持图片附件存储,独立表结构便于管理。 + * + * @param db 可写的数据库实例。 + */ + private void upgradeToV6(SQLiteDatabase db) { + // 创建附件表 + createAttachmentTable(db); + Log.d(TAG, "Database upgraded to version 6 (attachment table added)"); + } + + /** + * 升级数据库到版本 7。 + *

+ * 升级方式:渐进式迁移,修复附件表主键列名不一致问题。 + * 主要变更:将附件表的主键列名从 "id" 改为 "_id",与Android ContentProvider标准保持一致。 + * + * @param db 可写的数据库实例。 + */ + private void upgradeToV7(SQLiteDatabase db) { + // 检查附件表是否存在 + Cursor cursor = db.rawQuery("SELECT name FROM sqlite_master WHERE type='table' AND name='note_attachment'", null); + boolean tableExists = cursor != null && cursor.getCount() > 0; + if (cursor != null) { + cursor.close(); + } + + if (tableExists) { + // 检查当前表结构中的主键列名 + cursor = db.rawQuery("PRAGMA table_info(note_attachment)", null); + boolean hasIdColumn = false; + boolean hasUnderscoreIdColumn = false; + + if (cursor != null) { + while (cursor.moveToNext()) { + String columnName = cursor.getString(1); // 列名在索引1 + if ("id".equals(columnName)) { + hasIdColumn = true; + } else if ("_id".equals(columnName)) { + hasUnderscoreIdColumn = true; + } + } + cursor.close(); + } + + if (hasIdColumn && !hasUnderscoreIdColumn) { + // 需要将id列重命名为_id + Log.d(TAG, "Starting migration: renaming 'id' column to '_id' in note_attachment table"); + + // 使用事务确保迁移的原子性 + db.beginTransaction(); + try { + // 1. 创建临时表 + db.execSQL("CREATE TABLE note_attachment_temp (" + + "_id INTEGER PRIMARY KEY," + + "note_id INTEGER NOT NULL," + + "type INTEGER NOT NULL DEFAULT 0," + + "file_path TEXT NOT NULL," + + "created_time INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)" + + ")"); + + // 2. 复制数据 + db.execSQL("INSERT INTO note_attachment_temp (_id, note_id, type, file_path, created_time) " + + "SELECT id, note_id, type, file_path, created_time FROM note_attachment"); + + // 3. 删除原表 + db.execSQL("DROP TABLE note_attachment"); + + // 4. 重命名临时表 + db.execSQL("ALTER TABLE note_attachment_temp RENAME TO note_attachment"); + + // 5. 重新创建索引 + db.execSQL("CREATE INDEX IF NOT EXISTS attachment_note_id_index ON note_attachment(note_id)"); + + db.setTransactionSuccessful(); + Log.d(TAG, "Successfully migrated note_attachment table: renamed 'id' to '_id'"); + } catch (Exception e) { + Log.e(TAG, "Failed to migrate note_attachment table", e); + throw e; + } finally { + db.endTransaction(); + } + } else if (hasUnderscoreIdColumn) { + Log.d(TAG, "Table already has correct '_id' column, no migration needed"); + } else { + Log.w(TAG, "Unexpected table structure for note_attachment, recreating table"); + // 如果表结构异常,重新创建表 + db.execSQL("DROP TABLE IF EXISTS note_attachment"); + createAttachmentTable(db); + } + } else { + // 表不存在,直接创建新表 + createAttachmentTable(db); + Log.d(TAG, "Created note_attachment table with correct column names"); + } + + Log.d(TAG, "Database upgraded to version 7 (attachment table column name fixed)"); + } + + /** + * 升级数据库到版本 8。 + *

+ * 升级方式:渐进式迁移。 + * 主要变更:添加便签加锁功能,增加 is_locked 字段。 + *

+ * 设计说明: + * - is_locked: INTEGER类型,0表示未加锁,1表示已加锁 + * - 默认值为0,确保向后兼容 + * + * @param db 可写的数据库实例。 + */ + private void upgradeToV8(SQLiteDatabase db) { + try { + db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.IS_LOCKED + + " INTEGER NOT NULL DEFAULT 0"); + Log.d(TAG, "Database upgraded to version 8 (note lock support added)"); + } catch (Exception e) { + Log.e(TAG, "Failed to upgrade to version 8, column may already exist", e); + } + } } \ No newline at end of file diff --git a/src/notes/data/NotesProvider.java b/src/notes/data/NotesProvider.java index 77446df..a54a1b9 100644 --- a/src/notes/data/NotesProvider.java +++ b/src/notes/data/NotesProvider.java @@ -34,6 +34,7 @@ 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.Notes.AttachmentColumns; import net.micode.notes.data.NotesDatabaseHelper.TABLE; @@ -88,6 +89,16 @@ public class NotesProvider extends ContentProvider { */ private static final int URI_SEARCH_SUGGEST = 6; + /** + * URI类型:所有附件 + */ + private static final int URI_ATTACHMENT = 7; + + /** + * URI类型:单个附件 + */ + private static final int URI_ATTACHMENT_ITEM = 8; + /** * 静态初始化块,初始化UriMatcher */ @@ -101,6 +112,8 @@ public class NotesProvider extends ContentProvider { mMatcher.addURI(Notes.AUTHORITY, "search", URI_SEARCH); // content://micode_notes/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); // 带参数的搜索建议 + mMatcher.addURI(Notes.AUTHORITY, "attachment", URI_ATTACHMENT); // content://micode_notes/attachment + mMatcher.addURI(Notes.AUTHORITY, "attachment/#", URI_ATTACHMENT_ITEM); // content://micode_notes/attachment/1 } /** @@ -168,6 +181,15 @@ public class NotesProvider extends ContentProvider { c = db.query(TABLE.DATA, projection, DataColumns.ID + "=" + id + parseSelection(selection), selectionArgs, null, null, sortOrder); break; + case URI_ATTACHMENT: + c = db.query(TABLE.ATTACHMENT, projection, selection, selectionArgs, null, null, + sortOrder); + break; + case URI_ATTACHMENT_ITEM: + id = uri.getPathSegments().get(1); + c = db.query(TABLE.ATTACHMENT, projection, AttachmentColumns.ID + "=" + id + + parseSelection(selection), selectionArgs, null, null, sortOrder); + break; case URI_SEARCH: case URI_SEARCH_SUGGEST: if (sortOrder != null || projection != null) { @@ -465,6 +487,14 @@ public class NotesProvider extends ContentProvider { } insertedId = dataId = db.insert(TABLE.DATA, null, values); break; + case URI_ATTACHMENT: + if (values.containsKey(AttachmentColumns.NOTE_ID)) { + noteId = values.getAsLong(AttachmentColumns.NOTE_ID); + } else { + Log.d(TAG, "Wrong attachment format without note id:" + values.toString()); + } + insertedId = db.insert(TABLE.ATTACHMENT, null, values); + break; default: throw new IllegalArgumentException("Unknown URI " + uri); } @@ -524,6 +554,14 @@ public class NotesProvider extends ContentProvider { DataColumns.ID + "=" + id + parseSelection(selection), selectionArgs); deleteData = true; break; + case URI_ATTACHMENT: + count = db.delete(TABLE.ATTACHMENT, selection, selectionArgs); + break; + case URI_ATTACHMENT_ITEM: + id = uri.getPathSegments().get(1); + count = db.delete(TABLE.ATTACHMENT, + Notes.AttachmentColumns.ID + "=" + id + parseSelection(selection), selectionArgs); + break; default: throw new IllegalArgumentException("Unknown URI " + uri); } @@ -571,6 +609,14 @@ public class NotesProvider extends ContentProvider { + parseSelection(selection), selectionArgs); updateData = true; break; + case URI_ATTACHMENT: + count = db.update(TABLE.ATTACHMENT, values, selection, selectionArgs); + break; + case URI_ATTACHMENT_ITEM: + id = uri.getPathSegments().get(1); + count = db.update(TABLE.ATTACHMENT, values, "id=" + id + + parseSelection(selection), selectionArgs); + break; default: throw new IllegalArgumentException("Unknown URI " + uri); } diff --git a/src/notes/gtask/data/MetaData.java b/src/notes/gtask/data/MetaData.java deleted file mode 100644 index d1168aa..0000000 --- a/src/notes/gtask/data/MetaData.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.micode.notes.gtask.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类,用于存储与GTask同步相关的元数据信息 - * - * 该类主要用于在GTask同步过程中存储和管理元数据,包含与特定GTask相关联的ID信息 - * 重写了Task类的部分方法,以适应元数据的特殊处理需求 - */ -public class MetaData extends Task { - private final static String TAG = MetaData.class.getSimpleName(); - - // 与当前元数据关联的GTask ID - private String mRelatedGid = null; - - /** - * 设置元数据信息 - * - * @param gid 相关联的GTask ID - * @param metaInfo 元数据JSON对象 - */ - public void setMeta(String gid, JSONObject metaInfo) { - try { - // 将GTask ID添加到元数据中 - metaInfo.put(GTaskStringUtils.META_HEAD_GTASK_ID, gid); - } catch (JSONException e) { - Log.e(TAG, "failed to put related gid"); - } - // 将元数据以字符串形式存储到Task的notes字段中 - setNotes(metaInfo.toString()); - // 设置元数据任务的名称 - setName(GTaskStringUtils.META_NOTE_NAME); - } - - /** - * 获取与当前元数据关联的GTask ID - * - * @return 关联的GTask ID - */ - public String getRelatedGid() { - return mRelatedGid; - } - - /** - * 判断元数据是否值得保存 - * - * @return 如果有notes内容则返回true,否则返回false - */ - @Override - public boolean isWorthSaving() { - return getNotes() != null; - } - - /** - * 从远程JSON对象设置内容 - * - * @param js 远程JSON对象 - */ - @Override - public void setContentByRemoteJSON(JSONObject js) { - super.setContentByRemoteJSON(js); - // 如果有notes内容,则解析获取关联的GTask ID - if (getNotes() != null) { - try { - JSONObject metaInfo = new JSONObject(getNotes().trim()); - mRelatedGid = metaInfo.getString(GTaskStringUtils.META_HEAD_GTASK_ID); - } catch (JSONException e) { - Log.w(TAG, "failed to get related gid"); - mRelatedGid = null; - } - } - } - - /** - * 从本地JSON对象设置内容 - 该方法不应被调用 - * - * @param js 本地JSON对象 - * @throws IllegalAccessError 总是抛出该异常,表示方法不应被调用 - */ - @Override - public void setContentByLocalJSON(JSONObject js) { - // 元数据不支持从本地JSON设置内容 - throw new IllegalAccessError("MetaData:setContentByLocalJSON should not be called"); - } - - /** - * 从内容获取本地JSON对象 - 该方法不应被调用 - * - * @return 本地JSON对象 - * @throws IllegalAccessError 总是抛出该异常,表示方法不应被调用 - */ - @Override - public JSONObject getLocalJSONFromContent() { - // 元数据不支持获取本地JSON对象 - throw new IllegalAccessError("MetaData:getLocalJSONFromContent should not be called"); - } - - /** - * 获取同步操作类型 - 该方法不应被调用 - * - * @param c 数据库游标 - * @return 同步操作类型 - * @throws IllegalAccessError 总是抛出该异常,表示方法不应被调用 - */ - @Override - public int getSyncAction(Cursor c) { - // 元数据不支持获取同步操作类型 - throw new IllegalAccessError("MetaData:getSyncAction should not be called"); - } - -} diff --git a/src/notes/gtask/data/Node.java b/src/notes/gtask/data/Node.java deleted file mode 100644 index b23d7e5..0000000 --- a/src/notes/gtask/data/Node.java +++ /dev/null @@ -1,222 +0,0 @@ -/* - * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.micode.notes.gtask.data; - -import android.database.Cursor; - -import org.json.JSONObject; - -/** - * Node类 - GTask同步系统中的抽象基类,定义了所有同步节点的基本属性和行为 - * - * 该抽象类为TaskList、Task和MetaData等具体类提供了统一的接口和基础功能, - * 主要用于管理GTask同步系统中的节点对象及其与服务器的同步操作。 - */ -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; - - /** - * 节点在Google Tasks系统中的全局唯一ID - */ - private String mGid; - - /** - * 节点名称 - */ - private String mName; - - /** - * 节点最后修改时间戳 - */ - private long mLastModified; - - /** - * 节点是否已删除的标记 - */ - private boolean mDeleted; - - /** - * 构造函数 - 初始化节点的基本属性 - */ - public Node() { - mGid = null; - mName = ""; - mLastModified = 0; - mDeleted = false; - } - - /** - * 生成创建节点的JSON操作对象(抽象方法,子类必须实现) - * - * @param actionId 操作ID,用于标识该同步操作 - * @return 包含创建节点所需信息的JSON对象 - */ - public abstract JSONObject getCreateAction(int actionId); - - /** - * 生成更新节点的JSON操作对象(抽象方法,子类必须实现) - * - * @param actionId 操作ID,用于标识该同步操作 - * @return 包含更新节点所需信息的JSON对象 - */ - public abstract JSONObject getUpdateAction(int actionId); - - /** - * 根据远程JSON对象设置节点内容(抽象方法,子类必须实现) - * - * @param js 从Google Tasks服务器获取的JSON对象 - */ - public abstract void setContentByRemoteJSON(JSONObject js); - - /** - * 根据本地JSON对象设置节点内容(抽象方法,子类必须实现) - * - * @param js 本地存储的JSON对象 - */ - public abstract void setContentByLocalJSON(JSONObject js); - - /** - * 从节点内容生成本地JSON对象(抽象方法,子类必须实现) - * - * @return 包含节点本地存储信息的JSON对象 - */ - public abstract JSONObject getLocalJSONFromContent(); - - /** - * 根据本地数据库游标判断同步操作类型(抽象方法,子类必须实现) - * - * @param c 本地数据库游标,包含节点的本地存储信息 - * @return 同步操作类型(使用SYNC_ACTION_*常量) - */ - public abstract int getSyncAction(Cursor c); - - /** - * 设置节点在Google Tasks系统中的全局唯一ID - * - * @param gid 全局唯一ID - */ - public void setGid(String gid) { - this.mGid = gid; - } - - /** - * 设置节点名称 - * - * @param name 节点名称 - */ - public void setName(String name) { - this.mName = name; - } - - /** - * 设置节点最后修改时间戳 - * - * @param lastModified 最后修改时间戳 - */ - public void setLastModified(long lastModified) { - this.mLastModified = lastModified; - } - - /** - * 设置节点是否已删除 - * - * @param deleted 是否已删除 - */ - public void setDeleted(boolean deleted) { - this.mDeleted = deleted; - } - - /** - * 获取节点在Google Tasks系统中的全局唯一ID - * - * @return 全局唯一ID - */ - public String getGid() { - return this.mGid; - } - - /** - * 获取节点名称 - * - * @return 节点名称 - */ - public String getName() { - return this.mName; - } - - /** - * 获取节点最后修改时间戳 - * - * @return 最后修改时间戳 - */ - public long getLastModified() { - return this.mLastModified; - } - - /** - * 获取节点是否已删除 - * - * @return 是否已删除 - */ - public boolean getDeleted() { - return this.mDeleted; - } - -} diff --git a/src/notes/gtask/data/SqlData.java b/src/notes/gtask/data/SqlData.java deleted file mode 100644 index 83cd91b..0000000 --- a/src/notes/gtask/data/SqlData.java +++ /dev/null @@ -1,284 +0,0 @@ -/* - * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.micode.notes.gtask.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类 - 用于管理与笔记数据相关的SQL操作,处理Notes应用中数据行的创建、更新和内容同步 - * - * 该类主要负责: - * 1. 管理笔记数据的基本信息(ID、MIME类型、内容等) - * 2. 支持从Cursor加载数据和生成JSON格式内容 - * 3. 处理数据的创建和更新操作,使用ContentValues跟踪差异变化 - * 4. 将数据提交到数据库,支持版本验证 - */ -public class SqlData { - private static final String TAG = SqlData.class.getSimpleName(); - - /** - * 无效的ID值,用于标记未初始化或无效的数据ID - */ - private static final int INVALID_ID = -99999; - - /** - * 数据库查询投影,定义了查询数据表时需要返回的列 - */ - public static final String[] PROJECTION_DATA = new String[] { - DataColumns.ID, DataColumns.MIME_TYPE, DataColumns.CONTENT, DataColumns.DATA1, - DataColumns.DATA3 - }; - - /** - * PROJECTION_DATA中ID列的索引 - */ - public static final int DATA_ID_COLUMN = 0; - - /** - * PROJECTION_DATA中MIME类型列的索引 - */ - public static final int DATA_MIME_TYPE_COLUMN = 1; - - /** - * PROJECTION_DATA中内容列的索引 - */ - public static final int DATA_CONTENT_COLUMN = 2; - - /** - * PROJECTION_DATA中DATA1列的索引 - */ - public static final int DATA_CONTENT_DATA_1_COLUMN = 3; - - /** - * PROJECTION_DATA中DATA3列的索引 - */ - public static final int DATA_CONTENT_DATA_3_COLUMN = 4; - - /** - * 用于访问Android ContentProvider的ContentResolver对象 - */ - private ContentResolver mContentResolver; - - /** - * 标记当前数据是否为新创建的数据 - */ - private boolean mIsCreate; - - /** - * 数据的唯一标识符 - */ - private long mDataId; - - /** - * 数据的MIME类型 - */ - private String mDataMimeType; - - /** - * 数据的内容 - */ - private String mDataContent; - - /** - * 数据的额外信息字段1 - */ - private long mDataContentData1; - - /** - * 数据的额外信息字段3 - */ - private String mDataContentData3; - - /** - * 用于存储差异更新的ContentValues对象,只包含需要更新的字段 - */ - private ContentValues mDiffDataValues; - - /** - * 构造函数 - 创建一个新的SqlData对象 - * - * @param context 上下文对象,用于获取ContentResolver - */ - public SqlData(Context context) { - mContentResolver = context.getContentResolver(); - mIsCreate = true; - mDataId = INVALID_ID; - mDataMimeType = DataConstants.NOTE; - mDataContent = ""; - mDataContentData1 = 0; - mDataContentData3 = ""; - mDiffDataValues = new ContentValues(); - } - - /** - * 构造函数 - 从Cursor创建SqlData对象 - * - * @param context 上下文对象,用于获取ContentResolver - * @param c 包含数据的Cursor对象 - */ - public SqlData(Context context, Cursor c) { - mContentResolver = context.getContentResolver(); - mIsCreate = false; - loadFromCursor(c); - mDiffDataValues = new ContentValues(); - } - - /** - * 从Cursor加载数据到成员变量 - * - * @param c 包含数据的Cursor对象 - */ - private void loadFromCursor(Cursor c) { - mDataId = c.getLong(DATA_ID_COLUMN); - mDataMimeType = c.getString(DATA_MIME_TYPE_COLUMN); - mDataContent = c.getString(DATA_CONTENT_COLUMN); - mDataContentData1 = c.getLong(DATA_CONTENT_DATA_1_COLUMN); - mDataContentData3 = c.getString(DATA_CONTENT_DATA_3_COLUMN); - } - - /** - * 根据JSON对象设置数据内容 - * - * @param js 包含数据内容的JSON对象 - * @throws JSONException 如果解析JSON失败 - */ - public void setContent(JSONObject js) throws JSONException { - long dataId = js.has(DataColumns.ID) ? js.getLong(DataColumns.ID) : INVALID_ID; - if (mIsCreate || mDataId != dataId) { - mDiffDataValues.put(DataColumns.ID, dataId); - } - mDataId = dataId; - - String dataMimeType = js.has(DataColumns.MIME_TYPE) ? js.getString(DataColumns.MIME_TYPE) - : DataConstants.NOTE; - if (mIsCreate || !mDataMimeType.equals(dataMimeType)) { - mDiffDataValues.put(DataColumns.MIME_TYPE, dataMimeType); - } - mDataMimeType = dataMimeType; - - 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; - if (mIsCreate || mDataContentData1 != dataContentData1) { - mDiffDataValues.put(DataColumns.DATA1, dataContentData1); - } - mDataContentData1 = dataContentData1; - - String dataContentData3 = js.has(DataColumns.DATA3) ? js.getString(DataColumns.DATA3) : ""; - if (mIsCreate || !mDataContentData3.equals(dataContentData3)) { - mDiffDataValues.put(DataColumns.DATA3, dataContentData3); - } - mDataContentData3 = dataContentData3; - } - - /** - * 生成包含数据内容的JSON对象 - * - * @return 包含数据内容的JSON对象 - * @throws JSONException 如果生成JSON失败 - */ - public JSONObject getContent() throws JSONException { - if (mIsCreate) { - Log.e(TAG, "it seems that we haven't created this in database yet"); - return null; - } - JSONObject js = new JSONObject(); - js.put(DataColumns.ID, mDataId); - js.put(DataColumns.MIME_TYPE, mDataMimeType); - js.put(DataColumns.CONTENT, mDataContent); - js.put(DataColumns.DATA1, mDataContentData1); - js.put(DataColumns.DATA3, mDataContentData3); - return js; - } - - /** - * 将数据提交到数据库 - * - * @param noteId 所属笔记的ID - * @param validateVersion 是否验证版本号 - * @param version 验证的版本号 - * @throws ActionFailureException 如果创建数据失败 - */ - public void commit(long noteId, boolean validateVersion, long version) { - - if (mIsCreate) { - if (mDataId == INVALID_ID && mDiffDataValues.containsKey(DataColumns.ID)) { - mDiffDataValues.remove(DataColumns.ID); - } - - mDiffDataValues.put(DataColumns.NOTE_ID, noteId); - Uri uri = mContentResolver.insert(Notes.CONTENT_DATA_URI, mDiffDataValues); - try { - mDataId = Long.valueOf(uri.getPathSegments().get(1)); - } 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 - * - * @return 数据的唯一标识符 - */ - public long getId() { - return mDataId; - } -} diff --git a/src/notes/gtask/data/SqlNote.java b/src/notes/gtask/data/SqlNote.java deleted file mode 100644 index f22b521..0000000 --- a/src/notes/gtask/data/SqlNote.java +++ /dev/null @@ -1,615 +0,0 @@ -/* - * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.micode.notes.gtask.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; - - -/** - * SqlNote类 - 用于管理本地笔记数据的核心类 - * - * 该类负责处理笔记数据的加载、存储、更新和转换,是GTask同步机制与本地数据库之间的桥梁 - * 支持从Cursor加载数据、从JSON设置内容、提交更改到数据库等功能 - */ -public class SqlNote { - private static final String TAG = SqlNote.class.getSimpleName(); - - // 无效ID常量,用于初始化和验证 - private static final int INVALID_ID = -99999; - - // 笔记查询投影数组,定义了从数据库查询笔记时返回的列 - public static final String[] PROJECTION_NOTE = new String[] { - NoteColumns.ID, NoteColumns.ALERTED_DATE, NoteColumns.BG_COLOR_ID, - NoteColumns.CREATED_DATE, NoteColumns.HAS_ATTACHMENT, NoteColumns.MODIFIED_DATE, - 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; - // 内容解析器,用于与ContentProvider交互 - private ContentResolver mContentResolver; - // 是否为新创建的笔记 - private boolean mIsCreate; - // 笔记ID - private long mId; - // 提醒日期 - private long mAlertDate; - // 背景颜色ID - private int mBgColorId; - // 创建日期 - private long mCreatedDate; - // 是否有附件 - private int mHasAttachment; - // 修改日期 - private long mModifiedDate; - // 父文件夹ID - private long mParentId; - // 笔记摘要 - private String mSnippet; - // 笔记类型(普通笔记、文件夹等) - private int mType; - // 小部件ID - private int mWidgetId; - // 小部件类型 - private int mWidgetType; - // 原始父文件夹ID - private long mOriginParent; - // 版本号,用于并发控制 - private long mVersion; - // 差异内容值,用于记录需要更新的字段 - private ContentValues mDiffNoteValues; - - // 私有成员变量,用于存储SqlData类型的数据列表 - // 使用ArrayList作为数据结构,可以动态存储SqlData对象 - private ArrayList mDataList; - - /** - * 创建新笔记的构造函数 - * - * @param context 上下文对象,用于访问系统服务 - */ - public SqlNote(Context context) { - mContext = context; - mContentResolver = context.getContentResolver(); - mIsCreate = true; // 标记为新创建的笔记 - mId = INVALID_ID; // 初始化为无效ID - mAlertDate = 0; // 无提醒日期 - mBgColorId = ResourceParser.getDefaultBgId(context); // 使用默认背景颜色 - mCreatedDate = System.currentTimeMillis(); // 创建日期设为当前时间 - mHasAttachment = 0; // 初始没有附件 - mModifiedDate = System.currentTimeMillis(); // 修改日期设为当前时间 - mParentId = 0; // 默认父文件夹ID为0 - mSnippet = ""; // 摘要为空 - mType = Notes.TYPE_NOTE; // 默认类型为普通笔记 - mWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID; // 无效小部件ID - mWidgetType = Notes.TYPE_WIDGET_INVALIDE; // 无效小部件类型 - mOriginParent = 0; // 原始父文件夹ID - mVersion = 0; // 初始版本号 - mDiffNoteValues = new ContentValues(); // 用于记录差异的内容值 - mDataList = new ArrayList(); // 初始化数据列表 - } - - /** - * 从Cursor创建SqlNote对象的构造函数 - * - * @param context 上下文对象 - * @param c 包含笔记数据的Cursor对象 - */ - public SqlNote(Context context, Cursor c) { - mContext = context; - mContentResolver = context.getContentResolver(); - mIsCreate = false; // 标记为已存在的笔记 - loadFromCursor(c); // 从Cursor加载数据 - mDataList = new ArrayList(); // 初始化数据列表 - if (mType == Notes.TYPE_NOTE) // 如果是普通笔记,加载其数据内容 - loadDataContent(); - mDiffNoteValues = new ContentValues(); // 用于记录差异的内容值 - } - - /** - * 根据ID创建SqlNote对象的构造函数 - * - * @param context 上下文对象 - * @param id 笔记的ID - */ - public SqlNote(Context context, long id) { - mContext = context; - mContentResolver = context.getContentResolver(); - mIsCreate = false; // 标记为已存在的笔记 - loadFromCursor(id); // 根据ID从数据库加载数据 - mDataList = new ArrayList(); // 初始化数据列表 - if (mType == Notes.TYPE_NOTE) // 如果是普通笔记,加载其数据内容 - loadDataContent(); - mDiffNoteValues = new ContentValues(); // 用于记录差异的内容值 - } - - /** - * 根据ID从数据库加载笔记数据 - * - * @param id 笔记的ID - */ - private void loadFromCursor(long id) { - Cursor c = null; - try { - // 查询指定ID的笔记数据 - c = mContentResolver.query(Notes.CONTENT_NOTE_URI, PROJECTION_NOTE, "(_id=?)", - new String[] { - String.valueOf(id) - }, null); - if (c != null) { - c.moveToNext(); - loadFromCursor(c); // 调用Cursor版本的loadFromCursor方法 - } else { - Log.w(TAG, "loadFromCursor: cursor = null"); - } - } finally { - if (c != null) - c.close(); // 确保Cursor被关闭 - } - } - - /** - * 从Cursor对象加载笔记数据到成员变量 - * - * @param c 包含笔记数据的Cursor对象 - */ - 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); - } - - /** - * 加载笔记的具体数据内容 - * - * 该方法从数据库中查询与当前笔记关联的所有数据项,并将它们加载到mDataList中 - */ - private void loadDataContent() { - Cursor c = null; - mDataList.clear(); // 清空现有数据列表 - try { - // 查询与当前笔记ID关联的所有数据项 - 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; - } - // 遍历Cursor,创建SqlData对象并添加到数据列表 - 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(); // 确保Cursor被关闭 - } - } - - /** - * 从JSON对象设置笔记内容 - * - * @param js 包含笔记数据的JSON对象 - * @return 是否成功设置内容 - */ - 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) { - // 文件夹类型只能更新摘要和类型 - 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; - - // 尝试在现有数据列表中找到匹配的SqlData对象 - if (data.has(DataColumns.ID)) { - long dataId = data.getLong(DataColumns.ID); - for (SqlData temp : mDataList) { - if (dataId == temp.getId()) { - sqlData = temp; - } - } - } - - // 如果找不到匹配的SqlData对象,则创建新的 - if (sqlData == null) { - sqlData = new SqlData(mContext); - mDataList.add(sqlData); - } - - // 设置SqlData的内容 - sqlData.setContent(data); - } - } - } catch (JSONException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - return false; - } - return true; - } - - /** - * 获取笔记内容的JSON表示 - * - * @return 包含笔记内容的JSON对象,如果获取失败则返回null - */ - 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(); - - // 根据笔记类型构建不同的JSON结构 - 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; - } - - /** - * 设置笔记的父文件夹ID - * - * @param id 父文件夹的ID - */ - public void setParentId(long id) { - mParentId = id; - mDiffNoteValues.put(NoteColumns.PARENT_ID, id); - } - - /** - * 设置笔记的GTask ID(用于同步) - * - * @param gid GTask ID - */ - public void setGtaskId(String gid) { - mDiffNoteValues.put(NoteColumns.GTASK_ID, gid); - } - - /** - * 设置笔记的同步ID - * - * @param syncId 同步ID - */ - public void setSyncId(long syncId) { - mDiffNoteValues.put(NoteColumns.SYNC_ID, syncId); - } - - /** - * 重置本地修改标志 - * - * 将本地修改标志设置为0,表示当前内容与服务器同步 - */ - public void resetLocalModified() { - mDiffNoteValues.put(NoteColumns.LOCAL_MODIFIED, 0); - } - - /** - * 获取笔记的ID - * - * @return 笔记的ID - */ - public long getId() { - return mId; - } - - /** - * 获取笔记的父文件夹ID - * - * @return 父文件夹的ID - */ - public long getParentId() { - return mParentId; - } - - /** - * 获取笔记的摘要 - * - * @return 笔记的摘要 - */ - public String getSnippet() { - return mSnippet; - } - - /** - * 检查笔记是否为普通笔记类型 - * - * @return 是否为普通笔记类型 - */ - public boolean isNoteType() { - return mType == Notes.TYPE_NOTE; - } - - /** - * 将笔记内容提交到数据库 - * - * @param validateVersion 是否验证版本号 - * @throws ActionFailureException 如果创建笔记失败 - * @throws IllegalStateException 如果更新笔记时ID无效 - */ - public void commit(boolean validateVersion) { - if (mIsCreate) { - // 新创建的笔记,执行插入操作 - if (mId == INVALID_ID && mDiffNoteValues.containsKey(NoteColumns.ID)) { - mDiffNoteValues.remove(NoteColumns.ID); // 移除无效ID - } - - // 插入笔记到数据库 - Uri uri = mContentResolver.insert(Notes.CONTENT_NOTE_URI, mDiffNoteValues); - try { - mId = 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"); - } - 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); - } - } - } - - // 刷新本地信息,确保与数据库一致 - loadFromCursor(mId); - if (mType == Notes.TYPE_NOTE) - loadDataContent(); - - // 清空差异内容值并标记为已存在 - mDiffNoteValues.clear(); - mIsCreate = false; - } -} diff --git a/src/notes/gtask/data/Task.java b/src/notes/gtask/data/Task.java deleted file mode 100644 index 4bbe6a9..0000000 --- a/src/notes/gtask/data/Task.java +++ /dev/null @@ -1,470 +0,0 @@ -/* - * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.micode.notes.gtask.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; - - -/** - * Task类 - 继承自Node类,用于表示GTask同步系统中的单个任务 - * - * 该类主要负责: - * 1. 管理任务的基本信息(名称、备注、完成状态等) - * 2. 维护任务之间的关系(前一个兄弟任务、父任务列表) - * 3. 处理与GTask服务器的同步操作(创建、更新、同步状态判断) - * 4. 支持本地与远程JSON格式的相互转换 - * 5. 管理任务的元信息,用于与本地笔记系统关联 - */ -public class Task extends Node { - private static final String TAG = Task.class.getSimpleName(); - - /** - * 任务的完成状态 - */ - private boolean mCompleted; - - /** - * 任务的备注信息 - */ - private String mNotes; - - /** - * 任务的元信息,以JSON格式存储,用于与本地笔记系统关联 - */ - private JSONObject mMetaInfo; - - /** - * 当前任务的前一个兄弟任务,用于维护任务列表中的顺序 - */ - private Task mPriorSibling; - - /** - * 当前任务所属的父任务列表 - */ - private TaskList mParent; - - /** - * 构造函数 - 初始化任务的基本属性 - */ - public Task() { - super(); - mCompleted = false; - mNotes = null; - mPriorSibling = null; - mParent = null; - mMetaInfo = null; - } - - /** - * 生成创建任务的JSON操作对象 - * - * @param actionId 操作ID,用于标识该同步操作 - * @return 包含创建任务所需信息的JSON对象 - * @throws ActionFailureException 如果生成JSON失败 - */ - public JSONObject getCreateAction(int actionId) { - JSONObject js = new JSONObject(); - - try { - // action_type - js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, - GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE); - - // action_id - js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); - - // index - js.put(GTaskStringUtils.GTASK_JSON_INDEX, mParent.getChildTaskIndex(this)); - - // entity_delta - JSONObject entity = new JSONObject(); - entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); - entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null"); - entity.put(GTaskStringUtils.GTASK_JSON_ENTITY_TYPE, - GTaskStringUtils.GTASK_JSON_TYPE_TASK); - if (getNotes() != null) { - entity.put(GTaskStringUtils.GTASK_JSON_NOTES, getNotes()); - } - js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); - - // parent_id - js.put(GTaskStringUtils.GTASK_JSON_PARENT_ID, mParent.getGid()); - - // dest_parent_type - js.put(GTaskStringUtils.GTASK_JSON_DEST_PARENT_TYPE, - GTaskStringUtils.GTASK_JSON_TYPE_GROUP); - - // list_id - js.put(GTaskStringUtils.GTASK_JSON_LIST_ID, mParent.getGid()); - - // prior_sibling_id - if (mPriorSibling != null) { - js.put(GTaskStringUtils.GTASK_JSON_PRIOR_SIBLING_ID, mPriorSibling.getGid()); - } - - } catch (JSONException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new ActionFailureException("fail to generate task-create jsonobject"); - } - - return js; - } - - /** - * 生成更新任务的JSON操作对象 - * - * @param actionId 操作ID,用于标识该同步操作 - * @return 包含更新任务所需信息的JSON对象 - * @throws ActionFailureException 如果生成JSON失败 - */ - public JSONObject getUpdateAction(int actionId) { - JSONObject js = new JSONObject(); - - try { - // action_type - js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, - GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE); - - // action_id - js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); - - // id - js.put(GTaskStringUtils.GTASK_JSON_ID, getGid()); - - // entity_delta - JSONObject entity = new JSONObject(); - entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); - if (getNotes() != null) { - entity.put(GTaskStringUtils.GTASK_JSON_NOTES, getNotes()); - } - entity.put(GTaskStringUtils.GTASK_JSON_DELETED, getDeleted()); - js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); - - } catch (JSONException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new ActionFailureException("fail to generate task-update jsonobject"); - } - - return js; - } - - /** - * 根据远程JSON对象设置任务内容 - * - * @param js 从Google Tasks服务器获取的JSON对象 - * @throws ActionFailureException 如果解析JSON失败 - */ - public void setContentByRemoteJSON(JSONObject js) { - if (js != null) { - try { - // id - if (js.has(GTaskStringUtils.GTASK_JSON_ID)) { - setGid(js.getString(GTaskStringUtils.GTASK_JSON_ID)); - } - - // last_modified - if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) { - setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)); - } - - // name - if (js.has(GTaskStringUtils.GTASK_JSON_NAME)) { - setName(js.getString(GTaskStringUtils.GTASK_JSON_NAME)); - } - - // notes - if (js.has(GTaskStringUtils.GTASK_JSON_NOTES)) { - setNotes(js.getString(GTaskStringUtils.GTASK_JSON_NOTES)); - } - - // deleted - if (js.has(GTaskStringUtils.GTASK_JSON_DELETED)) { - setDeleted(js.getBoolean(GTaskStringUtils.GTASK_JSON_DELETED)); - } - - // completed - if (js.has(GTaskStringUtils.GTASK_JSON_COMPLETED)) { - setCompleted(js.getBoolean(GTaskStringUtils.GTASK_JSON_COMPLETED)); - } - } catch (JSONException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new ActionFailureException("fail to get task content from jsonobject"); - } - } - } - - /** - * 根据本地JSON对象设置任务内容 - * - * @param js 本地存储的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: nothing is avaiable"); - } - - try { - JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); - JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA); - - if (note.getInt(NoteColumns.TYPE) != Notes.TYPE_NOTE) { - Log.e(TAG, "invalid type"); - return; - } - - for (int i = 0; i < dataArray.length(); i++) { - JSONObject data = dataArray.getJSONObject(i); - if (TextUtils.equals(data.getString(DataColumns.MIME_TYPE), DataConstants.NOTE)) { - setName(data.getString(DataColumns.CONTENT)); - break; - } - } - - } catch (JSONException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - } - } - - /** - * 从任务内容生成本地JSON对象 - * - * @return 包含任务本地存储信息的JSON对象,如果生成失败则返回null - */ - public JSONObject getLocalJSONFromContent() { - String name = getName(); - try { - if (mMetaInfo == null) { - // new task created from web - if (name == null) { - Log.w(TAG, "the note seems to be an empty one"); - return null; - } - - JSONObject js = new JSONObject(); - JSONObject note = new JSONObject(); - JSONArray dataArray = new JSONArray(); - JSONObject data = new JSONObject(); - data.put(DataColumns.CONTENT, name); - dataArray.put(data); - js.put(GTaskStringUtils.META_HEAD_DATA, dataArray); - note.put(NoteColumns.TYPE, Notes.TYPE_NOTE); - js.put(GTaskStringUtils.META_HEAD_NOTE, note); - return js; - } else { - // synced task - JSONObject note = mMetaInfo.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); - JSONArray dataArray = mMetaInfo.getJSONArray(GTaskStringUtils.META_HEAD_DATA); - - for (int i = 0; i < dataArray.length(); i++) { - JSONObject data = dataArray.getJSONObject(i); - if (TextUtils.equals(data.getString(DataColumns.MIME_TYPE), DataConstants.NOTE)) { - data.put(DataColumns.CONTENT, getName()); - break; - } - } - - note.put(NoteColumns.TYPE, Notes.TYPE_NOTE); - return mMetaInfo; - } - } catch (JSONException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - return null; - } - } - - /** - * 设置任务的元信息 - * - * @param metaData 包含任务元信息的MetaData对象 - */ - 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; - } - } - } - - /** - * 根据本地数据库游标判断同步操作类型 - * - * @param c 本地数据库游标,包含任务的本地存储信息 - * @return 同步操作类型: - * - SYNC_ACTION_NONE: 无需同步 - * - SYNC_ACTION_UPDATE_LOCAL: 更新本地数据 - * - SYNC_ACTION_UPDATE_REMOTE: 更新远程数据 - * - SYNC_ACTION_UPDATE_CONFLICT: 同步冲突 - * - SYNC_ACTION_ERROR: 同步错误 - */ - public int getSyncAction(Cursor c) { - try { - JSONObject noteInfo = null; - if (mMetaInfo != null && mMetaInfo.has(GTaskStringUtils.META_HEAD_NOTE)) { - noteInfo = mMetaInfo.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); - } - - if (noteInfo == null) { - Log.w(TAG, "it seems that note meta has been deleted"); - return SYNC_ACTION_UPDATE_REMOTE; - } - - if (!noteInfo.has(NoteColumns.ID)) { - Log.w(TAG, "remote note id seems to be deleted"); - return SYNC_ACTION_UPDATE_LOCAL; - } - - // validate the note id now - if (c.getLong(SqlNote.ID_COLUMN) != noteInfo.getLong(NoteColumns.ID)) { - Log.w(TAG, "note id doesn't match"); - return SYNC_ACTION_UPDATE_LOCAL; - } - - if (c.getInt(SqlNote.LOCAL_MODIFIED_COLUMN) == 0) { - // there is no local update - if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { - // no update both side - return SYNC_ACTION_NONE; - } else { - // apply remote to local - return SYNC_ACTION_UPDATE_LOCAL; - } - } else { - // validate gtask id - if (!c.getString(SqlNote.GTASK_ID_COLUMN).equals(getGid())) { - Log.e(TAG, "gtask id doesn't match"); - return SYNC_ACTION_ERROR; - } - if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { - // local modification only - return SYNC_ACTION_UPDATE_REMOTE; - } else { - return SYNC_ACTION_UPDATE_CONFLICT; - } - } - } catch (Exception e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - } - - return SYNC_ACTION_ERROR; - } - - /** - * 判断任务是否值得保存 - * - * @return 如果任务有元信息、名称或备注,则返回true,否则返回false - */ - public boolean isWorthSaving() { - return mMetaInfo != null || (getName() != null && getName().trim().length() > 0) - || (getNotes() != null && getNotes().trim().length() > 0); - } - - /** - * 设置任务的完成状态 - * - * @param completed 任务的完成状态 - */ - public void setCompleted(boolean completed) { - this.mCompleted = completed; - } - - /** - * 设置任务的备注信息 - * - * @param notes 任务的备注信息 - */ - public void setNotes(String notes) { - this.mNotes = notes; - } - - /** - * 设置任务的前一个兄弟任务 - * - * @param priorSibling 前一个兄弟任务 - */ - public void setPriorSibling(Task priorSibling) { - this.mPriorSibling = priorSibling; - } - - /** - * 设置任务的父任务列表 - * - * @param parent 父任务列表 - */ - public void setParent(TaskList parent) { - this.mParent = parent; - } - - /** - * 获取任务的完成状态 - * - * @return 任务的完成状态 - */ - public boolean getCompleted() { - return this.mCompleted; - } - - /** - * 获取任务的备注信息 - * - * @return 任务的备注信息 - */ - public String getNotes() { - return this.mNotes; - } - - /** - * 获取任务的前一个兄弟任务 - * - * @return 前一个兄弟任务 - */ - public Task getPriorSibling() { - return this.mPriorSibling; - } - - /** - * 获取任务的父任务列表 - * - * @return 父任务列表 - */ - public TaskList getParent() { - return this.mParent; - } - -} diff --git a/src/notes/gtask/data/TaskList.java b/src/notes/gtask/data/TaskList.java deleted file mode 100644 index 4d80b34..0000000 --- a/src/notes/gtask/data/TaskList.java +++ /dev/null @@ -1,471 +0,0 @@ -/* - * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.micode.notes.gtask.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; - - -/** - * TaskList类 - 继承自Node类,用于表示任务列表并管理其中的Task对象集合 - * - * 该类主要负责: - * 1. 管理任务列表的基本信息(名称、ID、修改时间等) - * 2. 维护子Task对象的集合及其关系 - * 3. 处理与GTask服务器的同步操作(创建、更新、同步状态判断) - * 4. 支持本地与远程JSON格式的相互转换 - */ -public class TaskList extends Node { - private static final String TAG = TaskList.class.getSimpleName(); - - /** - * 任务列表在Google Tasks中的索引位置,用于排序 - */ - private int mIndex; - - /** - * 存储当前任务列表包含的所有Task对象 - */ - private ArrayList mChildren; - - /** - * 构造函数 - 初始化空的Task列表,设置默认索引为1 - */ - public TaskList() { - super(); - mChildren = new ArrayList(); - mIndex = 1; - } - - /** - * 生成创建任务列表的JSON操作对象 - * - * @param actionId 操作ID,用于标识该同步操作 - * @return 包含创建任务列表所需信息的JSON对象 - * @throws ActionFailureException 如果生成JSON失败 - */ - public JSONObject getCreateAction(int actionId) { - JSONObject js = new JSONObject(); - - try { - // action_type - js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, - GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE); - - // action_id - js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); - - // index - js.put(GTaskStringUtils.GTASK_JSON_INDEX, mIndex); - - // entity_delta - JSONObject entity = new JSONObject(); - entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); - entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null"); - entity.put(GTaskStringUtils.GTASK_JSON_ENTITY_TYPE, - GTaskStringUtils.GTASK_JSON_TYPE_GROUP); - js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); - - } catch (JSONException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new ActionFailureException("fail to generate tasklist-create jsonobject"); - } - - return js; - } - - /** - * 生成更新任务列表的JSON操作对象 - * - * @param actionId 操作ID,用于标识该同步操作 - * @return 包含更新任务列表所需信息的JSON对象 - * @throws ActionFailureException 如果生成JSON失败 - */ - public JSONObject getUpdateAction(int actionId) { - JSONObject js = new JSONObject(); - - try { - // action_type - js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, - GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE); - - // action_id - js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); - - // id - js.put(GTaskStringUtils.GTASK_JSON_ID, getGid()); - - // entity_delta - JSONObject entity = new JSONObject(); - entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); - entity.put(GTaskStringUtils.GTASK_JSON_DELETED, getDeleted()); - js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); - - } catch (JSONException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new ActionFailureException("fail to generate tasklist-update jsonobject"); - } - - return js; - } - - /** - * 根据远程JSON对象设置任务列表内容 - * - * @param js 从Google Tasks服务器获取的JSON对象 - * @throws ActionFailureException 如果解析JSON失败 - */ - public void setContentByRemoteJSON(JSONObject js) { - if (js != null) { - try { - // id - if (js.has(GTaskStringUtils.GTASK_JSON_ID)) { - setGid(js.getString(GTaskStringUtils.GTASK_JSON_ID)); - } - - // last_modified - if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) { - setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)); - } - - // name - if (js.has(GTaskStringUtils.GTASK_JSON_NAME)) { - setName(js.getString(GTaskStringUtils.GTASK_JSON_NAME)); - } - - } catch (JSONException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new ActionFailureException("fail to get tasklist content from jsonobject"); - } - } - } - - /** - * 根据本地JSON对象设置任务列表内容 - * - * @param js 本地存储的JSON对象 - */ - public void setContentByLocalJSON(JSONObject js) { - if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE)) { - Log.w(TAG, "setContentByLocalJSON: nothing is avaiable"); - } - - try { - JSONObject folder = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); - - if (folder.getInt(NoteColumns.TYPE) == Notes.TYPE_FOLDER) { - String name = folder.getString(NoteColumns.SNIPPET); - setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + name); - } else if (folder.getInt(NoteColumns.TYPE) == Notes.TYPE_SYSTEM) { - if (folder.getLong(NoteColumns.ID) == Notes.ID_ROOT_FOLDER) - setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT); - else if (folder.getLong(NoteColumns.ID) == Notes.ID_CALL_RECORD_FOLDER) - setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX - + GTaskStringUtils.FOLDER_CALL_NOTE); - else - Log.e(TAG, "invalid system folder"); - } else { - Log.e(TAG, "error type"); - } - } catch (JSONException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - } - } - - /** - * 从任务列表内容生成本地JSON对象 - * - * @return 包含任务列表本地存储信息的JSON对象,如果生成失败则返回null - */ - public JSONObject getLocalJSONFromContent() { - try { - JSONObject js = new JSONObject(); - JSONObject folder = new JSONObject(); - - String folderName = getName(); - if (getName().startsWith(GTaskStringUtils.MIUI_FOLDER_PREFFIX)) - folderName = folderName.substring(GTaskStringUtils.MIUI_FOLDER_PREFFIX.length(), - folderName.length()); - folder.put(NoteColumns.SNIPPET, folderName); - if (folderName.equals(GTaskStringUtils.FOLDER_DEFAULT) - || folderName.equals(GTaskStringUtils.FOLDER_CALL_NOTE)) - folder.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); - else - folder.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); - - js.put(GTaskStringUtils.META_HEAD_NOTE, folder); - - return js; - } catch (JSONException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - return null; - } - } - - /** - * 根据本地数据库游标判断同步操作类型 - * - * @param c 本地数据库游标,包含任务列表的本地存储信息 - * @return 同步操作类型: - * - SYNC_ACTION_NONE: 无需同步 - * - SYNC_ACTION_UPDATE_LOCAL: 更新本地数据 - * - SYNC_ACTION_UPDATE_REMOTE: 更新远程数据 - * - SYNC_ACTION_ERROR: 同步错误 - */ - public int getSyncAction(Cursor c) { - try { - if (c.getInt(SqlNote.LOCAL_MODIFIED_COLUMN) == 0) { - // there is no local update - if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { - // no update both side - return SYNC_ACTION_NONE; - } else { - // apply remote to local - return SYNC_ACTION_UPDATE_LOCAL; - } - } else { - // validate gtask id - if (!c.getString(SqlNote.GTASK_ID_COLUMN).equals(getGid())) { - Log.e(TAG, "gtask id doesn't match"); - return SYNC_ACTION_ERROR; - } - if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { - // local modification only - return SYNC_ACTION_UPDATE_REMOTE; - } else { - // for folder conflicts, just apply local modification - return SYNC_ACTION_UPDATE_REMOTE; - } - } - } catch (Exception e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - } - - return SYNC_ACTION_ERROR; - } - - /** - * 获取子任务数量 - * - * @return 子任务的总数 - */ - public int getChildTaskCount() { - return mChildren.size(); - } - - /** - * 添加子任务到任务列表末尾 - * - * @param task 要添加的子任务 - * @return 是否添加成功 - */ - public boolean addChildTask(Task task) { - boolean ret = false; - if (task != null && !mChildren.contains(task)) { - ret = mChildren.add(task); - if (ret) { - // need to set prior sibling and parent - task.setPriorSibling(mChildren.isEmpty() ? null : mChildren - .get(mChildren.size() - 1)); - task.setParent(this); - } - } - return ret; - } - - /** - * 在指定位置添加子任务 - * - * @param task 要添加的子任务 - * @param index 要插入的位置索引 - * @return 是否添加成功 - */ - public boolean addChildTask(Task task, int index) { - if (index < 0 || index > mChildren.size()) { - Log.e(TAG, "add child task: invalid index"); - return false; - } - - int pos = mChildren.indexOf(task); - if (task != null && pos == -1) { - mChildren.add(index, task); - - // update the task list - Task preTask = null; - Task afterTask = null; - if (index != 0) - preTask = mChildren.get(index - 1); - if (index != mChildren.size() - 1) - afterTask = mChildren.get(index + 1); - - task.setPriorSibling(preTask); - if (afterTask != null) - afterTask.setPriorSibling(task); - } - - return true; - } - - /** - * 从任务列表中移除子任务 - * - * @param task 要移除的子任务 - * @return 是否移除成功 - */ - public boolean removeChildTask(Task task) { - boolean ret = false; - int index = mChildren.indexOf(task); - if (index != -1) { - ret = mChildren.remove(task); - - if (ret) { - // reset prior sibling and parent - task.setPriorSibling(null); - task.setParent(null); - - // update the task list - if (index != mChildren.size()) { - mChildren.get(index).setPriorSibling( - index == 0 ? null : mChildren.get(index - 1)); - } - } - } - return ret; - } - - /** - * 移动子任务到指定位置 - * - * @param task 要移动的子任务 - * @param index 目标位置索引 - * @return 是否移动成功 - */ - public boolean moveChildTask(Task task, int index) { - - if (index < 0 || index >= mChildren.size()) { - Log.e(TAG, "move child task: invalid index"); - return false; - } - - int pos = mChildren.indexOf(task); - if (pos == -1) { - Log.e(TAG, "move child task: the task should in the list"); - return false; - } - - if (pos == index) - return true; - return (removeChildTask(task) && addChildTask(task, index)); - } - - /** - * 根据Gid查找子任务 - * - * @param gid 要查找的任务Gid - * @return 找到的Task对象,如果未找到则返回null - */ - public Task findChildTaskByGid(String gid) { - for (int i = 0; i < mChildren.size(); i++) { - Task t = mChildren.get(i); - if (t.getGid().equals(gid)) { - return t; - } - } - return null; - } - - /** - * 获取子任务在列表中的索引位置 - * - * @param task 要查找的子任务 - * @return 子任务在列表中的索引,未找到则返回-1 - */ - public int getChildTaskIndex(Task task) { - return mChildren.indexOf(task); - } - - /** - * 根据索引获取子任务 - * - * @param index 子任务的索引位置 - * @return 对应索引的Task对象,如果索引无效则返回null - */ - public Task getChildTaskByIndex(int index) { - if (index < 0 || index >= mChildren.size()) { - Log.e(TAG, "getTaskByIndex: invalid index"); - return null; - } - return mChildren.get(index); - } - - /** - * 根据Gid查找子任务(与findChildTaskByGid功能相同,为了兼容性保留) - * - * @param gid 要查找的任务Gid - * @return 找到的Task对象,如果未找到则返回null - */ - public Task getChilTaskByGid(String gid) { - for (Task task : mChildren) { - if (task.getGid().equals(gid)) - return task; - } - return null; - } - - /** - * 获取所有子任务的列表 - * - * @return 包含所有子任务的ArrayList - */ - public ArrayList getChildTaskList() { - return this.mChildren; - } - - /** - * 设置任务列表的索引位置 - * - * @param index 新的索引位置 - */ - public void setIndex(int index) { - this.mIndex = index; - } - - /** - * 获取任务列表的索引位置 - * - * @return 当前索引位置 - */ - public int getIndex() { - return this.mIndex; - } -} diff --git a/src/notes/gtask/exception/ActionFailureException.java b/src/notes/gtask/exception/ActionFailureException.java deleted file mode 100644 index 779c1ba..0000000 --- a/src/notes/gtask/exception/ActionFailureException.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.micode.notes.gtask.exception; - -/** - * ActionFailureException类 - 继承自RuntimeException,用于表示GTask同步操作失败的异常 - * - * 该异常主要在GTask同步过程中,当执行任务列表或任务的创建、更新、删除等操作失败时抛出 - * 作为运行时异常,可以由上层调用者选择性捕获,用于处理同步操作失败的情况 - * - * @author MiCode Open Source Community - */ -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); - } -} diff --git a/src/notes/gtask/exception/NetworkFailureException.java b/src/notes/gtask/exception/NetworkFailureException.java deleted file mode 100644 index 0b10634..0000000 --- a/src/notes/gtask/exception/NetworkFailureException.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.micode.notes.gtask.exception; - -/** - * NetworkFailureException类 - 继承自Exception,用于表示GTask同步过程中的网络故障异常 - * - * 该异常主要在GTask同步过程中,当与Google Tasks服务器通信时出现网络问题(如连接超时、网络不可用等)时抛出 - * 作为受检异常,必须由调用者显式捕获并处理,用于处理网络相关的错误情况 - * - * @author MiCode Open Source Community - */ -public class NetworkFailureException extends Exception { - private static final long serialVersionUID = 2107610287180234136L; - - /** - * 无参构造方法,创建一个没有详细消息和原因的NetworkFailureException - */ - public NetworkFailureException() { - super(); - } - - /** - * 构造方法,创建一个包含指定详细消息的NetworkFailureException - * - * @param paramString 详细错误消息,描述网络故障的原因 - */ - public NetworkFailureException(String paramString) { - super(paramString); - } - - /** - * 构造方法,创建一个包含指定详细消息和原因的NetworkFailureException - * - * @param paramString 详细错误消息,描述网络故障的原因 - * @param paramThrowable 导致当前异常的原因异常(如IOException等) - */ - public NetworkFailureException(String paramString, Throwable paramThrowable) { - super(paramString, paramThrowable); - } -} diff --git a/src/notes/gtask/remote/GTaskASyncTask.java b/src/notes/gtask/remote/GTaskASyncTask.java deleted file mode 100644 index 461cc41..0000000 --- a/src/notes/gtask/remote/GTaskASyncTask.java +++ /dev/null @@ -1,207 +0,0 @@ - -/* - * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.micode.notes.gtask.remote; - -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.os.AsyncTask; - -import net.micode.notes.R; -import net.micode.notes.ui.NotesListActivity; -import net.micode.notes.ui.NotesPreferenceActivity; - - -/** - * GTaskASyncTask类 - 继承自AsyncTask,用于在后台执行Google Tasks同步操作 - * - * 该类负责在后台线程中执行Google Tasks的同步工作,并在UI线程中更新同步进度和结果 - * 主要功能包括: - * - 在后台执行同步操作 - * - 显示同步进度通知 - * - 发送同步状态广播 - * - 处理同步完成后的回调 - * - 支持取消同步操作 - * - * @author MiCode Open Source Community - */ -public class GTaskASyncTask extends AsyncTask { - - /** - * 同步通知的唯一标识符 - */ - private static int GTASK_SYNC_NOTIFICATION_ID = 5234235; - - /** - * OnCompleteListener接口 - 同步完成事件的监听器 - * - * 该接口用于在同步操作完成后接收回调通知 - */ - public interface OnCompleteListener { - /** - * 同步完成时调用的方法 - */ - void onComplete(); - } - - private Context mContext; - - private NotificationManager mNotifiManager; - - 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(); - } - - /** - * 取消正在进行的同步操作 - * - * 该方法通过调用GTaskManager的cancelSync方法来取消同步 - */ - public void cancelSync() { - mTaskManager.cancelSync(); - } - - /** - * 发布同步进度消息 - * - * 该方法用于向UI线程发送同步进度更新 - * - * @param message 进度消息内容 - */ - public void publishProgess(String message) { - publishProgress(new String[] { - message - }); - } - - /** - * 显示同步通知 - * - * 该方法用于显示同步状态的通知消息,包括进度通知和结果通知 - * - * @param tickerId 通知标题的资源ID - * @param content 通知内容 - */ - private void showNotification(int tickerId, String content) { - // 1. 创建通知构建器(替代旧构造函数) - Notification.Builder builder = new Notification.Builder(mContext) - .setSmallIcon(R.drawable.notification) // 替代旧的第一个参数 - .setTicker(mContext.getString(tickerId)) // 替代旧的第二个参数 - .setWhen(System.currentTimeMillis()) // 替代旧的第三个参数 - .setDefaults(Notification.DEFAULT_LIGHTS) // 替代 notification.defaults - .setAutoCancel(true) // 替代 notification.flags = FLAG_AUTO_CANCEL - .setContentTitle(mContext.getString(R.string.app_name)) - .setContentText(content); - - // 2. 创建 PendingIntent - Intent intent; - if (tickerId != R.string.ticker_success) { - intent = new Intent(mContext, NotesPreferenceActivity.class); - } else { - intent = new Intent(mContext, NotesListActivity.class); - } - - // Android 12+ 需要 FLAG_IMMUTABLE 或 FLAG_MUTABLE - PendingIntent pendingIntent = PendingIntent.getActivity( - mContext, 0, intent, PendingIntent.FLAG_IMMUTABLE); - - // 3. 设置意图 - builder.setContentIntent(pendingIntent); - - // 4. 构建并发送通知 - Notification notification = builder.build(); - mNotifiManager.notify(GTASK_SYNC_NOTIFICATION_ID, notification); - } - - /** - * 在后台线程中执行同步操作 - * - * 该方法是AsyncTask的核心方法,在后台线程中执行Google Tasks的同步工作 - * - * @param unused 未使用的参数 - * @return 同步结果状态码,对应GTaskManager中的状态常量 - */ - @Override - protected Integer doInBackground(Void... unused) { - publishProgess(mContext.getString(R.string.sync_progress_login, NotesPreferenceActivity - .getSyncAccountName(mContext))); - return mTaskManager.sync(mContext, this); - } - - /** - * 在UI线程中更新同步进度 - * - * 当后台线程调用publishProgress方法时,该方法会在UI线程中被调用 - * - * @param progress 进度消息数组,包含最新的进度消息 - */ - @Override - protected void onProgressUpdate(String... progress) { - showNotification(R.string.ticker_syncing, progress[0]); - if (mContext instanceof GTaskSyncService) { - ((GTaskSyncService) mContext).sendBroadcast(progress[0]); - } - } - - /** - * 在UI线程中处理同步完成后的结果 - * - * 当后台线程的doInBackground方法执行完成后,该方法会在UI线程中被调用 - * - * @param result 同步结果状态码,对应GTaskManager中的状态常量 - */ - @Override - protected void onPostExecute(Integer result) { - if (result == GTaskManager.STATE_SUCCESS) { - showNotification(R.string.ticker_success, mContext.getString( - R.string.success_sync_account, mTaskManager.getSyncAccount())); - NotesPreferenceActivity.setLastSyncTime(mContext, System.currentTimeMillis()); - } else if (result == GTaskManager.STATE_NETWORK_ERROR) { - showNotification(R.string.ticker_fail, mContext.getString(R.string.error_sync_network)); - } else if (result == GTaskManager.STATE_INTERNAL_ERROR) { - showNotification(R.string.ticker_fail, mContext.getString(R.string.error_sync_internal)); - } else if (result == GTaskManager.STATE_SYNC_CANCELLED) { - showNotification(R.string.ticker_cancel, mContext - .getString(R.string.error_sync_cancelled)); - } - if (mOnCompleteListener != null) { - new Thread(new Runnable() { - - public void run() { - mOnCompleteListener.onComplete(); - } - }).start(); - } - } -} diff --git a/src/notes/gtask/remote/GTaskClient.java b/src/notes/gtask/remote/GTaskClient.java deleted file mode 100644 index 71b180c..0000000 --- a/src/notes/gtask/remote/GTaskClient.java +++ /dev/null @@ -1,694 +0,0 @@ -/* - * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.micode.notes.gtask.remote; - -import android.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服务器通信的核心客户端类,使用单例模式实现 - * - * 该类负责与Google Tasks服务器进行通信,处理用户认证、任务列表和任务的创建、更新、删除等操作 - * 封装了HTTP请求和响应处理,JSON数据解析,以及与Google Tasks API的交互逻辑 - * - * @author MiCode Open Source Community - */ -public class GTaskClient { - private static final String TAG = GTaskClient.class.getSimpleName(); - - private static final String GTASK_URL = "https://mail.google.com/tasks/"; - - private static final String GTASK_GET_URL = "https://mail.google.com/tasks/ig"; - - private static final String GTASK_POST_URL = "https://mail.google.com/tasks/r/ig"; - - private static GTaskClient mInstance = null; - - private DefaultHttpClient mHttpClient; - - private String mGetUrl; - - private String mPostUrl; - - private long mClientVersion; - - private boolean mLoggedin; - - private long mLastLoginTime; - - private int mActionId; - - private Account mAccount; - - private JSONArray mUpdateArray; - - private GTaskClient() { - mHttpClient = null; - mGetUrl = GTASK_GET_URL; - mPostUrl = GTASK_POST_URL; - mClientVersion = -1; - mLoggedin = false; - mLastLoginTime = 0; - mActionId = 1; - mAccount = null; - mUpdateArray = null; - } - - /** - * 获取GTaskClient的单例实例 - * - * 该方法采用同步机制,确保在多线程环境下只有一个GTaskClient实例 - * 单例模式保证了全局只有一个客户端实例,避免了重复创建连接和资源浪费 - * - * @return GTaskClient的单例实例 - */ - public static synchronized GTaskClient getInstance() { - if (mInstance == null) { - mInstance = new GTaskClient(); - } - return mInstance; - } - - /** - * 登录Google Tasks服务 - * - * 该方法负责处理用户认证,包括获取Google账户令牌、设置认证cookie等 - * 支持自定义域名和官方域名登录尝试,确保在各种环境下都能成功登录 - * 登录成功后会记录客户端版本和登录时间 - * - * @param activity 上下文Activity,用于获取账户管理器 - * @return 登录成功返回true,否则返回false - */ - public boolean login(Activity activity) { - // we suppose that the cookie would expire after 5 minutes - // then we need to re-login - final long interval = 1000 * 60 * 5; - if (mLastLoginTime + interval < System.currentTimeMillis()) { - mLoggedin = false; - } - - // need to re-login after account switch - if (mLoggedin - && !TextUtils.equals(getSyncAccount().name, NotesPreferenceActivity - .getSyncAccountName(activity))) { - mLoggedin = false; - } - - if (mLoggedin) { - Log.d(TAG, "already logged in"); - return true; - } - - mLastLoginTime = System.currentTimeMillis(); - String authToken = loginGoogleAccount(activity, false); - if (authToken == null) { - Log.e(TAG, "login google account failed"); - return false; - } - - // login with custom domain if necessary - if (!(mAccount.name.toLowerCase().endsWith("gmail.com") || mAccount.name.toLowerCase() - .endsWith("googlemail.com"))) { - StringBuilder url = new StringBuilder(GTASK_URL).append("a/"); - int index = mAccount.name.indexOf('@') + 1; - String suffix = mAccount.name.substring(index); - url.append(suffix + "/"); - mGetUrl = url.toString() + "ig"; - mPostUrl = url.toString() + "r/ig"; - - if (tryToLoginGtask(activity, authToken)) { - mLoggedin = true; - } - } - - // try to login with google official url - if (!mLoggedin) { - mGetUrl = GTASK_GET_URL; - mPostUrl = GTASK_POST_URL; - if (!tryToLoginGtask(activity, authToken)) { - return false; - } - } - - mLoggedin = true; - return true; - } - - private String loginGoogleAccount(Activity activity, boolean invalidateToken) { - String authToken; - AccountManager accountManager = AccountManager.get(activity); - Account[] accounts = accountManager.getAccountsByType("com.google"); - - if (accounts.length == 0) { - Log.e(TAG, "there is no available google account"); - return null; - } - - String accountName = NotesPreferenceActivity.getSyncAccountName(activity); - Account account = null; - for (Account a : accounts) { - if (a.name.equals(accountName)) { - account = a; - break; - } - } - if (account != null) { - mAccount = account; - } else { - Log.e(TAG, "unable to get an account with the same name in the settings"); - return null; - } - - // get the token now - AccountManagerFuture accountManagerFuture = accountManager.getAuthToken(account, - "goanna_mobile", null, activity, null, null); - try { - Bundle authTokenBundle = accountManagerFuture.getResult(); - authToken = authTokenBundle.getString(AccountManager.KEY_AUTHTOKEN); - if (invalidateToken) { - accountManager.invalidateAuthToken("com.google", authToken); - loginGoogleAccount(activity, false); - } - } catch (Exception e) { - Log.e(TAG, "get auth token failed"); - authToken = null; - } - - return authToken; - } - - private boolean tryToLoginGtask(Activity activity, String authToken) { - if (!loginGtask(authToken)) { - // maybe the auth token is out of date, now let's invalidate the - // token and try again - authToken = loginGoogleAccount(activity, true); - if (authToken == null) { - Log.e(TAG, "login google account failed"); - return false; - } - - if (!loginGtask(authToken)) { - Log.e(TAG, "login gtask failed"); - return false; - } - } - return true; - } - - private boolean loginGtask(String authToken) { - int timeoutConnection = 10000; - int timeoutSocket = 15000; - HttpParams httpParameters = new BasicHttpParams(); - HttpConnectionParams.setConnectionTimeout(httpParameters, timeoutConnection); - HttpConnectionParams.setSoTimeout(httpParameters, timeoutSocket); - mHttpClient = new DefaultHttpClient(httpParameters); - BasicCookieStore localBasicCookieStore = new BasicCookieStore(); - mHttpClient.setCookieStore(localBasicCookieStore); - HttpProtocolParams.setUseExpectContinue(mHttpClient.getParams(), false); - - // login gtask - try { - String loginUrl = mGetUrl + "?auth=" + authToken; - HttpGet httpGet = new HttpGet(loginUrl); - HttpResponse response = null; - response = mHttpClient.execute(httpGet); - - // get the cookie now - List cookies = mHttpClient.getCookieStore().getCookies(); - boolean hasAuthCookie = false; - for (Cookie cookie : cookies) { - if (cookie.getName().contains("GTL")) { - hasAuthCookie = true; - } - } - if (!hasAuthCookie) { - Log.w(TAG, "it seems that there is no auth cookie"); - } - - // get the client version - String resString = getResponseContent(response.getEntity()); - String jsBegin = "_setup("; - String jsEnd = ")}"; - int begin = resString.indexOf(jsBegin); - int end = resString.lastIndexOf(jsEnd); - String jsString = null; - if (begin != -1 && end != -1 && begin < end) { - jsString = resString.substring(begin + jsBegin.length(), end); - } - JSONObject js = new JSONObject(jsString); - mClientVersion = js.getLong("v"); - } catch (JSONException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - return false; - } catch (Exception e) { - // simply catch all exceptions - Log.e(TAG, "httpget gtask_url failed"); - return false; - } - - return true; - } - - private int getActionId() { - return mActionId++; - } - - private HttpPost createHttpPost() { - HttpPost httpPost = new HttpPost(mPostUrl); - httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8"); - httpPost.setHeader("AT", "1"); - return httpPost; - } - - private String getResponseContent(HttpEntity entity) throws IOException { - String contentEncoding = null; - if (entity.getContentEncoding() != null) { - contentEncoding = entity.getContentEncoding().getValue(); - Log.d(TAG, "encoding: " + contentEncoding); - } - - try (InputStream input = entity.getContent(); - InputStream finalInput = (contentEncoding != null && contentEncoding.equalsIgnoreCase("gzip")) - ? new GZIPInputStream(input) - : (contentEncoding != null && contentEncoding.equalsIgnoreCase("deflate")) - ? new InflaterInputStream(input, new Inflater(true)) - : input; - InputStreamReader isr = new InputStreamReader(finalInput); - BufferedReader br = new BufferedReader(isr)) { - - StringBuilder sb = new StringBuilder(); - String buff; - while ((buff = br.readLine()) != null) { - sb.append(buff); - } - return sb.toString(); - } - } - - private JSONObject postRequest(JSONObject js) throws NetworkFailureException { - if (!mLoggedin) { - Log.e(TAG, "please login first"); - throw new ActionFailureException("not logged in"); - } - - HttpPost httpPost = createHttpPost(); - try { - LinkedList list = new LinkedList(); - list.add(new BasicNameValuePair("r", js.toString())); - UrlEncodedFormEntity entity = new UrlEncodedFormEntity(list, "UTF-8"); - httpPost.setEntity(entity); - - // execute the post - HttpResponse response = mHttpClient.execute(httpPost); - String jsString = getResponseContent(response.getEntity()); - return new JSONObject(jsString); - - } catch (ClientProtocolException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new NetworkFailureException("postRequest failed"); - } catch (IOException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new NetworkFailureException("postRequest failed"); - } catch (JSONException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new ActionFailureException("unable to convert response content to jsonobject"); - } catch (Exception e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new ActionFailureException("error occurs when posting request"); - } - } - - /** - * 创建Google Tasks任务 - * - * 该方法将本地任务对象同步到Google Tasks服务器,创建新的任务 - * 创建成功后会从服务器返回的响应中获取并设置任务的全局唯一ID(GID) - * - * @param task 要创建的任务对象,包含任务的各种属性 - * @throws NetworkFailureException 网络通信失败时抛出此异常 - * @throws ActionFailureException 任务创建操作失败时抛出此异常 - */ - public void createTask(Task task) throws NetworkFailureException { - commitUpdate(); - try { - JSONObject jsPost = new JSONObject(); - JSONArray actionList = new JSONArray(); - - // action_list - actionList.put(task.getCreateAction(getActionId())); - jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); - - // client_version - jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); - - // post - JSONObject jsResponse = postRequest(jsPost); - JSONObject jsResult = (JSONObject) jsResponse.getJSONArray( - GTaskStringUtils.GTASK_JSON_RESULTS).get(0); - task.setGid(jsResult.getString(GTaskStringUtils.GTASK_JSON_NEW_ID)); - - } catch (JSONException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new ActionFailureException("create task: handing jsonobject failed"); - } - } - - /** - * 创建Google Tasks任务列表 - * - * 该方法将本地任务列表对象同步到Google Tasks服务器,创建新的任务列表 - * 创建成功后会从服务器返回的响应中获取并设置任务列表的全局唯一ID(GID) - * - * @param tasklist 要创建的任务列表对象,包含任务列表的各种属性 - * @throws NetworkFailureException 网络通信失败时抛出此异常 - * @throws ActionFailureException 任务列表创建操作失败时抛出此异常 - */ - public void createTaskList(TaskList tasklist) throws NetworkFailureException { - commitUpdate(); - try { - JSONObject jsPost = new JSONObject(); - JSONArray actionList = new JSONArray(); - - // action_list - actionList.put(tasklist.getCreateAction(getActionId())); - jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); - - // client version - jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); - - // post - JSONObject jsResponse = postRequest(jsPost); - JSONObject jsResult = (JSONObject) jsResponse.getJSONArray( - GTaskStringUtils.GTASK_JSON_RESULTS).get(0); - tasklist.setGid(jsResult.getString(GTaskStringUtils.GTASK_JSON_NEW_ID)); - - } catch (JSONException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new ActionFailureException("create tasklist: handing jsonobject failed"); - } - } - - /** - * 提交所有待更新的节点到服务器 - * - * 该方法将缓存的更新操作批量发送到Google Tasks服务器,实现批量更新 - * 主要用于优化性能,减少网络请求次数 - * - * @throws NetworkFailureException 网络通信失败时抛出此异常 - * @throws ActionFailureException 更新提交操作失败时抛出此异常 - */ - public void commitUpdate() throws NetworkFailureException { - if (mUpdateArray != null) { - try { - JSONObject jsPost = new JSONObject(); - - // action_list - jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, mUpdateArray); - - // client_version - jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); - - postRequest(jsPost); - mUpdateArray = null; - } catch (JSONException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new ActionFailureException("commit update: handing jsonobject failed"); - } - } - } - - /** - * 添加待更新的节点到缓存中 - * - * 该方法将需要更新的节点添加到缓存数组中,当缓存达到一定数量(10个)时自动提交 - * 用于实现批量更新,减少网络请求次数 - * - * @param node 要更新的节点对象 - * @throws NetworkFailureException 网络通信失败时抛出此异常 - * @throws ActionFailureException 添加节点操作失败时抛出此异常 - */ - public void addUpdateNode(Node node) throws NetworkFailureException { - if (node != null) { - // too many update items may result in an error - // set max to 10 items - if (mUpdateArray != null && mUpdateArray.length() > 10) { - commitUpdate(); - } - - if (mUpdateArray == null) - mUpdateArray = new JSONArray(); - mUpdateArray.put(node.getUpdateAction(getActionId())); - } - } - - /** - * 移动任务到指定的任务列表或同一列表内的新位置 - * - * 该方法支持两种移动操作: - * 1. 在同一任务列表内移动任务(调整任务顺序) - * 2. 在不同任务列表之间移动任务 - * - * @param task 要移动的任务对象 - * @param preParent 移动前的父任务列表 - * @param curParent 移动后的父任务列表 - * @throws NetworkFailureException 网络通信失败时抛出此异常 - * @throws ActionFailureException 任务移动操作失败时抛出此异常 - */ - public void moveTask(Task task, TaskList preParent, TaskList curParent) - throws NetworkFailureException { - commitUpdate(); - try { - JSONObject jsPost = new JSONObject(); - JSONArray actionList = new JSONArray(); - JSONObject action = new JSONObject(); - - // action_list - action.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, - GTaskStringUtils.GTASK_JSON_ACTION_TYPE_MOVE); - action.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, getActionId()); - action.put(GTaskStringUtils.GTASK_JSON_ID, task.getGid()); - if (preParent == curParent && task.getPriorSibling() != null) { - // put prioring_sibing_id only if moving within the tasklist and - // it is not the first one - action.put(GTaskStringUtils.GTASK_JSON_PRIOR_SIBLING_ID, task.getPriorSibling()); - } - action.put(GTaskStringUtils.GTASK_JSON_SOURCE_LIST, preParent.getGid()); - action.put(GTaskStringUtils.GTASK_JSON_DEST_PARENT, curParent.getGid()); - if (preParent != curParent) { - // put the dest_list only if moving between tasklists - action.put(GTaskStringUtils.GTASK_JSON_DEST_LIST, curParent.getGid()); - } - actionList.put(action); - jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); - - // client_version - jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); - - postRequest(jsPost); - - } catch (JSONException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new ActionFailureException("move task: handing jsonobject failed"); - } - } - - /** - * 删除指定的节点(任务或任务列表) - * - * 该方法将节点标记为已删除,并发送删除请求到Google Tasks服务器 - * - * @param node 要删除的节点对象 - * @throws NetworkFailureException 网络通信失败时抛出此异常 - * @throws ActionFailureException 节点删除操作失败时抛出此异常 - */ - public void deleteNode(Node node) throws NetworkFailureException { - commitUpdate(); - try { - JSONObject jsPost = new JSONObject(); - JSONArray actionList = new JSONArray(); - - // action_list - node.setDeleted(true); - actionList.put(node.getUpdateAction(getActionId())); - jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); - - // client_version - jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); - - postRequest(jsPost); - mUpdateArray = null; - } catch (JSONException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new ActionFailureException("delete node: handing jsonobject failed"); - } - } - - /** - * 获取所有任务列表 - * - * 该方法从Google Tasks服务器获取用户的所有任务列表 - * - * @return 包含所有任务列表信息的JSONArray - * @throws NetworkFailureException 网络通信失败时抛出此异常 - * @throws ActionFailureException 获取任务列表操作失败时抛出此异常 - */ - public JSONArray getTaskLists() throws NetworkFailureException { - if (!mLoggedin) { - Log.e(TAG, "please login first"); - throw new ActionFailureException("not logged in"); - } - - try { - HttpGet httpGet = new HttpGet(mGetUrl); - HttpResponse response = null; - response = mHttpClient.execute(httpGet); - - // get the task list - String resString = getResponseContent(response.getEntity()); - String jsBegin = "_setup("; - String jsEnd = ")}"; - int begin = resString.indexOf(jsBegin); - int end = resString.lastIndexOf(jsEnd); - String jsString = null; - if (begin != -1 && end != -1 && begin < end) { - jsString = resString.substring(begin + jsBegin.length(), end); - } - JSONObject js = new JSONObject(jsString); - return js.getJSONObject("t").getJSONArray(GTaskStringUtils.GTASK_JSON_LISTS); - } catch (ClientProtocolException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new NetworkFailureException("gettasklists: httpget failed"); - } catch (IOException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new NetworkFailureException("gettasklists: httpget failed"); - } catch (JSONException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new ActionFailureException("get task lists: handing jasonobject failed"); - } - } - - /** - * 获取指定任务列表的所有任务 - * - * 该方法从Google Tasks服务器获取指定任务列表中的所有任务 - * - * @param listGid 任务列表的全局唯一ID - * @return 包含指定任务列表所有任务信息的JSONArray - * @throws NetworkFailureException 网络通信失败时抛出此异常 - * @throws ActionFailureException 获取任务列表内容操作失败时抛出此异常 - */ - public JSONArray getTaskList(String listGid) throws NetworkFailureException { - commitUpdate(); - try { - JSONObject jsPost = new JSONObject(); - JSONArray actionList = new JSONArray(); - JSONObject action = new JSONObject(); - - // action_list - action.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, - GTaskStringUtils.GTASK_JSON_ACTION_TYPE_GETALL); - action.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, getActionId()); - action.put(GTaskStringUtils.GTASK_JSON_LIST_ID, listGid); - action.put(GTaskStringUtils.GTASK_JSON_GET_DELETED, false); - actionList.put(action); - jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); - - // client_version - jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); - - JSONObject jsResponse = postRequest(jsPost); - return jsResponse.getJSONArray(GTaskStringUtils.GTASK_JSON_TASKS); - } catch (JSONException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new ActionFailureException("get task list: handing jsonobject failed"); - } - } - - /** - * 获取当前同步使用的Google账户 - * - * @return 当前同步使用的Google账户对象 - */ - public Account getSyncAccount() { - return mAccount; - } - - /** - * 重置更新数组,清空所有待更新的节点 - * - * 该方法用于取消所有未提交的更新操作 - */ - public void resetUpdateArray() { - mUpdateArray = null; - } -} diff --git a/src/notes/gtask/remote/GTaskManager.java b/src/notes/gtask/remote/GTaskManager.java deleted file mode 100644 index 0cced43..0000000 --- a/src/notes/gtask/remote/GTaskManager.java +++ /dev/null @@ -1,871 +0,0 @@ -/* - * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.micode.notes.gtask.remote; - -import android.app.Activity; -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.util.Log; - -import net.micode.notes.R; -import net.micode.notes.data.Notes; -import net.micode.notes.data.Notes.DataColumns; -import net.micode.notes.data.Notes.NoteColumns; -import net.micode.notes.gtask.data.MetaData; -import net.micode.notes.gtask.data.Node; -import net.micode.notes.gtask.data.SqlNote; -import net.micode.notes.gtask.data.Task; -import net.micode.notes.gtask.data.TaskList; -import net.micode.notes.gtask.exception.ActionFailureException; -import net.micode.notes.gtask.exception.NetworkFailureException; -import net.micode.notes.tool.DataUtils; -import net.micode.notes.tool.GTaskStringUtils; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Map; - - -/** - * GTaskManager类 - Google Tasks同步管理的核心类,使用单例模式实现 - * - * 该类负责协调本地笔记数据与Google Tasks服务器之间的同步工作 - * 主要功能包括: - * - 管理同步生命周期和状态 - * - 协调本地与远程数据的增删改查操作 - * - 处理同步冲突和错误 - * - 维护本地与远程数据的映射关系 - * - 管理元数据和同步状态 - * - * @author MiCode Open Source Community - */ -public class GTaskManager { - private static final String TAG = GTaskManager.class.getSimpleName(); - - /** - * 同步状态:成功完成 - */ - public static final int STATE_SUCCESS = 0; - - /** - * 同步状态:网络错误 - */ - public static final int STATE_NETWORK_ERROR = 1; - - /** - * 同步状态:内部错误 - */ - public static final int STATE_INTERNAL_ERROR = 2; - - /** - * 同步状态:同步进行中 - */ - public static final int STATE_SYNC_IN_PROGRESS = 3; - - /** - * 同步状态:同步已取消 - */ - public static final int STATE_SYNC_CANCELLED = 4; - - private static GTaskManager mInstance = null; - - private Activity mActivity; - - private Context mContext; - - private ContentResolver mContentResolver; - - private boolean mSyncing; - - private boolean mCancelled; - - private HashMap mGTaskListHashMap; - - private HashMap mGTaskHashMap; - - private HashMap mMetaHashMap; - - private TaskList mMetaList; - - private HashSet mLocalDeleteIdMap; - - private HashMap mGidToNid; - - 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的单例实例 - * - * 该方法采用同步机制,确保在多线程环境下只有一个GTaskManager实例 - * 单例模式保证了全局只有一个同步管理器实例,避免了重复创建和资源浪费 - * - * @return GTaskManager的单例实例 - */ - public static synchronized GTaskManager getInstance() { - if (mInstance == null) { - mInstance = new GTaskManager(); - } - return mInstance; - } - - /** - * 设置Activity上下文 - * - * 该方法用于设置获取Google认证令牌所需的Activity上下文 - * - * @param activity 上下文Activity,用于获取账户管理器和认证令牌 - */ - public synchronized void setActivityContext(Activity activity) { - // used for getting authtoken - mActivity = activity; - } - - /** - * 执行Google Tasks同步操作 - * - * 该方法是同步的入口点,负责协调整个同步流程: - * 1. 登录Google Tasks服务 - * 2. 初始化任务列表 - * 3. 执行内容同步 - * 4. 处理同步结果和错误 - * - * @param context 上下文对象,用于获取内容解析器 - * @param asyncTask 异步任务对象,用于发布同步进度 - * @return 同步状态码: - * - STATE_SUCCESS:同步成功 - * - STATE_NETWORK_ERROR:网络错误 - * - STATE_INTERNAL_ERROR:内部错误 - * - STATE_SYNC_IN_PROGRESS:同步已在进行中 - * - STATE_SYNC_CANCELLED:同步已取消 - */ - public int sync(Context context, GTaskASyncTask asyncTask) { - if (mSyncing) { - Log.d(TAG, "Sync is in progress"); - return STATE_SYNC_IN_PROGRESS; - } - mContext = context; - mContentResolver = mContext.getContentResolver(); - mSyncing = true; - mCancelled = false; - mGTaskListHashMap.clear(); - mGTaskHashMap.clear(); - mMetaHashMap.clear(); - mLocalDeleteIdMap.clear(); - mGidToNid.clear(); - mNidToGid.clear(); - - try { - GTaskClient client = GTaskClient.getInstance(); - client.resetUpdateArray(); - - // login google task - if (!mCancelled) { - if (!client.login(mActivity)) { - throw new NetworkFailureException("login google task failed"); - } - } - - // get the task list from google - asyncTask.publishProgess(mContext.getString(R.string.sync_progress_init_list)); - initGTaskList(); - - // do content sync work - asyncTask.publishProgess(mContext.getString(R.string.sync_progress_syncing)); - syncContent(); - } catch (NetworkFailureException e) { - Log.e(TAG, e.toString()); - return STATE_NETWORK_ERROR; - } catch (ActionFailureException e) { - Log.e(TAG, e.toString()); - return STATE_INTERNAL_ERROR; - } catch (Exception e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - return STATE_INTERNAL_ERROR; - } finally { - mGTaskListHashMap.clear(); - mGTaskHashMap.clear(); - mMetaHashMap.clear(); - mLocalDeleteIdMap.clear(); - mGidToNid.clear(); - mNidToGid.clear(); - mSyncing = false; - } - - return mCancelled ? STATE_SYNC_CANCELLED : STATE_SUCCESS; - } - - private void initGTaskList() throws NetworkFailureException { - if (mCancelled) - return; - GTaskClient client = GTaskClient.getInstance(); - try { - JSONArray jsTaskLists = client.getTaskLists(); - - // init meta list first - mMetaList = null; - for (int i = 0; i < jsTaskLists.length(); i++) { - JSONObject object = jsTaskLists.getJSONObject(i); - String gid = object.getString(GTaskStringUtils.GTASK_JSON_ID); - String name = object.getString(GTaskStringUtils.GTASK_JSON_NAME); - - if (name - .equals(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_META)) { - mMetaList = new TaskList(); - mMetaList.setContentByRemoteJSON(object); - - // load meta data - JSONArray jsMetas = client.getTaskList(gid); - for (int j = 0; j < jsMetas.length(); j++) { - object = (JSONObject) jsMetas.getJSONObject(j); - MetaData metaData = new MetaData(); - metaData.setContentByRemoteJSON(object); - if (metaData.isWorthSaving()) { - mMetaList.addChildTask(metaData); - if (metaData.getGid() != null) { - mMetaHashMap.put(metaData.getRelatedGid(), metaData); - } - } - } - } - } - - // create meta list if not existed - if (mMetaList == null) { - mMetaList = new TaskList(); - mMetaList.setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX - + GTaskStringUtils.FOLDER_META); - GTaskClient.getInstance().createTaskList(mMetaList); - } - - // init task list - for (int i = 0; i < jsTaskLists.length(); i++) { - JSONObject object = jsTaskLists.getJSONObject(i); - String gid = object.getString(GTaskStringUtils.GTASK_JSON_ID); - String name = object.getString(GTaskStringUtils.GTASK_JSON_NAME); - - if (name.startsWith(GTaskStringUtils.MIUI_FOLDER_PREFFIX) - && !name.equals(GTaskStringUtils.MIUI_FOLDER_PREFFIX - + GTaskStringUtils.FOLDER_META)) { - TaskList tasklist = new TaskList(); - tasklist.setContentByRemoteJSON(object); - mGTaskListHashMap.put(gid, tasklist); - mGTaskHashMap.put(gid, tasklist); - - // load tasks - JSONArray jsTasks = client.getTaskList(gid); - for (int j = 0; j < jsTasks.length(); j++) { - object = (JSONObject) jsTasks.getJSONObject(j); - gid = object.getString(GTaskStringUtils.GTASK_JSON_ID); - Task task = new Task(); - task.setContentByRemoteJSON(object); - if (task.isWorthSaving()) { - task.setMetaInfo(mMetaHashMap.get(gid)); - tasklist.addChildTask(task); - mGTaskHashMap.put(gid, task); - } - } - } - } - } catch (JSONException e) { - Log.e(TAG, e.toString()); - e.printStackTrace(); - throw new ActionFailureException("initGTaskList: handing JSONObject failed"); - } - } - - private void syncContent() throws NetworkFailureException { - int syncType; - Cursor c = null; - String gid; - Node node; - - mLocalDeleteIdMap.clear(); - - if (mCancelled) { - return; - } - - // for local deleted note - try { - c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, - "(type<>? AND parent_id=?)", new String[] { - String.valueOf(Notes.TYPE_SYSTEM), String.valueOf(Notes.ID_TRASH_FOLDER) - }, null); - if (c != null) { - while (c.moveToNext()) { - gid = c.getString(SqlNote.GTASK_ID_COLUMN); - node = mGTaskHashMap.get(gid); - if (node != null) { - mGTaskHashMap.remove(gid); - doContentSync(Node.SYNC_ACTION_DEL_REMOTE, node, c); - } - - mLocalDeleteIdMap.add(c.getLong(SqlNote.ID_COLUMN)); - } - } else { - Log.w(TAG, "failed to query trash folder"); - } - } finally { - if (c != null) { - c.close(); - c = null; - } - } - - // sync folder first - syncFolder(); - - // for note existing in database - try { - c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, - "(type=? AND parent_id<>?)", new String[] { - String.valueOf(Notes.TYPE_NOTE), String.valueOf(Notes.ID_TRASH_FOLDER) - }, NoteColumns.TYPE + " DESC"); - if (c != null) { - while (c.moveToNext()) { - gid = c.getString(SqlNote.GTASK_ID_COLUMN); - node = mGTaskHashMap.get(gid); - if (node != null) { - mGTaskHashMap.remove(gid); - mGidToNid.put(gid, c.getLong(SqlNote.ID_COLUMN)); - mNidToGid.put(c.getLong(SqlNote.ID_COLUMN), gid); - syncType = node.getSyncAction(c); - } else { - if (c.getString(SqlNote.GTASK_ID_COLUMN).trim().length() == 0) { - // local add - syncType = Node.SYNC_ACTION_ADD_REMOTE; - } else { - // remote delete - syncType = Node.SYNC_ACTION_DEL_LOCAL; - } - } - doContentSync(syncType, node, c); - } - } else { - Log.w(TAG, "failed to query existing note in database"); - } - - } finally { - if (c != null) { - c.close(); - c = null; - } - } - - // go through remaining items - Iterator> iter = mGTaskHashMap.entrySet().iterator(); - while (iter.hasNext()) { - Map.Entry entry = iter.next(); - node = entry.getValue(); - doContentSync(Node.SYNC_ACTION_ADD_LOCAL, node, null); - } - - // mCancelled can be set by another thread, so we neet to check one by - // one - // clear local delete table - if (!mCancelled) { - if (!DataUtils.batchDeleteNotes(mContentResolver, mLocalDeleteIdMap)) { - throw new ActionFailureException("failed to batch-delete local deleted notes"); - } - } - - // refresh local sync id - if (!mCancelled) { - GTaskClient.getInstance().commitUpdate(); - refreshLocalSyncId(); - } - - } - - private void syncFolder() throws NetworkFailureException { - Cursor c = null; - String gid; - Node node; - int syncType; - - if (mCancelled) { - return; - } - - // for root folder - try { - c = mContentResolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, - Notes.ID_ROOT_FOLDER), SqlNote.PROJECTION_NOTE, null, null, null); - if (c != null) { - c.moveToNext(); - gid = c.getString(SqlNote.GTASK_ID_COLUMN); - node = mGTaskHashMap.get(gid); - if (node != null) { - mGTaskHashMap.remove(gid); - mGidToNid.put(gid, (long) Notes.ID_ROOT_FOLDER); - mNidToGid.put((long) Notes.ID_ROOT_FOLDER, gid); - // for system folder, only update remote name if necessary - if (!node.getName().equals( - GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT)) - doContentSync(Node.SYNC_ACTION_UPDATE_REMOTE, node, c); - } else { - doContentSync(Node.SYNC_ACTION_ADD_REMOTE, node, c); - } - } else { - Log.w(TAG, "failed to query root folder"); - } - } finally { - if (c != null) { - c.close(); - c = null; - } - } - - // for call-note folder - try { - c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, "(_id=?)", - new String[] { - String.valueOf(Notes.ID_CALL_RECORD_FOLDER) - }, null); - if (c != null) { - if (c.moveToNext()) { - gid = c.getString(SqlNote.GTASK_ID_COLUMN); - node = mGTaskHashMap.get(gid); - if (node != null) { - mGTaskHashMap.remove(gid); - mGidToNid.put(gid, (long) Notes.ID_CALL_RECORD_FOLDER); - mNidToGid.put((long) Notes.ID_CALL_RECORD_FOLDER, gid); - // for system folder, only update remote name if - // necessary - if (!node.getName().equals( - GTaskStringUtils.MIUI_FOLDER_PREFFIX - + GTaskStringUtils.FOLDER_CALL_NOTE)) - doContentSync(Node.SYNC_ACTION_UPDATE_REMOTE, node, c); - } else { - doContentSync(Node.SYNC_ACTION_ADD_REMOTE, node, c); - } - } - } else { - Log.w(TAG, "failed to query call note folder"); - } - } finally { - if (c != null) { - c.close(); - c = null; - } - } - - // for local existing folders - try { - c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, - "(type=? AND parent_id<>?)", new String[] { - String.valueOf(Notes.TYPE_FOLDER), String.valueOf(Notes.ID_TRASH_FOLDER) - }, NoteColumns.TYPE + " DESC"); - if (c != null) { - while (c.moveToNext()) { - gid = c.getString(SqlNote.GTASK_ID_COLUMN); - node = mGTaskHashMap.get(gid); - if (node != null) { - mGTaskHashMap.remove(gid); - mGidToNid.put(gid, c.getLong(SqlNote.ID_COLUMN)); - mNidToGid.put(c.getLong(SqlNote.ID_COLUMN), gid); - syncType = node.getSyncAction(c); - } else { - if (c.getString(SqlNote.GTASK_ID_COLUMN).trim().length() == 0) { - // local add - syncType = Node.SYNC_ACTION_ADD_REMOTE; - } else { - // remote delete - syncType = Node.SYNC_ACTION_DEL_LOCAL; - } - } - doContentSync(syncType, node, c); - } - } else { - Log.w(TAG, "failed to query existing folder"); - } - } finally { - if (c != null) { - c.close(); - c = null; - } - } - - // for remote add folders - Iterator> 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: - // merging both modifications maybe a good idea - // right now just use local update simply - updateRemoteNode(node, c); - break; - case Node.SYNC_ACTION_NONE: - break; - case Node.SYNC_ACTION_ERROR: - default: - throw new ActionFailureException("unkown sync action type"); - } - } - - private void addLocalNode(Node node) throws NetworkFailureException { - if (mCancelled) { - return; - } - - SqlNote sqlNote; - if (node instanceof TaskList) { - if (node.getName().equals( - GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT)) { - sqlNote = new SqlNote(mContext, Notes.ID_ROOT_FOLDER); - } else if (node.getName().equals( - GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_CALL_NOTE)) { - sqlNote = new SqlNote(mContext, Notes.ID_CALL_RECORD_FOLDER); - } else { - sqlNote = new SqlNote(mContext); - sqlNote.setContent(node.getLocalJSONFromContent()); - sqlNote.setParentId(Notes.ID_ROOT_FOLDER); - } - } else { - sqlNote = new SqlNote(mContext); - JSONObject js = node.getLocalJSONFromContent(); - try { - if (js.has(GTaskStringUtils.META_HEAD_NOTE)) { - JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); - if (note.has(NoteColumns.ID)) { - long id = note.getLong(NoteColumns.ID); - if (DataUtils.existInNoteDatabase(mContentResolver, id)) { - // the id is not available, have to create a new one - note.remove(NoteColumns.ID); - } - } - } - - if (js.has(GTaskStringUtils.META_HEAD_DATA)) { - JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA); - for (int i = 0; i < dataArray.length(); i++) { - JSONObject data = dataArray.getJSONObject(i); - if (data.has(DataColumns.ID)) { - long dataId = data.getLong(DataColumns.ID); - if (DataUtils.existInDataDatabase(mContentResolver, dataId)) { - // the data id is not available, have to create - // a new one - data.remove(DataColumns.ID); - } - } - } - - } - } catch (JSONException e) { - Log.w(TAG, e.toString()); - e.printStackTrace(); - } - sqlNote.setContent(js); - - Long parentId = mGidToNid.get(((Task) node).getParent().getGid()); - if (parentId == null) { - Log.e(TAG, "cannot find task's parent id locally"); - throw new ActionFailureException("cannot add local node"); - } - sqlNote.setParentId(parentId.longValue()); - } - - // create the local node - sqlNote.setGtaskId(node.getGid()); - sqlNote.commit(false); - - // update gid-nid mapping - mGidToNid.put(node.getGid(), sqlNote.getId()); - mNidToGid.put(sqlNote.getId(), node.getGid()); - - // update meta - updateRemoteMeta(node.getGid(), sqlNote); - } - - private void updateLocalNode(Node node, Cursor c) throws NetworkFailureException { - if (mCancelled) { - return; - } - - SqlNote sqlNote; - // update the note locally - sqlNote = new SqlNote(mContext, c); - sqlNote.setContent(node.getLocalJSONFromContent()); - - Long parentId = (node instanceof Task) ? mGidToNid.get(((Task) node).getParent().getGid()) - : new Long(Notes.ID_ROOT_FOLDER); - if (parentId == null) { - Log.e(TAG, "cannot find task's parent id locally"); - throw new ActionFailureException("cannot update local node"); - } - sqlNote.setParentId(parentId.longValue()); - sqlNote.commit(true); - - // update meta info - updateRemoteMeta(node.getGid(), sqlNote); - } - - private void addRemoteNode(Node node, Cursor c) throws NetworkFailureException { - if (mCancelled) { - return; - } - - SqlNote sqlNote = new SqlNote(mContext, c); - Node n; - - // update remotely - if (sqlNote.isNoteType()) { - Task task = new Task(); - task.setContentByLocalJSON(sqlNote.getContent()); - - String parentGid = mNidToGid.get(sqlNote.getParentId()); - if (parentGid == null) { - Log.e(TAG, "cannot find task's parent tasklist"); - throw new ActionFailureException("cannot add remote task"); - } - mGTaskListHashMap.get(parentGid).addChildTask(task); - - GTaskClient.getInstance().createTask(task); - n = (Node) task; - - // add meta - updateRemoteMeta(task.getGid(), sqlNote); - } else { - TaskList tasklist = null; - - // we need to skip folder if it has already existed - String folderName = GTaskStringUtils.MIUI_FOLDER_PREFFIX; - if (sqlNote.getId() == Notes.ID_ROOT_FOLDER) - folderName += GTaskStringUtils.FOLDER_DEFAULT; - else if (sqlNote.getId() == Notes.ID_CALL_RECORD_FOLDER) - folderName += GTaskStringUtils.FOLDER_CALL_NOTE; - else - folderName += sqlNote.getSnippet(); - - Iterator> 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; - } - } - - // no match we can add now - if (tasklist == null) { - tasklist = new TaskList(); - tasklist.setContentByLocalJSON(sqlNote.getContent()); - GTaskClient.getInstance().createTaskList(tasklist); - mGTaskListHashMap.put(tasklist.getGid(), tasklist); - } - n = (Node) tasklist; - } - - // update local note - sqlNote.setGtaskId(n.getGid()); - sqlNote.commit(false); - sqlNote.resetLocalModified(); - sqlNote.commit(true); - - // gid-id mapping - mGidToNid.put(n.getGid(), sqlNote.getId()); - mNidToGid.put(sqlNote.getId(), n.getGid()); - } - - private void updateRemoteNode(Node node, Cursor c) throws NetworkFailureException { - if (mCancelled) { - return; - } - - SqlNote sqlNote = new SqlNote(mContext, c); - - // update remotely - node.setContentByLocalJSON(sqlNote.getContent()); - GTaskClient.getInstance().addUpdateNode(node); - - // update meta - updateRemoteMeta(node.getGid(), sqlNote); - - // move task if necessary - if (sqlNote.isNoteType()) { - Task task = (Task) node; - TaskList preParentList = task.getParent(); - - String curParentGid = mNidToGid.get(sqlNote.getParentId()); - if (curParentGid == null) { - Log.e(TAG, "cannot find task's parent tasklist"); - throw new ActionFailureException("cannot update remote task"); - } - TaskList curParentList = mGTaskListHashMap.get(curParentGid); - - if (preParentList != curParentList) { - preParentList.removeChildTask(task); - curParentList.addChildTask(task); - GTaskClient.getInstance().moveTask(task, preParentList, curParentList); - } - } - - // clear local modified flag - sqlNote.resetLocalModified(); - sqlNote.commit(true); - } - - private void updateRemoteMeta(String gid, SqlNote sqlNote) throws NetworkFailureException { - if (sqlNote != null && sqlNote.isNoteType()) { - MetaData metaData = mMetaHashMap.get(gid); - if (metaData != null) { - metaData.setMeta(gid, sqlNote.getContent()); - GTaskClient.getInstance().addUpdateNode(metaData); - } else { - metaData = new MetaData(); - metaData.setMeta(gid, sqlNote.getContent()); - mMetaList.addChildTask(metaData); - mMetaHashMap.put(gid, metaData); - GTaskClient.getInstance().createTask(metaData); - } - } - } - - private void refreshLocalSyncId() throws NetworkFailureException { - if (mCancelled) { - return; - } - - // get the latest gtask list - mGTaskHashMap.clear(); - mGTaskListHashMap.clear(); - mMetaHashMap.clear(); - initGTaskList(); - - Cursor c = null; - try { - c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, - "(type<>? AND parent_id<>?)", new String[] { - String.valueOf(Notes.TYPE_SYSTEM), String.valueOf(Notes.ID_TRASH_FOLDER) - }, 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; - } - } - } - - /** - * 获取当前同步使用的Google账户名 - * - * @return 当前同步使用的Google账户名 - */ - public String getSyncAccount() { - return GTaskClient.getInstance().getSyncAccount().name; - } - - /** - * 取消正在进行的同步操作 - * - * 设置取消标志,使同步过程在适当的时机停止 - */ - public void cancelSync() { - mCancelled = true; - } -} diff --git a/src/notes/gtask/remote/GTaskSyncService.java b/src/notes/gtask/remote/GTaskSyncService.java deleted file mode 100644 index d4b1845..0000000 --- a/src/notes/gtask/remote/GTaskSyncService.java +++ /dev/null @@ -1,232 +0,0 @@ -/* - * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.micode.notes.gtask.remote; - -import android.app.Activity; -import android.app.Service; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.os.IBinder; - -/** - * GTaskSyncService类 - 继承自Android Service,用于管理Google Tasks同步服务 - * - * 该服务负责处理Google Tasks的同步生命周期,包括启动同步、取消同步以及发送同步状态广播 - * 它通过GTaskASyncTask在后台执行同步操作,并通过广播通知UI组件同步状态和进度 - * - * @author MiCode Open Source Community - */ -public class GTaskSyncService extends Service { - /** - * 同步操作类型的Intent额外数据键名 - */ - public final static String ACTION_STRING_NAME = "sync_action_type"; - - /** - * 同步操作类型:开始同步 - */ - public final static int ACTION_START_SYNC = 0; - - /** - * 同步操作类型:取消同步 - */ - public final static int ACTION_CANCEL_SYNC = 1; - - /** - * 同步操作类型:无效操作 - */ - public final static int ACTION_INVALID = 2; - - /** - * 同步服务广播的Action名称 - */ - public final static String GTASK_SERVICE_BROADCAST_NAME = "net.micode.notes.gtask.remote.gtask_sync_service"; - - /** - * 广播中表示同步状态的额外数据键名 - */ - public final static String GTASK_SERVICE_BROADCAST_IS_SYNCING = "isSyncing"; - - /** - * 广播中表示同步进度消息的额外数据键名 - */ - public final static String GTASK_SERVICE_BROADCAST_PROGRESS_MSG = "progressMsg"; - - /** - * 当前正在执行的同步任务实例 - */ - private static GTaskASyncTask mSyncTask = null; - - /** - * 当前同步进度消息 - */ - private static String mSyncProgress = ""; - - /** - * 开始同步操作 - * - * 如果当前没有正在执行的同步任务,则创建新的GTaskASyncTask实例并执行同步 - * 同步完成后会清理任务实例并停止服务 - */ - private void startSync() { - if (mSyncTask == null) { - mSyncTask = new GTaskASyncTask(this, new GTaskASyncTask.OnCompleteListener() { - public void onComplete() { - mSyncTask = null; - sendBroadcast(""); - stopSelf(); - } - }); - sendBroadcast(""); - mSyncTask.execute(); - } - } - - /** - * 取消当前同步操作 - * - * 如果当前有正在执行的同步任务,则调用其cancelSync方法取消同步 - */ - private void cancelSync() { - if (mSyncTask != null) { - mSyncTask.cancelSync(); - } - } - - @Override - public void onCreate() { - mSyncTask = null; - } - - /** - * 处理服务启动命令 - * - * 根据Intent中携带的操作类型,执行对应的同步操作(开始或取消) - * - * @param intent 启动服务的Intent,包含操作类型 - * @param flags 启动标志 - * @param startId 服务启动ID - * @return 服务启动模式,此处返回START_STICKY表示服务被杀死后会自动重启 - */ - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - Bundle bundle = intent.getExtras(); - if (bundle != null && bundle.containsKey(ACTION_STRING_NAME)) { - switch (bundle.getInt(ACTION_STRING_NAME, ACTION_INVALID)) { - case ACTION_START_SYNC: - startSync(); - break; - case ACTION_CANCEL_SYNC: - cancelSync(); - break; - default: - break; - } - return START_STICKY; - } - return super.onStartCommand(intent, flags, startId); - } - - /** - * 当系统内存不足时调用 - * - * 在此方法中取消当前正在执行的同步任务,以释放系统资源 - */ - @Override - public void onLowMemory() { - if (mSyncTask != null) { - mSyncTask.cancelSync(); - } - } - - /** - * 绑定服务时调用 - * - * 该服务不支持绑定,因此返回null - * - * @param intent 绑定服务的Intent - * @return 服务的IBinder接口,此处返回null - */ - public IBinder onBind(Intent intent) { - return null; - } - - /** - * 发送同步状态广播 - * - * 将当前同步状态和进度消息通过广播发送给所有监听该广播的组件 - * - * @param msg 当前同步进度消息 - */ - public void sendBroadcast(String msg) { - mSyncProgress = msg; - Intent intent = new Intent(GTASK_SERVICE_BROADCAST_NAME); - intent.putExtra(GTASK_SERVICE_BROADCAST_IS_SYNCING, mSyncTask != null); - intent.putExtra(GTASK_SERVICE_BROADCAST_PROGRESS_MSG, msg); - sendBroadcast(intent); - } - - /** - * 静态方法:启动同步服务 - * - * 通过Activity上下文启动GTaskSyncService并发送开始同步的命令 - * - * @param activity 用于启动服务的Activity上下文 - */ - public static void startSync(Activity activity) { - GTaskManager.getInstance().setActivityContext(activity); - Intent intent = new Intent(activity, GTaskSyncService.class); - intent.putExtra(GTaskSyncService.ACTION_STRING_NAME, GTaskSyncService.ACTION_START_SYNC); - activity.startService(intent); - } - - /** - * 静态方法:取消同步服务 - * - * 通过Context上下文启动GTaskSyncService并发送取消同步的命令 - * - * @param context 用于启动服务的上下文 - */ - public static void cancelSync(Context context) { - Intent intent = new Intent(context, GTaskSyncService.class); - intent.putExtra(GTaskSyncService.ACTION_STRING_NAME, GTaskSyncService.ACTION_CANCEL_SYNC); - context.startService(intent); - } - - /** - * 静态方法:检查是否正在同步 - * - * 判断当前是否有正在执行的同步任务 - * - * @return true表示正在同步,false表示未同步 - */ - public static boolean isSyncing() { - return mSyncTask != null; - } - - /** - * 静态方法:获取当前同步进度消息 - * - * 获取当前同步进度的字符串消息 - * - * @return 当前同步进度消息 - */ - public static String getProgressString() { - return mSyncProgress; - } -} diff --git a/src/notes/model/WorkingNote.java b/src/notes/model/WorkingNote.java index 084ae77..6d1f613 100644 --- a/src/notes/model/WorkingNote.java +++ b/src/notes/model/WorkingNote.java @@ -29,8 +29,13 @@ 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.data.AttachmentManager; import net.micode.notes.tool.ResourceParser.NoteBgResources; +import java.io.File; +import java.util.ArrayList; +import java.util.List; + /** * WorkingNote类 - 表示正在编辑或使用中的笔记 @@ -46,6 +51,10 @@ public class WorkingNote { * 内部封装的Note对象 */ private Note mNote; + /** + * 附件管理器 + */ + private AttachmentManager mAttachmentManager; /** * 笔记ID */ @@ -145,6 +154,7 @@ public class WorkingNote { mModifiedDate = System.currentTimeMillis(); mFolderId = folderId; mNote = new Note(); + mAttachmentManager = new AttachmentManager(context); mNoteId = 0; mIsDeleted = false; mMode = 0; @@ -158,6 +168,7 @@ public class WorkingNote { mFolderId = folderId; mIsDeleted = false; mNote = new Note(); + mAttachmentManager = new AttachmentManager(context); loadNote(); } @@ -379,6 +390,61 @@ public class WorkingNote { return mWidgetType; } + /** + * 添加附件 + * @param type 附件类型(Notes.ATTACHMENT_TYPE_GALLERY 或 Notes.ATTACHMENT_TYPE_CAMERA) + * @param sourceFile 源文件 + * @return 附件ID,失败返回-1 + */ + public long addAttachment(int type, File sourceFile) { + if (mAttachmentManager == null || mNoteId <= 0) { + return -1; + } + return mAttachmentManager.addAttachment(mNoteId, type, sourceFile); + } + + /** + * 删除附件 + * @param attachmentId 附件ID + * @return 是否成功 + */ + public boolean deleteAttachment(long attachmentId) { + if (mAttachmentManager == null) { + return false; + } + return mAttachmentManager.deleteAttachment(attachmentId); + } + + /** + * 删除笔记的所有附件 + * @return 删除的附件数量 + */ + public int deleteAllAttachments() { + if (mAttachmentManager == null) { + return 0; + } + return mAttachmentManager.deleteAttachmentsByNoteId(mNoteId); + } + + /** + * 获取笔记的所有附件 + * @return 附件列表 + */ + public List getAttachments() { + if (mAttachmentManager == null) { + return new ArrayList<>(); + } + return mAttachmentManager.getAttachmentsByNoteId(mNoteId); + } + + /** + * 获取附件管理器实例 + * @return 附件管理器 + */ + public AttachmentManager getAttachmentManager() { + return mAttachmentManager; + } + public interface NoteSettingChangedListener { /** * Called when the background color of current note has just changed diff --git a/src/notes/security/PasswordManager.java b/src/notes/security/PasswordManager.java new file mode 100644 index 0000000..7d521ce --- /dev/null +++ b/src/notes/security/PasswordManager.java @@ -0,0 +1,164 @@ +/* + * 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.security; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * 密码管理工具类 + * 负责便签加锁功能的密码存储、验证和哈希处理 + */ +public class PasswordManager { + private static final String TAG = "PasswordManager"; + private static final String PREF_PASSWORD_HASH = "pref_password_hash"; + private static final String PREF_PASSWORD_SET = "pref_password_set"; + private static final String PREF_NAME = "notes_preferences"; + + /** + * 检查是否已设置密码 + * @param context 上下文对象 + * @return 如果已设置密码返回true,否则返回false + */ + public static boolean isPasswordSet(Context context) { + try { + SharedPreferences sp = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + return sp.getBoolean(PREF_PASSWORD_SET, false); + } catch (Exception e) { + Log.e(TAG, "Error checking password status", e); + return false; + } + } + + /** + * 验证输入的密码是否正确 + * @param context 上下文对象 + * @param inputPassword 用户输入的密码 + * @return 如果密码正确返回true,否则返回false + */ + public static boolean verifyPassword(Context context, String inputPassword) { + if (inputPassword == null || inputPassword.isEmpty()) { + Log.w(TAG, "Empty password provided for verification"); + return false; + } + + try { + SharedPreferences sp = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + String storedHash = sp.getString(PREF_PASSWORD_HASH, ""); + String inputHash = hashPassword(inputPassword); + boolean result = storedHash.equals(inputHash); + + if (!result) { + Log.w(TAG, "Password verification failed"); + } + + return result; + } catch (Exception e) { + Log.e(TAG, "Error verifying password", e); + return false; + } + } + + /** + * 设置密码 + * @param context 上下文对象 + * @param password 要设置的密码 + * @return 设置成功返回true,失败返回false + */ + public static boolean setPassword(Context context, String password) { + if (password == null || password.isEmpty()) { + Log.w(TAG, "Empty password provided"); + return false; + } + + try { + SharedPreferences sp = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sp.edit(); + editor.putString(PREF_PASSWORD_HASH, hashPassword(password)); + editor.putBoolean(PREF_PASSWORD_SET, true); + boolean result = editor.commit(); + + if (result) { + Log.d(TAG, "Password set successfully"); + } else { + Log.e(TAG, "Failed to set password"); + } + + return result; + } catch (Exception e) { + Log.e(TAG, "Error setting password", e); + return false; + } + } + + /** + * 清除密码(取消密码设置) + * @param context 上下文对象 + * @return 清除成功返回true,失败返回false + */ + public static boolean clearPassword(Context context) { + try { + SharedPreferences sp = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sp.edit(); + editor.remove(PREF_PASSWORD_HASH); + editor.putBoolean(PREF_PASSWORD_SET, false); + boolean result = editor.commit(); + + if (result) { + Log.d(TAG, "Password cleared successfully"); + } else { + Log.e(TAG, "Failed to clear password"); + } + + return result; + } catch (Exception e) { + Log.e(TAG, "Error clearing password", e); + return false; + } + } + + /** + * 使用SHA-256算法哈希密码 + * @param password 原始密码 + * @return 哈希后的十六进制字符串,失败返回空字符串 + */ + private static String hashPassword(String password) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(password.getBytes(StandardCharsets.UTF_8)); + StringBuilder hexString = new StringBuilder(); + + for (byte b : hash) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + + return hexString.toString(); + } catch (NoSuchAlgorithmException e) { + Log.e(TAG, "Hash algorithm not found", e); + return ""; + } + } +} diff --git a/src/notes/tool/BackupUtils.java b/src/notes/tool/BackupUtils.java index dba5d6a..8fff9dc 100644 --- a/src/notes/tool/BackupUtils.java +++ b/src/notes/tool/BackupUtils.java @@ -249,7 +249,7 @@ public class BackupUtils { Notes.CONTENT_NOTE_URI, NOTE_PROJECTION, "(" + NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER + " AND " - + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER + ") OR " + + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLDER + ") OR " + NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER, null, null); if (folderCursor != null) { @@ -300,60 +300,9 @@ public class BackupUtils { 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_FOLDER + ") OR " - + NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER, null, null); - - if (folderCursor != null) { - if (folderCursor.moveToFirst()) { - do { - // 输出文件夹名称 - String folderName = ""; - if(folderCursor.getLong(NOTE_COLUMN_ID) == Notes.ID_CALL_RECORD_FOLDER) { - folderName = mContext.getString(R.string.call_record_folder_name); - } else { - folderName = folderCursor.getString(NOTE_COLUMN_SNIPPET); - } - if (!TextUtils.isEmpty(folderName)) { - ps.println(String.format(getFormat(FORMAT_FOLDER_NAME), folderName)); - } - // 导出该文件夹下的所有笔记 - String folderId = folderCursor.getString(NOTE_COLUMN_ID); - exportFolderToText(folderId, ps); - } while (folderCursor.moveToNext()); - } - folderCursor.close(); - } - // 第二部分:导出根目录下的笔记(不属于任何文件夹的笔记) - Cursor noteCursor = mContext.getContentResolver().query( - Notes.CONTENT_NOTE_URI, - NOTE_PROJECTION, - NoteColumns.TYPE + "=" + +Notes.TYPE_NOTE + " AND " + NoteColumns.PARENT_ID - + "=0", null, null); - if (noteCursor != null) { - if (noteCursor.moveToFirst()) { - do { - ps.println(String.format(getFormat(FORMAT_NOTE_DATE), DateFormat.format( - mContext.getString(R.string.format_datetime_mdhm), - noteCursor.getLong(NOTE_COLUMN_MODIFIED_DATE)))); - // 导出该笔记的内容 - String noteId = noteCursor.getString(NOTE_COLUMN_ID); - exportNoteToText(noteId, ps); - } while (noteCursor.moveToNext()); - } - noteCursor.close(); - } - // 关闭输出流 - ps.close(); - return STATE_SUCCESS; } /** diff --git a/src/notes/tool/DataUtils.java b/src/notes/tool/DataUtils.java index 77ae4f8..0798df1 100644 --- a/src/notes/tool/DataUtils.java +++ b/src/notes/tool/DataUtils.java @@ -376,4 +376,46 @@ public class DataUtils { } return snippet; } + + /** + * 批量设置便签的加锁状态 + * @param resolver ContentResolver实例 + * @param ids 要操作的便签ID集合 + * @param lock true表示加锁,false表示解锁 + * @return 操作是否成功 + */ + public static boolean batchSetLockStatus(ContentResolver resolver, HashSet ids, boolean lock) { + if (ids == null) { + Log.d(TAG, "the ids is null"); + return true; + } + if (ids.size() == 0) { + Log.d(TAG, "no id is in hashset"); + 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.IS_LOCKED, lock ? 1 : 0); + operationList.add(builder.build()); + } + + try { + // 执行批量操作 + ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operationList); + if (results == null || results.length == 0 || results[0] == null) { + Log.d(TAG, "set lock status 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; + } } diff --git a/src/notes/tool/ElderModeUtils.java b/src/notes/tool/ElderModeUtils.java index 11f4132..d2e4d4b 100644 --- a/src/notes/tool/ElderModeUtils.java +++ b/src/notes/tool/ElderModeUtils.java @@ -19,6 +19,7 @@ package net.micode.notes.tool; import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; +import android.util.TypedValue; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; @@ -66,7 +67,7 @@ public class ElderModeUtils { // 只增大用户输入内容的字体 if (id == R.id.tv_title || // 笔记列表中的标题(用户输入内容) id == R.id.note_edit_view) { // 编辑界面中的内容(用户输入) - textView.setTextSize(textView.getTextSize() * ELDER_MODE_FONT_SCALE); + textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textView.getTextSize() * ELDER_MODE_FONT_SCALE); } } else if (view instanceof ViewGroup) { // 递归处理ViewGroup中的所有子View @@ -84,9 +85,10 @@ public class ElderModeUtils { * @param view 要清除老年人模式的View */ public static void clearElderMode(Context context, View view) { - if (!isElderModeEnabled(context)) { - return; - } + // 移除这个检查,因为清除操作应该在老年人模式禁用时执行 + // if (!isElderModeEnabled(context)) { + // return; + // } // 只对用户输入的内容应用字体恢复,系统信息保持不变 if (view instanceof TextView) { @@ -96,7 +98,7 @@ public class ElderModeUtils { // 只恢复用户输入内容的字体 if (id == R.id.tv_title || // 笔记列表中的标题(用户输入内容) id == R.id.note_edit_view) { // 编辑界面中的内容(用户输入) - textView.setTextSize(textView.getTextSize() / ELDER_MODE_FONT_SCALE); + textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textView.getTextSize() / ELDER_MODE_FONT_SCALE); } } else if (view instanceof ViewGroup) { // 递归处理ViewGroup中的所有子View diff --git a/src/notes/tool/PermissionHelper.java b/src/notes/tool/PermissionHelper.java new file mode 100644 index 0000000..5c9e183 --- /dev/null +++ b/src/notes/tool/PermissionHelper.java @@ -0,0 +1,148 @@ +package net.micode.notes.tool; + +import android.app.Activity; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Build; + +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + +import java.util.ArrayList; +import java.util.List; + +/** + * 权限帮助类,简化运行时权限请求 + */ +public class PermissionHelper { + + /** + * 检查权限是否已授予 + * @param context 上下文 + * @param permission 权限 + * @return 是否已授权 + */ + public static boolean isPermissionGranted(Context context, String permission) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return true; + } + return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED; + } + + /** + * 检查多个权限是否已全部授予 + * @param context 上下文 + * @param permissions 权限数组 + * @return 是否全部已授权 + */ + public static boolean arePermissionsGranted(Context context, String[] permissions) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return true; + } + for (String permission : permissions) { + if (!isPermissionGranted(context, permission)) { + return false; + } + } + return true; + } + + /** + * 请求权限 + * @param activity 活动 + * @param permissions 权限数组 + * @param requestCode 请求码 + */ + public static void requestPermissions(Activity activity, String[] permissions, int requestCode) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return; + } + ActivityCompat.requestPermissions(activity, permissions, requestCode); + } + + /** + * 请求单个权限 + * @param activity 活动 + * @param permission 权限 + * @param requestCode 请求码 + */ + public static void requestPermission(Activity activity, String permission, int requestCode) { + requestPermissions(activity, new String[]{permission}, requestCode); + } + + /** + * 处理权限请求结果 + * @param requestCode 请求码 + * @param permissions 权限数组 + * @param grantResults 授权结果 + * @param listener 回调监听器 + */ + public static void handlePermissionResult(int requestCode, String[] permissions, int[] grantResults, + OnPermissionResultListener listener) { + if (listener == null) { + return; + } + + List granted = new ArrayList<>(); + List denied = new ArrayList<>(); + + for (int i = 0; i < permissions.length; i++) { + if (grantResults[i] == PackageManager.PERMISSION_GRANTED) { + granted.add(permissions[i]); + } else { + denied.add(permissions[i]); + } + } + + if (denied.isEmpty()) { + listener.onAllGranted(requestCode, granted); + } else { + listener.onDenied(requestCode, granted, denied); + } + } + + /** + * 检查是否应该显示权限请求说明 + * @param activity 活动 + * @param permission 权限 + * @return 是否应该显示说明 + */ + public static boolean shouldShowRequestPermissionRationale(Activity activity, String permission) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return false; + } + return ActivityCompat.shouldShowRequestPermissionRationale(activity, permission); + } + + /** + * 权限请求结果监听器 + */ + public interface OnPermissionResultListener { + /** + * 所有权限都被授予 + * @param requestCode 请求码 + * @param grantedPermissions 被授予的权限列表 + */ + void onAllGranted(int requestCode, List grantedPermissions); + + /** + * 部分或全部权限被拒绝 + * @param requestCode 请求码 + * @param grantedPermissions 被授予的权限列表 + * @param deniedPermissions 被拒绝的权限列表 + */ + void onDenied(int requestCode, List grantedPermissions, List deniedPermissions); + } + + /** + * 常用权限常量 + */ + public static class Permissions { + public static final String CAMERA = android.Manifest.permission.CAMERA; + public static final String READ_EXTERNAL_STORAGE = android.Manifest.permission.READ_EXTERNAL_STORAGE; + public static final String WRITE_EXTERNAL_STORAGE = android.Manifest.permission.WRITE_EXTERNAL_STORAGE; + + // Android 13+ 需要单独请求媒体权限 + public static final String READ_MEDIA_IMAGES = android.Manifest.permission.READ_MEDIA_IMAGES; + } +} \ No newline at end of file diff --git a/src/notes/ui/CameraPreviewDialogFragment.java b/src/notes/ui/CameraPreviewDialogFragment.java new file mode 100644 index 0000000..e5c0840 --- /dev/null +++ b/src/notes/ui/CameraPreviewDialogFragment.java @@ -0,0 +1,97 @@ +package net.micode.notes.ui; + +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.DialogInterface; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ImageView; + +import net.micode.notes.R; + +import java.io.File; + +/** + * 相机预览确认对话框 + */ +public class CameraPreviewDialogFragment extends DialogFragment { + + public static final String TAG = "CameraPreviewDialogFragment"; + + public interface OnCameraPreviewListener { + void onConfirm(File photoFile); + void onRetake(); + } + + private OnCameraPreviewListener mListener; + private File mPhotoFile; + + public CameraPreviewDialogFragment() { + // Required empty public constructor + } + + public void setPhotoFile(File photoFile) { + mPhotoFile = photoFile; + } + + public void setOnCameraPreviewListener(OnCameraPreviewListener listener) { + mListener = listener; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + Dialog dialog = new Dialog(getActivity()); + dialog.setContentView(R.layout.dialog_camera_preview); + return dialog; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.dialog_camera_preview, container, false); + + ImageView previewImage = view.findViewById(R.id.preview_image); + Button btnRetake = view.findViewById(R.id.btn_retake); + Button btnConfirm = view.findViewById(R.id.btn_confirm); + + // 加载预览图片 + if (mPhotoFile != null && mPhotoFile.exists()) { + // 使用BitmapFactory解码图片,避免OOM + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inSampleSize = 4; // 降低采样率,减少内存占用 + options.inJustDecodeBounds = false; + Bitmap bitmap = BitmapFactory.decodeFile(mPhotoFile.getAbsolutePath(), options); + if (bitmap != null) { + previewImage.setImageBitmap(bitmap); + } + } + + btnRetake.setOnClickListener(v -> { + if (mListener != null) { + mListener.onRetake(); + } + dismiss(); + }); + + btnConfirm.setOnClickListener(v -> { + if (mListener != null && mPhotoFile != null) { + mListener.onConfirm(mPhotoFile); + } + dismiss(); + }); + + return view; + } + + @Override + public void onDismiss(DialogInterface dialog) { + super.onDismiss(dialog); + mListener = null; + mPhotoFile = null; + } +} \ No newline at end of file diff --git a/src/notes/ui/ImageInsertDialogFragment.java b/src/notes/ui/ImageInsertDialogFragment.java new file mode 100644 index 0000000..abda5b9 --- /dev/null +++ b/src/notes/ui/ImageInsertDialogFragment.java @@ -0,0 +1,74 @@ +package net.micode.notes.ui; + +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import net.micode.notes.R; + +/** + * 图片插入对话框,提供从相册选择或拍照两种方式 + */ +public class ImageInsertDialogFragment extends DialogFragment { + + public static final String TAG = "ImageInsertDialogFragment"; + + public interface OnImageInsertListener { + void onGallerySelected(); + void onCameraSelected(); + } + + private OnImageInsertListener mListener; + + public ImageInsertDialogFragment() { + // Required empty public constructor + } + + public void setOnImageInsertListener(OnImageInsertListener listener) { + mListener = listener; + } + + @Override + public int getTheme() { + return R.style.NoteTheme; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.dialog_image_insert, container, false); + + View btnGallery = view.findViewById(R.id.btn_gallery); + View btnCamera = view.findViewById(R.id.btn_camera); + View btnCancel = view.findViewById(R.id.btn_cancel); + + btnGallery.setOnClickListener(v -> { + if (mListener != null) { + mListener.onGallerySelected(); + } + dismiss(); + }); + + btnCamera.setOnClickListener(v -> { + if (mListener != null) { + mListener.onCameraSelected(); + } + dismiss(); + }); + + btnCancel.setOnClickListener(v -> dismiss()); + + return view; + } + + @Override + public void onDismiss(DialogInterface dialog) { + super.onDismiss(dialog); + mListener = null; + } +} \ No newline at end of file diff --git a/src/notes/ui/LoginActivity.java b/src/notes/ui/LoginActivity.java new file mode 100644 index 0000000..7ae1637 --- /dev/null +++ b/src/notes/ui/LoginActivity.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may 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.ActionBar; +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Toast; + +import net.micode.notes.R; +import net.micode.notes.account.AccountManager; +import com.google.android.material.appbar.MaterialToolbar; + +public class LoginActivity extends Activity { + private EditText etUsername; + private EditText etPassword; + private Button btnLogin; + private Button btnCancel; + private TextView tvRegister; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_login); + + MaterialToolbar toolbar = (MaterialToolbar) findViewById(R.id.toolbar); + if (toolbar != null) { + ActionBar actionBar = getActionBar(); + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setDisplayShowTitleEnabled(false); + } + } + + initViews(); + setupListeners(); + } + + private void initViews() { + etUsername = (EditText) findViewById(R.id.et_login_username); + etPassword = (EditText) findViewById(R.id.et_login_password); + btnLogin = (Button) findViewById(R.id.btn_login); + btnCancel = (Button) findViewById(R.id.btn_login_cancel); + tvRegister = (TextView) findViewById(R.id.tv_login_register); + } + + private void setupListeners() { + btnLogin.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + handleLogin(); + } + }); + + btnCancel.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + finish(); + } + }); + + tvRegister.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(LoginActivity.this, RegisterActivity.class); + startActivity(intent); + } + }); + } + + private void handleLogin() { + String username = etUsername.getText().toString().trim(); + String password = etPassword.getText().toString().trim(); + + if (TextUtils.isEmpty(username)) { + Toast.makeText(this, R.string.error_username_empty, Toast.LENGTH_SHORT).show(); + return; + } + + if (TextUtils.isEmpty(password)) { + Toast.makeText(this, R.string.error_password_empty, Toast.LENGTH_SHORT).show(); + return; + } + + if (AccountManager.login(this, username, password)) { + Toast.makeText(this, R.string.toast_login_success, Toast.LENGTH_SHORT).show(); + finish(); + } else { + Toast.makeText(this, R.string.error_login_failed, Toast.LENGTH_SHORT).show(); + } + } +} diff --git a/src/notes/ui/NoteEditActivity.java b/src/notes/ui/NoteEditActivity.java index 7507430..a645b21 100644 --- a/src/notes/ui/NoteEditActivity.java +++ b/src/notes/ui/NoteEditActivity.java @@ -27,10 +27,16 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; +import android.database.Cursor; import android.graphics.Paint; +import android.net.Uri; +import android.os.AsyncTask; import android.os.Bundle; +import android.os.Environment; import android.preference.PreferenceManager; import android.text.Html; +import android.text.InputType; + import android.text.Spannable; import android.text.SpannableString; import android.text.Spanned; @@ -52,6 +58,7 @@ import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.EditText; +import android.widget.ImageButton; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; @@ -70,19 +77,38 @@ import net.micode.notes.ui.DateTimePickerDialog.OnDateTimeSetListener; import net.micode.notes.ui.NoteEditText.OnTextViewChangeListener; import net.micode.notes.widget.NoteWidgetProvider_2x; import net.micode.notes.widget.NoteWidgetProvider_4x; - +import net.micode.notes.data.AttachmentManager; +import net.micode.notes.tool.PermissionHelper; +import net.micode.notes.security.PasswordManager; +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; import java.util.HashMap; import java.util.HashSet; +import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.content.FileProvider; +import com.google.android.material.appbar.MaterialToolbar; + /** * 笔记编辑Activity - 小米便签的核心编辑界面 * 实现OnClickListener, NoteSettingChangedListener, OnTextViewChangeListener接口 * 处理笔记的创建、编辑、保存、删除等所有操作 */ -public class NoteEditActivity extends Activity implements OnClickListener, +public class NoteEditActivity extends AppCompatActivity implements OnClickListener, NoteSettingChangedListener, OnTextViewChangeListener { /** @@ -156,17 +182,27 @@ public class NoteEditActivity extends Activity implements OnClickListener, private String mUserQuery; // 用户搜索查询(用于高亮显示) private Pattern mPattern; // 用于高亮搜索结果的模式 + // 附件相关 + private LinearLayout mAttachmentContainer; // 附件容器 + private static final int REQUEST_GALLERY = 1001; + private static final int REQUEST_CAMERA = 1002; + private static final int REQUEST_PERMISSION_CAMERA = 1003; + private static final int REQUEST_PERMISSION_STORAGE = 1004; + private static final int REQUEST_CAMERA_PREVIEW = 1005; + private String mCurrentPhotoPath; // 相机拍照临时文件路径 + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); this.setContentView(R.layout.note_edit); // 设置布局文件 + + initResources(); // 初始化资源 // 初始化Activity状态,如果初始化失败则结束Activity if (savedInstanceState == null && !initActivityState(getIntent())) { finish(); return; } - initResources(); // 初始化资源 } /** @@ -215,18 +251,32 @@ public class NoteEditActivity extends Activity implements OnClickListener, finish(); return false; } else { - // 加载笔记 - mWorkingNote = WorkingNote.load(this, noteId); - if (mWorkingNote == null) { - Log.e(TAG, "load note failed with note id" + noteId); - finish(); - return false; - } + // 在后台线程加载笔记,避免阻塞UI线程 + new AsyncTask() { + @Override + protected WorkingNote doInBackground(Long... params) { + long id = params[0]; + try { + return WorkingNote.load(NoteEditActivity.this, id); + } catch (Exception e) { + Log.e(TAG, "load note failed with note id" + id, e); + return null; + } + } + + @Override + protected void onPostExecute(WorkingNote result) { + if (result != null) { + mWorkingNote = result; + setupWorkingNoteAndInitializeUI(Intent.ACTION_VIEW); + } else { + Log.e(TAG, "load note failed"); + finish(); + } + } + }.execute(noteId); + return true; } - // 隐藏软键盘 - getWindow().setSoftInputMode( - WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN - | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); } // 处理新建或编辑笔记的请求 else if(TextUtils.equals(Intent.ACTION_INSERT_OR_EDIT, intent.getAction())) { @@ -250,45 +300,167 @@ public class NoteEditActivity extends Activity implements OnClickListener, // 检查是否已存在相同的通话记录笔记 if ((noteId = DataUtils.getNoteIdByPhoneNumberAndCallDate(getContentResolver(), phoneNumber, callDate)) > 0) { - mWorkingNote = WorkingNote.load(this, noteId); - if (mWorkingNote == null) { - Log.e(TAG, "load call note failed with note id" + noteId); - finish(); - return false; - } + // 在后台线程加载通话记录笔记 + new AsyncTask() { + @Override + protected WorkingNote doInBackground(Long... params) { + long id = params[0]; + try { + return WorkingNote.load(NoteEditActivity.this, id); + } catch (Exception e) { + Log.e(TAG, "load call note failed with note id" + id, e); + return null; + } + } + + @Override + protected void onPostExecute(WorkingNote result) { + if (result != null) { + mWorkingNote = result; + setupWorkingNoteAndInitializeUI(Intent.ACTION_INSERT_OR_EDIT); + } else { + Log.e(TAG, "load call note failed"); + finish(); + } + } + }.execute(noteId); } else { - // 创建新的通话记录笔记 + // 创建新的通话记录笔记(无需数据库操作,直接在UI线程处理) mWorkingNote = WorkingNote.createEmptyNote(this, folderId, widgetId, widgetType, bgResId); mWorkingNote.convertToCallNote(phoneNumber, callDate); + setupWorkingNoteAndInitializeUI(Intent.ACTION_INSERT_OR_EDIT); } } else { - // 创建普通笔记 + // 创建普通笔记(无需数据库操作,直接在UI线程处理) mWorkingNote = WorkingNote.createEmptyNote(this, folderId, widgetId, widgetType, bgResId); + setupWorkingNoteAndInitializeUI(Intent.ACTION_INSERT_OR_EDIT); } - - // 显示软键盘 - getWindow().setSoftInputMode( - WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE - | WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); } else { Log.e(TAG, "Intent not specified action, should not support"); finish(); return false; } - // 设置笔记设置变化监听器 - mWorkingNote.setOnSettingStatusChangedListener(this); return true; } + + /** + * 设置WorkingNote并初始化UI + */ + private void setupWorkingNoteAndInitializeUI(String action) { + if (mWorkingNote != null) { + // 设置笔记设置变化监听器 + mWorkingNote.setOnSettingStatusChangedListener(this); + + // 根据不同的操作类型设置软键盘模式 + if (TextUtils.equals(Intent.ACTION_VIEW, action)) { + // 检查便签是否加锁 + if (isNoteLocked()) { + showPasswordDialogForLockedNote(); + } else { + // 隐藏软键盘 + getWindow().setSoftInputMode( + WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN + | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); + // 初始化UI界面 + initNoteScreen(); + // 加载附件 + loadAttachments(); + } + } else if (TextUtils.equals(Intent.ACTION_INSERT_OR_EDIT, action)) { + // 显示软键盘 + getWindow().setSoftInputMode( + WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE + | WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); + + // 初始化UI界面 + initNoteScreen(); + + // 加载附件 + loadAttachments(); + } + } + } + + /** + * 检查笔记是否加锁 + */ + private boolean isNoteLocked() { + try { + // 查询笔记的加锁状态 + String[] projection = new String[] { Notes.NoteColumns.IS_LOCKED }; + Cursor cursor = getContentResolver().query( + ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mWorkingNote.getNoteId()), + projection, null, null, null); + if (cursor != null) { + try { + if (cursor.moveToFirst()) { + int isLocked = cursor.getInt(0); + return isLocked == 1; + } + } finally { + cursor.close(); + } + } + } catch (Exception e) { + Log.e(TAG, "Error checking note lock status", e); + } + return false; + } + + /** + * 显示密码输入对话框用于打开加锁便签 + */ + private void showPasswordDialogForLockedNote() { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + View view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_text, null); + final EditText etPassword = (EditText) view.findViewById(R.id.et_foler_name); + etPassword.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); + etPassword.setHint(R.string.hint_enter_password); + etPassword.setText(""); + + builder.setTitle(R.string.title_locked_note); + builder.setView(view); + builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String password = etPassword.getText().toString(); + if (password.isEmpty()) { + Toast.makeText(NoteEditActivity.this, R.string.error_password_empty, Toast.LENGTH_SHORT).show(); + return; + } + if (PasswordManager.verifyPassword(NoteEditActivity.this, password)) { + // 密码验证通过,初始化UI + getWindow().setSoftInputMode( + WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN + | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); + initNoteScreen(); + loadAttachments(); + } else { + Toast.makeText(NoteEditActivity.this, R.string.error_wrong_password, Toast.LENGTH_SHORT).show(); + finish(); + } + } + }); + builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + finish(); + } + }); + builder.show(); + builder.setCancelable(false); + } @Override protected void onResume() { super.onResume(); - initNoteScreen(); // 初始化笔记界面 - // 每次恢复时应用老年人模式 - ElderModeUtils.applyElderMode(this, findViewById(android.R.id.content)); + // 只有当mWorkingNote已经初始化完成时才初始化界面 + if (mWorkingNote != null) { + initNoteScreen(); // 初始化笔记界面(已包含老年人模式应用) + } } /** @@ -327,6 +499,9 @@ public class NoteEditActivity extends Activity implements OnClickListener, // 显示闹钟提醒头部 showAlertHeader(); + + // 在设置字体外观后应用老年人模式,确保老年人模式的字体设置不被覆盖 + ElderModeUtils.applyElderMode(this, mNoteEditor); } /** @@ -412,6 +587,11 @@ public class NoteEditActivity extends Activity implements OnClickListener, * 初始化资源,获取布局中的控件引用 */ private void initResources() { + // 设置Material Toolbar作为ActionBar + MaterialToolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + getSupportActionBar().setDisplayShowTitleEnabled(false); + mHeadViewPanel = findViewById(R.id.note_title); mNoteHeaderHolder = new HeadViewHolder(); mNoteHeaderHolder.tvModified = (TextView) findViewById(R.id.tv_modified_date); @@ -450,6 +630,9 @@ public class NoteEditActivity extends Activity implements OnClickListener, mEditTextList = (LinearLayout) findViewById(R.id.note_edit_list); + // 初始化附件容器 + mAttachmentContainer = (LinearLayout) findViewById(R.id.note_attachment_container); + // 应用老年人模式 ElderModeUtils.applyElderMode(this, findViewById(android.R.id.content)); } @@ -568,6 +751,19 @@ public class NoteEditActivity extends Activity implements OnClickListener, mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId()); } + /** + * 创建选项菜单 + */ + @Override + public boolean onCreateOptionsMenu(Menu menu) { + if (mWorkingNote.getFolderId() == Notes.ID_CALL_RECORD_FOLDER) { + getMenuInflater().inflate(R.menu.call_note_edit, menu); // 通话记录笔记菜单 + } else { + getMenuInflater().inflate(R.menu.note_edit, menu); // 普通笔记菜单 + } + return true; + } + /** * 准备选项菜单 */ @@ -638,6 +834,9 @@ public class NoteEditActivity extends Activity implements OnClickListener, mWorkingNote.setCheckListMode(mWorkingNote.getCheckListMode() == 0 ? TextNote.MODE_CHECK_LIST : 0); break; + case R.id.menu_insert_image: + handleInsertImage(); // 插入图片 + break; case R.id.menu_share: getWorkingText(); // 获取工作文本 sendTo(this, mWorkingNote.getContent()); // 分享笔记 @@ -738,9 +937,10 @@ public class NoteEditActivity extends Activity implements OnClickListener, /** * 检查是否启用同步模式 + * 注意: Google Sync 功能已移除,此方法总是返回 false */ private boolean isSyncMode() { - return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0; + return false; } /** @@ -1221,4 +1421,293 @@ public class NoteEditActivity extends Activity implements OnClickListener, private void showToast(int resId, int duration) { Toast.makeText(this, resId, duration).show(); } + + // ==================== 附件相关方法 ==================== + + /** + * 加载附件列表并显示 + */ + private void loadAttachments() { + if (mWorkingNote == null || mWorkingNote.getNoteId() <= 0) { + return; + } + + List attachments = mWorkingNote.getAttachments(); + if (attachments.isEmpty()) { + mAttachmentContainer.setVisibility(View.GONE); + return; + } + + mAttachmentContainer.setVisibility(View.VISIBLE); + mAttachmentContainer.removeAllViews(); + + for (AttachmentManager.Attachment attachment : attachments) { + addAttachmentView(attachment); + } + } + + /** + * 添加附件视图 + * @param attachment 附件数据 + */ + private void addAttachmentView(AttachmentManager.Attachment attachment) { + View view = LayoutInflater.from(this).inflate(R.layout.image_item, mAttachmentContainer, false); + ImageView imageView = view.findViewById(R.id.image_view); + ImageButton deleteButton = view.findViewById(R.id.btn_delete); + + // 使用Glide加载图片 + Glide.with(this) + .load(new File(attachment.filePath)) + .override(800, 800) + .centerCrop() + .transition(DrawableTransitionOptions.withCrossFade()) + .into(imageView); + + // 长按删除 + view.setOnLongClickListener(v -> { + new AlertDialog.Builder(this) + .setTitle(R.string.delete_image) + .setMessage(R.string.confirm_delete_image) + .setPositiveButton(android.R.string.ok, (dialog, which) -> { + if (mWorkingNote.deleteAttachment(attachment.id)) { + mAttachmentContainer.removeView(view); + if (mAttachmentContainer.getChildCount() == 0) { + mAttachmentContainer.setVisibility(View.GONE); + } + showToast(R.string.image_deleted); + } + }) + .setNegativeButton(android.R.string.cancel, null) + .show(); + return true; + }); + + // 删除按钮点击 + deleteButton.setOnClickListener(v -> view.performLongClick()); + + mAttachmentContainer.addView(view); + } + + /** + * 处理菜单项点击:插入图片 + */ + private void handleInsertImage() { + ImageInsertDialogFragment dialog = new ImageInsertDialogFragment(); + dialog.setOnImageInsertListener(new ImageInsertDialogFragment.OnImageInsertListener() { + @Override + public void onGallerySelected() { + requestGalleryPermission(); + } + + @Override + public void onCameraSelected() { + requestCameraPermission(); + } + }); + dialog.show(getFragmentManager(), ImageInsertDialogFragment.TAG); + } + + /** + * 请求相册权限 + */ + private void requestGalleryPermission() { + String[] permissions; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) { + permissions = new String[]{PermissionHelper.Permissions.READ_MEDIA_IMAGES}; + } else { + permissions = new String[]{PermissionHelper.Permissions.READ_EXTERNAL_STORAGE}; + } + + if (PermissionHelper.arePermissionsGranted(this, permissions)) { + openGallery(); + } else { + PermissionHelper.requestPermissions(this, permissions, REQUEST_PERMISSION_STORAGE); + } + } + + /** + * 请求相机权限 + */ + private void requestCameraPermission() { + String[] permissions = {PermissionHelper.Permissions.CAMERA}; + if (PermissionHelper.arePermissionsGranted(this, permissions)) { + openCamera(); + } else { + PermissionHelper.requestPermissions(this, permissions, REQUEST_PERMISSION_CAMERA); + } + } + + /** + * 打开相册选择图片 + */ + private void openGallery() { + Intent intent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI); + intent.setType("image/*"); + startActivityForResult(intent, REQUEST_GALLERY); + } + + /** + * 打开相机拍照 + */ + private void openCamera() { + Intent takePictureIntent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE); + File photoFile = createImageFile(); + if (photoFile != null) { + mCurrentPhotoPath = photoFile.getAbsolutePath(); + try { + Uri photoURI = FileProvider.getUriForFile(this, + getApplicationContext().getPackageName() + ".fileprovider", + photoFile); + takePictureIntent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, photoURI); + startActivityForResult(takePictureIntent, REQUEST_CAMERA); + } catch (Exception e) { + Log.e(TAG, "Failed to get URI from FileProvider", e); + } + } else { + Log.e(TAG, "Failed to create image file"); + } + } + + /** + * 创建临时图片文件 + * @return 文件对象,失败返回null + */ + private File createImageFile() { + String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date()); + String imageFileName = "JPEG_" + timeStamp + "_"; + File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES); + try { + File image = File.createTempFile(imageFileName, ".jpg", storageDir); + return image; + } catch (IOException e) { + Log.e(TAG, "Failed to create image file", e); + return null; + } + } + + /** + * 处理权限请求结果 + */ + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + PermissionHelper.handlePermissionResult(requestCode, permissions, grantResults, + new PermissionHelper.OnPermissionResultListener() { + @Override + public void onAllGranted(int requestCode, List grantedPermissions) { + if (requestCode == REQUEST_PERMISSION_STORAGE) { + openGallery(); + } else if (requestCode == REQUEST_PERMISSION_CAMERA) { + openCamera(); + } + } + + @Override + public void onDenied(int requestCode, List grantedPermissions, List deniedPermissions) { + showToast(R.string.permission_denied); + } + }); + } + + /** + * 处理Activity结果(图片选择/拍照) + */ + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (resultCode != RESULT_OK) { + return; + } + + if (requestCode == REQUEST_GALLERY && data != null) { + Uri selectedImage = data.getData(); + if (selectedImage != null) { + processSelectedImage(selectedImage, Notes.ATTACHMENT_TYPE_GALLERY); + } + } else if (requestCode == REQUEST_CAMERA) { + if (mCurrentPhotoPath != null) { + File photoFile = new File(mCurrentPhotoPath); + if (photoFile.exists()) { + showCameraPreviewDialog(photoFile); + } + } + } + } + + /** + * 处理选择的图片 + * @param imageUri 图片URI + * @param type 附件类型 + */ + private void processSelectedImage(Uri imageUri, int type) { + try { + File sourceFile = new File(imageUri.getPath()); + if (!sourceFile.exists()) { + // 如果直接路径不存在,尝试通过ContentResolver获取 + sourceFile = getFileFromUri(imageUri); + } + if (sourceFile != null && sourceFile.exists()) { + long attachmentId = mWorkingNote.addAttachment(type, sourceFile); + if (attachmentId > 0) { + loadAttachments(); + showToast(R.string.image_added); + } else { + showToast(R.string.failed_to_add_image); + } + } + } catch (Exception e) { + Log.e(TAG, "Failed to process image", e); + showToast(R.string.failed_to_add_image); + } + } + + /** + * 从URI获取文件 + * @param uri 图片URI + * @return 文件对象,失败返回null + */ + private File getFileFromUri(Uri uri) { + // 简化实现:对于content:// URI,直接复制到临时文件 + try { + InputStream inputStream = getContentResolver().openInputStream(uri); + if (inputStream == null) return null; + + File tempFile = File.createTempFile("temp_image", ".jpg", getCacheDir()); + FileOutputStream outputStream = new FileOutputStream(tempFile); + byte[] buffer = new byte[4096]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + outputStream.close(); + inputStream.close(); + return tempFile; + } catch (IOException e) { + Log.e(TAG, "Failed to get file from URI", e); + return null; + } + } + + /** + * 显示相机预览对话框 + * @param photoFile 拍照生成的图片文件 + */ + private void showCameraPreviewDialog(File photoFile) { + CameraPreviewDialogFragment dialog = new CameraPreviewDialogFragment(); + dialog.setPhotoFile(photoFile); + dialog.setOnCameraPreviewListener(new CameraPreviewDialogFragment.OnCameraPreviewListener() { + @Override + public void onConfirm(File photoFile) { + // 确认使用照片,添加到附件 + processSelectedImage(Uri.fromFile(photoFile), Notes.ATTACHMENT_TYPE_CAMERA); + } + + @Override + public void onRetake() { + // 重拍,重新打开相机 + openCamera(); + } + }); + dialog.show(getFragmentManager(), CameraPreviewDialogFragment.TAG); + } } diff --git a/src/notes/ui/NoteItemData.java b/src/notes/ui/NoteItemData.java index 25173b8..42fdd67 100644 --- a/src/notes/ui/NoteItemData.java +++ b/src/notes/ui/NoteItemData.java @@ -46,6 +46,7 @@ public class NoteItemData { NoteColumns.TYPE, // 类型(笔记、文件夹、系统文件夹等) NoteColumns.WIDGET_ID, // 小部件ID NoteColumns.WIDGET_TYPE, // 小部件类型 + NoteColumns.IS_LOCKED, // 是否加锁 }; // 字段索引常量定义,对应PROJECTION数组中的位置 @@ -61,6 +62,7 @@ public class NoteItemData { private static final int TYPE_COLUMN = 9; private static final int WIDGET_ID_COLUMN = 10; private static final int WIDGET_TYPE_COLUMN = 11; + private static final int IS_LOCKED_COLUMN = 12; // 笔记数据字段 private long mId; // 笔记ID @@ -75,6 +77,7 @@ public class NoteItemData { private int mType; // 笔记类型 private int mWidgetId; // 关联的小部件ID private int mWidgetType; // 小部件类型 + private boolean mIsLocked; // 是否加锁 // 通话记录相关字段 private String mName; // 联系人姓名 @@ -113,6 +116,7 @@ public class NoteItemData { mType = cursor.getInt(TYPE_COLUMN); mWidgetId = cursor.getInt(WIDGET_ID_COLUMN); mWidgetType = cursor.getInt(WIDGET_TYPE_COLUMN); + mIsLocked = (cursor.getInt(IS_LOCKED_COLUMN) > 0); // 初始化通话记录相关字段 mPhoneNumber = ""; @@ -344,6 +348,14 @@ public class NoteItemData { return (mParentId == Notes.ID_CALL_RECORD_FOLDER && !TextUtils.isEmpty(mPhoneNumber)); } + /** + * 判断便签是否加锁 + * @return 如果便签已加锁返回true + */ + public boolean isLocked() { + return mIsLocked; + } + /** * 静态方法:从游标获取笔记类型 * @param cursor 数据库游标 diff --git a/src/notes/ui/NotesListActivity.java b/src/notes/ui/NotesListActivity.java index 8e25e92..1166d91 100644 --- a/src/notes/ui/NotesListActivity.java +++ b/src/notes/ui/NotesListActivity.java @@ -65,15 +65,18 @@ import android.widget.PopupMenu; import android.widget.TextView; import android.widget.Toast; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; + 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; import net.micode.notes.model.WorkingNote; import net.micode.notes.tool.BackupUtils; import net.micode.notes.tool.DataUtils; import net.micode.notes.tool.ElderModeUtils; import net.micode.notes.tool.ResourceParser; +import net.micode.notes.security.PasswordManager; import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute; import net.micode.notes.widget.NoteWidgetProvider_2x; import net.micode.notes.widget.NoteWidgetProvider_4x; @@ -88,7 +91,7 @@ import java.util.HashSet; * 笔记列表主界面Activity * 显示所有笔记列表,支持笔记的增删改查、文件夹管理、批量操作等功能 */ -public class NotesListActivity extends Activity implements OnClickListener, OnItemLongClickListener { +public class NotesListActivity extends AppCompatActivity implements OnClickListener, OnItemLongClickListener { // 查询令牌常量 private static final int FOLDER_NOTE_LIST_QUERY_TOKEN = 0; // 查询文件夹内笔记列表的令牌 private static final int FOLDER_LIST_QUERY_TOKEN = 1; // 查询文件夹列表的令牌 @@ -97,6 +100,8 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt 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; // 文件夹改名菜单项 + private static final int MENU_LOCK_NOTE = 3; // 便签加锁菜单项 + private static final int MENU_UNLOCK_NOTE = 4; // 便签解锁菜单项 private static final int MENU_FOLDER_ENCRYPT = 3; // 文件夹加密菜单项 private static final int MENU_FOLDER_DECRYPT = 4; // 文件夹取消加密菜单项 @@ -121,7 +126,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt // 笔记列表视图 private ListView mNotesListView; // 新建笔记按钮 - private Button mAddNewNote; + private View mAddNewNote; // 触摸事件分发标记 private boolean mDispatch; // 触摸起始Y坐标 @@ -301,12 +306,14 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt mNotesListView.setOnItemLongClickListener(this); mNotesListAdapter = new NotesListAdapter(this); mNotesListView.setAdapter(mNotesListAdapter); - mAddNewNote = (Button) findViewById(R.id.btn_new_note); + mAddNewNote = findViewById(R.id.fab_new_note); mAddNewNote.setOnClickListener(this); mAddNewNote.setOnTouchListener(new NewNoteOnTouchListener()); // 设置特殊触摸监听器 mDispatch = false; mDispatchY = 0; mOriginY = 0; + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); mTitleBar = (TextView) findViewById(R.id.tv_title_bar); mState = ListEditState.NOTE_LIST; // 初始状态为普通笔记列表 mModeCallBack = new ModeCallback(); // 多选模式回调 @@ -339,6 +346,14 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt mMoveMenu.setVisible(true); mMoveMenu.setOnMenuItemClickListener(this); } + MenuItem lockMenu = menu.findItem(R.id.lock); + MenuItem unlockMenu = menu.findItem(R.id.unlock); + if (lockMenu != null) { + lockMenu.setOnMenuItemClickListener(this); + } + if (unlockMenu != null) { + unlockMenu.setOnMenuItemClickListener(this); + } mActionMode = mode; mNotesListAdapter.setChoiceMode(true); // 进入多选模式 mNotesListView.setLongClickable(false); // 禁用长按 @@ -451,6 +466,14 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt // 批量移动到文件夹 startQueryDestinationFolders(); break; + case R.id.lock: + // 批量加锁 + handleBatchLockNotes(true); + break; + case R.id.unlock: + // 批量解锁 + handleBatchLockNotes(false); + break; default: return false; } @@ -757,7 +780,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt @Override public void onClick(View v) { switch (v.getId()) { - case R.id.btn_new_note: + case R.id.fab_new_note: createNewNote(); // 创建新笔记 break; default: @@ -944,6 +967,24 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } }; + /** + * 便签长按上下文菜单监听器 + */ + private final OnCreateContextMenuListener mNoteOnCreateContextMenuListener = new OnCreateContextMenuListener() { + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + if (mFocusNoteDataItem != null) { + menu.setHeaderTitle(mFocusNoteDataItem.getSnippet()); // 设置菜单标题为便签标题 + // 根据锁状态显示不同的菜单项 + if (mFocusNoteDataItem.isLocked()) { + menu.add(0, MENU_UNLOCK_NOTE, 0, R.string.menu_unlock); + } else { + menu.add(0, MENU_LOCK_NOTE, 0, R.string.menu_lock); + } + } + } + }; + @Override public void onContextMenuClosed(Menu menu) { if (mNotesListView != null) { @@ -989,6 +1030,12 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt // 取消加密文件夹 decryptFolder(mFocusNoteDataItem.getId()); break; + case MENU_LOCK_NOTE: + handleLockNote(); // 加锁便签 + break; + case MENU_UNLOCK_NOTE: + handleUnlockNote(); // 解锁便签 + break; default: break; } @@ -1002,9 +1049,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt // 根据当前状态加载不同的菜单 if (mState == ListEditState.NOTE_LIST) { getMenuInflater().inflate(R.menu.note_list, menu); - // 根据同步状态设置同步菜单项标题 - menu.findItem(R.id.menu_sync).setTitle( - GTaskSyncService.isSyncing() ? R.string.menu_sync_cancel : R.string.menu_sync); + // 注意: Google Sync 功能已移除,同步菜单项已禁用 } else if (mState == ListEditState.SUB_FOLDER) { getMenuInflater().inflate(R.menu.sub_folder, menu); } else if (mState == ListEditState.CALL_RECORD_FOLDER) { @@ -1025,16 +1070,8 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt exportNoteToText(); // 导出笔记为文本 break; case R.id.menu_sync: - // 同步功能处理 - if (isSyncMode()) { - if (TextUtils.equals(item.getTitle(), getString(R.string.menu_sync))) { - GTaskSyncService.startSync(this); // 开始同步 - } else { - GTaskSyncService.cancelSync(this); // 取消同步 - } - } else { - startPreferenceActivity(); // 未设置同步账户,跳转到设置 - } + // 同步功能已移除,显示提示信息 + Toast.makeText(this, R.string.error_sync_not_available, Toast.LENGTH_SHORT).show(); break; case R.id.menu_setting: startPreferenceActivity(); // 打开设置 @@ -1103,9 +1140,10 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt /** * 检查是否处于同步模式 + * 注意: Google Sync 功能已移除,此方法总是返回 false */ private boolean isSyncMode() { - return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0; + return false; } /** @@ -1172,6 +1210,152 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } } + /** + * 处理单条便签加锁 + */ + private void handleLockNote() { + if (mFocusNoteDataItem == null) { + return; + } + + // 检查是否已设置密码 + if (!PasswordManager.isPasswordSet(this)) { + Toast.makeText(this, R.string.error_no_password_set, Toast.LENGTH_SHORT).show(); + return; + } + + // 直接加锁,无需密码验证 + HashSet ids = new HashSet<>(); + ids.add(mFocusNoteDataItem.getId()); + if (DataUtils.batchSetLockStatus(mContentResolver, ids, true)) { + Toast.makeText(this, R.string.toast_lock_success, Toast.LENGTH_SHORT).show(); + startAsyncNotesListQuery(); // 刷新列表 + } else { + Toast.makeText(this, R.string.error_operation_failed, Toast.LENGTH_SHORT).show(); + } + } + + /** + * 处理单条便签解锁 + */ + private void handleUnlockNote() { + if (mFocusNoteDataItem == null) { + return; + } + + // 检查是否已设置密码 + if (!PasswordManager.isPasswordSet(this)) { + Toast.makeText(this, R.string.error_no_password_set, Toast.LENGTH_SHORT).show(); + return; + } + + // 显示密码输入对话框 + showPasswordDialogForUnlock(); + } + + /** + * 显示密码输入对话框用于解锁便签 + */ + private void showPasswordDialogForUnlock() { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + View view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_text, null); + final EditText etPassword = (EditText) view.findViewById(R.id.et_foler_name); + etPassword.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD); + etPassword.setHint(R.string.hint_enter_password); + etPassword.setText(""); + + builder.setTitle(R.string.title_unlock_note); + builder.setView(view); + builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String password = etPassword.getText().toString(); + if (PasswordManager.verifyPassword(NotesListActivity.this, password)) { + HashSet ids = new HashSet<>(); + ids.add(mFocusNoteDataItem.getId()); + if (DataUtils.batchSetLockStatus(mContentResolver, ids, false)) { + Toast.makeText(NotesListActivity.this, R.string.toast_unlock_success, Toast.LENGTH_SHORT).show(); + startAsyncNotesListQuery(); // 刷新列表 + } else { + Toast.makeText(NotesListActivity.this, R.string.error_operation_failed, Toast.LENGTH_SHORT).show(); + } + } else { + Toast.makeText(NotesListActivity.this, R.string.error_wrong_password, Toast.LENGTH_SHORT).show(); + } + } + }); + builder.setNegativeButton(android.R.string.cancel, null); + builder.show(); + } + + /** + * 处理批量加锁/解锁便签 + * @param lock true表示加锁,false表示解锁 + */ + private void handleBatchLockNotes(boolean lock) { + if (mNotesListAdapter.getSelectedCount() == 0) { + return; + } + + // 检查是否已设置密码 + if (!PasswordManager.isPasswordSet(this)) { + Toast.makeText(this, R.string.error_no_password_set, Toast.LENGTH_SHORT).show(); + return; + } + + // 如果是解锁操作,需要验证密码 + if (!lock) { + showPasswordDialogForBatchUnlock(); + return; + } + + // 直接执行批量加锁 + HashSet ids = mNotesListAdapter.getSelectedItemIds(); + if (DataUtils.batchSetLockStatus(mContentResolver, ids, true)) { + Toast.makeText(this, R.string.toast_lock_success, Toast.LENGTH_SHORT).show(); + mModeCallBack.finishActionMode(); // 结束多选模式 + startAsyncNotesListQuery(); // 刷新列表 + } else { + Toast.makeText(this, R.string.error_operation_failed, Toast.LENGTH_SHORT).show(); + } + } + + /** + * 显示密码输入对话框用于批量解锁便签 + */ + private void showPasswordDialogForBatchUnlock() { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + View view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_text, null); + final EditText etPassword = (EditText) view.findViewById(R.id.et_foler_name); + etPassword.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD); + etPassword.setHint(R.string.hint_enter_password); + etPassword.setText(""); + + builder.setTitle(R.string.title_unlock_note); + builder.setMessage(getString(R.string.message_batch_unlock, mNotesListAdapter.getSelectedCount())); + builder.setView(view); + builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String password = etPassword.getText().toString(); + if (PasswordManager.verifyPassword(NotesListActivity.this, password)) { + HashSet ids = mNotesListAdapter.getSelectedItemIds(); + if (DataUtils.batchSetLockStatus(mContentResolver, ids, false)) { + Toast.makeText(NotesListActivity.this, R.string.toast_unlock_success, Toast.LENGTH_SHORT).show(); + mModeCallBack.finishActionMode(); // 结束多选模式 + startAsyncNotesListQuery(); // 刷新列表 + } else { + Toast.makeText(NotesListActivity.this, R.string.error_operation_failed, Toast.LENGTH_SHORT).show(); + } + } else { + Toast.makeText(NotesListActivity.this, R.string.error_wrong_password, Toast.LENGTH_SHORT).show(); + } + } + }); + builder.setNegativeButton(android.R.string.cancel, null); + builder.show(); + } + /** * 查询目标文件夹列表(用于批量移动) */ @@ -1210,6 +1394,9 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } else if (mFocusNoteDataItem.getType() == Notes.TYPE_FOLDER) { // 文件夹长按:显示上下文菜单 mNotesListView.setOnCreateContextMenuListener(mFolderOnCreateContextMenuListener); + } else if (mFocusNoteDataItem.getType() == Notes.TYPE_NOTE) { + // 笔记长按:显示上下文菜单(加锁/解锁) + mNotesListView.setOnCreateContextMenuListener(mNoteOnCreateContextMenuListener); } } return false; diff --git a/src/notes/ui/NotesListItem.java b/src/notes/ui/NotesListItem.java index d049ed1..c2309b2 100644 --- a/src/notes/ui/NotesListItem.java +++ b/src/notes/ui/NotesListItem.java @@ -27,6 +27,7 @@ import android.widget.TextView; import net.micode.notes.R; import net.micode.notes.data.Notes; import net.micode.notes.tool.DataUtils; +import net.micode.notes.tool.ElderModeUtils; import net.micode.notes.tool.ResourceParser.NoteItemBgResources; /** @@ -35,6 +36,7 @@ import net.micode.notes.tool.ResourceParser.NoteItemBgResources; */ public class NotesListItem extends LinearLayout { private ImageView mAlert; // 提醒图标 + private ImageView mLockIcon; // 锁图标 private TextView mTitle; // 标题/内容摘要 private TextView mTime; // 修改时间 private TextView mCallName; // 通话记录联系人姓名 @@ -50,6 +52,7 @@ public class NotesListItem extends LinearLayout { inflate(context, R.layout.note_item, this); // 从布局文件初始化视图 // 初始化视图组件 mAlert = (ImageView) findViewById(R.id.iv_alert_icon); + mLockIcon = (ImageView) findViewById(R.id.iv_lock_icon); mTitle = (TextView) findViewById(R.id.tv_title); mTime = (TextView) findViewById(R.id.tv_time); mCallName = (TextView) findViewById(R.id.tv_name); @@ -74,6 +77,17 @@ public class NotesListItem extends LinearLayout { mItemData = data; // 保存数据引用 + // 根据加锁状态显示/隐藏锁图标 + if (data.getType() == Notes.TYPE_NOTE) { + if (data.isLocked()) { + mLockIcon.setVisibility(View.VISIBLE); + } else { + mLockIcon.setVisibility(View.GONE); + } + } else { + mLockIcon.setVisibility(View.GONE); + } + // 根据不同数据类型设置不同的显示内容 if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { // 通话记录文件夹的特殊显示 @@ -141,6 +155,9 @@ public class NotesListItem extends LinearLayout { // 根据数据类型和位置设置背景 setBackground(data); + + // 在设置完所有内容后应用老年人模式,确保字体大小正确 + ElderModeUtils.applyElderMode(context, mTitle); } /** diff --git a/src/notes/ui/NotesPreferenceActivity.java b/src/notes/ui/NotesPreferenceActivity.java index b928e68..8bc5f47 100644 --- a/src/notes/ui/NotesPreferenceActivity.java +++ b/src/notes/ui/NotesPreferenceActivity.java @@ -2,7 +2,7 @@ * 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 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 @@ -16,24 +16,15 @@ 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; @@ -45,34 +36,15 @@ 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; +import net.micode.notes.security.PasswordManager; +import net.micode.notes.account.AccountManager; +import com.google.android.material.appbar.MaterialToolbar; -/** - * 笔记应用的设置界面Activity - * 主要负责Google Task同步账户的设置和管理 - */ public class NotesPreferenceActivity extends PreferenceActivity { - // 偏好设置文件名 - public static final String PREFERENCE_NAME = "notes_preferences"; - // 同步账户名称的键11 - 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_SECURITY_KEY = "pref_security_settings"; + private static final String PREFERENCE_USER_CENTER_KEY = "pref_user_center"; public static final String PREFERENCE_ELDER_MODE_KEY = "pref_key_elder_mode"; - // 同步账户设置的键 - 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; // 标记是否添加了新账户 + public static final String PREFERENCE_SET_BG_COLOR_KEY = "pref_key_bg_random_appear"; @Override protected void onCreate(Bundle icicle) { @@ -94,8 +66,15 @@ public class NotesPreferenceActivity extends PreferenceActivity { filter.addAction(GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME); // 注册同步服务广播 registerReceiver(mReceiver, filter, Context.RECEIVER_EXPORTED); // 注册广播接收器 - mOriAccounts = null; - // 添加设置界面的头部视图 + MaterialToolbar toolbar = (MaterialToolbar) findViewById(R.id.toolbar); + if (toolbar != null) { + android.app.ActionBar actionBar = getActionBar(); + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setDisplayShowTitleEnabled(false); + } + } + View header = LayoutInflater.from(this).inflate(R.layout.settings_header, null); getListView().addHeaderView(header, null, true); } @@ -103,38 +82,47 @@ public class NotesPreferenceActivity extends PreferenceActivity { @Override protected void onResume() { super.onResume(); + loadSecurityPreference(); + loadUserCenterPreference(); + } - // 如果用户添加了新账户,需要自动设置同步账户 - if (mHasAddedAccount) { - Account[] accounts = getGoogleAccounts(); - // 检查是否有新账户添加 - if (mOriAccounts != null && accounts.length > mOriAccounts.length) { - for (Account accountNew : accounts) { - boolean isFound = false; - for (Account accountOld : mOriAccounts) { - if (TextUtils.equals(accountOld.name, accountNew.name)) { - isFound = true; - break; - } - } - // 发现新账户,自动设置为同步账户 - if (!isFound) { - setSyncAccount(accountNew.name); - break; - } - } + private void loadSecurityPreference() { + Preference securityPref = findPreference(PREFERENCE_SECURITY_KEY); + if (securityPref != null) { + if (PasswordManager.isPasswordSet(this)) { + securityPref.setSummary(R.string.preferences_password_set); + } else { + securityPref.setSummary(R.string.preferences_password_not_set); } - } - refreshUI(); // 刷新界面 + securityPref.setOnPreferenceClickListener(new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + showSecuritySettingsDialog(); + return true; + } + }); + } } - @Override - protected void onDestroy() { - if (mReceiver != null) { - unregisterReceiver(mReceiver); // 注销广播接收器 + private void loadUserCenterPreference() { + Preference userCenterPref = findPreference(PREFERENCE_USER_CENTER_KEY); + if (userCenterPref != null) { + if (AccountManager.isUserLoggedIn(this)) { + String currentUser = AccountManager.getCurrentUser(this); + userCenterPref.setSummary(getString(R.string.preferences_user_center_summary, currentUser)); + } else { + userCenterPref.setSummary(getString(R.string.preferences_user_center_not_logged_in)); + } + + userCenterPref.setOnPreferenceClickListener(new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + showUserCenterDialog(); + return true; + } + }); } - super.onDestroy(); } /** @@ -314,37 +302,42 @@ public class NotesPreferenceActivity extends PreferenceActivity { return !name.isEmpty() && !birthday.isEmpty(); } - /** - * 加载账户偏好设置项 - */ - 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() { + private void showUserCenterDialog() { + final boolean isLoggedIn = AccountManager.isUserLoggedIn(this); + + String[] items; + if (isLoggedIn) { + items = new String[] { + getString(R.string.menu_logout) + }; + } else { + items = new String[] { + getString(R.string.menu_login), + getString(R.string.menu_register) + }; + } + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.preferences_user_center_title)); + builder.setItems(items, new DialogInterface.OnClickListener() { @Override - public boolean onPreferenceClick(Preference preference) { - // 如果不在同步中才能操作账户设置 - if (!GTaskSyncService.isSyncing()) { - if (TextUtils.isEmpty(defaultAccount)) { - // 首次设置账户,显示选择账户对话框 - showSelectAccountAlertDialog(); - } else { - // 已有账户设置,显示更改账户确认对话框 - showChangeAccountConfirmAlertDialog(); + public void onClick(DialogInterface dialog, int which) { + if (isLoggedIn) { + if (which == 0) { + handleLogout(); } } else { - Toast.makeText(NotesPreferenceActivity.this, - R.string.preferences_toast_cannot_change_account, Toast.LENGTH_SHORT) - .show(); // 同步中不能更改账户 + if (which == 0) { + Intent intent = new Intent(NotesPreferenceActivity.this, LoginActivity.class); + startActivity(intent); + } else if (which == 1) { + Intent intent = new Intent(NotesPreferenceActivity.this, RegisterActivity.class); + startActivity(intent); + } } - return true; } }); - + builder.show(); // 添加修改密保问题的选项 Preference securityPref = new Preference(this); securityPref.setTitle("修改密保问题"); @@ -470,294 +463,193 @@ public class NotesPreferenceActivity extends PreferenceActivity { builder.show(); } - /** - * 加载同步按钮和同步状态显示 - */ - 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() { - @Override - public void onClick(View v) { - GTaskSyncService.cancelSync(NotesPreferenceActivity.this); // 取消同步 - } - }); + private void handleLogout() { + if (AccountManager.logout(this)) { + Toast.makeText(this, R.string.toast_logout_success, Toast.LENGTH_SHORT).show(); + loadUserCenterPreference(); } else { - syncButton.setText(getString(R.string.preferences_button_sync_immediately)); - syncButton.setOnClickListener(new View.OnClickListener() { - @Override - 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); // 从未同步过 - } + Toast.makeText(this, R.string.error_operation_failed, Toast.LENGTH_SHORT).show(); } } - /** - * 刷新整个设置界面 - */ - private void refreshUI() { - loadAccountPreference(); - loadSyncButton(); - } + private void showSecuritySettingsDialog() { + final boolean passwordSet = PasswordManager.isPasswordSet(this); - /** - * 显示选择账户的对话框 - */ - 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); // 不显示确定按钮 - - 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() { - @Override - public void onClick(DialogInterface dialog, int which) { - // 选择账户并保存 - setSyncAccount(itemMapping[which].toString()); - dialog.dismiss(); - refreshUI(); // 刷新界面 - } - }); + String[] items; + if (passwordSet) { + items = new String[] { + getString(R.string.menu_change_password), + getString(R.string.menu_clear_password) + }; + } else { + items = new String[] { + getString(R.string.menu_set_password) + }; } - // 添加"添加账户"选项 - 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() { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.preferences_security_title)); + builder.setItems(items, new DialogInterface.OnClickListener() { @Override - 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" // 限制为Google账户 - }); - startActivityForResult(intent, -1); - dialog.dismiss(); // 关闭对话框 + public void onClick(DialogInterface dialog, int which) { + if (passwordSet) { + if (which == 0) { + showChangePasswordDialog(); + } else if (which == 1) { + showClearPasswordConfirmDialog(); + } + } else { + showSetPasswordDialog(); + } } }); + builder.show(); } - /** - * 显示更改账户的确认对话框 - */ - 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() { + private void showSetPasswordDialog() { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + View view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_text, null); + final EditText etPassword = (EditText) view.findViewById(R.id.et_foler_name); + etPassword.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD); + etPassword.setHint(R.string.hint_login_password); + etPassword.setText(""); + + builder.setTitle(R.string.title_set_password); + builder.setView(view); + builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - if (which == 0) { - showSelectAccountAlertDialog(); // 显示选择账户对话框 - } else if (which == 1) { - removeSyncAccount(); // 移除同步账户 - refreshUI(); // 刷新界面 + String password = etPassword.getText().toString(); + if (password.isEmpty()) { + Toast.makeText(NotesPreferenceActivity.this, R.string.error_password_empty, Toast.LENGTH_SHORT).show(); + return; + } + if (PasswordManager.setPassword(NotesPreferenceActivity.this, password)) { + Toast.makeText(NotesPreferenceActivity.this, R.string.toast_password_set_success, Toast.LENGTH_SHORT).show(); + loadSecurityPreference(); + } else { + Toast.makeText(NotesPreferenceActivity.this, R.string.error_operation_failed, Toast.LENGTH_SHORT).show(); } - // which == 2 取消,不做任何操作 } }); - dialogBuilder.show(); - } - - /** - * 获取所有Google账户 - * @return Google账户数组 - */ - private Account[] getGoogleAccounts() { - AccountManager accountManager = AccountManager.get(this); - return accountManager.getAccountsByType("com.google"); // 获取Google类型账户 + builder.setNegativeButton(android.R.string.cancel, null); + builder.show(); } - /** - * 设置同步账户 - * @param account 账户名称 - */ - private void setSyncAccount(String account) { - // 只有当账户变更时才执行操作 - if (!getSyncAccountName(this).equals(account)) { - SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); - SharedPreferences.Editor editor = settings.edit(); - if (account != null) { - editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, account); // 保存账户名 - } else { - editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, ""); // 清空账户名 - } - editor.commit(); - - // 清除上次同步时间 - setLastSyncTime(this, 0); - - // 在新线程中清除本地与GTask相关的同步信息 - new Thread(new Runnable() { - @Override - public void run() { - ContentValues values = new ContentValues(); - values.put(NoteColumns.GTASK_ID, ""); // 清空GTask ID - values.put(NoteColumns.SYNC_ID, 0); // 重置同步ID - getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null); + private void showChangePasswordDialog() { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + View view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_text, null); + final EditText etPassword = (EditText) view.findViewById(R.id.et_foler_name); + etPassword.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD); + etPassword.setHint(R.string.hint_new_password); + etPassword.setText(""); + + builder.setTitle(R.string.title_change_password); + builder.setView(view); + builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String newPassword = etPassword.getText().toString(); + if (newPassword.isEmpty()) { + Toast.makeText(NotesPreferenceActivity.this, R.string.error_password_empty, Toast.LENGTH_SHORT).show(); + return; } - }).start(); - - Toast.makeText(NotesPreferenceActivity.this, - getString(R.string.preferences_toast_success_set_accout, account), - Toast.LENGTH_SHORT).show(); // 显示设置成功提示 - } + showOldPasswordVerificationDialog(newPassword); + } + }); + builder.setNegativeButton(android.R.string.cancel, null); + builder.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() { + private void showOldPasswordVerificationDialog(final String newPassword) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + View view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_text, null); + final EditText etPassword = (EditText) view.findViewById(R.id.et_foler_name); + etPassword.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD); + etPassword.setHint(R.string.hint_old_password); + etPassword.setText(""); + + builder.setTitle(R.string.title_verify_password); + builder.setView(view); + builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { @Override - public void run() { - ContentValues values = new ContentValues(); - values.put(NoteColumns.GTASK_ID, ""); // 清空GTask ID - values.put(NoteColumns.SYNC_ID, 0); // 重置同步ID - getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null); + public void onClick(DialogInterface dialog, int which) { + String oldPassword = etPassword.getText().toString(); + if (oldPassword.isEmpty()) { + Toast.makeText(NotesPreferenceActivity.this, R.string.error_password_empty, Toast.LENGTH_SHORT).show(); + return; + } + if (PasswordManager.verifyPassword(NotesPreferenceActivity.this, oldPassword)) { + if (PasswordManager.setPassword(NotesPreferenceActivity.this, newPassword)) { + Toast.makeText(NotesPreferenceActivity.this, R.string.toast_password_change_success, Toast.LENGTH_SHORT).show(); + loadSecurityPreference(); + } else { + Toast.makeText(NotesPreferenceActivity.this, R.string.error_operation_failed, Toast.LENGTH_SHORT).show(); + } + } else { + Toast.makeText(NotesPreferenceActivity.this, R.string.error_wrong_password, Toast.LENGTH_SHORT).show(); + } } - }).start(); - } - - /** - * 静态方法:获取同步账户名称 - * @param context 上下文 - * @return 同步账户名称,如果不存在则返回空字符串 - */ - public static String getSyncAccountName(Context context) { - SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, - Context.MODE_PRIVATE); - return settings.getString(PREFERENCE_SYNC_ACCOUNT_NAME, ""); // 默认返回空字符串 - } - - /** - * 静态方法:设置上次同步时间 - * @param context 上下文 - * @param time 同步时间戳 - */ - public static void setLastSyncTime(Context context, long time) { - SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, - Context.MODE_PRIVATE); - SharedPreferences.Editor editor = settings.edit(); - editor.putLong(PREFERENCE_LAST_SYNC_TIME, time); - editor.commit(); + }); + builder.setNegativeButton(android.R.string.cancel, null); + builder.show(); } - /** - * 静态方法:获取上次同步时间 - * @param context 上下文 - * @return 上次同步时间戳,如果不存在则返回0 - */ - public static long getLastSyncTime(Context context) { - SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, - Context.MODE_PRIVATE); - return settings.getLong(PREFERENCE_LAST_SYNC_TIME, 0); // 默认返回0 + private void showClearPasswordConfirmDialog() { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.alert_title_delete)); + builder.setIcon(android.R.drawable.ic_dialog_alert); + builder.setMessage(getString(R.string.message_clear_password_confirm)); + builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + showPasswordDialogForClear(); + } + }); + builder.setNegativeButton(android.R.string.cancel, null); + builder.show(); } - /** - * 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)); + private void showPasswordDialogForClear() { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + View view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_text, null); + final EditText etPassword = (EditText) view.findViewById(R.id.et_foler_name); + etPassword.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD); + etPassword.setHint(R.string.hint_enter_password); + etPassword.setText(""); + + builder.setTitle(R.string.title_verify_password); + builder.setView(view); + builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String password = etPassword.getText().toString(); + if (password.isEmpty()) { + Toast.makeText(NotesPreferenceActivity.this, R.string.error_password_empty, Toast.LENGTH_SHORT).show(); + return; + } + if (PasswordManager.verifyPassword(NotesPreferenceActivity.this, password)) { + if (PasswordManager.clearPassword(NotesPreferenceActivity.this)) { + Toast.makeText(NotesPreferenceActivity.this, R.string.toast_password_cleared, Toast.LENGTH_SHORT).show(); + loadSecurityPreference(); + } else { + Toast.makeText(NotesPreferenceActivity.this, R.string.error_operation_failed, Toast.LENGTH_SHORT).show(); + } + } else { + Toast.makeText(NotesPreferenceActivity.this, R.string.error_wrong_password, Toast.LENGTH_SHORT).show(); + } } - } + }); + builder.setNegativeButton(android.R.string.cancel, null); + builder.show(); } @Override 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); // 清理Activity栈 + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); return true; default: diff --git a/src/notes/ui/RegisterActivity.java b/src/notes/ui/RegisterActivity.java new file mode 100644 index 0000000..acb14c9 --- /dev/null +++ b/src/notes/ui/RegisterActivity.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may 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.ActionBar; +import android.app.Activity; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.Toast; + +import net.micode.notes.R; +import net.micode.notes.account.AccountManager; +import com.google.android.material.appbar.MaterialToolbar; + +public class RegisterActivity extends Activity { + private EditText etUsername; + private EditText etPassword; + private EditText etConfirmPassword; + private Button btnRegister; + private Button btnCancel; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_register); + + MaterialToolbar toolbar = (MaterialToolbar) findViewById(R.id.toolbar); + if (toolbar != null) { + ActionBar actionBar = getActionBar(); + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setDisplayShowTitleEnabled(false); + } + } + + initViews(); + setupListeners(); + } + + private void initViews() { + etUsername = (EditText) findViewById(R.id.et_register_username); + etPassword = (EditText) findViewById(R.id.et_register_password); + etConfirmPassword = (EditText) findViewById(R.id.et_register_confirm_password); + btnRegister = (Button) findViewById(R.id.btn_register); + btnCancel = (Button) findViewById(R.id.btn_register_cancel); + } + + private void setupListeners() { + btnRegister.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + handleRegister(); + } + }); + + btnCancel.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + finish(); + } + }); + } + + private void handleRegister() { + String username = etUsername.getText().toString().trim(); + String password = etPassword.getText().toString().trim(); + String confirmPassword = etConfirmPassword.getText().toString().trim(); + + if (TextUtils.isEmpty(username)) { + Toast.makeText(this, R.string.error_username_empty, Toast.LENGTH_SHORT).show(); + return; + } + + if (TextUtils.isEmpty(password)) { + Toast.makeText(this, R.string.error_password_empty, Toast.LENGTH_SHORT).show(); + return; + } + + if (!password.equals(confirmPassword)) { + Toast.makeText(this, R.string.error_password_mismatch, Toast.LENGTH_SHORT).show(); + return; + } + + if (AccountManager.isUserExists(this, username)) { + Toast.makeText(this, R.string.error_username_exists, Toast.LENGTH_SHORT).show(); + return; + } + + if (AccountManager.register(this, username, password)) { + Toast.makeText(this, R.string.toast_register_success, Toast.LENGTH_SHORT).show(); + finish(); + } else { + Toast.makeText(this, R.string.error_register_failed, Toast.LENGTH_SHORT).show(); + } + } +} diff --git a/src/res/drawable/ic_add.xml b/src/res/drawable/ic_add.xml new file mode 100644 index 0000000..937e7ff --- /dev/null +++ b/src/res/drawable/ic_add.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/src/res/drawable/ic_alarm.xml b/src/res/drawable/ic_alarm.xml new file mode 100644 index 0000000..a5f498b --- /dev/null +++ b/src/res/drawable/ic_alarm.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/src/res/drawable/ic_lock.xml b/src/res/drawable/ic_lock.xml new file mode 100644 index 0000000..02c610f --- /dev/null +++ b/src/res/drawable/ic_lock.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/src/res/drawable/ic_lock_open.xml b/src/res/drawable/ic_lock_open.xml new file mode 100644 index 0000000..eea5538 --- /dev/null +++ b/src/res/drawable/ic_lock_open.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/src/res/layout/activity_login.xml b/src/res/layout/activity_login.xml new file mode 100644 index 0000000..bd69481 --- /dev/null +++ b/src/res/layout/activity_login.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + +