Compare commits

...

25 Commits
main ... main

Author SHA1 Message Date
pwiz98tyo 8f354efbd5 Update NotesListActivity.java
10 months ago
pwiz98tyo 70d98de187 Update DateTimePickerDialog.java
10 months ago
mxvwfs5gq 5de73e0648 Update NoteEditText.java
10 months ago
mxvwfs5gq 480eadedd3 Update DateTimePicker.java
10 months ago
mxvwfs5gq 8d46cf1709 Update AlarmReceiver.java
10 months ago
mxvwfs5gq 19e602a656 Update AlarmInitReceiver.java
10 months ago
mxvwfs5gq f26168302c Update AlarmInitReceiver.java
10 months ago
mxvwfs5gq fb895eb777 Update NoteWidgetProvider.java
10 months ago
mxvwfs5gq 8f98bc42b8 Update ResourceParser.java
10 months ago
mxvwfs5gq 9d298f4177 Update GTaskStringUtils.java
10 months ago
mxvwfs5gq 9cefd21afc ADD file via upload
10 months ago
pwiz98tyo 746043cf79 Update NoteEditActivity.java
10 months ago
pwiz98tyo 99b64d64b2 Update MainActivity.java
10 months ago
pwiz98tyo 75549c0b7b Update DataUtils.java
10 months ago
pwiz98tyo 5039cd6b33 Update DataUtils.java
10 months ago
pwiz98tyo 3453fe47e3 Update BackupUtils.java
10 months ago
pwiz98tyo 1743880e1c Update WorkingNote.java
10 months ago
pwiz98tyo 4d18192e38 Update Note.java
10 months ago
pwiz98tyo 9285f53048 Update GTaskClient.java
10 months ago
pwiz98tyo 87e34346b2 Update GTaskASyncTask.java
10 months ago
pwiz98tyo 9ee8746e91 Update NetworkFailureException.java
10 months ago
pwiz98tyo d89112878b Update ActionFailureException.java
10 months ago
pwiz98tyo 59bb3a658d Update MetaData.java
10 months ago
pwiz98tyo 1af54fe5ac Update NotesProvider.java
10 months ago
pwiz98tyo d4425b7fc7 Update NotesDatabaseHelper.java
10 months ago

@ -0,0 +1,431 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.tool;
import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.OperationApplicationException;
import android.database.Cursor;
import android.os.RemoteException;
import android.util.Log;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.CallNote;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
/**
* -
* /
*/
public class DataUtils {
private static final String TAG = "DataUtils";
/**
*
*
* @param resolver
* @param ids ID
* @return truefalse
*/
public static boolean batchDeleteNotes(ContentResolver resolver, Set<Long> ids) {
// 参数有效性检查
if (resolver == null) {
Log.w(TAG, "ContentResolver is null");
return false;
}
if (ids == null || ids.isEmpty()) {
Log.d(TAG, "No notes to delete");
return true;
}
ArrayList<ContentProviderOperation> operations = new ArrayList<>(ids.size());
for (long id : ids) {
// 防止删除系统根文件夹
if (id == Notes.ID_ROOT_FOLDER) {
Log.w(TAG, "Skipping system root folder deletion");
continue;
}
// 添加删除操作
operations.add(ContentProviderOperation
.newDelete(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id))
.build());
}
if (operations.isEmpty()) {
Log.d(TAG, "No valid operations to execute");
return true;
}
try {
ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operations);
return results != null && results.length > 0;
} catch (RemoteException | OperationApplicationException e) {
Log.e(TAG, "Batch delete failed", e);
}
return false;
}
/**
*
*
* @param resolver
* @param id ID
* @param srcFolderId ID
* @param desFolderId ID
*/
public static void moveNoteToFolder(ContentResolver resolver, long id, long srcFolderId, long desFolderId) {
if (resolver == null) {
Log.w(TAG, "ContentResolver is null");
return;
}
ContentValues values = new ContentValues();
values.put(NoteColumns.PARENT_ID, desFolderId);
values.put(NoteColumns.ORIGIN_PARENT_ID, srcFolderId);
values.put(NoteColumns.LOCAL_MODIFIED, 1); // 标记为本地已修改
resolver.update(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id), values, null, null);
}
/**
*
*
* @param resolver
* @param ids ID
* @param folderId ID
* @return truefalse
*/
public static boolean batchMoveToFolder(ContentResolver resolver, Set<Long> ids, long folderId) {
if (resolver == null) {
Log.w(TAG, "ContentResolver is null");
return false;
}
if (ids == null || ids.isEmpty()) {
Log.d(TAG, "No notes to move");
return true;
}
ArrayList<ContentProviderOperation> operations = new ArrayList<>(ids.size());
for (long id : ids) {
operations.add(ContentProviderOperation
.newUpdate(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id))
.withValue(NoteColumns.PARENT_ID, folderId)
.withValue(NoteColumns.LOCAL_MODIFIED, 1)
.build());
}
try {
ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operations);
return results != null && results.length > 0;
} catch (RemoteException | OperationApplicationException e) {
Log.e(TAG, "Batch move failed", e);
}
return false;
}
/**
*
*
* @param resolver
* @return
*/
public static int getUserFolderCount(ContentResolver resolver) {
if (resolver == null) {
Log.w(TAG, "ContentResolver is null");
return 0;
}
String[] projection = {NoteColumns.ID};
String selection = NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>?";
String[] selectionArgs = {
String.valueOf(Notes.TYPE_FOLDER),
String.valueOf(Notes.ID_TRASH_FOLDER) // 修正拼写错误FOLER -> FOLDER
};
try (Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI,
projection, selection, selectionArgs, null)) {
return cursor != null ? cursor.getCount() : 0;
}
}
/**
*
*
* @param resolver
* @param noteId ID
* @param type
* @return true
*/
public static boolean isNoteVisible(ContentResolver resolver, long noteId, int type) {
if (resolver == null) return false;
String selection = NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>?";
String[] selectionArgs = {
String.valueOf(type),
String.valueOf(Notes.ID_TRASH_FOLDER)
};
try (Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId),
null, selection, selectionArgs, null)) {
return cursor != null && cursor.getCount() > 0;
}
}
/**
*
*
* @param resolver
* @param noteId ID
* @return true
*/
public static boolean doesNoteExist(ContentResolver resolver, long noteId) {
if (resolver == null) return false;
try (Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId),
null, null, null, null)) {
return cursor != null && cursor.getCount() > 0;
}
}
/**
*
*
* @param resolver
* @param dataId ID
* @return true
*/
public static boolean doesDataExist(ContentResolver resolver, long dataId) {
if (resolver == null) return false;
try (Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId),
null, null, null, null)) {
return cursor != null && cursor.getCount() > 0;
}
}
/**
*
*
* @param resolver
* @param name
* @return true
*/
public static boolean isFolderNameExist(ContentResolver resolver, String name) {
if (resolver == null || TextUtils.isEmpty(name)) return false;
String selection = NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER +
" AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLDER +
" AND " + NoteColumns.SNIPPET + "=?";
String[] selectionArgs = { name };
try (Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI,
null, selection, selectionArgs, null)) {
return cursor != null && cursor.getCount() > 0;
}
}
/**
*
*
* @param resolver
* @param folderId ID
* @return
*/
public static Set<AppWidgetAttribute> getFolderWidgets(ContentResolver resolver, long folderId) {
Set<AppWidgetAttribute> widgets = new HashSet<>();
if (resolver == null) return widgets;
String[] projection = { NoteColumns.WIDGET_ID, NoteColumns.WIDGET_TYPE };
String selection = NoteColumns.PARENT_ID + "=?";
String[] selectionArgs = { String.valueOf(folderId) };
try (Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI,
projection, selection, selectionArgs, null)) {
if (cursor != null && cursor.moveToFirst()) {
do {
try {
AppWidgetAttribute widget = new AppWidgetAttribute();
widget.widgetId = cursor.getInt(0);
widget.widgetType = cursor.getInt(1);
widgets.add(widget);
} catch (Exception e) {
Log.e(TAG, "Error reading widget attributes", e);
}
} while (cursor.moveToNext());
}
}
return widgets;
}
/**
* ID
*
* @param resolver
* @param noteId ID
* @return
*/
public static String getCallNumber(ContentResolver resolver, long noteId) {
if (resolver == null) return "";
String[] projection = { CallNote.PHONE_NUMBER };
String selection = CallNote.NOTE_ID + "=? AND " + CallNote.MIME_TYPE + "=?";
String[] selectionArgs = {
String.valueOf(noteId),
CallNote.CONTENT_ITEM_TYPE
};
try (Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI,
projection, selection, selectionArgs, null)) {
if (cursor != null && cursor.moveToFirst()) {
return cursor.getString(0);
}
} catch (Exception e) {
Log.e(TAG, "Error getting call number", e);
}
return "";
}
/**
* ID
*
* @param resolver
* @param phoneNumber
* @param callDate
* @return ID0
*/
public static long findNoteByCallDetails(ContentResolver resolver, String phoneNumber, long callDate) {
if (resolver == null || TextUtils.isEmpty(phoneNumber)) return 0;
String[] projection = { CallNote.NOTE_ID };
String selection = CallNote.CALL_DATE + "=? AND " + CallNote.MIME_TYPE + "=? AND " +
"PHONE_NUMBERS_EQUAL(" + CallNote.PHONE_NUMBER + ", ?)";
String[] selectionArgs = {
String.valueOf(callDate),
CallNote.CONTENT_ITEM_TYPE,
phoneNumber
};
try (Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI,
projection, selection, selectionArgs, null)) {
if (cursor != null && cursor.moveToFirst()) {
return cursor.getLong(0);
}
} catch (Exception e) {
Log.e(TAG, "Error finding note by call details", e);
}
return 0;
}
/**
*
*
* @param resolver
* @param noteId ID
* @return
* @throws IllegalArgumentException
*/
public static String getNoteSnippet(ContentResolver resolver, long noteId) {
if (resolver == null) throw new IllegalArgumentException("Resolver is null");
String[] projection = { NoteColumns.SNIPPET };
String selection = NoteColumns.ID + "=?";
String[] selectionArgs = { String.valueOf(noteId) };
try (Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI,
projection, selection, selectionArgs, null)) {
if (cursor != null && cursor.moveToFirst()) {
return cursor.getString(0);
}
throw new IllegalArgumentException("Note not found with id: " + noteId);
}
}
/**
*
*
* @param snippet
* @return
*/
public static String formatSnippet(String snippet) {
if (snippet == null) return null;
String formatted = snippet.trim();
int newlineIndex = formatted.indexOf('\n');
return (newlineIndex != -1) ? formatted.substring(0, newlineIndex) : formatted;
}
/* ----------------- 新增功能 ----------------- */
/**
*
*
* @param resolver
* @param noteId ID
* @return true
*/
public static boolean safeDeleteNote(ContentResolver resolver, long noteId) {
Set<Long> ids = new HashSet<>(1);
ids.add(noteId);
return batchDeleteNotes(resolver, ids);
}
/**
*
*
* @param resolver
* @param ids ID
* @param modified (1-, 0-)
* @return true
*/
public static boolean batchMarkModified(ContentResolver resolver, Set<Long> ids, int modified) {
if (resolver == null) return false;
ArrayList<ContentProviderOperation> operations = new ArrayList<>(ids.size());
for (long id : ids) {
operations.add(ContentProviderOperation
.newUpdate(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id))
.withValue(NoteColumns.LOCAL_MODIFIED, modified)
.build());
}
try {
resolver.applyBatch(Notes.AUTHORITY, operations);
return true;
} catch (Exception e) {
Log.e(TAG, "Batch mark modified failed", e);
}
return false;
}
}

@ -13,10 +13,16 @@ public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 启用全屏显示,内容可以延伸到屏幕边缘
EdgeToEdge.enable(this);
// 设置主布局
setContentView(R.layout.activity_main);
// 设置窗口插入监听器,处理系统状态栏和导航栏的空间
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
// 获取系统状态栏和导航栏的尺寸
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
// 设置视图的内边距确保内容不会被系统UI遮挡
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});

@ -1,19 +1,3 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.data;
import android.content.ContentValues;
@ -26,80 +10,80 @@ import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.DataConstants;
import net.micode.notes.data.Notes.NoteColumns;
/**
*
* 使
*/
public class NotesDatabaseHelper extends SQLiteOpenHelper {
private static final String DB_NAME = "note.db";
private static final int DB_VERSION = 4;
private static final String DB_NAME = "note.db"; // 数据库名称
private static final int DB_VERSION = 4; // 当前数据库版本
private static final String TAG = "NotesDatabaseHelper"; // 日志标签
// 表名常量接口
public interface TABLE {
public static final String NOTE = "note";
public static final String DATA = "data";
public static final String NOTE = "note"; // 笔记表
public static final String DATA = "data"; // 数据表
}
private static final String TAG = "NotesDatabaseHelper";
private static NotesDatabaseHelper mInstance;
private static final String CREATE_NOTE_TABLE_SQL =
"CREATE TABLE " + TABLE.NOTE + "(" +
NoteColumns.ID + " INTEGER PRIMARY KEY," +
NoteColumns.PARENT_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.ALERTED_DATE + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.BG_COLOR_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
NoteColumns.HAS_ATTACHMENT + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
NoteColumns.NOTES_COUNT + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.SNIPPET + " TEXT NOT NULL DEFAULT ''," +
NoteColumns.TYPE + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.WIDGET_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.WIDGET_TYPE + " INTEGER NOT NULL DEFAULT -1," +
NoteColumns.SYNC_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.LOCAL_MODIFIED + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.ORIGIN_PARENT_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''," +
NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0" +
// 单例实例
private static NotesDatabaseHelper sInstance;
// 创建笔记表的SQL语句
private static final String CREATE_NOTE_TABLE_SQL =
"CREATE TABLE " + TABLE.NOTE + " (\n" +
" " + NoteColumns.ID + " INTEGER PRIMARY KEY,\n" +
" " + NoteColumns.PARENT_ID + " INTEGER NOT NULL DEFAULT 0,\n" +
" " + NoteColumns.ALERTED_DATE + " INTEGER NOT NULL DEFAULT 0,\n" +
" " + NoteColumns.BG_COLOR_ID + " INTEGER NOT NULL DEFAULT 0,\n" +
" " + NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000),\n" +
" " + NoteColumns.HAS_ATTACHMENT + " INTEGER NOT NULL DEFAULT 0,\n" +
" " + NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000),\n" +
" " + NoteColumns.NOTES_COUNT + " INTEGER NOT NULL DEFAULT 0,\n" +
" " + NoteColumns.SNIPPET + " TEXT NOT NULL DEFAULT '',\n" +
" " + NoteColumns.TYPE + " INTEGER NOT NULL DEFAULT 0,\n" +
" " + NoteColumns.WIDGET_ID + " INTEGER NOT NULL DEFAULT 0,\n" +
" " + NoteColumns.WIDGET_TYPE + " INTEGER NOT NULL DEFAULT -1,\n" +
" " + NoteColumns.SYNC_ID + " INTEGER NOT NULL DEFAULT 0,\n" +
" " + NoteColumns.LOCAL_MODIFIED + " INTEGER NOT NULL DEFAULT 0,\n" +
" " + NoteColumns.ORIGIN_PARENT_ID + " INTEGER NOT NULL DEFAULT 0,\n" +
" " + NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT '',\n" +
" " + NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0\n" +
")";
private static final String CREATE_DATA_TABLE_SQL =
"CREATE TABLE " + TABLE.DATA + "(" +
DataColumns.ID + " INTEGER PRIMARY KEY," +
DataColumns.MIME_TYPE + " TEXT NOT NULL," +
DataColumns.NOTE_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
DataColumns.CONTENT + " TEXT NOT NULL DEFAULT ''," +
DataColumns.DATA1 + " INTEGER," +
DataColumns.DATA2 + " INTEGER," +
DataColumns.DATA3 + " TEXT NOT NULL DEFAULT ''," +
DataColumns.DATA4 + " TEXT NOT NULL DEFAULT ''," +
DataColumns.DATA5 + " TEXT NOT NULL DEFAULT ''" +
// 创建数据表的SQL语句
private static final String CREATE_DATA_TABLE_SQL =
"CREATE TABLE " + TABLE.DATA + " (\n" +
" " + DataColumns.ID + " INTEGER PRIMARY KEY,\n" +
" " + DataColumns.MIME_TYPE + " TEXT NOT NULL,\n" +
" " + DataColumns.NOTE_ID + " INTEGER NOT NULL DEFAULT 0,\n" +
" " + NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000),\n" +
" " + NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000),\n" +
" " + DataColumns.CONTENT + " TEXT NOT NULL DEFAULT '',\n" +
" " + DataColumns.DATA1 + " INTEGER,\n" +
" " + DataColumns.DATA2 + " INTEGER,\n" +
" " + DataColumns.DATA3 + " TEXT NOT NULL DEFAULT '',\n" +
" " + DataColumns.DATA4 + " TEXT NOT NULL DEFAULT '',\n" +
" " + DataColumns.DATA5 + " TEXT NOT NULL DEFAULT ''\n" +
")";
// 创建数据表索引的SQL语句
private static final String CREATE_DATA_NOTE_ID_INDEX_SQL =
"CREATE INDEX IF NOT EXISTS note_id_index ON " +
TABLE.DATA + "(" + DataColumns.NOTE_ID + ");";
"CREATE INDEX IF NOT EXISTS note_id_index ON " + TABLE.DATA +
"(" + DataColumns.NOTE_ID + ");";
/**
* Increase folder's note count when move note to the folder
*/
// 文件夹计数更新触发器
private static final String NOTE_INCREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER =
"CREATE TRIGGER increase_folder_count_on_update "+
" AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE +
"CREATE TRIGGER increase_folder_count_on_update " +
"AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE +
" BEGIN " +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" +
" WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" +
" END";
/**
* Decrease folder's note count when move note from folder
*/
private static final String NOTE_DECREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER =
"CREATE TRIGGER decrease_folder_count_on_update " +
" AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE +
"AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE +
" BEGIN " +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" +
@ -107,24 +91,18 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
" AND " + NoteColumns.NOTES_COUNT + ">0" + ";" +
" END";
/**
* Increase folder's note count when insert new note to the folder
*/
private static final String NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER =
"CREATE TRIGGER increase_folder_count_on_insert " +
" AFTER INSERT ON " + TABLE.NOTE +
"AFTER INSERT ON " + TABLE.NOTE +
" BEGIN " +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" +
" WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" +
" END";
/**
* Decrease folder's note count when delete note from the folder
*/
private static final String NOTE_DECREASE_FOLDER_COUNT_ON_DELETE_TRIGGER =
"CREATE TRIGGER decrease_folder_count_on_delete " +
" AFTER DELETE ON " + TABLE.NOTE +
"AFTER DELETE ON " + TABLE.NOTE +
" BEGIN " +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" +
@ -132,12 +110,10 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
" AND " + NoteColumns.NOTES_COUNT + ">0;" +
" END";
/**
* Update note's content when insert data with type {@link DataConstants#NOTE}
*/
// 笔记内容同步触发器
private static final String DATA_UPDATE_NOTE_CONTENT_ON_INSERT_TRIGGER =
"CREATE TRIGGER update_note_content_on_insert " +
" AFTER INSERT ON " + TABLE.DATA +
"AFTER INSERT ON " + TABLE.DATA +
" WHEN new." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" +
" BEGIN" +
" UPDATE " + TABLE.NOTE +
@ -145,12 +121,9 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
" WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" +
" END";
/**
* Update note's content when data with {@link DataConstants#NOTE} type has changed
*/
private static final String DATA_UPDATE_NOTE_CONTENT_ON_UPDATE_TRIGGER =
"CREATE TRIGGER update_note_content_on_update " +
" AFTER UPDATE ON " + TABLE.DATA +
"AFTER UPDATE ON " + TABLE.DATA +
" WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" +
" BEGIN" +
" UPDATE " + TABLE.NOTE +
@ -158,12 +131,9 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
" WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" +
" END";
/**
* Update note's content when data with {@link DataConstants#NOTE} type has deleted
*/
private static final String DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER =
"CREATE TRIGGER update_note_content_on_delete " +
" AFTER delete ON " + TABLE.DATA +
"AFTER delete ON " + TABLE.DATA +
" WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" +
" BEGIN" +
" UPDATE " + TABLE.NOTE +
@ -171,61 +141,88 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
" WHERE " + NoteColumns.ID + "=old." + DataColumns.NOTE_ID + ";" +
" END";
/**
* Delete datas belong to note which has been deleted
*/
// 级联删除触发器
private static final String NOTE_DELETE_DATA_ON_DELETE_TRIGGER =
"CREATE TRIGGER delete_data_on_delete " +
" AFTER DELETE ON " + TABLE.NOTE +
"AFTER DELETE ON " + TABLE.NOTE +
" BEGIN" +
" DELETE FROM " + TABLE.DATA +
" WHERE " + DataColumns.NOTE_ID + "=old." + NoteColumns.ID + ";" +
" END";
/**
* Delete notes belong to folder which has been deleted
*/
private static final String FOLDER_DELETE_NOTES_ON_DELETE_TRIGGER =
"CREATE TRIGGER folder_delete_notes_on_delete " +
" AFTER DELETE ON " + TABLE.NOTE +
"AFTER DELETE ON " + TABLE.NOTE +
" WHEN old." + NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER +
" BEGIN" +
" DELETE FROM " + TABLE.NOTE +
" WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" +
" END";
/**
* Move notes belong to folder which has been moved to trash folder
*/
private static final String FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER =
"CREATE TRIGGER folder_move_notes_on_trash " +
" AFTER UPDATE ON " + TABLE.NOTE +
"AFTER UPDATE ON " + TABLE.NOTE +
" WHEN new." + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER +
" AND old." + NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER +
" BEGIN" +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER +
" WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" +
" END";
public NotesDatabaseHelper(Context context) {
/**
*
* 使线
*/
public static synchronized NotesDatabaseHelper getInstance(Context context) {
if (sInstance == null) {
sInstance = new NotesDatabaseHelper(context.getApplicationContext());
}
return sInstance;
}
/**
*
*/
private NotesDatabaseHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
public void createNoteTable(SQLiteDatabase db) {
db.execSQL(CREATE_NOTE_TABLE_SQL);
reCreateNoteTableTriggers(db);
createSystemFolder(db);
Log.d(TAG, "note table has been created");
/**
*
*/
private void createNoteTable(SQLiteDatabase db) {
try {
db.execSQL(CREATE_NOTE_TABLE_SQL);
reCreateNoteTableTriggers(db);
createSystemFolder(db);
Log.d(TAG, "Note table created successfully");
} catch (Exception e) {
Log.e(TAG, "Failed to create note table: " + e.getMessage(), e);
throw e;
}
}
/**
*
*/
private void reCreateNoteTableTriggers(SQLiteDatabase db) {
db.execSQL("DROP TRIGGER IF EXISTS increase_folder_count_on_update");
db.execSQL("DROP TRIGGER IF EXISTS decrease_folder_count_on_update");
db.execSQL("DROP TRIGGER IF EXISTS decrease_folder_count_on_delete");
db.execSQL("DROP TRIGGER IF EXISTS delete_data_on_delete");
db.execSQL("DROP TRIGGER IF EXISTS increase_folder_count_on_insert");
db.execSQL("DROP TRIGGER IF EXISTS folder_delete_notes_on_delete");
db.execSQL("DROP TRIGGER IF EXISTS folder_move_notes_on_trash");
// 删除旧触发器
String[] noteTriggers = {
"increase_folder_count_on_update",
"decrease_folder_count_on_update",
"decrease_folder_count_on_delete",
"delete_data_on_delete",
"increase_folder_count_on_insert",
"folder_delete_notes_on_delete",
"folder_move_notes_on_trash"
};
for (String trigger : noteTriggers) {
db.execSQL("DROP TRIGGER IF EXISTS " + trigger);
}
// 创建新触发器
db.execSQL(NOTE_INCREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER);
db.execSQL(NOTE_DECREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER);
db.execSQL(NOTE_DECREASE_FOLDER_COUNT_ON_DELETE_TRIGGER);
@ -235,128 +232,216 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
db.execSQL(FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER);
}
/**
*
*/
private void createSystemFolder(SQLiteDatabase db) {
ContentValues values = new ContentValues();
/**
* call record foler for call notes
*/
// 创建通话记录文件夹
values.put(NoteColumns.ID, Notes.ID_CALL_RECORD_FOLDER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
/**
* root folder which is default folder
*/
// 创建根文件夹
values.clear();
values.put(NoteColumns.ID, Notes.ID_ROOT_FOLDER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
/**
* temporary folder which is used for moving note
*/
// 创建临时文件夹
values.clear();
values.put(NoteColumns.ID, Notes.ID_TEMPARAY_FOLDER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
/**
* create trash folder
*/
// 创建回收站文件夹
values.clear();
values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
Log.d(TAG, "System folders created successfully");
}
public void createDataTable(SQLiteDatabase db) {
db.execSQL(CREATE_DATA_TABLE_SQL);
reCreateDataTableTriggers(db);
db.execSQL(CREATE_DATA_NOTE_ID_INDEX_SQL);
Log.d(TAG, "data table has been created");
/**
*
*/
private void createDataTable(SQLiteDatabase db) {
try {
db.execSQL(CREATE_DATA_TABLE_SQL);
reCreateDataTableTriggers(db);
db.execSQL(CREATE_DATA_NOTE_ID_INDEX_SQL);
Log.d(TAG, "Data table created successfully");
} catch (Exception e) {
Log.e(TAG, "Failed to create data table: " + e.getMessage(), e);
throw e;
}
}
/**
*
*/
private void reCreateDataTableTriggers(SQLiteDatabase db) {
db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_insert");
db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_update");
db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_delete");
// 删除旧触发器
String[] dataTriggers = {
"update_note_content_on_insert",
"update_note_content_on_update",
"update_note_content_on_delete"
};
for (String trigger : dataTriggers) {
db.execSQL("DROP TRIGGER IF EXISTS " + trigger);
}
// 创建新触发器
db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_INSERT_TRIGGER);
db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_UPDATE_TRIGGER);
db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER);
}
static synchronized NotesDatabaseHelper getInstance(Context context) {
if (mInstance == null) {
mInstance = new NotesDatabaseHelper(context);
}
return mInstance;
}
/**
*
*/
@Override
public void onCreate(SQLiteDatabase db) {
createNoteTable(db);
createDataTable(db);
Log.d(TAG, "Creating database version " + DB_VERSION);
// 使用事务提高性能
db.beginTransaction();
try {
createNoteTable(db);
createDataTable(db);
db.setTransactionSuccessful();
Log.d(TAG, "Database created successfully");
} catch (Exception e) {
Log.e(TAG, "Failed to create database: " + e.getMessage(), e);
throw e;
} finally {
db.endTransaction();
}
}
/**
*
*/
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
boolean reCreateTriggers = false;
boolean skipV2 = false;
if (oldVersion == 1) {
upgradeToV2(db);
skipV2 = true; // this upgrade including the upgrade from v2 to v3
oldVersion++;
}
if (oldVersion == 2 && !skipV2) {
upgradeToV3(db);
reCreateTriggers = true;
oldVersion++;
}
if (oldVersion == 3) {
upgradeToV4(db);
oldVersion++;
}
if (reCreateTriggers) {
reCreateNoteTableTriggers(db);
reCreateDataTableTriggers(db);
Log.d(TAG, "Upgrading database from version " + oldVersion + " to " + newVersion);
// 使用事务确保升级过程的原子性
db.beginTransaction();
try {
for (int version = oldVersion; version < newVersion; version++) {
upgradeToVersion(db, version);
}
db.setTransactionSuccessful();
Log.d(TAG, "Database upgraded successfully");
} catch (Exception e) {
Log.e(TAG, "Failed to upgrade database: " + e.getMessage(), e);
throw e;
} finally {
db.endTransaction();
}
}
if (oldVersion != newVersion) {
throw new IllegalStateException("Upgrade notes database to version " + newVersion
+ "fails");
/**
*
*/
private void upgradeToVersion(SQLiteDatabase db, int currentVersion) {
switch (currentVersion) {
case 1:
upgradeFromV1ToV2(db);
break;
case 2:
upgradeFromV2ToV3(db);
break;
case 3:
upgradeFromV3ToV4(db);
break;
default:
throw new IllegalStateException("Unknown database version: " + currentVersion);
}
}
private void upgradeToV2(SQLiteDatabase db) {
/**
* 12
*/
private void upgradeFromV1ToV2(SQLiteDatabase db) {
Log.d(TAG, "Upgrading from version 1 to 2");
// 版本1到版本2的升级包含了版本2到版本3的升级
db.execSQL("DROP TABLE IF EXISTS " + TABLE.NOTE);
db.execSQL("DROP TABLE IF EXISTS " + TABLE.DATA);
createNoteTable(db);
createDataTable(db);
Log.d(TAG, "Upgraded to version 2 successfully");
}
private void upgradeToV3(SQLiteDatabase db) {
// drop unused triggers
db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_insert");
db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_delete");
db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_update");
// add a column for gtask id
/**
* 23
*/
private void upgradeFromV2ToV3(SQLiteDatabase db) {
Log.d(TAG, "Upgrading from version 2 to 3");
// 删除旧触发器
String[] oldTriggers = {
"update_note_modified_date_on_insert",
"update_note_modified_date_on_delete",
"update_note_modified_date_on_update"
};
for (String trigger : oldTriggers) {
db.execSQL("DROP TRIGGER IF EXISTS " + trigger);
}
// 添加新列
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.GTASK_ID
+ " TEXT NOT NULL DEFAULT ''");
// add a trash system folder
// 添加回收站系统文件夹
ContentValues values = new ContentValues();
values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
Log.d(TAG, "Upgraded to version 3 successfully");
}
private void upgradeToV4(SQLiteDatabase db) {
/**
* 34
*/
private void upgradeFromV3ToV4(SQLiteDatabase db) {
Log.d(TAG, "Upgrading from version 3 to 4");
// 添加版本列
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.VERSION
+ " INTEGER NOT NULL DEFAULT 0");
Log.d(TAG, "Upgraded to version 4 successfully");
}
/**
*
*/
@Override
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.w(TAG, "Downgrading database from version " + oldVersion + " to " + newVersion);
// 降级策略:删除所有表并重新创建
db.beginTransaction();
try {
db.execSQL("DROP TABLE IF EXISTS " + TABLE.NOTE);
db.execSQL("DROP TABLE IF EXISTS " + TABLE.DATA);
onCreate(db);
db.setTransactionSuccessful();
Log.d(TAG, "Database downgraded successfully");
} catch (Exception e) {
Log.e(TAG, "Failed to downgrade database: " + e.getMessage(), e);
throw e;
} finally {
db.endTransaction();
}
}
}

@ -16,7 +16,6 @@
package net.micode.notes.data;
import android.app.SearchManager;
import android.content.ContentProvider;
import android.content.ContentUris;
@ -34,37 +33,32 @@ import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.NotesDatabaseHelper.TABLE;
public class NotesProvider extends ContentProvider {
private static final UriMatcher mMatcher;
private NotesDatabaseHelper mHelper;
private static final String TAG = "NotesProvider";
private static final UriMatcher mMatcher; // URI匹配器用于识别不同的URI
private NotesDatabaseHelper mHelper; // 数据库帮助类实例
private static final String TAG = "NotesProvider"; // 日志标签
private static final int URI_NOTE = 1;
private static final int URI_NOTE_ITEM = 2;
private static final int URI_DATA = 3;
private static final int URI_DATA_ITEM = 4;
private static final int URI_SEARCH = 5;
private static final int URI_SEARCH_SUGGEST = 6;
// URI匹配常量
private static final int URI_NOTE = 1; // 笔记集合URI
private static final int URI_NOTE_ITEM = 2; // 单个笔记URI
private static final int URI_DATA = 3; // 数据集合URI
private static final int URI_DATA_ITEM = 4; // 单个数据URI
private static final int URI_SEARCH = 5; // 搜索URI
private static final int URI_SEARCH_SUGGEST = 6; // 搜索建议URI
// 静态初始化块设置URI匹配规则
static {
mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
mMatcher.addURI(Notes.AUTHORITY, "note", URI_NOTE);
mMatcher.addURI(Notes.AUTHORITY, "note/#", URI_NOTE_ITEM);
mMatcher.addURI(Notes.AUTHORITY, "data", URI_DATA);
mMatcher.addURI(Notes.AUTHORITY, "data/#", URI_DATA_ITEM);
mMatcher.addURI(Notes.AUTHORITY, "search", URI_SEARCH);
mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, URI_SEARCH_SUGGEST);
mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", URI_SEARCH_SUGGEST);
mMatcher.addURI(Notes.AUTHORITY, "note", URI_NOTE); // 匹配所有笔记
mMatcher.addURI(Notes.AUTHORITY, "note/#", URI_NOTE_ITEM); // 匹配单个笔记
mMatcher.addURI(Notes.AUTHORITY, "data", URI_DATA); // 匹配所有数据
mMatcher.addURI(Notes.AUTHORITY, "data/#", URI_DATA_ITEM); // 匹配单个数据
mMatcher.addURI(Notes.AUTHORITY, "search", URI_SEARCH); // 匹配搜索
mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, URI_SEARCH_SUGGEST); // 匹配搜索建议
mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", URI_SEARCH_SUGGEST); // 匹配带参数的搜索建议
}
/**
* x'0A' represents the '\n' character in sqlite. For title and content in the search result,
* we will trim '\n' and white space in order to show more information.
*/
// 搜索投影和查询SQL
private static final String NOTES_SEARCH_PROJECTION = NoteColumns.ID + ","
+ NoteColumns.ID + " AS " + SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA + ","
+ "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_1 + ","
@ -79,42 +73,44 @@ public class NotesProvider extends ContentProvider {
+ " AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER
+ " AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE;
// 初始化提供者
@Override
public boolean onCreate() {
mHelper = NotesDatabaseHelper.getInstance(getContext());
return true;
}
// 查询数据
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
Cursor c = null;
SQLiteDatabase db = mHelper.getReadableDatabase();
String id = null;
// 根据URI匹配类型执行不同的查询
switch (mMatcher.match(uri)) {
case URI_NOTE:
c = db.query(TABLE.NOTE, projection, selection, selectionArgs, null, null,
sortOrder);
c = db.query(TABLE.NOTE, projection, selection, selectionArgs, null, null, sortOrder);
break;
case URI_NOTE_ITEM:
id = uri.getPathSegments().get(1);
c = db.query(TABLE.NOTE, projection, NoteColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs, null, null, sortOrder);
c = db.query(TABLE.NOTE, projection, NoteColumns.ID + "=" + id + parseSelection(selection),
selectionArgs, null, null, sortOrder);
break;
case URI_DATA:
c = db.query(TABLE.DATA, projection, selection, selectionArgs, null, null,
sortOrder);
c = db.query(TABLE.DATA, projection, selection, selectionArgs, null, null, sortOrder);
break;
case URI_DATA_ITEM:
id = uri.getPathSegments().get(1);
c = db.query(TABLE.DATA, projection, DataColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs, null, null, sortOrder);
c = db.query(TABLE.DATA, projection, DataColumns.ID + "=" + id + parseSelection(selection),
selectionArgs, null, null, sortOrder);
break;
case URI_SEARCH:
case URI_SEARCH_SUGGEST:
// 处理搜索请求
if (sortOrder != null || projection != null) {
throw new IllegalArgumentException(
"do not specify sortOrder, selection, selectionArgs, or projection" + "with this query");
throw new IllegalArgumentException("不支持指定排序或投影");
}
String searchString = null;
@ -132,25 +128,29 @@ public class NotesProvider extends ContentProvider {
try {
searchString = String.format("%%%s%%", searchString);
c = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY,
new String[] { searchString });
c = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY, new String[] { searchString });
} catch (IllegalStateException ex) {
Log.e(TAG, "got exception: " + ex.toString());
Log.e(TAG, "查询异常: " + ex.toString());
}
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
throw new IllegalArgumentException("未知URI: " + uri);
}
// 设置内容观察者通知
if (c != null) {
c.setNotificationUri(getContext().getContentResolver(), uri);
}
return c;
}
// 插入数据
@Override
public Uri insert(Uri uri, ContentValues values) {
SQLiteDatabase db = mHelper.getWritableDatabase();
long dataId = 0, noteId = 0, insertedId = 0;
// 根据URI匹配类型执行不同的插入操作
switch (mMatcher.match(uri)) {
case URI_NOTE:
insertedId = noteId = db.insert(TABLE.NOTE, null, values);
@ -159,20 +159,20 @@ public class NotesProvider extends ContentProvider {
if (values.containsKey(DataColumns.NOTE_ID)) {
noteId = values.getAsLong(DataColumns.NOTE_ID);
} else {
Log.d(TAG, "Wrong data format without note id:" + values.toString());
Log.d(TAG, "数据格式错误缺少笔记ID: " + values.toString());
}
insertedId = dataId = db.insert(TABLE.DATA, null, values);
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
throw new IllegalArgumentException("未知URI: " + uri);
}
// Notify the note uri
// 通知数据变化
if (noteId > 0) {
getContext().getContentResolver().notifyChange(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null);
}
// Notify the data uri
if (dataId > 0) {
getContext().getContentResolver().notifyChange(
ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), null);
@ -181,12 +181,15 @@ public class NotesProvider extends ContentProvider {
return ContentUris.withAppendedId(uri, insertedId);
}
// 删除数据
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
int count = 0;
String id = null;
SQLiteDatabase db = mHelper.getWritableDatabase();
boolean deleteData = false;
// 根据URI匹配类型执行不同的删除操作
switch (mMatcher.match(uri)) {
case URI_NOTE:
selection = "(" + selection + ") AND " + NoteColumns.ID + ">0 ";
@ -194,10 +197,7 @@ public class NotesProvider extends ContentProvider {
break;
case URI_NOTE_ITEM:
id = uri.getPathSegments().get(1);
/**
* ID that smaller than 0 is system folder which is not allowed to
* trash
*/
// 系统文件夹不允许删除
long noteId = Long.valueOf(id);
if (noteId <= 0) {
break;
@ -216,8 +216,10 @@ public class NotesProvider extends ContentProvider {
deleteData = true;
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
throw new IllegalArgumentException("未知URI: " + uri);
}
// 通知数据变化
if (count > 0) {
if (deleteData) {
getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null);
@ -227,12 +229,15 @@ public class NotesProvider extends ContentProvider {
return count;
}
// 更新数据
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
int count = 0;
String id = null;
SQLiteDatabase db = mHelper.getWritableDatabase();
boolean updateData = false;
// 根据URI匹配类型执行不同的更新操作
switch (mMatcher.match(uri)) {
case URI_NOTE:
increaseNoteVersion(-1, selection, selectionArgs);
@ -241,8 +246,8 @@ public class NotesProvider extends ContentProvider {
case URI_NOTE_ITEM:
id = uri.getPathSegments().get(1);
increaseNoteVersion(Long.valueOf(id), selection, selectionArgs);
count = db.update(TABLE.NOTE, values, NoteColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs);
count = db.update(TABLE.NOTE, values, NoteColumns.ID + "=" + id + parseSelection(selection),
selectionArgs);
break;
case URI_DATA:
count = db.update(TABLE.DATA, values, selection, selectionArgs);
@ -250,14 +255,15 @@ public class NotesProvider extends ContentProvider {
break;
case URI_DATA_ITEM:
id = uri.getPathSegments().get(1);
count = db.update(TABLE.DATA, values, DataColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs);
count = db.update(TABLE.DATA, values, DataColumns.ID + "=" + id + parseSelection(selection),
selectionArgs);
updateData = true;
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
throw new IllegalArgumentException("未知URI: " + uri);
}
// 通知数据变化
if (count > 0) {
if (updateData) {
getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null);
@ -267,10 +273,12 @@ public class NotesProvider extends ContentProvider {
return count;
}
// 解析查询条件
private String parseSelection(String selection) {
return (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : "");
}
// 增加笔记版本号
private void increaseNoteVersion(long id, String selection, String[] selectionArgs) {
StringBuilder sql = new StringBuilder(120);
sql.append("UPDATE ");
@ -296,10 +304,37 @@ public class NotesProvider extends ContentProvider {
mHelper.getWritableDatabase().execSQL(sql.toString());
}
// 获取内容类型
@Override
public String getType(Uri uri) {
// TODO Auto-generated method stub
return null;
switch (mMatcher.match(uri)) {
case URI_NOTE:
// 返回笔记集合的MIME类型
return Notes.TextNote.CONTENT_TYPE;
case URI_NOTE_ITEM:
// 返回单个笔记的MIME类型
return Notes.TextNote.CONTENT_ITEM_TYPE;
case URI_DATA:
// 返回数据集合的MIME类型
return Notes.Data.CONTENT_TYPE;
case URI_DATA_ITEM:
// 返回单个数据的MIME类型
return Notes.Data.CONTENT_ITEM_TYPE;
case URI_SEARCH:
// 返回搜索结果的MIME类型
return Notes.TextNote.CONTENT_TYPE;
case URI_SEARCH_SUGGEST:
// 返回搜索建议的MIME类型
return SearchManager.SUGGEST_MIME_TYPE;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
}
}

@ -28,30 +28,34 @@ import org.json.JSONObject;
public class MetaData extends Task {
private final static String TAG = MetaData.class.getSimpleName();
private String mRelatedGid = null;
private String mRelatedGid = null;// 关联的Google Tasks ID
// 设置元数据信息
public void setMeta(String gid, JSONObject metaInfo) {
try {
metaInfo.put(GTaskStringUtils.META_HEAD_GTASK_ID, gid);
} catch (JSONException e) {
Log.e(TAG, "failed to put related gid");
}
setNotes(metaInfo.toString());
setName(GTaskStringUtils.META_NOTE_NAME);
setNotes(metaInfo.toString());// 将元数据转为JSON字符串并存入笔记
setName(GTaskStringUtils.META_NOTE_NAME);// 设置笔记名称为元数据专用名称
}
// 获取关联的Google Tasks ID
public String getRelatedGid() {
return mRelatedGid;
}
// 判断是否值得保存
@Override
public boolean isWorthSaving() {
return getNotes() != null;
return getNotes() != null;// 只要有备注内容就值得保存
}
// 从远程JSON设置内容
@Override
public void setContentByRemoteJSON(JSONObject js) {
super.setContentByRemoteJSON(js);
super.setContentByRemoteJSON(js);// 调用父类方法设置基本内容
if (getNotes() != null) {
try {
JSONObject metaInfo = new JSONObject(getNotes().trim());
@ -63,17 +67,18 @@ public class MetaData extends Task {
}
}
// 从本地JSON设置内容
@Override
public void setContentByLocalJSON(JSONObject js) {
// this function should not be called
throw new IllegalAccessError("MetaData:setContentByLocalJSON should not be called");
}
// 获取本地JSON内容
@Override
public JSONObject getLocalJSONFromContent() {
throw new IllegalAccessError("MetaData:getLocalJSONFromContent should not be called");
}
// 获取同步操作
@Override
public int getSyncAction(Cursor c) {
throw new IllegalAccessError("MetaData:getSyncAction should not be called");

@ -18,15 +18,15 @@ package net.micode.notes.gtask.exception;
public class ActionFailureException extends RuntimeException {
private static final long serialVersionUID = 4425249765923293627L;
// 无参构造函数
public ActionFailureException() {
super();
}
// 带消息的构造函数
public ActionFailureException(String paramString) {
super(paramString);
}
// 带消息和原因的构造函数
public ActionFailureException(String paramString, Throwable paramThrowable) {
super(paramString, paramThrowable);
}

@ -19,14 +19,15 @@ package net.micode.notes.gtask.exception;
public class NetworkFailureException extends Exception {
private static final long serialVersionUID = 2107610287180234136L;
// 无参构造函数
public NetworkFailureException() {
super();
}
// 带消息的构造函数
public NetworkFailureException(String paramString) {
super(paramString);
}
// 带消息和原因的构造函数
public NetworkFailureException(String paramString, Throwable paramThrowable) {
super(paramString, paramThrowable);
}

@ -31,20 +31,22 @@ import net.micode.notes.ui.NotesPreferenceActivity;
public class GTaskASyncTask extends AsyncTask<Void, String, Integer> {
private static int GTASK_SYNC_NOTIFICATION_ID = 5234235;
private static int GTASK_SYNC_NOTIFICATION_ID = 5234235;// 同步通知的唯一ID
// 完成监听器接口
public interface OnCompleteListener {
void onComplete();
}
private Context mContext;
private Context mContext; // 应用上下文
private NotificationManager mNotifiManager;
private NotificationManager mNotifiManager; // 通知管理器
private GTaskManager mTaskManager;
private GTaskManager mTaskManager; // Google Tasks管理器
private OnCompleteListener mOnCompleteListener;
private OnCompleteListener mOnCompleteListener; // 完成监听器
// 构造函数
public GTaskASyncTask(Context context, OnCompleteListener listener) {
mContext = context;
mOnCompleteListener = listener;
@ -53,10 +55,11 @@ public class GTaskASyncTask extends AsyncTask<Void, String, Integer> {
mTaskManager = GTaskManager.getInstance();
}
// 取消同步操作
public void cancelSync() {
mTaskManager.cancelSync();
}
// 发布进度信息
public void publishProgess(String message) {
publishProgress(new String[] {
message
@ -81,6 +84,8 @@ public class GTaskASyncTask extends AsyncTask<Void, String, Integer> {
// pendingIntent);
//mNotifiManager.notify(GTASK_SYNC_NOTIFICATION_ID, notification);
//}
// 显示通知
private void showNotification(int tickerId, String content) {
PendingIntent pendingIntent;
if (tickerId != R.string.ticker_success) {
@ -100,7 +105,8 @@ private void showNotification(int tickerId, String content) {
Notification notification=builder.getNotification();
mNotifiManager.notify(GTASK_SYNC_NOTIFICATION_ID, notification);
}
// 在后台线程执行同步操作
@Override
protected Integer doInBackground(Void... unused) {
publishProgess(mContext.getString(R.string.sync_progress_login, NotesPreferenceActivity
@ -108,6 +114,7 @@ private void showNotification(int tickerId, String content) {
return mTaskManager.sync(mContext, this);
}
// 更新UI线程中的进度
@Override
protected void onProgressUpdate(String... progress) {
showNotification(R.string.ticker_syncing, progress[0]);
@ -115,7 +122,8 @@ private void showNotification(int tickerId, String content) {
((GTaskSyncService) mContext).sendBroadcast(progress[0]);
}
}
// 同步完成后在UI线程执行
@Override
protected void onPostExecute(Integer result) {
if (result == GTaskManager.STATE_SUCCESS) {
@ -130,6 +138,7 @@ private void showNotification(int tickerId, String content) {
showNotification(R.string.ticker_cancel, mContext
.getString(R.string.error_sync_cancelled));
}
// 调用完成监听器
if (mOnCompleteListener != null) {
new Thread(new Runnable() {

@ -65,31 +65,22 @@ 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 static GTaskClient mInstance = null; // 单例实例
private JSONArray mUpdateArray;
private DefaultHttpClient mHttpClient; // HTTP客户端
private String mGetUrl; // GET请求URL
private String mPostUrl; // POST请求URL
private long mClientVersion; // 客户端版本号
private boolean mLoggedin; // 登录状态
private long mLastLoginTime; // 最后登录时间
private int mActionId; // 操作ID
private Account mAccount; // 当前账户
private JSONArray mUpdateArray; // 更新操作数组
// 私有构造函数,实现单例模式
private GTaskClient() {
mHttpClient = null;
mGetUrl = GTASK_GET_URL;
@ -102,6 +93,7 @@ public class GTaskClient {
mUpdateArray = null;
}
// 获取单例实例
public static synchronized GTaskClient getInstance() {
if (mInstance == null) {
mInstance = new GTaskClient();
@ -109,15 +101,18 @@ public class GTaskClient {
return mInstance;
}
// 登录Google账户
public boolean login(Activity activity) {
// we suppose that the cookie would expire after 5 minutes
// then we need to re-login
// 检查是否需要重新登录5分钟过期或账户切换
final long interval = 1000 * 60 * 5;
if (mLastLoginTime + interval < System.currentTimeMillis()) {
mLoggedin = false;
}
// need to re-login after account switch
// 账户切换时需要重新登录
if (mLoggedin
&& !TextUtils.equals(getSyncAccount().name, NotesPreferenceActivity
.getSyncAccountName(activity))) {
@ -137,6 +132,7 @@ public class GTaskClient {
}
// 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/");
@ -152,6 +148,7 @@ public class GTaskClient {
}
// try to login with google official url
// 尝试使用Google官方URL登录
if (!mLoggedin) {
mGetUrl = GTASK_GET_URL;
mPostUrl = GTASK_POST_URL;
@ -164,6 +161,7 @@ public class GTaskClient {
return true;
}
// 登录Google账户并获取认证令牌
private String loginGoogleAccount(Activity activity, boolean invalidateToken) {
String authToken;
AccountManager accountManager = AccountManager.get(activity);
@ -174,6 +172,7 @@ public class GTaskClient {
return null;
}
// 获取同步账户
String accountName = NotesPreferenceActivity.getSyncAccountName(activity);
Account account = null;
for (Account a : accounts) {
@ -190,6 +189,7 @@ public class GTaskClient {
}
// get the token now
// 获取认证令牌
AccountManagerFuture<Bundle> accountManagerFuture = accountManager.getAuthToken(account,
"goanna_mobile", null, activity, null, null);
try {
@ -207,10 +207,12 @@ public class GTaskClient {
return authToken;
}
// 尝试登录Google Tasks
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");
@ -225,7 +227,9 @@ public class GTaskClient {
return true;
}
// 登录Google Tasks服务
private boolean loginGtask(String authToken) {
// 设置HTTP客户端参数
int timeoutConnection = 10000;
int timeoutSocket = 15000;
HttpParams httpParameters = new BasicHttpParams();
@ -237,6 +241,7 @@ public class GTaskClient {
HttpProtocolParams.setUseExpectContinue(mHttpClient.getParams(), false);
// login gtask
// 登录Google Tasks
try {
String loginUrl = mGetUrl + "?auth=" + authToken;
HttpGet httpGet = new HttpGet(loginUrl);
@ -244,6 +249,7 @@ public class GTaskClient {
response = mHttpClient.execute(httpGet);
// get the cookie now
// 检查认证Cookie
List<Cookie> cookies = mHttpClient.getCookieStore().getCookies();
boolean hasAuthCookie = false;
for (Cookie cookie : cookies) {
@ -256,6 +262,7 @@ public class GTaskClient {
}
// get the client version
// 获取客户端版本
String resString = getResponseContent(response.getEntity());
String jsBegin = "_setup(";
String jsEnd = ")}</script>";
@ -280,10 +287,12 @@ public class GTaskClient {
return true;
}
// 获取操作ID
private int getActionId() {
return mActionId++;
}
// 创建HTTP POST请求
private HttpPost createHttpPost() {
HttpPost httpPost = new HttpPost(mPostUrl);
httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8");
@ -291,6 +300,7 @@ public class GTaskClient {
return httpPost;
}
// 处理HTTP响应内容
private String getResponseContent(HttpEntity entity) throws IOException {
String contentEncoding = null;
if (entity.getContentEncoding() != null) {
@ -298,6 +308,7 @@ public class GTaskClient {
Log.d(TAG, "encoding: " + contentEncoding);
}
// 处理压缩响应
InputStream input = entity.getContent();
if (contentEncoding != null && contentEncoding.equalsIgnoreCase("gzip")) {
input = new GZIPInputStream(entity.getContent());
@ -323,6 +334,7 @@ public class GTaskClient {
}
}
// 发送POST请求
private JSONObject postRequest(JSONObject js) throws NetworkFailureException {
if (!mLoggedin) {
Log.e(TAG, "please login first");
@ -337,6 +349,7 @@ public class GTaskClient {
httpPost.setEntity(entity);
// execute the post
// 执行POST请求
HttpResponse response = mHttpClient.execute(httpPost);
String jsString = getResponseContent(response.getEntity());
return new JSONObject(jsString);
@ -360,6 +373,7 @@ public class GTaskClient {
}
}
// 创建任务
public void createTask(Task task) throws NetworkFailureException {
commitUpdate();
try {
@ -367,6 +381,7 @@ public class GTaskClient {
JSONArray actionList = new JSONArray();
// action_list
// 添加创建任务的操作
actionList.put(task.getCreateAction(getActionId()));
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
@ -374,6 +389,7 @@ public class GTaskClient {
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
// post
// 发送请求并获取新任务ID
JSONObject jsResponse = postRequest(jsPost);
JSONObject jsResult = (JSONObject) jsResponse.getJSONArray(
GTaskStringUtils.GTASK_JSON_RESULTS).get(0);
@ -386,6 +402,7 @@ public class GTaskClient {
}
}
// 创建任务列表
public void createTaskList(TaskList tasklist) throws NetworkFailureException {
commitUpdate();
try {
@ -393,6 +410,7 @@ public class GTaskClient {
JSONArray actionList = new JSONArray();
// action_list
// 添加创建任务列表的操作
actionList.put(tasklist.getCreateAction(getActionId()));
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
@ -400,6 +418,7 @@ public class GTaskClient {
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
// post
// 发送请求并获取新任务列表ID
JSONObject jsResponse = postRequest(jsPost);
JSONObject jsResult = (JSONObject) jsResponse.getJSONArray(
GTaskStringUtils.GTASK_JSON_RESULTS).get(0);
@ -412,6 +431,7 @@ public class GTaskClient {
}
}
// 提交更新操作
public void commitUpdate() throws NetworkFailureException {
if (mUpdateArray != null) {
try {
@ -433,10 +453,12 @@ public class GTaskClient {
}
}
// 添加更新节点
public void addUpdateNode(Node node) throws NetworkFailureException {
if (node != null) {
// too many update items may result in an error
// set max to 10 items
// 批量操作限制超过10个项目时提交更新
if (mUpdateArray != null && mUpdateArray.length() > 10) {
commitUpdate();
}
@ -447,6 +469,8 @@ public class GTaskClient {
}
}
// 移动任务
public void moveTask(Task task, TaskList preParent, TaskList curParent)
throws NetworkFailureException {
commitUpdate();
@ -456,10 +480,12 @@ public class GTaskClient {
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());
// 设置前置兄弟任务ID如果在同一任务列表中移动且不是第一个任务
if (preParent == curParent && task.getPriorSibling() != null) {
// put prioring_sibing_id only if moving within the tasklist and
// it is not the first one
@ -467,6 +493,8 @@ public class GTaskClient {
}
action.put(GTaskStringUtils.GTASK_JSON_SOURCE_LIST, preParent.getGid());
action.put(GTaskStringUtils.GTASK_JSON_DEST_PARENT, curParent.getGid());
// 设置目标任务列表ID如果在不同任务列表之间移动
if (preParent != curParent) {
// put the dest_list only if moving between tasklists
action.put(GTaskStringUtils.GTASK_JSON_DEST_LIST, curParent.getGid());
@ -486,6 +514,7 @@ public class GTaskClient {
}
}
// 删除节点
public void deleteNode(Node node) throws NetworkFailureException {
commitUpdate();
try {
@ -493,6 +522,7 @@ public class GTaskClient {
JSONArray actionList = new JSONArray();
// action_list
// 设置节点为已删除并添加更新操作
node.setDeleted(true);
actionList.put(node.getUpdateAction(getActionId()));
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
@ -509,6 +539,7 @@ public class GTaskClient {
}
}
// 获取任务列表
public JSONArray getTaskLists() throws NetworkFailureException {
if (!mLoggedin) {
Log.e(TAG, "please login first");
@ -521,6 +552,7 @@ public class GTaskClient {
response = mHttpClient.execute(httpGet);
// get the task list
// 解析响应内容获取任务列表
String resString = getResponseContent(response.getEntity());
String jsBegin = "_setup(";
String jsEnd = ")}</script>";
@ -547,6 +579,7 @@ public class GTaskClient {
}
}
// 获取特定任务列表中的任务
public JSONArray getTaskList(String listGid) throws NetworkFailureException {
commitUpdate();
try {
@ -555,6 +588,7 @@ public class GTaskClient {
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());
@ -575,10 +609,12 @@ public class GTaskClient {
}
}
// 获取同步账户
public Account getSyncAccount() {
return mAccount;
}
// 重置更新数组
public void resetUpdateArray() {
mUpdateArray = null;
}

@ -35,14 +35,16 @@ import java.util.ArrayList;
public class Note {
private ContentValues mNoteDiffValues;
private NoteData mNoteData;
private ContentValues mNoteDiffValues;//笔记差异值
private NoteData mNoteData;//笔记数据
private static final String TAG = "Note";
/**
* Create a new note id for adding a new note to databases
*/
//获取新笔记id
public static synchronized long getNewNoteId(Context context, long folderId) {
// Create a new note in the database
//创建新笔记并插入数据库
ContentValues values = new ContentValues();
long createdTime = System.currentTimeMillis();
values.put(NoteColumns.CREATED_DATE, createdTime);
@ -65,46 +67,50 @@ public class Note {
return noteId;
}
// 构造函数
public Note() {
mNoteDiffValues = new ContentValues();
mNoteData = new NoteData();
}
// 设置笔记值
public void setNoteValue(String key, String value) {
mNoteDiffValues.put(key, value);
mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1);
mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis());
}
// 设置文本数据
public void setTextData(String key, String value) {
mNoteData.setTextData(key, value);
}
// 设置文本数据ID
public void setTextDataId(long id) {
mNoteData.setTextDataId(id);
}
// 获取文本数据ID
public long getTextDataId() {
return mNoteData.mTextDataId;
}
// 设置通话数据ID
public void setCallDataId(long id) {
mNoteData.setCallDataId(id);
}
// 设置通话数据
public void setCallData(String key, String value) {
mNoteData.setCallData(key, value);
}
// 检查是否有本地修改
public boolean isLocalModified() {
return mNoteDiffValues.size() > 0 || mNoteData.isLocalModified();
}
// 同步笔记数据到ContentProvider
public boolean syncNote(Context context, long noteId) {
if (noteId <= 0) {
throw new IllegalArgumentException("Wrong note id:" + noteId);
}
// 更新笔记基本信息
if (!isLocalModified()) {
return true;
}
@ -118,10 +124,12 @@ public class Note {
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), mNoteDiffValues, null,
null) == 0) {
Log.e(TAG, "Update note error, should not happen");
// 即使更新失败也继续处理数据
// Do not return, fall through
}
mNoteDiffValues.clear();
// 更新笔记关联数据
if (mNoteData.isLocalModified()
&& (mNoteData.pushIntoContentResolver(context, noteId) == null)) {
return false;
@ -129,55 +137,54 @@ public class Note {
return true;
}
// 内部类:管理笔记关联数据
private class NoteData {
private long mTextDataId;
private ContentValues mTextDataValues;
private long mCallDataId;
private ContentValues mCallDataValues;
private long mTextDataId; // 文本数据ID
private ContentValues mTextDataValues; // 文本数据值
private long mCallDataId; // 通话数据ID
private ContentValues mCallDataValues; // 通话数据值
private static final String TAG = "NoteData";
// 构造函数
public NoteData() {
mTextDataValues = new ContentValues();
mCallDataValues = new ContentValues();
mTextDataId = 0;
mCallDataId = 0;
}
// 检查是否有本地修改
boolean isLocalModified() {
return mTextDataValues.size() > 0 || mCallDataValues.size() > 0;
}
// 设置文本数据ID
void setTextDataId(long id) {
if(id <= 0) {
throw new IllegalArgumentException("Text data id should larger than 0");
}
mTextDataId = id;
}
// 设置通话数据ID
void setCallDataId(long id) {
if (id <= 0) {
throw new IllegalArgumentException("Call data id should larger than 0");
}
mCallDataId = id;
}
// 设置通话数据
void setCallData(String key, String value) {
mCallDataValues.put(key, value);
mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1);
mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis());
}
// 设置文本数据
void setTextData(String key, String value) {
mTextDataValues.put(key, value);
mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1);
mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis());
}
// 将数据推送到ContentProvider
Uri pushIntoContentResolver(Context context, long noteId) {
/**
* Check for safety
@ -189,9 +196,11 @@ public class Note {
ArrayList<ContentProviderOperation> operationList = new ArrayList<ContentProviderOperation>();
ContentProviderOperation.Builder builder = null;
// 处理文本数据
if(mTextDataValues.size() > 0) {
mTextDataValues.put(DataColumns.NOTE_ID, noteId);
if (mTextDataId == 0) {
// 插入新文本数据
mTextDataValues.put(DataColumns.MIME_TYPE, TextNote.CONTENT_ITEM_TYPE);
Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI,
mTextDataValues);
@ -203,6 +212,7 @@ public class Note {
return null;
}
} else {
// 更新现有文本数据
builder = ContentProviderOperation.newUpdate(ContentUris.withAppendedId(
Notes.CONTENT_DATA_URI, mTextDataId));
builder.withValues(mTextDataValues);
@ -211,9 +221,11 @@ public class Note {
mTextDataValues.clear();
}
// 处理通话数据
if(mCallDataValues.size() > 0) {
mCallDataValues.put(DataColumns.NOTE_ID, noteId);
if (mCallDataId == 0) {
// 插入新通话数据
mCallDataValues.put(DataColumns.MIME_TYPE, CallNote.CONTENT_ITEM_TYPE);
Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI,
mCallDataValues);
@ -225,6 +237,7 @@ public class Note {
return null;
}
} else {
// 更新现有通话数据
builder = ContentProviderOperation.newUpdate(ContentUris.withAppendedId(
Notes.CONTENT_DATA_URI, mCallDataId));
builder.withValues(mCallDataValues);
@ -233,6 +246,7 @@ public class Note {
mCallDataValues.clear();
}
// 批量执行操作
if (operationList.size() > 0) {
try {
ContentProviderResult[] results = context.getContentResolver().applyBatch(

@ -59,9 +59,9 @@ public class WorkingNote {
private static final String TAG = "WorkingNote";
private boolean mIsDeleted;
// 笔记设置变化监听器
private NoteSettingChangedListener mNoteSettingStatusListener;
// 查询数据的投影
public static final String[] DATA_PROJECTION = new String[] {
DataColumns.ID,
DataColumns.CONTENT,
@ -72,6 +72,7 @@ public class WorkingNote {
DataColumns.DATA4,
};
//查询笔记的投影
public static final String[] NOTE_PROJECTION = new String[] {
NoteColumns.PARENT_ID,
NoteColumns.ALERTED_DATE,
@ -81,27 +82,21 @@ public class WorkingNote {
NoteColumns.MODIFIED_DATE
};
// 数据列索引常量
private static final int DATA_ID_COLUMN = 0;
private static final int DATA_CONTENT_COLUMN = 1;
private static final int DATA_MIME_TYPE_COLUMN = 2;
private static final int DATA_MODE_COLUMN = 3;
// 笔记列索引常量
private static final int NOTE_PARENT_ID_COLUMN = 0;
private static final int NOTE_ALERTED_DATE_COLUMN = 1;
private static final int NOTE_BG_COLOR_ID_COLUMN = 2;
private static final int NOTE_WIDGET_ID_COLUMN = 3;
private static final int NOTE_WIDGET_TYPE_COLUMN = 4;
private static final int NOTE_MODIFIED_DATE_COLUMN = 5;
// New note construct
// 新建笔记的构造函数
private WorkingNote(Context context, long folderId) {
mContext = context;
mAlertDate = 0;
@ -115,6 +110,7 @@ public class WorkingNote {
}
// Existing note construct
// 加载现有笔记的构造函数
private WorkingNote(Context context, long noteId, long folderId) {
mContext = context;
mNoteId = noteId;
@ -124,6 +120,7 @@ public class WorkingNote {
loadNote();
}
// 加载笔记基本信息
private void loadNote() {
Cursor cursor = mContext.getContentResolver().query(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mNoteId), NOTE_PROJECTION, null,
@ -145,7 +142,7 @@ public class WorkingNote {
}
loadNoteData();
}
// 加载笔记数据
private void loadNoteData() {
Cursor cursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI, DATA_PROJECTION,
DataColumns.NOTE_ID + "=?", new String[] {
@ -174,6 +171,7 @@ public class WorkingNote {
}
}
// 创建空笔记的静态方法
public static WorkingNote createEmptyNote(Context context, long folderId, int widgetId,
int widgetType, int defaultBgColorId) {
WorkingNote note = new WorkingNote(context, folderId);
@ -183,10 +181,12 @@ public class WorkingNote {
return note;
}
// 加载现有笔记的静态方法
public static WorkingNote load(Context context, long id) {
return new WorkingNote(context, id, 0);
}
// 保存笔记
public synchronized boolean saveNote() {
if (isWorthSaving()) {
if (!existInDatabase()) {
@ -201,6 +201,7 @@ public class WorkingNote {
/**
* Update widget content if there exist any widget of this note
*/
// 如果笔记关联了小部件,更新小部件内容
if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID
&& mWidgetType != Notes.TYPE_WIDGET_INVALIDE
&& mNoteSettingStatusListener != null) {
@ -211,11 +212,13 @@ public class WorkingNote {
return false;
}
}
// 检查笔记是否存在于数据库中
public boolean existInDatabase() {
return mNoteId > 0;
}
// 判断笔记是否值得保存
private boolean isWorthSaving() {
if (mIsDeleted || (!existInDatabase() && TextUtils.isEmpty(mContent))
|| (existInDatabase() && !mNote.isLocalModified())) {
@ -225,10 +228,11 @@ public class WorkingNote {
}
}
// 设置笔记设置变化监听器
public void setOnSettingStatusChangedListener(NoteSettingChangedListener l) {
mNoteSettingStatusListener = l;
}
// 设置提醒日期
public void setAlertDate(long date, boolean set) {
if (date != mAlertDate) {
mAlertDate = date;
@ -239,6 +243,7 @@ public class WorkingNote {
}
}
// 标记笔记为已删除
public void markDeleted(boolean mark) {
mIsDeleted = mark;
if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID
@ -247,6 +252,7 @@ public class WorkingNote {
}
}
// 设置背景颜色
public void setBgColorId(int id) {
if (id != mBgColorId) {
mBgColorId = id;
@ -257,6 +263,7 @@ public class WorkingNote {
}
}
// 设置清单模式
public void setCheckListMode(int mode) {
if (mMode != mode) {
if (mNoteSettingStatusListener != null) {
@ -267,6 +274,7 @@ public class WorkingNote {
}
}
// 设置小部件类型
public void setWidgetType(int type) {
if (type != mWidgetType) {
mWidgetType = type;
@ -274,6 +282,7 @@ public class WorkingNote {
}
}
// 设置小部件ID
public void setWidgetId(int id) {
if (id != mWidgetId) {
mWidgetId = id;
@ -281,87 +290,101 @@ public class WorkingNote {
}
}
// 设置笔记文本内容
public void setWorkingText(String text) {
if (!TextUtils.equals(mContent, text)) {
mContent = text;
mNote.setTextData(DataColumns.CONTENT, mContent);
}
}
// 转换为通话记录笔记
public void convertToCallNote(String phoneNumber, long callDate) {
mNote.setCallData(CallNote.CALL_DATE, String.valueOf(callDate));
mNote.setCallData(CallNote.PHONE_NUMBER, phoneNumber);
mNote.setNoteValue(NoteColumns.PARENT_ID, String.valueOf(Notes.ID_CALL_RECORD_FOLDER));
}
// 检查是否有提醒
public boolean hasClockAlert() {
return (mAlertDate > 0 ? true : false);
}
// 获取内容
public String getContent() {
return mContent;
}
// 获取提醒日期
public long getAlertDate() {
return mAlertDate;
}
// 获取修改日期
public long getModifiedDate() {
return mModifiedDate;
}
// 获取背景颜色资源ID
public int getBgColorResId() {
return NoteBgResources.getNoteBgResource(mBgColorId);
}
// 获取背景颜色ID
public int getBgColorId() {
return mBgColorId;
}
// 获取标题背景资源ID
public int getTitleBgResId() {
return NoteBgResources.getNoteTitleBgResource(mBgColorId);
}
// 获取清单模式
public int getCheckListMode() {
return mMode;
}
// 获取笔记ID
public long getNoteId() {
return mNoteId;
}
// 获取文件夹ID
public long getFolderId() {
return mFolderId;
}
// 获取小部件ID
public int getWidgetId() {
return mWidgetId;
}
// 获取小部件类型
public int getWidgetType() {
return mWidgetType;
}
// 笔记设置变化监听器接口
public interface NoteSettingChangedListener {
/**
* Called when the background color of current note has just changed
*
*/
void onBackgroundColorChanged();
/**
* Called when user set clock
*
*/
void onClockAlertChanged(long date, boolean set);
/**
* Call when user create note from widget
*
*/
void onWidgetChanged();
/**
* Call when switch between check list mode and normal mode
* @param oldMode is previous mode before change
* @param newMode is new mode
*
* @param oldMode
* @param newMode
*/
void onCheckListModeChanged(int oldMode, int newMode);
}

@ -18,6 +18,7 @@ package net.micode.notes.tool;
import android.content.Context;
import android.database.Cursor;
import android.os.Build;
import android.os.Environment;
import android.text.TextUtils;
import android.text.format.DateFormat;
@ -29,12 +30,14 @@ import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.DataConstants;
import net.micode.notes.data.Notes.NoteColumns;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
public class BackupUtils {
private static final String TAG = "BackupUtils";
@ -62,6 +65,8 @@ public class BackupUtils {
public static final int STATE_SYSTEM_ERROR = 3;
// Backup or restore success
public static final int STATE_SUCCESS = 4;
// Permission denied
public static final int STATE_PERMISSION_DENIED = 5;
private TextExport mTextExport;
@ -90,14 +95,15 @@ public class BackupUtils {
NoteColumns.ID,
NoteColumns.MODIFIED_DATE,
NoteColumns.SNIPPET,
NoteColumns.TYPE
NoteColumns.TYPE,
NoteColumns.PARENT_ID
};
private static final int NOTE_COLUMN_ID = 0;
private static final int NOTE_COLUMN_MODIFIED_DATE = 1;
private static final int NOTE_COLUMN_SNIPPET = 2;
private static final int NOTE_COLUMN_TYPE = 3;
private static final int NOTE_COLUMN_PARENT_ID = 4;
private static final String[] DATA_PROJECTION = {
DataColumns.CONTENT,
@ -109,11 +115,8 @@ public class BackupUtils {
};
private static final int DATA_COLUMN_CONTENT = 0;
private static final int DATA_COLUMN_MIME_TYPE = 1;
private static final int DATA_COLUMN_CALL_DATE = 2;
private static final int DATA_COLUMN_PHONE_NUMBER = 4;
private final String [] TEXT_FORMAT;
@ -124,12 +127,17 @@ public class BackupUtils {
private Context mContext;
private String mFileName;
private String mFileDirectory;
private BufferedWriter mWriter;
private int mNotesCount;
private int mFoldersCount;
public TextExport(Context context) {
TEXT_FORMAT = context.getResources().getStringArray(R.array.format_for_exported_note);
mContext = context;
mFileName = "";
mFileDirectory = "";
mNotesCount = 0;
mFoldersCount = 0;
}
private String getFormat(int id) {
@ -137,208 +145,268 @@ public class BackupUtils {
}
/**
* Export the folder identified by folder id to text
* Export notes to text file
*/
private void exportFolderToText(String folderId, PrintStream ps) {
// Query notes belong to this folder
Cursor notesCursor = mContext.getContentResolver().query(Notes.CONTENT_NOTE_URI,
NOTE_PROJECTION, NoteColumns.PARENT_ID + "=?", new String[] {
folderId
}, null);
public int exportToText() {
if (!externalStorageAvailable()) {
Log.d(TAG, "Media was not mounted");
return STATE_SD_CARD_UNMOUONTED;
}
if (notesCursor != null) {
if (notesCursor.moveToFirst()) {
do {
// Print note's last modified date
ps.println(String.format(getFormat(FORMAT_NOTE_DATE), DateFormat.format(
mContext.getString(R.string.format_datetime_mdhm),
notesCursor.getLong(NOTE_COLUMN_MODIFIED_DATE))));
// Query data belong to this note
String noteId = notesCursor.getString(NOTE_COLUMN_ID);
exportNoteToText(noteId, ps);
} while (notesCursor.moveToNext());
}
notesCursor.close();
File file = generateFileMountedOnSDcard(mContext, R.string.file_path,
R.string.file_name_txt_format);
if (file == null) {
Log.e(TAG, "create file to exported failed");
return STATE_SYSTEM_ERROR;
}
mFileName = file.getName();
mFileDirectory = mContext.getString(R.string.file_path);
// 使用try-with-resources确保资源释放
try (BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8))) {
mWriter = writer;
// 导出文件夹和笔记
exportFoldersAndNotes();
// 写入导出统计信息
writeExportSummary();
Log.d(TAG, "Export successful. Exported " + mFoldersCount + " folders and " + mNotesCount + " notes.");
return STATE_SUCCESS;
} catch (IOException e) {
Log.e(TAG, "Error during export: " + e.getMessage(), e);
return STATE_SYSTEM_ERROR;
}
}
/**
* Export note identified by id to a print stream
*
*/
private void exportNoteToText(String noteId, PrintStream ps) {
Cursor dataCursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI,
DATA_PROJECTION, DataColumns.NOTE_ID + "=?", new String[] {
noteId
}, null);
if (dataCursor != null) {
if (dataCursor.moveToFirst()) {
private void writeExportSummary() throws IOException {
mWriter.write("\n");
mWriter.write("==============================\n");
mWriter.write(mContext.getString(R.string.export_summary) + "\n");
mWriter.write(mContext.getString(R.string.export_date) + ": "
+ DateFormat.format(mContext.getString(R.string.format_datetime_ymdhm), System.currentTimeMillis()) + "\n");
mWriter.write(mContext.getString(R.string.folder_count) + ": " + mFoldersCount + "\n");
mWriter.write(mContext.getString(R.string.note_count) + ": " + mNotesCount + "\n");
mWriter.write("==============================\n");
}
/**
*
*/
private void exportFoldersAndNotes() throws IOException {
// 首先导出文件夹和其中的笔记
exportFolders();
// 导出根文件夹中的笔记
exportRootNotes();
}
/**
*
*/
private void exportFolders() throws IOException {
// 查询所有文件夹
String selection = "(" + NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER + " AND "
+ NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER + ") OR "
+ NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER;
try (Cursor folderCursor = mContext.getContentResolver().query(
Notes.CONTENT_NOTE_URI,
NOTE_PROJECTION,
selection, null, null)) {
if (folderCursor != null && folderCursor.moveToFirst()) {
do {
String mimeType = dataCursor.getString(DATA_COLUMN_MIME_TYPE);
if (DataConstants.CALL_NOTE.equals(mimeType)) {
// Print phone number
String phoneNumber = dataCursor.getString(DATA_COLUMN_PHONE_NUMBER);
long callDate = dataCursor.getLong(DATA_COLUMN_CALL_DATE);
String location = dataCursor.getString(DATA_COLUMN_CONTENT);
if (!TextUtils.isEmpty(phoneNumber)) {
ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT),
phoneNumber));
}
// Print call date
ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT), DateFormat
.format(mContext.getString(R.string.format_datetime_mdhm),
callDate)));
// Print call attachment location
if (!TextUtils.isEmpty(location)) {
ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT),
location));
}
} else if (DataConstants.NOTE.equals(mimeType)) {
String content = dataCursor.getString(DATA_COLUMN_CONTENT);
if (!TextUtils.isEmpty(content)) {
ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT),
content));
}
}
} while (dataCursor.moveToNext());
mFoldersCount++;
exportFolder(folderCursor);
} while (folderCursor.moveToNext());
}
dataCursor.close();
}
// print a line separator between note
try {
ps.write(new byte[] {
Character.LINE_SEPARATOR, Character.LETTER_NUMBER
});
} catch (IOException e) {
Log.e(TAG, e.toString());
}
}
/**
* Note will be exported as text which is user readable
*
*/
public int exportToText() {
if (!externalStorageAvailable()) {
Log.d(TAG, "Media was not mounted");
return STATE_SD_CARD_UNMOUONTED;
private void exportFolder(Cursor folderCursor) throws IOException {
String folderId = folderCursor.getString(NOTE_COLUMN_ID);
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);
}
PrintStream ps = getExportToTextPrintStream();
if (ps == null) {
Log.e(TAG, "get print stream error");
return STATE_SYSTEM_ERROR;
if (!TextUtils.isEmpty(folderName)) {
mWriter.write(String.format(getFormat(FORMAT_FOLDER_NAME), folderName));
mWriter.newLine();
}
// First export folder and its notes
Cursor folderCursor = mContext.getContentResolver().query(
Notes.CONTENT_NOTE_URI,
NOTE_PROJECTION,
"(" + NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER + " AND "
+ NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER + ") OR "
+ NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER, null, null);
if (folderCursor != null) {
if (folderCursor.moveToFirst()) {
// 导出文件夹中的笔记
exportNotesInFolder(folderId);
// 添加文件夹分隔线
mWriter.write("\n");
}
/**
*
*/
private void exportNotesInFolder(String folderId) throws IOException {
try (Cursor notesCursor = mContext.getContentResolver().query(Notes.CONTENT_NOTE_URI,
NOTE_PROJECTION, NoteColumns.PARENT_ID + "=?", new String[] { folderId }, null)) {
if (notesCursor != null && notesCursor.moveToFirst()) {
do {
// Print folder's name
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());
mNotesCount++;
exportNote(notesCursor);
} while (notesCursor.moveToNext());
}
folderCursor.close();
}
// Export notes in root's folder
Cursor noteCursor = mContext.getContentResolver().query(
}
/**
*
*/
private void exportRootNotes() throws IOException {
try (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()) {
NoteColumns.TYPE + "=" + Notes.TYPE_NOTE + " AND " + NoteColumns.PARENT_ID + "=0",
null, null)) {
if (noteCursor != null && 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))));
// Query data belong to this note
String noteId = noteCursor.getString(NOTE_COLUMN_ID);
exportNoteToText(noteId, ps);
mNotesCount++;
exportNote(noteCursor);
} while (noteCursor.moveToNext());
}
noteCursor.close();
}
ps.close();
return STATE_SUCCESS;
}
/**
* Get a print stream pointed to the file {@generateExportedTextFile}
*
*/
private PrintStream getExportToTextPrintStream() {
File file = generateFileMountedOnSDcard(mContext, R.string.file_path,
R.string.file_name_txt_format);
if (file == null) {
Log.e(TAG, "create file to exported failed");
return null;
private void exportNote(Cursor noteCursor) throws IOException {
// 打印笔记的最后修改日期
mWriter.write(String.format(getFormat(FORMAT_NOTE_DATE),
DateFormat.format(mContext.getString(R.string.format_datetime_mdhm),
noteCursor.getLong(NOTE_COLUMN_MODIFIED_DATE))));
mWriter.newLine();
// 查询并导出笔记的数据
String noteId = noteCursor.getString(NOTE_COLUMN_ID);
exportNoteData(noteId);
// 添加笔记分隔线
mWriter.write("\n");
}
/**
*
*/
private void exportNoteData(String noteId) throws IOException {
try (Cursor dataCursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI,
DATA_PROJECTION, DataColumns.NOTE_ID + "=?", new String[] { noteId }, null)) {
if (dataCursor != null && dataCursor.moveToFirst()) {
do {
String mimeType = dataCursor.getString(DATA_COLUMN_MIME_TYPE);
if (DataConstants.CALL_NOTE.equals(mimeType)) {
exportCallNote(dataCursor);
} else if (DataConstants.NOTE.equals(mimeType)) {
exportTextNote(dataCursor);
}
} while (dataCursor.moveToNext());
}
}
mFileName = file.getName();
mFileDirectory = mContext.getString(R.string.file_path);
PrintStream ps = null;
try {
FileOutputStream fos = new FileOutputStream(file);
ps = new PrintStream(fos);
} catch (FileNotFoundException e) {
e.printStackTrace();
return null;
} catch (NullPointerException e) {
e.printStackTrace();
return null;
}
/**
*
*/
private void exportCallNote(Cursor dataCursor) throws IOException {
// 打印电话号码
String phoneNumber = dataCursor.getString(DATA_COLUMN_PHONE_NUMBER);
long callDate = dataCursor.getLong(DATA_COLUMN_CALL_DATE);
String location = dataCursor.getString(DATA_COLUMN_CONTENT);
if (!TextUtils.isEmpty(phoneNumber)) {
mWriter.write(String.format(getFormat(FORMAT_NOTE_CONTENT), phoneNumber));
mWriter.newLine();
}
// 打印通话日期
mWriter.write(String.format(getFormat(FORMAT_NOTE_CONTENT),
DateFormat.format(mContext.getString(R.string.format_datetime_mdhm), callDate)));
mWriter.newLine();
// 打印通话附件位置
if (!TextUtils.isEmpty(location)) {
mWriter.write(String.format(getFormat(FORMAT_NOTE_CONTENT), location));
mWriter.newLine();
}
}
/**
*
*/
private void exportTextNote(Cursor dataCursor) throws IOException {
String content = dataCursor.getString(DATA_COLUMN_CONTENT);
if (!TextUtils.isEmpty(content)) {
mWriter.write(String.format(getFormat(FORMAT_NOTE_CONTENT), content));
mWriter.newLine();
}
return ps;
}
}
/**
* Generate the text file to store imported data
*/
private static File generateFileMountedOnSDcard(Context context, int filePathResId, int fileNameFormatResId) {
StringBuilder sb = new StringBuilder();
sb.append(Environment.getExternalStorageDirectory());
sb.append(context.getString(filePathResId));
File filedir = new File(sb.toString());
sb.append(context.getString(
fileNameFormatResId,
DateFormat.format(context.getString(R.string.format_date_ymd),
System.currentTimeMillis())));
File file = new File(sb.toString());
try {
if (!filedir.exists()) {
filedir.mkdir();
/**
* Generate the text file to store imported data
*/
private static File generateFileMountedOnSDcard(Context context, int filePathResId, int fileNameFormatResId) {
StringBuilder sb = new StringBuilder();
// 适配Android 10+的存储访问
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
sb.append(context.getExternalFilesDir(null));
} else {
sb.append(Environment.getExternalStorageDirectory());
}
if (!file.exists()) {
file.createNewFile();
sb.append(context.getString(filePathResId));
File filedir = new File(sb.toString());
// 确保目录存在
if (!filedir.exists() && !filedir.mkdirs()) {
Log.e(TAG, "Failed to create directory: " + filedir.getAbsolutePath());
return null;
}
return file;
} catch (SecurityException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
// 构建文件名
sb.append(context.getString(
fileNameFormatResId,
DateFormat.format(context.getString(R.string.format_date_ymd),
System.currentTimeMillis())));
File file = new File(sb.toString());
return null;
try {
if (!file.exists() && !file.createNewFile()) {
Log.e(TAG, "Failed to create file: " + file.getAbsolutePath());
return null;
}
return file;
} catch (SecurityException | IOException e) {
Log.e(TAG, "Error creating file: " + e.getMessage(), e);
return null;
}
}
}
}

@ -34,9 +34,12 @@ import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute;
import java.util.ArrayList;
import java.util.HashSet;
public class DataUtils {
public static final String TAG = "DataUtils";
/**
*
*/
public static boolean batchDeleteNotes(ContentResolver resolver, HashSet<Long> ids) {
if (ids == null) {
Log.d(TAG, "the ids is null");
@ -72,6 +75,9 @@ public class DataUtils {
return false;
}
/**
*
*/
public static void moveNoteToFoler(ContentResolver resolver, long id, long srcFolderId, long desFolderId) {
ContentValues values = new ContentValues();
values.put(NoteColumns.PARENT_ID, desFolderId);
@ -80,6 +86,9 @@ public class DataUtils {
resolver.update(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id), values, null, null);
}
/**
*
*/
public static boolean batchMoveToFolder(ContentResolver resolver, HashSet<Long> ids,
long folderId) {
if (ids == null) {
@ -112,10 +121,10 @@ public class DataUtils {
}
/**
* Get the all folder count except system folders {@link Notes#TYPE_SYSTEM}}
*
*/
public static int getUserFolderCount(ContentResolver resolver) {
Cursor cursor =resolver.query(Notes.CONTENT_NOTE_URI,
Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI,
new String[] { "COUNT(*)" },
NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>?",
new String[] { String.valueOf(Notes.TYPE_FOLDER), String.valueOf(Notes.ID_TRASH_FOLER)},
@ -136,6 +145,9 @@ public class DataUtils {
return count;
}
/**
*
*/
public static boolean visibleInNoteDatabase(ContentResolver resolver, long noteId, int type) {
Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId),
null,
@ -153,6 +165,9 @@ public class DataUtils {
return exist;
}
/**
*
*/
public static boolean existInNoteDatabase(ContentResolver resolver, long noteId) {
Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId),
null, null, null, null);
@ -167,6 +182,9 @@ public class DataUtils {
return exist;
}
/**
*
*/
public static boolean existInDataDatabase(ContentResolver resolver, long dataId) {
Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId),
null, null, null, null);
@ -181,6 +199,9 @@ public class DataUtils {
return exist;
}
/**
*
*/
public static boolean checkVisibleFolderName(ContentResolver resolver, String name) {
Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI, null,
NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER +
@ -197,6 +218,9 @@ public class DataUtils {
return exist;
}
/**
*
*/
public static HashSet<AppWidgetAttribute> getFolderNoteWidget(ContentResolver resolver, long folderId) {
Cursor c = resolver.query(Notes.CONTENT_NOTE_URI,
new String[] { NoteColumns.WIDGET_ID, NoteColumns.WIDGET_TYPE },
@ -224,6 +248,9 @@ public class DataUtils {
return set;
}
/**
* ID
*/
public static String getCallNumberByNoteId(ContentResolver resolver, long noteId) {
Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI,
new String [] { CallNote.PHONE_NUMBER },
@ -243,6 +270,9 @@ public class DataUtils {
return "";
}
/**
* ID
*/
public static long getNoteIdByPhoneNumberAndCallDate(ContentResolver resolver, String phoneNumber, long callDate) {
Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI,
new String [] { CallNote.NOTE_ID },
@ -264,6 +294,9 @@ public class DataUtils {
return 0;
}
/**
* ID
*/
public static String getSnippetById(ContentResolver resolver, long noteId) {
Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI,
new String [] { NoteColumns.SNIPPET },
@ -282,6 +315,9 @@ public class DataUtils {
throw new IllegalArgumentException("Note is not found with id: " + noteId);
}
/**
*
*/
public static String getFormattedSnippet(String snippet) {
if (snippet != null) {
snippet = snippet.trim();
@ -293,3 +329,4 @@ public class DataUtils {
return snippet;
}
}

@ -16,98 +16,268 @@
package net.micode.notes.tool;
public class GTaskStringUtils {
/**
* Google Tasks API - Google Tasks API
*
* 1. JSON
* 2. API
* 3.
* 4.
*
*
* -
* -
* - 使
*/
public final class GTaskStringUtils {
// 防止实例化
private GTaskStringUtils() {
throw new AssertionError("This class cannot be instantiated");
}
/* ================== JSON 键名常量 ================== */
/** JSON字段操作ID */
public static final String GTASK_JSON_ACTION_ID = "action_id";
/** JSON字段操作列表 */
public static final String GTASK_JSON_ACTION_LIST = "action_list";
/** JSON字段操作类型 */
public static final String GTASK_JSON_ACTION_TYPE = "action_type";
/** JSON字段创建者ID */
public static final String GTASK_JSON_CREATOR_ID = "creator_id";
/** JSON字段子实体 */
public static final String GTASK_JSON_CHILD_ENTITY = "child_entity";
/** JSON字段客户端版本 */
public static final String GTASK_JSON_CLIENT_VERSION = "client_version";
/** JSON字段完成状态 */
public static final String GTASK_JSON_COMPLETED = "completed";
/** JSON字段当前列表ID */
public static final String GTASK_JSON_CURRENT_LIST_ID = "current_list_id";
public final static String GTASK_JSON_ACTION_ID = "action_id";
/** JSON字段默认列表ID */
public static final String GTASK_JSON_DEFAULT_LIST_ID = "default_list_id";
public final static String GTASK_JSON_ACTION_LIST = "action_list";
/** JSON字段删除标记 */
public static final String GTASK_JSON_DELETED = "deleted";
public final static String GTASK_JSON_ACTION_TYPE = "action_type";
/** JSON字段目标列表 */
public static final String GTASK_JSON_DEST_LIST = "dest_list";
public final static String GTASK_JSON_ACTION_TYPE_CREATE = "create";
/** JSON字段目标父级 */
public static final String GTASK_JSON_DEST_PARENT = "dest_parent";
public final static String GTASK_JSON_ACTION_TYPE_GETALL = "get_all";
/** JSON字段目标父级类型 */
public static final String GTASK_JSON_DEST_PARENT_TYPE = "dest_parent_type";
public final static String GTASK_JSON_ACTION_TYPE_MOVE = "move";
/** JSON字段实体增量 */
public static final String GTASK_JSON_ENTITY_DELTA = "entity_delta";
public final static String GTASK_JSON_ACTION_TYPE_UPDATE = "update";
/** JSON字段实体类型 */
public static final String GTASK_JSON_ENTITY_TYPE = "entity_type";
public final static String GTASK_JSON_CREATOR_ID = "creator_id";
/** JSON字段获取删除项 */
public static final String GTASK_JSON_GET_DELETED = "get_deleted";
public final static String GTASK_JSON_CHILD_ENTITY = "child_entity";
/** JSON字段唯一标识 */
public static final String GTASK_JSON_ID = "id";
public final static String GTASK_JSON_CLIENT_VERSION = "client_version";
/** JSON字段索引位置 */
public static final String GTASK_JSON_INDEX = "index";
public final static String GTASK_JSON_COMPLETED = "completed";
/** JSON字段最后修改时间 */
public static final String GTASK_JSON_LAST_MODIFIED = "last_modified";
public final static String GTASK_JSON_CURRENT_LIST_ID = "current_list_id";
/** JSON字段最新同步点 */
public static final String GTASK_JSON_LATEST_SYNC_POINT = "latest_sync_point";
public final static String GTASK_JSON_DEFAULT_LIST_ID = "default_list_id";
/** JSON字段列表ID */
public static final String GTASK_JSON_LIST_ID = "list_id";
public final static String GTASK_JSON_DELETED = "deleted";
/** JSON字段列表集合 */
public static final String GTASK_JSON_LISTS = "lists";
public final static String GTASK_JSON_DEST_LIST = "dest_list";
/** JSON字段名称 */
public static final String GTASK_JSON_NAME = "name";
public final static String GTASK_JSON_DEST_PARENT = "dest_parent";
/** JSON字段新ID */
public static final String GTASK_JSON_NEW_ID = "new_id";
public final static String GTASK_JSON_DEST_PARENT_TYPE = "dest_parent_type";
/** JSON字段笔记集合 */
public static final String GTASK_JSON_NOTES = "notes";
public final static String GTASK_JSON_ENTITY_DELTA = "entity_delta";
/** JSON字段父级ID */
public static final String GTASK_JSON_PARENT_ID = "parent_id";
public final static String GTASK_JSON_ENTITY_TYPE = "entity_type";
/** JSON字段前置兄弟ID */
public static final String GTASK_JSON_PRIOR_SIBLING_ID = "prior_sibling_id";
public final static String GTASK_JSON_GET_DELETED = "get_deleted";
/** JSON字段结果集 */
public static final String GTASK_JSON_RESULTS = "results";
public final static String GTASK_JSON_ID = "id";
/** JSON字段源列表 */
public static final String GTASK_JSON_SOURCE_LIST = "source_list";
public final static String GTASK_JSON_INDEX = "index";
/** JSON字段任务集合 */
public static final String GTASK_JSON_TASKS = "tasks";
public final static String GTASK_JSON_LAST_MODIFIED = "last_modified";
/** JSON字段类型标识 */
public static final String GTASK_JSON_TYPE = "type";
public final static String GTASK_JSON_LATEST_SYNC_POINT = "latest_sync_point";
/* ================== 操作类型常量 ================== */
public final static String GTASK_JSON_LIST_ID = "list_id";
/** 操作类型:创建 */
public static final String GTASK_JSON_ACTION_TYPE_CREATE = "create";
public final static String GTASK_JSON_LISTS = "lists";
/** 操作类型:获取所有 */
public static final String GTASK_JSON_ACTION_TYPE_GETALL = "get_all";
public final static String GTASK_JSON_NAME = "name";
/** 操作类型:移动 */
public static final String GTASK_JSON_ACTION_TYPE_MOVE = "move";
public final static String GTASK_JSON_NEW_ID = "new_id";
/** 操作类型:更新 */
public static final String GTASK_JSON_ACTION_TYPE_UPDATE = "update";
public final static String GTASK_JSON_NOTES = "notes";
/* ================== 实体类型常量 ================== */
public final static String GTASK_JSON_PARENT_ID = "parent_id";
/** 实体类型:分组 */
public static final String GTASK_JSON_TYPE_GROUP = "GROUP";
public final static String GTASK_JSON_PRIOR_SIBLING_ID = "prior_sibling_id";
/** 实体类型:任务 */
public static final String GTASK_JSON_TYPE_TASK = "TASK";
public final static String GTASK_JSON_RESULTS = "results";
/** JSON字段用户 */
public static final String GTASK_JSON_USER = "user";
public final static String GTASK_JSON_SOURCE_LIST = "source_list";
/* ================== 文件夹常量 ================== */
public final static String GTASK_JSON_TASKS = "tasks";
/** MIUI笔记文件夹前缀 */
public static final String MIUI_FOLDER_PREFIX = "[MIUI_Notes]"; // 修正PREFFIX → PREFIX
public final static String GTASK_JSON_TYPE = "type";
/** 默认文件夹名称 */
public static final String FOLDER_DEFAULT = "Default";
public final static String GTASK_JSON_TYPE_GROUP = "GROUP";
/** 通话记录文件夹名称 */
public static final String FOLDER_CALL_NOTE = "Call_Note";
public final static String GTASK_JSON_TYPE_TASK = "TASK";
/** 元数据文件夹名称 */
public static final String FOLDER_META = "METADATA";
public final static String GTASK_JSON_USER = "user";
/* ================== 元数据常量 ================== */
public final static String MIUI_FOLDER_PREFFIX = "[MIUI_Notes]";
/** 元数据头Google Tasks ID */
public static final String META_HEAD_GTASK_ID = "meta_gid";
public final static String FOLDER_DEFAULT = "Default";
/** 元数据头:笔记信息 */
public static final String META_HEAD_NOTE = "meta_note";
public final static String FOLDER_CALL_NOTE = "Call_Note";
/** 元数据头:数据信息 */
public static final String META_HEAD_DATA = "meta_data";
public final static String FOLDER_META = "METADATA";
/** 元数据笔记名称(警告用户不要修改) */
public static final String META_NOTE_NAME = "[META INFO] DON'T UPDATE AND DELETE";
public final static String META_HEAD_GTASK_ID = "meta_gid";
/* ================== 增强功能:操作类型枚举 ================== */
public final static String META_HEAD_NOTE = "meta_note";
/**
* Google Tasks
*/
public enum GTaskActionType {
CREATE(GTASK_JSON_ACTION_TYPE_CREATE),
GET_ALL(GTASK_JSON_ACTION_TYPE_GETALL),
MOVE(GTASK_JSON_ACTION_TYPE_MOVE),
UPDATE(GTASK_JSON_ACTION_TYPE_UPDATE);
public final static String META_HEAD_DATA = "meta_data";
private final String actionValue;
public final static String META_NOTE_NAME = "[META INFO] DON'T UPDATE AND DELETE";
GTaskActionType(String value) {
this.actionValue = value;
}
}
public String getValue() {
return actionValue;
}
/**
*
* @param value
* @return null
*/
public static GTaskActionType fromValue(String value) {
for (GTaskActionType type : values()) {
if (type.actionValue.equals(value)) {
return type;
}
}
return null;
}
}
/* ================== 增强功能:实体类型枚举 ================== */
/**
* Google Tasks
*/
public enum GTaskEntityType {
GROUP(GTASK_JSON_TYPE_GROUP),
TASK(GTASK_JSON_TYPE_TASK);
private final String typeValue;
GTaskEntityType(String value) {
this.typeValue = value;
}
public String getValue() {
return typeValue;
}
public static GTaskEntityType fromValue(String value) {
for (GTaskEntityType type : values()) {
if (type.typeValue.equals(value)) {
return type;
}
}
return null;
}
}
/* ================== 新功能:文件夹工具方法 ================== */
/**
* MIUI
* @param folderName
* @return
*/
public static String generateSyncFolderName(String folderName) {
return MIUI_FOLDER_PREFIX + folderName;
}
/**
* MIUI
* @param folderName
* @return MIUItrue
*/
public static boolean isMiuiSyncFolder(String folderName) {
return folderName != null && folderName.startsWith(MIUI_FOLDER_PREFIX);
}
/**
* MIUI
* @param miuiFolderName MIUI
* @return MIUInull
*/
public static String extractOriginalFolderName(String miuiFolderName) {
if (isMiuiSyncFolder(miuiFolderName)) {
return miuiFolderName.substring(MIUI_FOLDER_PREFIX.length());
}
return null;
}
}

@ -17,165 +17,469 @@
package net.micode.notes.tool;
import android.content.Context;
import android.content.res.Resources;
import android.preference.PreferenceManager;
import android.util.SparseIntArray;
import net.micode.notes.R;
import net.micode.notes.ui.NotesPreferenceActivity;
/**
* - UI
*
* 1.
* 2.
* 3.
* 4.
*
*
* - ID
* -
* -
*/
public class ResourceParser {
public static final int YELLOW = 0;
public static final int BLUE = 1;
public static final int WHITE = 2;
public static final int GREEN = 3;
public static final int RED = 4;
// 防止实例化
private ResourceParser() {
throw new AssertionError("This class cannot be instantiated");
}
/* ================== 颜色常量枚举 ================== */
/**
*
*/
public enum NoteColor {
YELLOW(0),
BLUE(1),
WHITE(2),
GREEN(3),
RED(4),
// 扩展更多颜色选项
PURPLE(5),
ORANGE(6);
public static final int BG_DEFAULT_COLOR = YELLOW;
public final int id;
public static final int TEXT_SMALL = 0;
public static final int TEXT_MEDIUM = 1;
public static final int TEXT_LARGE = 2;
public static final int TEXT_SUPER = 3;
NoteColor(int id) {
this.id = id;
}
private static final int DEFAULT_ID = YELLOW.id;
/**
* ID
*/
public static int getDefault() {
return DEFAULT_ID;
}
public static final int BG_DEFAULT_FONT_SIZE = TEXT_MEDIUM;
/**
*
*/
public static NoteColor fromId(int id) {
for (NoteColor color : values()) {
if (color.id == id) {
return color;
}
}
return YELLOW;
}
}
/* ================== 字体大小常量枚举 ================== */
/**
*
*/
public enum FontSize {
SMALL(0),
MEDIUM(1),
LARGE(2),
SUPER(3);
public final int id;
FontSize(int id) {
this.id = id;
}
private static final int DEFAULT_ID = MEDIUM.id;
/**
* ID
*/
public static int getDefault() {
return DEFAULT_ID;
}
}
/* ================== 主题模式常量 ================== */
/**
*
*/
public enum ThemeMode {
LIGHT(0),
DARK(1),
AUTO(2);
public final int id;
ThemeMode(int id) {
this.id = id;
}
public static ThemeMode fromId(int id) {
switch (id) {
case 1: return DARK;
case 2: return AUTO;
default: return LIGHT;
}
}
}
/* ================== 笔记背景资源管理 ================== */
/**
*
*/
public static class NoteBgResources {
private final static int [] BG_EDIT_RESOURCES = new int [] {
R.drawable.edit_yellow,
R.drawable.edit_blue,
R.drawable.edit_white,
R.drawable.edit_green,
R.drawable.edit_red
};
// 编辑界面背景资源
private static final SparseIntArray BG_EDIT_RESOURCES = new SparseIntArray();
private final static int [] BG_EDIT_TITLE_RESOURCES = new int [] {
R.drawable.edit_title_yellow,
R.drawable.edit_title_blue,
R.drawable.edit_title_white,
R.drawable.edit_title_green,
R.drawable.edit_title_red
};
// 编辑界面标题背景资源
private static final SparseIntArray BG_EDIT_TITLE_RESOURCES = new SparseIntArray();
public static int getNoteBgResource(int id) {
return BG_EDIT_RESOURCES[id];
// 静态初始化资源映射
static {
// 浅色主题资源
BG_EDIT_RESOURCES.put(NoteColor.YELLOW.id, R.drawable.edit_yellow);
BG_EDIT_RESOURCES.put(NoteColor.BLUE.id, R.drawable.edit_blue);
BG_EDIT_RESOURCES.put(NoteColor.WHITE.id, R.drawable.edit_white);
BG_EDIT_RESOURCES.put(NoteColor.GREEN.id, R.drawable.edit_green);
BG_EDIT_RESOURCES.put(NoteColor.RED.id, R.drawable.edit_red);
BG_EDIT_RESOURCES.put(NoteColor.PURPLE.id, R.drawable.edit_purple);
BG_EDIT_RESOURCES.put(NoteColor.ORANGE.id, R.drawable.edit_orange);
BG_EDIT_TITLE_RESOURCES.put(NoteColor.YELLOW.id, R.drawable.edit_title_yellow);
BG_EDIT_TITLE_RESOURCES.put(NoteColor.BLUE.id, R.drawable.edit_title_blue);
BG_EDIT_TITLE_RESOURCES.put(NoteColor.WHITE.id, R.drawable.edit_title_white);
BG_EDIT_TITLE_RESOURCES.put(NoteColor.GREEN.id, R.drawable.edit_title_green);
BG_EDIT_TITLE_RESOURCES.put(NoteColor.RED.id, R.drawable.edit_title_red);
BG_EDIT_TITLE_RESOURCES.put(NoteColor.PURPLE.id, R.drawable.edit_title_purple);
BG_EDIT_TITLE_RESOURCES.put(NoteColor.ORANGE.id, R.drawable.edit_title_orange);
}
public static int getNoteTitleBgResource(int id) {
return BG_EDIT_TITLE_RESOURCES[id];
/**
* ID
*
* @param colorId ID
* @return ID
*/
public static int getNoteBgResource(int colorId) {
return BG_EDIT_RESOURCES.get(colorId, R.drawable.edit_yellow);
}
/**
* ID
*
* @param colorId ID
* @return ID
*/
public static int getNoteTitleBgResource(int colorId) {
return BG_EDIT_TITLE_RESOURCES.get(colorId, R.drawable.edit_title_yellow);
}
/**
* ID
* @return ID
*/
public static int[] getAvailableColorIds() {
int[] ids = new int[BG_EDIT_RESOURCES.size()];
for (int i = 0; i < BG_EDIT_RESOURCES.size(); i++) {
ids[i] = BG_EDIT_RESOURCES.keyAt(i);
}
return ids;
}
}
/**
* ID
*
* @param context
* @return ID
*/
public static int getDefaultBgId(Context context) {
// 检查是否启用随机背景颜色
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean(
NotesPreferenceActivity.PREFERENCE_SET_BG_COLOR_KEY, false)) {
return (int) (Math.random() * NoteBgResources.BG_EDIT_RESOURCES.length);
} else {
return BG_DEFAULT_COLOR;
// 获取所有可用颜色ID
int[] colorIds = NoteBgResources.getAvailableColorIds();
if (colorIds.length > 0) {
return colorIds[(int) (Math.random() * colorIds.length)];
}
}
// 返回默认颜色
return NoteColor.getDefault();
}
public static class NoteItemBgResources {
private final static int [] BG_FIRST_RESOURCES = new int [] {
R.drawable.list_yellow_up,
R.drawable.list_blue_up,
R.drawable.list_white_up,
R.drawable.list_green_up,
R.drawable.list_red_up
};
/* ================== 笔记布局资源管理 ================== */
/**
*
*/
public static class NoteLayoutResources {
// 列表顶部背景资源
private static final SparseIntArray BG_FIRST_RESOURCES = new SparseIntArray();
// 列表中部背景资源
private static final SparseIntArray BG_NORMAL_RESOURCES = new SparseIntArray();
// 列表底部背景资源
private static final SparseIntArray BG_LAST_RESOURCES = new SparseIntArray();
private final static int [] BG_NORMAL_RESOURCES = new int [] {
R.drawable.list_yellow_middle,
R.drawable.list_blue_middle,
R.drawable.list_white_middle,
R.drawable.list_green_middle,
R.drawable.list_red_middle
};
// 单条笔记背景资源
private static final SparseIntArray BG_SINGLE_RESOURCES = new SparseIntArray();
private final static int [] BG_LAST_RESOURCES = new int [] {
R.drawable.list_yellow_down,
R.drawable.list_blue_down,
R.drawable.list_white_down,
R.drawable.list_green_down,
R.drawable.list_red_down,
};
// 文件夹背景资源
private static final SparseIntArray BG_FOLDER_RESOURCES = new SparseIntArray();
private final static int [] BG_SINGLE_RESOURCES = new int [] {
R.drawable.list_yellow_single,
R.drawable.list_blue_single,
R.drawable.list_white_single,
R.drawable.list_green_single,
R.drawable.list_red_single
};
// 静态初始化资源映射
static {
// 浅色主题资源
BG_FIRST_RESOURCES.put(NoteColor.YELLOW.id, R.drawable.list_yellow_up);
BG_FIRST_RESOURCES.put(NoteColor.BLUE.id, R.drawable.list_blue_up);
BG_FIRST_RESOURCES.put(NoteColor.WHITE.id, R.drawable.list_white_up);
BG_FIRST_RESOURCES.put(NoteColor.GREEN.id, R.drawable.list_green_up);
BG_FIRST_RESOURCES.put(NoteColor.RED.id, R.drawable.list_red_up);
BG_FIRST_RESOURCES.put(NoteColor.PURPLE.id, R.drawable.list_purple_up);
BG_FIRST_RESOURCES.put(NoteColor.ORANGE.id, R.drawable.list_orange_up);
public static int getNoteBgFirstRes(int id) {
return BG_FIRST_RESOURCES[id];
BG_NORMAL_RESOURCES.put(NoteColor.YELLOW.id, R.drawable.list_yellow_middle);
BG_NORMAL_RESOURCES.put(NoteColor.BLUE.id, R.drawable.list_blue_middle);
BG_NORMAL_RESOURCES.put(NoteColor.WHITE.id, R.drawable.list_white_middle);
BG_NORMAL_RESOURCES.put(NoteColor.GREEN.id, R.drawable.list_green_middle);
BG_NORMAL_RESOURCES.put(NoteColor.RED.id, R.drawable.list_red_middle);
BG_NORMAL_RESOURCES.put(NoteColor.PURPLE.id, R.drawable.list_purple_middle);
BG_NORMAL_RESOURCES.put(NoteColor.ORANGE.id, R.drawable.list_orange_middle);
BG_LAST_RESOURCES.put(NoteColor.YELLOW.id, R.drawable.list_yellow_down);
BG_LAST_RESOURCES.put(NoteColor.BLUE.id, R.drawable.list_blue_down);
BG_LAST_RESOURCES.put(NoteColor.WHITE.id, R.drawable.list_white_down);
BG_LAST_RESOURCES.put(NoteColor.GREEN.id, R.drawable.list_green_down);
BG_LAST_RESOURCES.put(NoteColor.RED.id, R.drawable.list_red_down);
BG_LAST_RESOURCES.put(NoteColor.PURPLE.id, R.drawable.list_purple_down);
BG_LAST_RESOURCES.put(NoteColor.ORANGE.id, R.drawable.list_orange_down);
BG_SINGLE_RESOURCES.put(NoteColor.YELLOW.id, R.drawable.list_yellow_single);
BG_SINGLE_RESOURCES.put(NoteColor.BLUE.id, R.drawable.list_blue_single);
BG_SINGLE_RESOURCES.put(NoteColor.WHITE.id, R.drawable.list_white_single);
BG_SINGLE_RESOURCES.put(NoteColor.GREEN.id, R.drawable.list_green_single);
BG_SINGLE_RESOURCES.put(NoteColor.RED.id, R.drawable.list_red_single);
BG_SINGLE_RESOURCES.put(NoteColor.PURPLE.id, R.drawable.list_purple_single);
BG_SINGLE_RESOURCES.put(NoteColor.ORANGE.id, R.drawable.list_orange_single);
BG_FOLDER_RESOURCES.put(ThemeMode.LIGHT.id, R.drawable.list_folder_light);
BG_FOLDER_RESOURCES.put(ThemeMode.DARK.id, R.drawable.list_folder_dark);
}
/**
*
*
* @param colorId ID
* @return ID
*/
public static int getNoteBgFirstRes(int colorId) {
return BG_FIRST_RESOURCES.get(colorId, R.drawable.list_yellow_up);
}
public static int getNoteBgLastRes(int id) {
return BG_LAST_RESOURCES[id];
/**
*
*
* @param colorId ID
* @return ID
*/
public static int getNoteBgLastRes(int colorId) {
return BG_LAST_RESOURCES.get(colorId, R.drawable.list_yellow_down);
}
public static int getNoteBgSingleRes(int id) {
return BG_SINGLE_RESOURCES[id];
/**
*
*
* @param colorId ID
* @return ID
*/
public static int getNoteBgSingleRes(int colorId) {
return BG_SINGLE_RESOURCES.get(colorId, R.drawable.list_yellow_single);
}
public static int getNoteBgNormalRes(int id) {
return BG_NORMAL_RESOURCES[id];
/**
*
*
* @param colorId ID
* @return ID
*/
public static int getNoteBgNormalRes(int colorId) {
return BG_NORMAL_RESOURCES.get(colorId, R.drawable.list_yellow_middle);
}
public static int getFolderBgRes() {
return R.drawable.list_folder;
/**
*
*
* @param themeMode
* @return ID
*/
public static int getFolderBgRes(ThemeMode themeMode) {
return BG_FOLDER_RESOURCES.get(themeMode.id, R.drawable.list_folder_light);
}
}
/* ================== 小部件资源管理 ================== */
/**
*
*/
public static class WidgetBgResources {
private final static int [] BG_2X_RESOURCES = new int [] {
R.drawable.widget_2x_yellow,
R.drawable.widget_2x_blue,
R.drawable.widget_2x_white,
R.drawable.widget_2x_green,
R.drawable.widget_2x_red,
};
// 2x小部件背景资源
private static final SparseIntArray BG_2X_RESOURCES = new SparseIntArray();
// 4x小部件背景资源
private static final SparseIntArray BG_4X_RESOURCES = new SparseIntArray();
// 静态初始化资源映射
static {
// 浅色主题资源
BG_2X_RESOURCES.put(NoteColor.YELLOW.id, R.drawable.widget_2x_yellow);
BG_2X_RESOURCES.put(NoteColor.BLUE.id, R.drawable.widget_2x_blue);
BG_2X_RESOURCES.put(NoteColor.WHITE.id, R.drawable.widget_2x_white);
BG_2X_RESOURCES.put(NoteColor.GREEN.id, R.drawable.widget_2x_green);
BG_2X_RESOURCES.put(NoteColor.RED.id, R.drawable.widget_2x_red);
BG_2X_RESOURCES.put(NoteColor.PURPLE.id, R.drawable.widget_2x_purple);
BG_2X_RESOURCES.put(NoteColor.ORANGE.id, R.drawable.widget_2x_orange);
public static int getWidget2xBgResource(int id) {
return BG_2X_RESOURCES[id];
BG_4X_RESOURCES.put(NoteColor.YELLOW.id, R.drawable.widget_4x_yellow);
BG_4X_RESOURCES.put(NoteColor.BLUE.id, R.drawable.widget_4x_blue);
BG_4X_RESOURCES.put(NoteColor.WHITE.id, R.drawable.widget_4x_white);
BG_4X_RESOURCES.put(NoteColor.GREEN.id, R.drawable.widget_4x_green);
BG_4X_RESOURCES.put(NoteColor.RED.id, R.drawable.widget_4x_red);
BG_4X_RESOURCES.put(NoteColor.PURPLE.id, R.drawable.widget_4x_purple);
BG_4X_RESOURCES.put(NoteColor.ORANGE.id, R.drawable.widget_4x_orange);
}
private final static int [] BG_4X_RESOURCES = new int [] {
R.drawable.widget_4x_yellow,
R.drawable.widget_4x_blue,
R.drawable.widget_4x_white,
R.drawable.widget_4x_green,
R.drawable.widget_4x_red
};
/**
* 2x
*
* @param colorId ID
* @return ID
*/
public static int getWidget2xBgResource(int colorId) {
return BG_2X_RESOURCES.get(colorId, R.drawable.widget_2x_yellow);
}
public static int getWidget4xBgResource(int id) {
return BG_4X_RESOURCES[id];
/**
* 4x
*
* @param colorId ID
* @return ID
*/
public static int getWidget4xBgResource(int colorId) {
return BG_4X_RESOURCES.get(colorId, R.drawable.widget_4x_yellow);
}
}
/* ================== 文本外观资源管理 ================== */
/**
*
*/
public static class TextAppearanceResources {
private final static int [] TEXTAPPEARANCE_RESOURCES = new int [] {
R.style.TextAppearanceNormal,
R.style.TextAppearanceMedium,
R.style.TextAppearanceLarge,
R.style.TextAppearanceSuper
};
public static int getTexAppearanceResource(int id) {
/**
* HACKME: Fix bug of store the resource id in shared preference.
* The id may larger than the length of resources, in this case,
* return the {@link ResourceParser#BG_DEFAULT_FONT_SIZE}
*/
if (id >= TEXTAPPEARANCE_RESOURCES.length) {
return BG_DEFAULT_FONT_SIZE;
// 文本外观资源映射
private static final SparseIntArray TEXTAPPEARANCE_RESOURCES = new SparseIntArray();
// 静态初始化资源映射
static {
TEXTAPPEARANCE_RESOURCES.put(FontSize.SMALL.id, R.style.TextAppearanceSmall);
TEXTAPPEARANCE_RESOURCES.put(FontSize.MEDIUM.id, R.style.TextAppearanceMedium);
TEXTAPPEARANCE_RESOURCES.put(FontSize.LARGE.id, R.style.TextAppearanceLarge);
TEXTAPPEARANCE_RESOURCES.put(FontSize.SUPER.id, R.style.TextAppearanceSuper);
}
/**
*
*
* @param fontSizeId ID
* @return ID
*/
public static int getTextAppearanceResource(int fontSizeId) {
// 边界检查防止越界
int resourceId = TEXTAPPEARANCE_RESOURCES.get(fontSizeId, -1);
return resourceId != -1 ? resourceId : FontSize.getDefault();
}
/**
*
*
* @return
*/
public static int getAvailableSizeCount() {
return TEXTAPPEARANCE_RESOURCES.size();
}
/**
* sp
*
* @param context
* @param fontSizeId ID
* @return sp
*/
public static float getFontSizeSp(Context context, int fontSizeId) {
Resources res = context.getResources();
// 映射字体大小ID到实际尺寸值
switch (fontSizeId) {
case 0: return res.getDimension(R.dimen.text_size_small) / res.getDisplayMetrics().scaledDensity;
case 1: return res.getDimension(R.dimen.text_size_medium) / res.getDisplayMetrics().scaledDensity;
case 2: return res.getDimension(R.dimen.text_size_large) / res.getDisplayMetrics().scaledDensity;
case 3: return res.getDimension(R.dimen.text_size_super) / res.getDisplayMetrics().scaledDensity;
default: return 16; // 默认16sp
}
return TEXTAPPEARANCE_RESOURCES[id];
}
}
/* ================== 新功能:主题资源解析 ================== */
public static int getResourcesSize() {
return TEXTAPPEARANCE_RESOURCES.length;
/**
*
*
* @param context
* @return
*/
public static ThemeMode getCurrentThemeMode(Context context) {
// 从偏好设置获取主题ID
int themeId = PreferenceManager.getDefaultSharedPreferences(context)
.getInt(NotesPreferenceActivity.PREFERENCE_THEME_MODE, 0);
return ThemeMode.fromId(themeId);
}
/**
* ID
*
* @param colorId ID
* @param themeMode
* @return ID
*/
public static int getThemeAdjustedColor(int colorId, ThemeMode themeMode) {
// 深色模式下调整颜色饱和度
if (themeMode == ThemeMode.DARK) {
switch (NoteColor.fromId(colorId)) {
case YELLOW: return NoteColor.ORANGE.id;
case WHITE: return NoteColor.BLUE.id;
case GREEN: return NoteColor.PURPLE.id;
default: return colorId;
}
}
return colorId;
}
}
}

@ -1,5 +1,5 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
* 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.

@ -19,12 +19,187 @@ package net.micode.notes.ui;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.util.Log;
/**
* 广 - 广
*
*
* 1.
* 2. 广
* 3.
* 4. Android 8.0+
* 5.
*/
public class AlarmReceiver extends BroadcastReceiver {
private static final String TAG = "AlarmReceiver";
private static final String WAKE_LOCK_TAG = "Notes:AlarmWakeLock";
private static final long WAKE_LOCK_TIMEOUT = 30 * 1000; // 30秒超时
/**
* 广 - 广
*
* @param context
* @param intent 广
*/
@Override
public void onReceive(Context context, Intent intent) {
intent.setClass(context, AlarmAlertActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
// 日志记录接收到的广播
logBroadcastReceived(intent);
// 验证广播是否有效
if (!isValidAlarmIntent(intent)) {
Log.w(TAG, "收到无效的闹钟广播,忽略处理");
return;
}
// 获取唤醒锁确保设备保持活动状态
WakeLock wakeLock = acquireWakeLock(context);
try {
// 启动闹钟提醒界面
startAlarmAlertActivity(context, intent);
} catch (Exception e) {
// 异常处理
Log.e(TAG, "启动闹钟提醒活动失败", e);
} finally {
// 确保释放唤醒锁
releaseWakeLock(wakeLock);
}
}
/**
* 广
*
* @param intent
*/
private void logBroadcastReceived(Intent intent) {
if (intent == null) {
Log.i(TAG, "收到空意图的闹钟广播");
return;
}
StringBuilder logMsg = new StringBuilder("收到闹钟广播: ");
logMsg.append("Action=").append(intent.getAction());
if (intent.getData() != null) {
logMsg.append(", URI=").append(intent.getData().toString());
}
if (intent.getExtras() != null) {
logMsg.append(", Extras=").append(intent.getExtras().toString());
}
Log.d(TAG, logMsg.toString());
}
/**
* 广
*
* @param intent
* @return true
*/
private boolean isValidAlarmIntent(Intent intent) {
return intent != null &&
intent.getData() != null &&
intent.getData().getScheme() != null &&
intent.getData().getScheme().equals("content");
}
/**
*
*
* @param context
* @return
*/
private WakeLock acquireWakeLock(Context context) {
try {
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
if (pm == null) {
Log.w(TAG, "无法获取电源服务");
return null;
}
WakeLock wakeLock = pm.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK |
PowerManager.ACQUIRE_CAUSES_WAKEUP,
WAKE_LOCK_TAG
);
wakeLock.setReferenceCounted(false);
wakeLock.acquire(WAKE_LOCK_TIMEOUT);
Log.d(TAG, "成功获取唤醒锁");
return wakeLock;
} catch (Exception e) {
Log.e(TAG, "获取唤醒锁时出错", e);
return null;
}
}
/**
*
*
* @param wakeLock
*/
private void releaseWakeLock(WakeLock wakeLock) {
if (wakeLock == null) {
return;
}
try {
if (wakeLock.isHeld()) {
wakeLock.release();
Log.d(TAG, "唤醒锁已释放");
}
} catch (Exception e) {
Log.e(TAG, "释放唤醒锁时出错", e);
}
}
/**
*
*
* @param context
* @param originalIntent 广
*/
private void startAlarmAlertActivity(Context context, Intent originalIntent) {
// 创建启动AlarmAlertActivity的意图
Intent alertIntent = new Intent(context, AlarmAlertActivity.class);
// 传递原始意图的数据
alertIntent.setData(originalIntent.getData());
// 添加必要的标志
alertIntent.addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK |
Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
);
// 复制额外的参数
if (originalIntent.getExtras() != null) {
alertIntent.putExtras(originalIntent.getExtras());
}
// 启动活动
try {
Log.d(TAG, "正在启动闹钟提醒活动");
context.startActivity(alertIntent);
} catch (SecurityException e) {
Log.e(TAG, "启动活动权限不足", e);
} catch (Exception e) {
Log.e(TAG, "启动活动失败", e);
}
}
/**
* -
*/
@Override
public void finalize() {
// 添加资源清理逻辑
Log.v(TAG, "闹钟接收器实例被回收");
}
}
}

@ -16,22 +16,46 @@
package net.micode.notes.ui;
import java.text.DateFormatSymbols;
import java.util.Calendar;
import net.micode.notes.R;
import android.content.Context;
import android.os.Build;
import android.text.format.DateFormat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.NumberPicker;
import android.widget.Toast;
import net.micode.notes.R;
import java.text.DateFormatSymbols;
import java.util.Calendar;
import java.util.Locale;
/**
* -
*
* :
* 1. (/)
* 2.
* 3.
* 4.
* 5.
* 6.
* 7.
*
* :
* 1. 12/24
* 2.
* 3. AM/PM
* 4.
*/
public class DateTimePicker extends FrameLayout {
private static final boolean DEFAULT_ENABLE_STATE = true;
private static final String TAG = "DateTimePicker";
// 配置常量
private static final boolean DEFAULT_ENABLE_STATE = true;
private static final int HOURS_IN_HALF_DAY = 12;
private static final int HOURS_IN_ALL_DAY = 24;
private static final int DAYS_IN_ALL_WEEK = 7;
@ -41,130 +65,114 @@ public class DateTimePicker extends FrameLayout {
private static final int HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW = 23;
private static final int HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW = 1;
private static final int HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW = 12;
private static final int MINUT_SPINNER_MIN_VAL = 0;
private static final int MINUT_SPINNER_MAX_VAL = 59;
private static final int MINUTE_SPINNER_MIN_VAL = 0;
private static final int MINUTE_SPINNER_MAX_VAL = 59;
private static final int AMPM_SPINNER_MIN_VAL = 0;
private static final int AMPM_SPINNER_MAX_VAL = 1;
// 状态常量
private static final int MODE_INITIALIZING = 0;
private static final int MODE_NORMAL = 1;
// 范围限制 (默认无限制)
private long mMinDate = Long.MIN_VALUE;
private long mMaxDate = Long.MAX_VALUE;
// UI组件
private final NumberPicker mDateSpinner;
private final NumberPicker mHourSpinner;
private final NumberPicker mMinuteSpinner;
private final NumberPicker mAmPmSpinner;
// 数据模型
private Calendar mDate;
private String[] mDateDisplayValues = new String[DAYS_IN_ALL_WEEK];
private boolean mIsAm;
private boolean mIs24HourView;
private boolean mIsEnabled = DEFAULT_ENABLE_STATE;
private int mState = MODE_INITIALIZING;
private boolean mInitialising;
// 监听器
private OnDateTimeChangedListener mOnDateTimeChangedListener;
private NumberPicker.OnValueChangeListener mOnDateChangedListener = new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
mDate.add(Calendar.DAY_OF_YEAR, newVal - oldVal);
/**
*
*/
private final NumberPicker.OnValueChangeListener mOnDateChangedListener =
(picker, oldVal, newVal) -> {
// 计算日期变化差值
int dayDiff = newVal - oldVal;
mDate.add(Calendar.DAY_OF_YEAR, dayDiff);
// 验证日期范围
if (!isDateInRange()) {
revertDateChange();
showDateRangeWarning();
return;
}
updateDateControl();
onDateTimeChanged();
}
};
private NumberPicker.OnValueChangeListener mOnHourChangedListener = new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
boolean isDateChanged = false;
Calendar cal = Calendar.getInstance();
if (!mIs24HourView) {
if (!mIsAm && oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) {
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, 1);
isDateChanged = true;
} else if (mIsAm && oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) {
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, -1);
isDateChanged = true;
}
if (oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY ||
oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) {
mIsAm = !mIsAm;
updateAmPmControl();
}
} else {
if (oldVal == HOURS_IN_ALL_DAY - 1 && newVal == 0) {
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, 1);
isDateChanged = true;
} else if (oldVal == 0 && newVal == HOURS_IN_ALL_DAY - 1) {
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, -1);
isDateChanged = true;
}
}
int newHour = mHourSpinner.getValue() % HOURS_IN_HALF_DAY + (mIsAm ? 0 : HOURS_IN_HALF_DAY);
};
/**
*
*/
private final NumberPicker.OnValueChangeListener mOnHourChangedListener =
(picker, oldVal, newVal) -> {
int newHour = computeNewHour(oldVal, newVal);
mDate.set(Calendar.HOUR_OF_DAY, newHour);
onDateTimeChanged();
if (isDateChanged) {
setCurrentYear(cal.get(Calendar.YEAR));
setCurrentMonth(cal.get(Calendar.MONTH));
setCurrentDay(cal.get(Calendar.DAY_OF_MONTH));
}
}
};
private NumberPicker.OnValueChangeListener mOnMinuteChangedListener = new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
int minValue = mMinuteSpinner.getMinValue();
int maxValue = mMinuteSpinner.getMaxValue();
int offset = 0;
if (oldVal == maxValue && newVal == minValue) {
offset += 1;
} else if (oldVal == minValue && newVal == maxValue) {
offset -= 1;
}
if (offset != 0) {
mDate.add(Calendar.HOUR_OF_DAY, offset);
mHourSpinner.setValue(getCurrentHour());
updateDateControl();
int newHour = getCurrentHourOfDay();
if (newHour >= HOURS_IN_HALF_DAY) {
mIsAm = false;
updateAmPmControl();
} else {
mIsAm = true;
updateAmPmControl();
}
}
};
/**
*
*/
private final NumberPicker.OnValueChangeListener mOnMinuteChangedListener =
(picker, oldVal, newVal) -> {
mDate.set(Calendar.MINUTE, newVal);
onDateTimeChanged();
}
};
private NumberPicker.OnValueChangeListener mOnAmPmChangedListener = new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
mIsAm = !mIsAm;
if (mIsAm) {
mDate.add(Calendar.HOUR_OF_DAY, -HOURS_IN_HALF_DAY);
} else {
mDate.add(Calendar.HOUR_OF_DAY, HOURS_IN_HALF_DAY);
}
updateAmPmControl();
};
/**
* AM/PM
*/
private final NumberPicker.OnValueChangeListener mOnAmPmChangedListener =
(picker, oldVal, newVal) -> {
// 切换AM/PM状态
mIsAm = (newVal == Calendar.AM);
// 调整时间AM->PM加12小时PM->AM减12小时
int hour = mDate.get(Calendar.HOUR_OF_DAY);
hour = mIsAm ? hour % 12 : hour % 12 + 12;
mDate.set(Calendar.HOUR_OF_DAY, hour);
updateHourControl();
onDateTimeChanged();
}
};
};
/**
*
*/
public interface OnDateTimeChangedListener {
void onDateTimeChanged(DateTimePicker view, int year, int month,
int dayOfMonth, int hourOfDay, int minute);
}
/*************************** 构造函数 ***************************/
public DateTimePicker(Context context) {
this(context, System.currentTimeMillis());
this(context, null);
}
public DateTimePicker(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public DateTimePicker(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 使用当前时间初始化
init(context, System.currentTimeMillis(), DateFormat.is24HourFormat(context));
}
public DateTimePicker(Context context, long date) {
@ -173,57 +181,85 @@ public class DateTimePicker extends FrameLayout {
public DateTimePicker(Context context, long date, boolean is24HourView) {
super(context);
init(context, date, is24HourView);
}
/**
*
*/
private void init(Context context, long date, boolean is24HourView) {
// 初始化日期对象
mDate = Calendar.getInstance();
mInitialising = true;
mIsAm = getCurrentHourOfDay() >= HOURS_IN_HALF_DAY;
mState = MODE_INITIALIZING;
mIs24HourView = is24HourView;
// 确定初始AM/PM状态
int currentHour = (int) (date / (60 * 60 * 1000) % 24);
mIsAm = currentHour < 12;
// 加载布局
inflate(context, R.layout.datetime_picker, this);
mDateSpinner = (NumberPicker) findViewById(R.id.date);
// 初始化日期选择器
mDateSpinner = findViewById(R.id.date);
mDateSpinner.setMinValue(DATE_SPINNER_MIN_VAL);
mDateSpinner.setMaxValue(DATE_SPINNER_MAX_VAL);
mDateSpinner.setOnValueChangedListener(mOnDateChangedListener);
mDateSpinner.setWrapSelectorWheel(false);
mHourSpinner = (NumberPicker) findViewById(R.id.hour);
// 初始化小时选择器
mHourSpinner = findViewById(R.id.hour);
mHourSpinner.setOnValueChangedListener(mOnHourChangedListener);
mMinuteSpinner = (NumberPicker) findViewById(R.id.minute);
mMinuteSpinner.setMinValue(MINUT_SPINNER_MIN_VAL);
mMinuteSpinner.setMaxValue(MINUT_SPINNER_MAX_VAL);
// 初始化分钟选择器
mMinuteSpinner = findViewById(R.id.minute);
mMinuteSpinner.setMinValue(MINUTE_SPINNER_MIN_VAL);
mMinuteSpinner.setMaxValue(MINUTE_SPINNER_MAX_VAL);
mMinuteSpinner.setOnLongPressUpdateInterval(100);
mMinuteSpinner.setOnValueChangedListener(mOnMinuteChangedListener);
String[] stringsForAmPm = new DateFormatSymbols().getAmPmStrings();
mAmPmSpinner = (NumberPicker) findViewById(R.id.amPm);
// 初始化AM/PM选择器
mAmPmSpinner = findViewById(R.id.amPm);
mAmPmSpinner.setMinValue(AMPM_SPINNER_MIN_VAL);
mAmPmSpinner.setMaxValue(AMPM_SPINNER_MAX_VAL);
mAmPmSpinner.setDisplayedValues(stringsForAmPm);
// 本地化AM/PM符号
updateAmPmSymbols();
mAmPmSpinner.setOnValueChangedListener(mOnAmPmChangedListener);
// update controls to initial state
// 更新UI控件
updateDateControl();
updateHourControl();
updateAmPmControl();
set24HourView(is24HourView);
// set to current time
// 设置初始时间
setCurrentDate(date);
// 应用启用状态
setEnabled(isEnabled());
// set the content descriptions
mInitialising = false;
// 应用深色模式
applyDarkMode();
// 初始化完成
mState = MODE_NORMAL;
}
/*************************** 公共方法 ***************************/
@Override
public void setEnabled(boolean enabled) {
if (mIsEnabled == enabled) {
return;
}
super.setEnabled(enabled);
// 设置子控件状态
mDateSpinner.setEnabled(enabled);
mMinuteSpinner.setEnabled(enabled);
mHourSpinner.setEnabled(enabled);
mAmPmSpinner.setEnabled(enabled);
mIsEnabled = enabled;
}
@ -233,37 +269,37 @@ public class DateTimePicker extends FrameLayout {
}
/**
* Get the current date in millis
*
* @return the current date in millis
* ()
*/
public long getCurrentDateInTimeMillis() {
return mDate.getTimeInMillis();
}
/**
* Set the current date
*
* @param date The current date in millis
*
*/
public void setCurrentDate(long date) {
// 验证日期范围
if (date < mMinDate || date > mMaxDate) {
Log.w(TAG, "设置日期超出范围: " + date);
return;
}
Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(date);
setCurrentDate(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH),
cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE));
setCurrentDate(
cal.get(Calendar.YEAR),
cal.get(Calendar.MONTH),
cal.get(Calendar.DAY_OF_MONTH),
cal.get(Calendar.HOUR_OF_DAY),
cal.get(Calendar.MINUTE)
);
}
/**
* Set the current date
*
* @param year The current year
* @param month The current month
* @param dayOfMonth The current dayOfMonth
* @param hourOfDay The current hourOfDay
* @param minute The current minute
*
*/
public void setCurrentDate(int year, int month,
int dayOfMonth, int hourOfDay, int minute) {
public void setCurrentDate(int year, int month, int dayOfMonth, int hourOfDay, int minute) {
setCurrentYear(year);
setCurrentMonth(month);
setCurrentDay(dayOfMonth);
@ -272,21 +308,18 @@ public class DateTimePicker extends FrameLayout {
}
/**
* Get current year
*
* @return The current year
*
*/
public int getCurrentYear() {
return mDate.get(Calendar.YEAR);
}
/**
* Set current year
*
* @param year The current year
*
*/
public void setCurrentYear(int year) {
if (!mInitialising && year == getCurrentYear()) {
// 检查状态避免不必要的更新
if (mState != MODE_INITIALIZING && year == getCurrentYear()) {
return;
}
mDate.set(Calendar.YEAR, year);
@ -295,21 +328,17 @@ public class DateTimePicker extends FrameLayout {
}
/**
* Get current month in the year
*
* @return The current month in the year
*
*/
public int getCurrentMonth() {
return mDate.get(Calendar.MONTH);
}
/**
* Set current month in the year
*
* @param month The month in the year
*
*/
public void setCurrentMonth(int month) {
if (!mInitialising && month == getCurrentMonth()) {
if (mState != MODE_INITIALIZING && month == getCurrentMonth()) {
return;
}
mDate.set(Calendar.MONTH, month);
@ -318,21 +347,17 @@ public class DateTimePicker extends FrameLayout {
}
/**
* Get current day of the month
*
* @return The day of the month
*
*/
public int getCurrentDay() {
return mDate.get(Calendar.DAY_OF_MONTH);
}
/**
* Set current day of the month
*
* @param dayOfMonth The day of the month
*
*/
public void setCurrentDay(int dayOfMonth) {
if (!mInitialising && dayOfMonth == getCurrentDay()) {
if (mState != MODE_INITIALIZING && dayOfMonth == getCurrentDay()) {
return;
}
mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth);
@ -341,68 +366,66 @@ public class DateTimePicker extends FrameLayout {
}
/**
* Get current hour in 24 hour mode, in the range (0~23)
* @return The current hour in 24 hour mode
* (24)
*/
public int getCurrentHourOfDay() {
return mDate.get(Calendar.HOUR_OF_DAY);
}
private int getCurrentHour() {
if (mIs24HourView){
return getCurrentHourOfDay();
/**
*
*/
private int getCurrentDisplayHour() {
int hour = getCurrentHourOfDay();
if (mIs24HourView) {
return hour;
} else {
int hour = getCurrentHourOfDay();
if (hour > HOURS_IN_HALF_DAY) {
return hour - HOURS_IN_HALF_DAY;
} else {
return hour == 0 ? HOURS_IN_HALF_DAY : hour;
if (hour == 0 || hour == 12) {
return 12;
}
return hour % 12;
}
}
/**
* Set current hour in 24 hour mode, in the range (0~23)
*
* @param hourOfDay
* (24)
*/
public void setCurrentHour(int hourOfDay) {
if (!mInitialising && hourOfDay == getCurrentHourOfDay()) {
// 验证小时值是否有效
if (hourOfDay < 0 || hourOfDay > 23) {
Log.e(TAG, "无效的小时值: " + hourOfDay);
return;
}
if (mState != MODE_INITIALIZING && hourOfDay == getCurrentHourOfDay()) {
return;
}
mDate.set(Calendar.HOUR_OF_DAY, hourOfDay);
// 更新AM/PM状态
if (!mIs24HourView) {
if (hourOfDay >= HOURS_IN_HALF_DAY) {
mIsAm = false;
if (hourOfDay > HOURS_IN_HALF_DAY) {
hourOfDay -= HOURS_IN_HALF_DAY;
}
} else {
mIsAm = true;
if (hourOfDay == 0) {
hourOfDay = HOURS_IN_HALF_DAY;
}
}
mIsAm = (hourOfDay < 12);
updateAmPmControl();
}
mHourSpinner.setValue(hourOfDay);
// 更新显示值
mHourSpinner.setValue(getCurrentDisplayHour());
onDateTimeChanged();
}
/**
* Get currentMinute
*
* @return The Current Minute
*
*/
public int getCurrentMinute() {
return mDate.get(Calendar.MINUTE);
}
/**
* Set current minute
*
*/
public void setCurrentMinute(int minute) {
if (!mInitialising && minute == getCurrentMinute()) {
if (mState != MODE_INITIALIZING && minute == getCurrentMinute()) {
return;
}
mMinuteSpinner.setValue(minute);
@ -411,53 +434,148 @@ public class DateTimePicker extends FrameLayout {
}
/**
* @return true if this is in 24 hour view else false.
* 使24
*/
public boolean is24HourView () {
return mIs24HourView;
}
/**
* Set whether in 24 hour or AM/PM mode.
*
* @param is24HourView True for 24 hour mode. False for AM/PM mode.
* 24
*/
public void set24HourView(boolean is24HourView) {
if (mIs24HourView == is24HourView) {
return;
}
mIs24HourView = is24HourView;
mAmPmSpinner.setVisibility(is24HourView ? View.GONE : View.VISIBLE);
int hour = getCurrentHourOfDay();
// 更新AM/PM显示
int amPmVisibility = is24HourView ? View.GONE : View.VISIBLE;
mAmPmSpinner.setVisibility(amPmVisibility);
// 更新小时控制
updateHourControl();
setCurrentHour(hour);
updateAmPmControl();
setCurrentHour(getCurrentHourOfDay());
}
/**
*
*/
public void setMinDate(long minDate) {
mMinDate = minDate;
validateCurrentDate();
}
/**
*
*/
public void setMaxDate(long maxDate) {
mMaxDate = maxDate;
validateCurrentDate();
}
/**
*
*/
public void setOnDateTimeChangedListener(OnDateTimeChangedListener listener) {
mOnDateTimeChangedListener = listener;
}
/*************************** 私有方法 ***************************/
/**
* ()
*/
private int computeNewHour(int oldVal, int newVal) {
int currentHour = getCurrentHourOfDay();
if (mIs24HourView) {
// 24小时制处理
if (oldVal == HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW && newVal == 0) {
return 0; // 23 -> 0
} else if (oldVal == 0 && newVal == HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW) {
return 23; // 0 -> 23
}
} else {
// 12小时制处理
if (oldVal == 12 && newVal == 11) {
return mIsAm ? 11 : 23; // PM状态下12->11实际上是23点
} else if (oldVal == 11 && newVal == 12) {
return mIsAm ? 12 : 0; // AM状态下11->12是中午12点
}
}
// 基本转换
int hour = newVal;
if (!mIs24HourView && !mIsAm && hour != 12) {
hour += 12;
}
// 特殊处理中午12点
if (hour == 24) hour = 0;
return hour;
}
/**
*
*/
private void updateDateControl() {
Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, -DAYS_IN_ALL_WEEK / 2 - 1);
mDateSpinner.setDisplayedValues(null);
for (int i = 0; i < DAYS_IN_ALL_WEEK; ++i) {
// 以当前日期为中心生成一周的日期
cal.add(Calendar.DAY_OF_YEAR, -DAYS_IN_ALL_WEEK / 2);
for (int i = 0; i < DAYS_IN_ALL_WEEK; i++) {
// 生成日期显示字符串 (带星期)
mDateDisplayValues[i] = formatDateWithWeekday(cal);
cal.add(Calendar.DAY_OF_YEAR, 1);
mDateDisplayValues[i] = (String) DateFormat.format("MM.dd EEEE", cal);
}
// 设置日期选择器显示内容
mDateSpinner.setDisplayedValues(mDateDisplayValues);
mDateSpinner.setValue(DAYS_IN_ALL_WEEK / 2);
mDateSpinner.invalidate();
mDateSpinner.setValue(DAYS_IN_ALL_WEEK / 2); // 当前日期在中间位置
}
/**
* ()
*/
private String formatDateWithWeekday(Calendar calendar) {
java.text.DateFormat dateFormat = java.text.DateFormat.getDateInstance(
java.text.DateFormat.MEDIUM, Locale.getDefault());
// 添加星期显示
String weekday = getWeekdayName(calendar.get(Calendar.DAY_OF_WEEK));
return String.format("%s (%s)",
dateFormat.format(calendar.getTime()),
weekday
);
}
/**
*
*/
private String getWeekdayName(int dayOfWeek) {
String[] weekdays = new DateFormatSymbols().getWeekdays();
if (dayOfWeek >= Calendar.SUNDAY && dayOfWeek <= Calendar.SATURDAY) {
return weekdays[dayOfWeek];
}
return "";
}
/**
* AM/PM
*/
private void updateAmPmControl() {
if (mIs24HourView) {
mAmPmSpinner.setVisibility(View.GONE);
} else {
int index = mIsAm ? Calendar.AM : Calendar.PM;
mAmPmSpinner.setValue(index);
mAmPmSpinner.setVisibility(View.VISIBLE);
return;
}
mAmPmSpinner.setValue(mIsAm ? 0 : 1);
}
/**
*
*/
private void updateHourControl() {
if (mIs24HourView) {
mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW);
@ -466,20 +584,101 @@ public class DateTimePicker extends FrameLayout {
mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW);
mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW);
}
mHourSpinner.setValue(getCurrentDisplayHour());
}
/**
* AM/PM
*/
private void updateAmPmSymbols() {
String[] ampmSymbols = new DateFormatSymbols().getAmPmStrings();
if (ampmSymbols.length < 2) {
Log.e(TAG, "AM/PM符号获取失败");
ampmSymbols = new String[]{"AM", "PM"}; // 默认值
}
mAmPmSpinner.setDisplayedValues(ampmSymbols);
}
/**
*
*/
private void applyDarkMode() {
// 在Android 10+系统上添加深色模式支持
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
setForceDarkAllowed(true);
}
}
/**
*
*/
private void validateCurrentDate() {
long currentTime = getCurrentDateInTimeMillis();
if (currentTime < mMinDate || currentTime > mMaxDate) {
// 自动调整到有效范围
long newTime = Math.max(mMinDate, Math.min(currentTime, mMaxDate));
setCurrentDate(newTime);
// 触发更新
updateDateControl();
updateHourControl();
updateAmPmControl();
}
}
/**
* Set the callback that indicates the 'Set' button has been pressed.
* @param callback the callback, if null will do nothing
*
*/
public void setOnDateTimeChangedListener(OnDateTimeChangedListener callback) {
mOnDateTimeChangedListener = callback;
private boolean isDateInRange() {
long currentTime = mDate.getTimeInMillis();
return currentTime >= mMinDate && currentTime <= mMaxDate;
}
/**
*
*/
private void revertDateChange() {
// 回滚到上一次有效日期
if (mDateSpinner.getValue() > DAYS_IN_ALL_WEEK / 2) {
mDateSpinner.setValue(mDateSpinner.getValue() - 1);
} else if (mDateSpinner.getValue() < DAYS_IN_ALL_WEEK / 2) {
mDateSpinner.setValue(mDateSpinner.getValue() + 1);
}
}
/**
*
*/
private void showDateRangeWarning() {
Context context = getContext();
Calendar minCal = Calendar.getInstance();
minCal.setTimeInMillis(mMinDate);
Calendar maxCal = Calendar.getInstance();
maxCal.setTimeInMillis(mMaxDate);
java.text.DateFormat df = java.text.DateFormat.getDateInstance();
String msg = String.format(
context.getString(R.string.date_range_warning),
df.format(minCal.getTime()),
df.format(maxCal.getTime())
);
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
}
/**
*
*/
private void onDateTimeChanged() {
if (mOnDateTimeChangedListener != null) {
mOnDateTimeChangedListener.onDateTimeChanged(this, getCurrentYear(),
getCurrentMonth(), getCurrentDay(), getCurrentHourOfDay(), getCurrentMinute());
mOnDateTimeChangedListener.onDateTimeChanged(
this,
getCurrentYear(),
getCurrentMonth(),
getCurrentDay(),
getCurrentHourOfDay(),
getCurrentMinute()
);
}
}
}
}

@ -14,6 +14,10 @@
* limitations under the License.
*/
/**
*
*
*/
package net.micode.notes.ui;
import java.util.Calendar;
@ -29,62 +33,130 @@ import android.content.DialogInterface.OnClickListener;
import android.text.format.DateFormat;
import android.text.format.DateUtils;
/**
*
*
*
*/
public class DateTimePickerDialog extends AlertDialog implements OnClickListener {
// 用于存储用户选择的日期和时间
private Calendar mDate = Calendar.getInstance();
// 是否使用24小时制显示时间
private boolean mIs24HourView;
// 日期时间设置回调接口
private OnDateTimeSetListener mOnDateTimeSetListener;
// 自定义的日期时间选择器视图
private DateTimePicker mDateTimePicker;
/**
*
*
*/
public interface OnDateTimeSetListener {
/**
*
*
* @param dialog
* @param date
*/
void OnDateTimeSet(AlertDialog dialog, long date);
}
/**
*
*
* @param context
* @param date
*/
public DateTimePickerDialog(Context context, long date) {
super(context);
// 初始化日期时间选择器视图
mDateTimePicker = new DateTimePicker(context);
setView(mDateTimePicker);
// 设置日期时间变化监听器,当用户调整日期或时间时更新内部日期对象和标题
mDateTimePicker.setOnDateTimeChangedListener(new OnDateTimeChangedListener() {
public void onDateTimeChanged(DateTimePicker view, int year, int month,
int dayOfMonth, int hourOfDay, int minute) {
// 更新内部日期对象的年、月、日、时、分
mDate.set(Calendar.YEAR, year);
mDate.set(Calendar.MONTH, month);
mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth);
mDate.set(Calendar.HOUR_OF_DAY, hourOfDay);
mDate.set(Calendar.MINUTE, minute);
// 更新对话框标题,显示当前选择的日期时间
updateTitle(mDate.getTimeInMillis());
}
});
// 设置初始日期时间并将秒设置为0
mDate.setTimeInMillis(date);
mDate.set(Calendar.SECOND, 0);
// 设置日期时间选择器的当前日期
mDateTimePicker.setCurrentDate(mDate.getTimeInMillis());
// 设置对话框按钮
setButton(context.getString(R.string.datetime_dialog_ok), this);
setButton2(context.getString(R.string.datetime_dialog_cancel), (OnClickListener)null);
// 根据系统设置确定是否使用24小时制
set24HourView(DateFormat.is24HourFormat(this.getContext()));
// 更新对话框标题
updateTitle(mDate.getTimeInMillis());
}
/**
* 使24
*
* @param is24HourView true使24false使12
*/
public void set24HourView(boolean is24HourView) {
mIs24HourView = is24HourView;
}
/**
*
*
* @param callBack OnDateTimeSetListener
*/
public void setOnDateTimeSetListener(OnDateTimeSetListener callBack) {
mOnDateTimeSetListener = callBack;
}
/**
*
*
* @param date
*/
private void updateTitle(long date) {
// 设置日期时间格式化标志
int flag =
DateUtils.FORMAT_SHOW_YEAR |
DateUtils.FORMAT_SHOW_DATE |
DateUtils.FORMAT_SHOW_TIME;
DateUtils.FORMAT_SHOW_YEAR | // 显示年份
DateUtils.FORMAT_SHOW_DATE | // 显示日期(月、日)
DateUtils.FORMAT_SHOW_TIME; // 显示时间(时、分)
// 根据24小时制设置添加相应的格式化标志
flag |= mIs24HourView ? DateUtils.FORMAT_24HOUR : DateUtils.FORMAT_24HOUR;
// 使用DateFormat格式化日期时间并设置为对话框标题
setTitle(DateUtils.formatDateTime(this.getContext(), date, flag));
}
/**
*
*
* @param arg0
* @param arg1
*/
public void onClick(DialogInterface arg0, int arg1) {
// 当用户点击确定按钮时,调用回调函数通知日期时间已设置
if (mOnDateTimeSetListener != null) {
mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis());
}
}
}

File diff suppressed because it is too large Load Diff

@ -17,124 +17,292 @@
package net.micode.notes.ui;
import android.content.Context;
import android.content.Intent;
import android.graphics.Rect;
import android.net.Uri;
import android.text.Layout;
import android.text.Selection;
import android.text.Spannable;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.method.LinkMovementMethod;
import android.text.style.URLSpan;
import android.util.AttributeSet;
import android.util.Log;
import android.view.ActionMode;
import android.view.ContextMenu;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MenuItem.OnMenuItemClickListener;
import android.view.MotionEvent;
import android.widget.EditText;
import android.widget.Toast;
import androidx.core.text.util.LinkifyCompat;
import net.micode.notes.R;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
/**
* -
*
* :
* 1. (////)
* 2.
* 3. (/)
* 4.
* 5.
* 6.
* 7. /
*
* :
* 1.
* 2.
* 3.
* 4.
*/
public class NoteEditText extends EditText {
private static final String TAG = "NoteEditText";
private int mIndex;
private int mSelectionStartBeforeDelete;
private static final String SCHEME_TEL = "tel:" ;
private static final String SCHEME_HTTP = "http:" ;
private static final String SCHEME_EMAIL = "mailto:" ;
// 段落管理常量
private static final int NO_INDEX = -1;
private int mParagraphIndex = NO_INDEX;
private int mSelectionStartBeforeDelete;
private boolean mIsLastParagraph = false;
private static final Map<String, Integer> sSchemaActionResMap = new HashMap<String, Integer>();
// 链接类型定义
private static final String SCHEME_TEL = "tel:";
private static final String SCHEME_MAILTO = "mailto:";
private static final String SCHEME_HTTP = "http:";
private static final String SCHEME_HTTPS = "https:";
private static final String SCHEME_GEO = "geo:";
private static final String SCHEME_NOTE = "note://";
// 链接模式定义
private static final Pattern USER_MENTION_PATTERN = Pattern.compile("@\\w+");
private static final Pattern NOTE_REF_PATTERN = Pattern.compile("#\\w+");
// 链接操作映射表
private static final Map<String, Integer> sSchemaActionResMap = new HashMap<>();
static {
sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel);
sSchemaActionResMap.put(SCHEME_MAILTO, R.string.note_link_email);
sSchemaActionResMap.put(SCHEME_HTTP, R.string.note_link_web);
sSchemaActionResMap.put(SCHEME_EMAIL, R.string.note_link_email);
sSchemaActionResMap.put(SCHEME_HTTPS, R.string.note_link_web);
sSchemaActionResMap.put(SCHEME_GEO, R.string.note_link_map);
sSchemaActionResMap.put(SCHEME_NOTE, R.string.note_link_note);
}
// 智能编辑标记
private static final int UNDO_HISTORY_SIZE = 50;
private int mIndentLevel = 0;
private boolean mInBulletList = false;
/**
* Call by the {@link NoteEditActivity} to delete or add edit text
*
*/
public interface OnTextViewChangeListener {
/**
* Delete current edit text when {@link KeyEvent#KEYCODE_DEL} happens
* and the text is null
* ()
*
* @param paragraphIndex
*/
void onEditTextDelete(int index, String text);
void onParagraphDelete(int paragraphIndex);
/**
* Add edit text after current edit text when {@link KeyEvent#KEYCODE_ENTER}
* happen
* ()
*
* @param fromParagraphIndex
* @param newParagraphText
* @param offset
*/
void onEditTextEnter(int index, String text);
void onParagraphSplit(int fromParagraphIndex, String newParagraphText, int offset);
/**
* Hide or show item option when text change
*
*
* @param paragraphIndex
* @param hasText
*/
void onTextChange(int index, boolean hasText);
void onTextChange(int paragraphIndex, boolean hasText);
/**
*
*
* @param url URL
* @return
*/
boolean onLinkClick(String url);
}
private OnTextViewChangeListener mOnTextViewChangeListener;
// 历史管理类
private static class EditHistory {
private static final int MAX_UNDO = UNDO_HISTORY_SIZE;
private final HistoryItem[] items = new HistoryItem[MAX_UNDO];
private int position = -1;
private int size = 0;
static class HistoryItem {
String before;
String after;
int selectionStart;
int selectionEnd;
HistoryItem(CharSequence before, CharSequence after, int selStart, int selEnd) {
this.before = before.toString();
this.after = after.toString();
this.selectionStart = selStart;
this.selectionEnd = selEnd;
}
}
void add(HistoryItem item) {
if (size < MAX_UNDO) {
size++;
}
position = (position + 1) % MAX_UNDO;
items[position] = item;
}
HistoryItem getLast() {
if (size == 0) return null;
return items[position];
}
boolean canUndo() {
return size > 0;
}
}
private final EditHistory mEditHistory = new EditHistory();
private boolean mIsUndoRedoInProgress = false;
public NoteEditText(Context context) {
super(context, null);
mIndex = 0;
this(context, null);
}
public void setIndex(int index) {
mIndex = index;
public NoteEditText(Context context, AttributeSet attrs) {
this(context, attrs, android.R.attr.editTextStyle);
}
public void setOnTextViewChangeListener(OnTextViewChangeListener listener) {
mOnTextViewChangeListener = listener;
public NoteEditText(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
public NoteEditText(Context context, AttributeSet attrs) {
super(context, attrs, android.R.attr.editTextStyle);
/**
*
*/
private void init() {
// 配置富文本支持
setMovementMethod(new CustomLinkMovementMethod());
// 设置自动链接类型
int linkifyMask = LinkifyCompat.WEB_URLS
| LinkifyCompat.EMAIL_ADDRESSES
| LinkifyCompat.PHONE_NUMBERS
| LinkifyCompat.MAP_ADDRESSES;
LinkifyCompat.addLinks(this, linkifyMask);
// 添加自定义链接
addCustomLinkPatterns();
// 启用撤销支持
enableUndoRedo();
}
/**
*
*
* @param paragraphIndex
* @param isLastParagraph
*/
public void setParagraphInfo(int paragraphIndex, boolean isLastParagraph) {
this.mParagraphIndex = paragraphIndex;
this.mIsLastParagraph = isLastParagraph;
}
public NoteEditText(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// TODO Auto-generated constructor stub
/**
*
*/
public void setOnTextViewChangeListener(OnTextViewChangeListener listener) {
this.mOnTextViewChangeListener = listener;
}
/**
*
*
* @param isDarkMode
*/
public void setDarkMode(boolean isDarkMode) {
if (isDarkMode) {
setBackgroundColor(getResources().getColor(R.color.note_bg_dark));
setTextColor(getResources().getColor(R.color.text_color_dark));
} else {
setBackgroundColor(getResources().getColor(R.color.note_bg_light));
setTextColor(getResources().getColor(R.color.text_color_light));
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
int x = (int) event.getX();
int y = (int) event.getY();
x -= getTotalPaddingLeft();
y -= getTotalPaddingTop();
x += getScrollX();
y += getScrollY();
Layout layout = getLayout();
int line = layout.getLineForVertical(y);
int off = layout.getOffsetForHorizontal(line, x);
Selection.setSelection(getText(), off);
break;
try {
return super.onTouchEvent(event);
} catch (Exception e) {
Log.e(TAG, "触摸事件处理异常", e);
return false;
}
return super.onTouchEvent(event);
}
@Override
public boolean onTextContextMenuItem(int id) {
// 处理自定义上下文菜单项
if (id == R.id.note_menu_undo) {
undo();
return true;
} else if (id == R.id.note_menu_redo) {
redo();
return true;
}
return super.onTextContextMenuItem(id);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_ENTER:
if (mOnTextViewChangeListener != null) {
return false;
}
break;
case KeyEvent.KEYCODE_DEL:
mSelectionStartBeforeDelete = getSelectionStart();
break;
default:
break;
// 检测删除键,记录删除前的光标位置
if (keyCode == KeyEvent.KEYCODE_DEL) {
mSelectionStartBeforeDelete = getSelectionStart();
}
// 处理自定义快捷键
if (event.isCtrlPressed()) {
switch (keyCode) {
case KeyEvent.KEYCODE_Z:
if (event.isShiftPressed()) {
redo();
} else {
undo();
}
return true;
case KeyEvent.KEYCODE_B:
toggleBulletList();
return true;
case KeyEvent.KEYCODE_L:
indent();
return true;
case KeyEvent.KEYCODE_M:
dedent();
return true;
}
}
return super.onKeyDown(keyCode, event);
}
@ -142,25 +310,17 @@ public class NoteEditText extends EditText {
public boolean onKeyUp(int keyCode, KeyEvent event) {
switch(keyCode) {
case KeyEvent.KEYCODE_DEL:
if (mOnTextViewChangeListener != null) {
if (0 == mSelectionStartBeforeDelete && mIndex != 0) {
mOnTextViewChangeListener.onEditTextDelete(mIndex, getText().toString());
return true;
}
} else {
Log.d(TAG, "OnTextViewChangeListener was not seted");
}
break;
handleDeleteAction();
return true;
case KeyEvent.KEYCODE_ENTER:
if (mOnTextViewChangeListener != null) {
int selectionStart = getSelectionStart();
String text = getText().subSequence(selectionStart, length()).toString();
setText(getText().subSequence(0, selectionStart));
mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, text);
} else {
Log.d(TAG, "OnTextViewChangeListener was not seted");
}
break;
handleEnterAction();
return true;
case KeyEvent.KEYCODE_TAB:
handleTabAction();
return true;
default:
break;
}
@ -169,49 +329,531 @@ public class NoteEditText extends EditText {
@Override
protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
super.onFocusChanged(focused, direction, previouslyFocusedRect);
if (mOnTextViewChangeListener != null) {
if (!focused && TextUtils.isEmpty(getText())) {
mOnTextViewChangeListener.onTextChange(mIndex, false);
} else {
mOnTextViewChangeListener.onTextChange(mIndex, true);
}
boolean hasText = !TextUtils.isEmpty(getText());
mOnTextViewChangeListener.onTextChange(mParagraphIndex, hasText);
}
super.onFocusChanged(focused, direction, previouslyFocusedRect);
}
@Override
protected void onCreateContextMenu(ContextMenu menu) {
if (getText() instanceof Spanned) {
int selStart = getSelectionStart();
int selEnd = getSelectionEnd();
int min = Math.min(selStart, selEnd);
int max = Math.max(selStart, selEnd);
final URLSpan[] urls = ((Spanned) getText()).getSpans(min, max, URLSpan.class);
if (urls.length == 1) {
int defaultResId = 0;
for(String schema: sSchemaActionResMap.keySet()) {
if(urls[0].getURL().indexOf(schema) >= 0) {
defaultResId = sSchemaActionResMap.get(schema);
break;
}
// 添加自定义菜单项
menu.add(Menu.NONE, R.id.note_menu_undo, 0, R.string.note_undo)
.setIcon(R.drawable.ic_undo)
.setEnabled(canUndo());
menu.add(Menu.NONE, R.id.note_menu_redo, 1, R.string.note_redo)
.setIcon(R.drawable.ic_redo)
.setEnabled(canRedo());
// 添加分割线
menu.add(Menu.NONE, -1, Menu.NONE, "");
// 处理链接菜单
handleLinkContextMenu(menu);
// 添加系统默认菜单
super.onCreateContextMenu(menu);
}
/**
* /
*/
private void enableUndoRedo() {
addTextChangedListener(new TextWatcher() {
private EditHistory.HistoryItem mItem;
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
if (mIsUndoRedoInProgress) return;
mItem = new EditHistory.HistoryItem(
s,
"",
getSelectionStart(),
getSelectionEnd()
);
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
// 不做处理
}
@Override
public void afterTextChanged(android.text.Editable s) {
if (mIsUndoRedoInProgress) return;
if (mItem != null) {
mItem.after = s.toString();
mItem.selectionStart = getSelectionStart();
mItem.selectionEnd = getSelectionEnd();
mEditHistory.add(mItem);
mItem = null;
}
}
});
}
/**
*
*/
public void undo() {
if (!canUndo()) return;
EditHistory.HistoryItem item = mEditHistory.getLast();
if (item != null) {
mIsUndoRedoInProgress = true;
setText(item.before);
setSelection(item.selectionStart, item.selectionEnd);
mIsUndoRedoInProgress = false;
}
}
/**
* ()
*/
public void redo() {
// 实际实现需要维护重做栈
Toast.makeText(getContext(), "重做功能正在开发中", Toast.LENGTH_SHORT).show();
}
public boolean canUndo() {
return mEditHistory.canUndo();
}
public boolean canRedo() {
return false; // 未实现
}
if (defaultResId == 0) {
defaultResId = R.string.note_link_other;
/*************************** 链接处理 ***************************/
/**
*
*/
private void addCustomLinkPatterns() {
addLinkPattern(NOTE_REF_PATTERN, SCHEME_NOTE,
new LinkifyCompat.MatchFilter() {
@Override
public boolean acceptMatch(CharSequence s, int start, int end) {
return start == 0 || s.charAt(start - 1) != '!';
}
});
addLinkPattern(USER_MENTION_PATTERN, "user://",
new LinkifyCompat.MatchFilter() {
@Override
public boolean acceptMatch(CharSequence s, int start, int end) {
return true;
}
});
}
/**
*
*/
private void addLinkPattern(Pattern pattern, String scheme, LinkifyCompat.MatchFilter filter) {
LinkifyCompat.addLinks(this, pattern, scheme, null, filter, null);
}
/**
*
*/
private void handleLinkContextMenu(ContextMenu menu) {
Spannable text = getText();
int min = Math.max(0, getSelectionStart());
int max = Math.min(text.length(), getSelectionEnd());
final URLSpan[] urls = text.getSpans(min, max, URLSpan.class);
if (urls.length > 0) {
URLSpan primaryUrl = getPrimaryUrl(urls);
if (primaryUrl != null) {
addLinkActionsToMenu(menu, primaryUrl);
}
}
}
/**
* ()
*/
private URLSpan getPrimaryUrl(URLSpan[] urls) {
if (urls.length == 1) {
return urls[0];
}
// 寻找覆盖选区最长的链接
URLSpan bestMatch = null;
int maxOverlap = 0;
int selStart = getSelectionStart();
int selEnd = getSelectionEnd();
for (URLSpan url : urls) {
int spanStart = getText().getSpanStart(url);
int spanEnd = getText().getSpanEnd(url);
int overlap = Math.min(selEnd, spanEnd) - Math.max(selStart, spanStart);
if (overlap > maxOverlap) {
maxOverlap = overlap;
bestMatch = url;
}
}
return bestMatch;
}
/**
*
*/
private void addLinkActionsToMenu(ContextMenu menu, final URLSpan urlSpan) {
String url = urlSpan.getURL();
String displayText = getDisplayTextForUrl(urlSpan);
// 添加标题项
menu.setHeaderTitle(displayText);
// 添加默认操作
Integer resId = sSchemaActionResMap.get(getScheme(url));
if (resId == null) {
resId = R.string.note_link_open;
}
menu.add(0, 0, 0, resId).setOnMenuItemClickListener(item -> {
handleLinkClick(url);
return true;
});
// 添加复制链接操作
menu.add(0, 1, 1, R.string.note_link_copy).setOnMenuItemClickListener(item -> {
copyToClipboard(displayText, url);
return true;
});
}
/**
*
*/
private String getDisplayTextForUrl(URLSpan urlSpan) {
String url = urlSpan.getURL();
if (url.startsWith(SCHEME_NOTE)) {
return url.substring(SCHEME_NOTE.length());
}
return url;
}
/**
* scheme
*/
private String getScheme(String url) {
int colonPos = url.indexOf(':');
if (colonPos != -1) {
return url.substring(0, colonPos + 1);
}
return url;
}
/**
*
*/
private boolean handleLinkClick(String url) {
if (mOnTextViewChangeListener != null) {
if (mOnTextViewChangeListener.onLinkClick(url)) {
return true;
}
}
// 默认链接处理
if (url.startsWith(SCHEME_TEL)) {
callPhoneNumber(url.substring(SCHEME_TEL.length()));
} else if (url.startsWith(SCHEME_MAILTO)) {
sendEmail(url.substring(SCHEME_MAILTO.length()));
} else if (url.startsWith(SCHEME_HTTP) || url.startsWith(SCHEME_HTTPS)) {
openWebPage(url);
} else if (url.startsWith(SCHEME_GEO)) {
openMap(url.substring(SCHEME_GEO.length()));
} else if (url.startsWith(SCHEME_NOTE)) {
openNoteReference(url.substring(SCHEME_NOTE.length()));
} else {
openWebPage(url); // 尝试打开
}
return true;
}
/**
* ()
*/
private class CustomLinkMovementMethod extends LinkMovementMethod {
@Override
public boolean onTouchEvent(android.widget.TextView widget, Spannable buffer, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
int x = (int) event.getX();
int y = (int) event.getY();
x -= widget.getTotalPaddingLeft();
y -= widget.getTotalPaddingTop();
x += widget.getScrollX();
y += widget.getScrollY();
Layout layout = widget.getLayout();
int line = layout.getLineForVertical(y);
int off = layout.getOffsetForHorizontal(line, x);
URLSpan[] links = buffer.getSpans(off, off, URLSpan.class);
if (links.length != 0) {
String url = links[0].getURL();
if (handleLinkClick(url)) {
return true;
}
}
}
return super.onTouchEvent(widget, buffer, event);
}
}
menu.add(0, 0, 0, defaultResId).setOnMenuItemClickListener(
new OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
// goto a new intent
urls[0].onClick(NoteEditText.this);
return true;
}
});
/*************************** 编辑操作处理 ***************************/
/**
*
*/
private void handleDeleteAction() {
if (mOnTextViewChangeListener == null) {
return;
}
// 处理空段落删除逻辑
if (mSelectionStartBeforeDelete == 0 && mParagraphIndex != NO_INDEX) {
// 只有整个段落为空时才删除
if (TextUtils.isEmpty(getText())) {
mOnTextViewChangeListener.onParagraphDelete(mParagraphIndex);
}
}
super.onCreateContextMenu(menu);
}
}
/**
*
*/
private void handleEnterAction() {
if (mOnTextViewChangeListener == null) {
return;
}
int selectionStart = getSelectionStart();
String remainingText = "";
// 获取光标后的文本
if (selectionStart < length()) {
remainingText = getText().subSequence(selectionStart, length()).toString();
}
// 分割前的文本
String currentText = getText().subSequence(0, selectionStart).toString();
// 应用智能回车规则
if (currentText.endsWith("- - ") || currentText.endsWith("-- ")) {
// 智能列表结束
setText(currentText.substring(0, currentText.length() - 3));
} else if (mInBulletList) {
// 项目符号自动继续
setText(currentText + "\n• ");
setSelection(getText().length());
} else if (mIndentLevel > 0) {
// 保持缩进
String indent = createIndentString(mIndentLevel);
setText(currentText + "\n" + indent);
setSelection(getText().length());
} else {
// 普通回车
setText(currentText);
mOnTextViewChangeListener.onParagraphSplit(mParagraphIndex, remainingText, selectionStart);
}
}
/**
* Tab
*/
private void handleTabAction() {
int start = getSelectionStart();
int end = getSelectionEnd();
// 有选中文本时增加缩进
if (start != end) {
String selectedText = getText().subSequence(start, end).toString();
String replacement = createIndentString(mIndentLevel + 1) + selectedText;
getText().replace(start, end, replacement);
setSelection(start, start + replacement.length());
mIndentLevel++;
} else {
// 光标位置插入Tab
getText().insert(start, " ");
setSelection(start + 4);
}
}
/**
*
*/
private String createIndentString(int level) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < level; i++) {
sb.append(" "); // 4空格缩进
}
return sb.toString();
}
/**
*
*/
private void toggleBulletList() {
mInBulletList = !mInBulletList;
int selectionStart = getSelectionStart();
if (mInBulletList) {
getText().insert(0, "• ");
setSelection(selectionStart + 2);
} else {
String text = getText().toString();
if (text.startsWith("• ")) {
getText().replace(0, 2, "");
setSelection(Math.max(0, selectionStart - 2));
}
}
}
/**
*
*/
public void indent() {
if (mIndentLevel < 5) {
mIndentLevel++;
updateIndent();
}
}
/**
*
*/
public void dedent() {
if (mIndentLevel > 0) {
mIndentLevel--;
updateIndent();
}
}
/**
*
*/
private void updateIndent() {
setText(getText()); // 触发重绘
// 实际应用中可以设置段落缩进
}
/*************************** 辅助功能 ***************************/
/**
*
*/
private void copyToClipboard(String label, String url) {
android.content.ClipboardManager clipboard =
(android.content.ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
android.content.ClipData clip = android.content.ClipData.newPlainText(label, url);
clipboard.setPrimaryClip(clip);
Toast.makeText(getContext(), R.string.note_link_copied, Toast.LENGTH_SHORT).show();
}
/**
*
*/
private void callPhoneNumber(String phoneNumber) {
try {
Intent intent = new Intent(Intent.ACTION_DIAL);
intent.setData(Uri.parse(SCHEME_TEL + phoneNumber));
getContext().startActivity(intent);
} catch (Exception e) {
showActionError(R.string.note_cant_call);
}
}
/**
*
*/
private void sendEmail(String email) {
try {
Intent intent = new Intent(Intent.ACTION_SENDTO);
intent.setData(Uri.parse(SCHEME_MAILTO + email));
getContext().startActivity(intent);
} catch (Exception e) {
showActionError(R.string.note_cant_email);
}
}
/**
*
*/
private void openWebPage(String url) {
try {
// 确保有协议前缀
if (!url.startsWith(SCHEME_HTTP) && !url.startsWith(SCHEME_HTTPS)) {
url = "http://" + url;
}
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
getContext().startActivity(intent);
} catch (Exception e) {
showActionError(R.string.note_cant_open_link);
}
}
/**
*
*/
private void openMap(String location) {
try {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("geo:0,0?q=" + Uri.encode(location)));
getContext().startActivity(intent);
} catch (Exception e) {
showActionError(R.string.note_cant_open_map);
}
}
/**
*
*/
private void openNoteReference(String noteRef) {
Toast.makeText(
getContext(),
getContext().getString(R.string.note_link_jump, noteRef),
Toast.LENGTH_SHORT
).show();
// 实际实现应该跳转到对应笔记
}
/**
*
*/
private void showActionError(int resId) {
Toast.makeText(getContext(), resId, Toast.LENGTH_SHORT).show();
}
/**
* (使)
*/
public int getParagraphIndex() {
return mParagraphIndex;
}
/**
* ()
*/
@Override
public ActionMode startActionMode(ActionMode.Callback callback, int type) {
// 添加自定义action mode支持
return super.startActionMode(callback, type);
}
}

File diff suppressed because it is too large Load Diff

@ -15,6 +15,7 @@
*/
package net.micode.notes.widget;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
@ -22,6 +23,7 @@ import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.util.Log;
import android.widget.RemoteViews;
@ -32,101 +34,344 @@ import net.micode.notes.tool.ResourceParser;
import net.micode.notes.ui.NoteEditActivity;
import net.micode.notes.ui.NotesListActivity;
/**
*
*
* :
* 1.
* 2.
* 3.
* 4.
*
*
* - getBgResourceId() : ID
* - getLayoutId() : ID
* - getWidgetType() :
*/
public abstract class NoteWidgetProvider extends AppWidgetProvider {
public static final String [] PROJECTION = new String [] {
// 数据库查询字段
private static final String[] PROJECTION = new String[] {
NoteColumns.ID,
NoteColumns.BG_COLOR_ID,
NoteColumns.SNIPPET
NoteColumns.SNIPPET,
NoteColumns.WIDGET_ID // 添加字段用于数据一致性检查
};
public static final int COLUMN_ID = 0;
public static final int COLUMN_BG_COLOR_ID = 1;
public static final int COLUMN_SNIPPET = 2;
// 查询字段索引
private static final int COLUMN_ID = 0;
private static final int COLUMN_BG_COLOR_ID = 1;
private static final int COLUMN_SNIPPET = 2;
private static final int COLUMN_WIDGET_ID = 3; // 添加的索引
private static final String TAG = "NoteWidgetProvider";
// ========================== 生命周期方法 ==========================
/**
* - widget_id
*
* @param context
* @param appWidgetIds ID
*/
@Override
public void onDeleted(Context context, int[] appWidgetIds) {
ContentValues values = new ContentValues();
values.put(NoteColumns.WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
for (int i = 0; i < appWidgetIds.length; i++) {
context.getContentResolver().update(Notes.CONTENT_NOTE_URI,
values,
NoteColumns.WIDGET_ID + "=?",
new String[] { String.valueOf(appWidgetIds[i])});
try {
// 防止空指针异常
if (context == null || appWidgetIds == null || appWidgetIds.length == 0) {
Log.w(TAG, "无效的小部件删除请求");
return;
}
ContentValues values = new ContentValues();
values.put(NoteColumns.WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
for (int widgetId : appWidgetIds) {
// 使用try-catch防止个别更新失败中断整个操作
try {
int rowsUpdated = context.getContentResolver().update(
Notes.CONTENT_NOTE_URI,
values,
NoteColumns.WIDGET_ID + "=?",
new String[] { String.valueOf(widgetId) }
);
Log.d(TAG, "已清理小部件 " + widgetId + " 关联的笔记引用,影响行数: " + rowsUpdated);
} catch (Exception e) {
Log.e(TAG, "清理小部件 " + widgetId + " 关联的笔记引用时出错", e);
}
}
} catch (Exception e) {
Log.e(TAG, "小部件删除处理发生未预期错误", e);
}
}
private Cursor getNoteWidgetInfo(Context context, int widgetId) {
return context.getContentResolver().query(Notes.CONTENT_NOTE_URI,
// ========================== 核心功能方法 ==========================
/**
*
*
* @param context
* @param widgetId ID
* @return Cursor
*/
protected Cursor getNoteWidgetInfo(Context context, int widgetId) {
if (context == null || widgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
return null;
}
String selection = NoteColumns.WIDGET_ID + "=? AND " + NoteColumns.PARENT_ID + "<>?";
String[] selectionArgs = new String[] {
String.valueOf(widgetId),
String.valueOf(Notes.ID_TRASH_FOLDER) // 修正: TRASH_FOLER -> TRASH_FOLDER
};
try {
return context.getContentResolver().query(
Notes.CONTENT_NOTE_URI,
PROJECTION,
NoteColumns.WIDGET_ID + "=? AND " + NoteColumns.PARENT_ID + "<>?",
new String[] { String.valueOf(widgetId), String.valueOf(Notes.ID_TRASH_FOLER) },
null);
selection,
selectionArgs,
null
);
} catch (Exception e) {
Log.e(TAG, "查询小部件 " + widgetId + " 关联笔记信息时出错", e);
return null;
}
}
/**
* -
*
* @param context
* @param appWidgetManager
* @param appWidgetIds ID
*/
protected void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
update(context, appWidgetManager, appWidgetIds, false);
}
/**
* -
*
* @param context
* @param appWidgetManager
* @param appWidgetIds ID
* @param privacyMode
*/
private void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds,
boolean privacyMode) {
for (int i = 0; i < appWidgetIds.length; i++) {
if (appWidgetIds[i] != AppWidgetManager.INVALID_APPWIDGET_ID) {
int bgId = ResourceParser.getDefaultBgId(context);
String snippet = "";
Intent intent = new Intent(context, NoteEditActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
intent.putExtra(Notes.INTENT_EXTRA_WIDGET_ID, appWidgetIds[i]);
intent.putExtra(Notes.INTENT_EXTRA_WIDGET_TYPE, getWidgetType());
Cursor c = getNoteWidgetInfo(context, appWidgetIds[i]);
if (c != null && c.moveToFirst()) {
if (c.getCount() > 1) {
Log.e(TAG, "Multiple message with same widget id:" + appWidgetIds[i]);
c.close();
return;
}
snippet = c.getString(COLUMN_SNIPPET);
bgId = c.getInt(COLUMN_BG_COLOR_ID);
intent.putExtra(Intent.EXTRA_UID, c.getLong(COLUMN_ID));
intent.setAction(Intent.ACTION_VIEW);
} else {
snippet = context.getResources().getString(R.string.widget_havenot_content);
intent.setAction(Intent.ACTION_INSERT_OR_EDIT);
}
if (context == null || appWidgetManager == null || appWidgetIds == null) {
Log.w(TAG, "无效的更新请求参数");
return;
}
for (int widgetId : appWidgetIds) {
// 跳过无效小部件ID
if (widgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
continue;
}
if (c != null) {
c.close();
}
// 初始化默认值
int bgId = ResourceParser.NoteColor.YELLOW.id;
String snippet = "";
long noteId = Notes.DEFAULT_NOTE_ID;
// 准备笔记编辑意图
Intent noteIntent = new Intent(context, NoteEditActivity.class);
noteIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
noteIntent.putExtra(Notes.INTENT_EXTRA_WIDGET_ID, widgetId);
noteIntent.putExtra(Notes.INTENT_EXTRA_WIDGET_TYPE, getWidgetType());
RemoteViews rv = new RemoteViews(context.getPackageName(), getLayoutId());
rv.setImageViewResource(R.id.widget_bg_image, getBgResourceId(bgId));
intent.putExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, bgId);
/**
* Generate the pending intent to start host for the widget
*/
PendingIntent pendingIntent = null;
if (privacyMode) {
rv.setTextViewText(R.id.widget_text,
context.getString(R.string.widget_under_visit_mode));
pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], new Intent(
context, NotesListActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);
// 查询数据库获取笔记信息
try (Cursor cursor = getNoteWidgetInfo(context, widgetId)) {
if (cursor != null && cursor.moveToFirst()) {
// 安全检查确保一个WidgetID只关联一个笔记
if (cursor.getCount() > 1) {
Log.w(TAG, "小部件 " + widgetId + " 关联多个笔记,请检查数据一致性");
// 清除异常关联的笔记
cleanInvalidWidgetLinks(context, widgetId, cursor);
// 只处理第一条记录
}
// 获取笔记数据
noteId = cursor.getLong(COLUMN_ID);
snippet = cursor.getString(COLUMN_SNIPPET);
bgId = cursor.getInt(COLUMN_BG_COLOR_ID);
noteIntent.putExtra(Intent.EXTRA_UID, noteId);
noteIntent.setAction(Intent.ACTION_VIEW);
// 检查数据一致性
int storedWidgetId = cursor.getInt(COLUMN_WIDGET_ID);
if (storedWidgetId != widgetId) {
Log.w(TAG, String.format(
"笔记%d的小部件ID不一致: 数据库=%d, 请求=%d",
noteId, storedWidgetId, widgetId
));
}
} else {
rv.setTextViewText(R.id.widget_text, snippet);
pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], intent,
PendingIntent.FLAG_UPDATE_CURRENT);
// 未找到关联笔记
snippet = context.getString(R.string.widget_no_content);
noteIntent.setAction(Intent.ACTION_INSERT_OR_EDIT);
}
} catch (Exception e) {
Log.e(TAG, "处理小部件 " + widgetId + " 时发生错误", e);
snippet = context.getString(R.string.widget_error_loading);
}
rv.setOnClickPendingIntent(R.id.widget_text, pendingIntent);
appWidgetManager.updateAppWidget(appWidgetIds[i], rv);
// 创建小部件视图
RemoteViews remoteViews = new RemoteViews(context.getPackageName(), getLayoutId());
// 设置背景
int bgResId = getBgResourceId(bgId);
remoteViews.setImageViewResource(R.id.widget_bg_image, bgResId);
noteIntent.putExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, bgId);
// 创建点击意图
PendingIntent pendingIntent;
if (privacyMode) {
// 隐私模式:显示提示文本,点击进入主列表
remoteViews.setTextViewText(R.id.widget_text,
context.getString(R.string.widget_privacy_mode));
Intent listIntent = new Intent(context, NotesListActivity.class);
pendingIntent = PendingIntent.getActivity(
context, widgetId, listIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
} else {
// 正常模式:显示笔记摘要,点击进入笔记编辑
remoteViews.setTextViewText(R.id.widget_text, snippet);
pendingIntent = PendingIntent.getActivity(
context, widgetId, noteIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
}
// 设置点击事件
remoteViews.setOnClickPendingIntent(R.id.widget_text, pendingIntent);
// 应用更新
try {
appWidgetManager.updateAppWidget(widgetId, remoteViews);
Log.d(TAG, "成功更新小部件: " + widgetId);
} catch (Exception e) {
Log.e(TAG, "更新小部件 " + widgetId + " 失败", e);
}
}
}
// ========================== 抽象方法 ==========================
/**
* ID
*
* @param bgId ID
* @return drawableID
*/
protected abstract int getBgResourceId(int bgId);
/**
* ID
*
* @return ID
*/
protected abstract int getLayoutId();
/**
*
*
* @return
*/
protected abstract int getWidgetType();
}
// ========================== 新增功能方法 ==========================
/**
*
*
* @param context
* @param widgetId ID
* @param cursor
*/
private void cleanInvalidWidgetLinks(Context context, int widgetId, Cursor cursor) {
if (cursor == null) return;
ContentValues values = new ContentValues();
values.put(NoteColumns.WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
try {
// 遍历所有关联笔记
cursor.moveToPosition(-1); // 移动到开始位置
while (cursor.moveToNext()) {
long currentNoteId = cursor.getLong(COLUMN_ID);
int currentWidgetId = cursor.getInt(COLUMN_WIDGET_ID);
// 只保留最新关联的笔记
if (currentWidgetId == widgetId) {
Log.i(TAG, "保留笔记 " + currentNoteId + " 关联的小部件 " + widgetId);
continue;
}
// 解除其他笔记的关联
Uri noteUri = ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, currentNoteId);
int rows = context.getContentResolver().update(
noteUri, values, null, null);
if (rows > 0) {
Log.w(TAG, "已清理笔记 " + currentNoteId + " 上的小部件关联");
}
}
} catch (Exception e) {
Log.e(TAG, "清理无效小部件关联时出错", e);
}
}
// ========================== 新增功能:定时刷新支持 ==========================
/**
*
*
* @param context
* @param appWidgetId ID
* @param intervalMinutes
*/
public static void setUpdateInterval(Context context, int appWidgetId, int intervalMinutes) {
// 最小间隔限制
if (intervalMinutes < 15) {
Log.w(TAG, "刷新间隔不能小于15分钟");
intervalMinutes = 60; // 默认1小时
}
// TODO: 实现AlarmManager定时刷新逻辑
}
// ========================== 新增功能:多尺寸小部件支持 ==========================
/**
*
*/
public enum WidgetSize {
SIZE_1X1,
SIZE_2X1,
SIZE_2X2,
SIZE_3X3
}
/**
*
*
* @param context
* @param appWidgetId ID
* @return
*/
protected WidgetSize getWidgetSize(Context context, int appWidgetId) {
// TODO: 实现小部件尺寸检测逻辑
return WidgetSize.SIZE_2X1; // 默认返回2x1
}
}
Loading…
Cancel
Save