插入图片功能完成

pull/8/head
s2_cc 1 month ago
parent 43b38729fa
commit 263fbe4620

@ -1,6 +1,10 @@
package net.micode.notes;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.os.StrictMode;
import android.util.Log;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
@ -8,11 +12,21 @@ import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import net.micode.notes.data.NotesDatabaseHelper;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 启用StrictMode检测UI线程的磁盘操作
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.penaltyLog()
.build());
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
@ -21,4 +35,44 @@ public class MainActivity extends AppCompatActivity {
return insets;
});
}
private void checkDatabase() {
new Thread(() -> {
try {
NotesDatabaseHelper dbHelper = NotesDatabaseHelper.getInstance(this);
SQLiteDatabase db = dbHelper.getReadableDatabase();
// 检查表是否存在
Cursor cursor = db.rawQuery(
"SELECT name FROM sqlite_master WHERE type='table' AND name='note_attachment'",
null);
boolean tableExists = cursor != null && cursor.getCount() > 0;
Log.d("Debug", "附件表存在: " + tableExists);
if (cursor != null) cursor.close();
// 查询附件数据
cursor = db.rawQuery("SELECT COUNT(*) FROM note_attachment", null);
if (cursor != null && cursor.moveToFirst()) {
int count = cursor.getInt(0);
Log.d("Debug", "附件表记录数: " + count);
}
if (cursor != null) cursor.close();
// 列出所有附件
cursor = db.query("note_attachment", null, null, null, null, null, null);
if (cursor != null) {
Log.d("Debug", "附件详情 - 总数: " + cursor.getCount());
while (cursor.moveToNext()) {
long id = cursor.getLong(cursor.getColumnIndexOrThrow("_id"));
long noteId = cursor.getLong(cursor.getColumnIndexOrThrow("note_id"));
String path = cursor.getString(cursor.getColumnIndexOrThrow("file_path"));
Log.d("Debug", String.format("附件: id=%d, noteId=%d, path=%s", id, noteId, path));
}
cursor.close();
}
} catch (Exception e) {
Log.e("Debug", "数据库检查失败", e);
}
}).start();
}
}

@ -0,0 +1,344 @@
package net.micode.notes.data;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Environment;
import android.text.TextUtils;
import android.util.Log;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.List;
/**
*
*/
public class AttachmentManager {
private static final String TAG = "AttachmentManager";
private final Context mContext;
private final ContentResolver mContentResolver;
/**
*
*/
private static final String ATTACHMENT_DIR = "attachments";
/**
*
* @param context
*/
public AttachmentManager(Context context) {
mContext = context.getApplicationContext();
mContentResolver = mContext.getContentResolver();
}
/**
*
* @param noteId ID
* @param type Notes.ATTACHMENT_TYPE_GALLERY Notes.ATTACHMENT_TYPE_CAMERA
* @param sourceFile
* @return ID-1
*/
public long addAttachment(long noteId, int type, File sourceFile) {
if (sourceFile == null || !sourceFile.exists()) {
Log.e(TAG, "Source file does not exist");
return -1;
}
// 复制文件到应用私有存储
File destFile = copyToPrivateStorage(sourceFile, noteId);
if (destFile == null) {
Log.e(TAG, "Failed to copy file to private storage");
return -1;
}
// 插入数据库记录
ContentValues values = new ContentValues();
values.put(Notes.AttachmentColumns.NOTE_ID, noteId);
values.put(Notes.AttachmentColumns.TYPE, type);
values.put(Notes.AttachmentColumns.FILE_PATH, destFile.getAbsolutePath());
values.put(Notes.AttachmentColumns.CREATED_TIME, System.currentTimeMillis());
try {
Uri uri = mContentResolver.insert(Notes.CONTENT_ATTACHMENT_URI, values);
if (uri != null) {
String idStr = uri.getLastPathSegment();
if (!TextUtils.isEmpty(idStr)) {
long attachmentId = Long.parseLong(idStr);
// 更新笔记的附件状态
updateNoteAttachmentStatus(noteId, true);
return attachmentId;
}
}
} catch (Exception e) {
Log.e(TAG, "Failed to insert attachment", e);
// 插入失败时删除已复制的文件
destFile.delete();
}
return -1;
}
/**
*
* @param attachmentId ID
* @return
*/
public boolean deleteAttachment(long attachmentId) {
// 先获取笔记ID
long noteId = getNoteIdByAttachmentId(attachmentId);
if (noteId <= 0) {
Log.e(TAG, "Failed to get note id for attachment " + attachmentId);
return false;
}
// 获取文件路径并删除文件
String filePath = getAttachmentFilePath(attachmentId);
if (filePath != null) {
File file = new File(filePath);
if (file.exists()) {
file.delete();
}
}
// 删除数据库记录
Uri uri = Uri.withAppendedPath(Notes.CONTENT_ATTACHMENT_URI, String.valueOf(attachmentId));
int count = mContentResolver.delete(uri, null, null);
boolean success = count > 0;
if (success) {
// 检查笔记是否还有其他附件
List<Attachment> remainingAttachments = getAttachmentsByNoteId(noteId);
if (remainingAttachments.isEmpty()) {
// 没有附件了,更新笔记状态
updateNoteAttachmentStatus(noteId, false);
}
}
return success;
}
/**
*
* @param noteId ID
* @return
*/
public int deleteAttachmentsByNoteId(long noteId) {
List<Attachment> attachments = getAttachmentsByNoteId(noteId);
int deletedCount = 0;
for (Attachment attachment : attachments) {
if (deleteAttachment(attachment.id)) {
deletedCount++;
}
}
return deletedCount;
}
/**
*
* @param attachmentId ID
* @return null
*/
public String getAttachmentFilePath(long attachmentId) {
String[] projection = { Notes.AttachmentColumns.FILE_PATH };
Uri uri = Uri.withAppendedPath(Notes.CONTENT_ATTACHMENT_URI, String.valueOf(attachmentId));
Cursor cursor = null;
try {
cursor = mContentResolver.query(uri, projection, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
return cursor.getString(0);
}
} catch (Exception e) {
Log.e(TAG, "Failed to get attachment file path", e);
} finally {
if (cursor != null) {
cursor.close();
}
}
return null;
}
/**
*
* @param noteId ID
* @return
*/
public List<Attachment> getAttachmentsByNoteId(long noteId) {
List<Attachment> attachments = new ArrayList<>();
String[] projection = {
Notes.AttachmentColumns.ID,
Notes.AttachmentColumns.NOTE_ID,
Notes.AttachmentColumns.TYPE,
Notes.AttachmentColumns.FILE_PATH,
Notes.AttachmentColumns.CREATED_TIME
};
String selection = Notes.AttachmentColumns.NOTE_ID + "=?";
String[] selectionArgs = { String.valueOf(noteId) };
String sortOrder = Notes.AttachmentColumns.CREATED_TIME + " ASC";
Cursor cursor = null;
try {
cursor = mContentResolver.query(Notes.CONTENT_ATTACHMENT_URI,
projection, selection, selectionArgs, sortOrder);
if (cursor != null) {
while (cursor.moveToNext()) {
Attachment attachment = new Attachment();
attachment.id = cursor.getLong(0);
attachment.noteId = cursor.getLong(1);
attachment.type = cursor.getInt(2);
attachment.filePath = cursor.getString(3);
attachment.createdTime = cursor.getLong(4);
attachments.add(attachment);
}
}
} catch (Exception e) {
Log.e(TAG, "Failed to get attachments", e);
} finally {
if (cursor != null) {
cursor.close();
}
}
return attachments;
}
/**
*
* @param sourceFile
* @param noteId ID
* @return null
*/
private File copyToPrivateStorage(File sourceFile, long noteId) {
File destDir = getAttachmentStorageDir();
if (destDir == null || !destDir.exists() && !destDir.mkdirs()) {
Log.e(TAG, "Failed to create attachment directory");
return null;
}
// 生成唯一文件名note_{noteId}_{timestamp}_{random}.jpg
String timestamp = String.valueOf(System.currentTimeMillis());
String random = String.valueOf((int)(Math.random() * 1000));
String extension = getFileExtension(sourceFile.getName());
String fileName = "note_" + noteId + "_" + timestamp + "_" + random + extension;
File destFile = new File(destDir, fileName);
try {
copyFile(sourceFile, destFile);
return destFile;
} catch (IOException e) {
Log.e(TAG, "Failed to copy file", e);
return null;
}
}
/**
*
* @return
*/
public File getAttachmentStorageDir() {
File picturesDir = mContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
if (picturesDir == null) {
picturesDir = new File(mContext.getFilesDir(), "Pictures");
}
return new File(picturesDir, ATTACHMENT_DIR);
}
/**
*
* @param fileName
* @return
*/
private String getFileExtension(String fileName) {
int dotIndex = fileName.lastIndexOf('.');
if (dotIndex > 0 && dotIndex < fileName.length() - 1) {
return fileName.substring(dotIndex);
}
return ".jpg";
}
/**
*
* @param source
* @param dest
* @throws IOException IO
*/
private void copyFile(File source, File dest) throws IOException {
try (FileChannel sourceChannel = new FileInputStream(source).getChannel();
FileChannel destChannel = new FileOutputStream(dest).getChannel()) {
destChannel.transferFrom(sourceChannel, 0, sourceChannel.size());
}
}
/**
* IDID
* @param attachmentId ID
* @return ID-1
*/
private long getNoteIdByAttachmentId(long attachmentId) {
String[] projection = { Notes.AttachmentColumns.NOTE_ID };
Uri uri = Uri.withAppendedPath(Notes.CONTENT_ATTACHMENT_URI, String.valueOf(attachmentId));
Cursor cursor = null;
try {
cursor = mContentResolver.query(uri, projection, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
return cursor.getLong(0);
}
} catch (Exception e) {
Log.e(TAG, "Failed to get note id by attachment id", e);
} finally {
if (cursor != null) {
cursor.close();
}
}
return -1;
}
/**
*
* @param noteId ID
* @param hasAttachment
*/
private void updateNoteAttachmentStatus(long noteId, boolean hasAttachment) {
ContentValues values = new ContentValues();
values.put(Notes.NoteColumns.HAS_ATTACHMENT, hasAttachment ? 1 : 0);
values.put(Notes.NoteColumns.LOCAL_MODIFIED, 1);
values.put(Notes.NoteColumns.MODIFIED_DATE, System.currentTimeMillis());
Uri uri = ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId);
try {
mContentResolver.update(uri, values, null, null);
} catch (Exception e) {
Log.e(TAG, "Failed to update note attachment status", e);
}
}
/**
*
*/
public static class Attachment {
public long id;
public long noteId;
public int type;
public String filePath;
public long createdTime;
@Override
public String toString() {
return "Attachment{" +
"id=" + id +
", noteId=" + noteId +
", type=" + type +
", filePath='" + filePath + '\'' +
", createdTime=" + createdTime +
'}';
}
}
}

@ -140,6 +140,47 @@ public class Notes {
public static final String CALL_NOTE = CallNote.CONTENT_ITEM_TYPE;
}
/**
*
*/
public static final int ATTACHMENT_TYPE_GALLERY = 0;
public static final int ATTACHMENT_TYPE_CAMERA = 1;
/**
*
*/
public interface AttachmentColumns {
/**
* The unique ID for a row
* <P> Type: INTEGER (long) </P>
*/
public static final String ID = "_id";
/**
* The note's id this attachment belongs to
* <P> Type: INTEGER (long) </P>
*/
public static final String NOTE_ID = "note_id";
/**
* Attachment type (0=gallery, 1=camera)
* <P> Type: INTEGER </P>
*/
public static final String TYPE = "type";
/**
* File path in private storage
* <P> Type: TEXT </P>
*/
public static final String FILE_PATH = "file_path";
/**
* Created time
* <P> Type: INTEGER (long) </P>
*/
public static final String CREATED_TIME = "created_time";
}
/**
* Uri
*/
@ -150,6 +191,11 @@ public class Notes {
*/
public static final Uri CONTENT_DATA_URI = Uri.parse("content://" + AUTHORITY + "/data");
/**
* Uri
*/
public static final Uri CONTENT_ATTACHMENT_URI = Uri.parse("content://" + AUTHORITY + "/attachment");
/**
*
*/

@ -18,6 +18,7 @@ package net.micode.notes.data;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
@ -48,7 +49,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
/**
* {@link #onUpgrade}
*/
private static final int DB_VERSION = 4;
private static final int DB_VERSION = 7;
/**
*
@ -56,6 +57,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
*
* 1. {@link #NOTE}便
* 2. {@link #DATA}便 MIME
* 3. {@link #ATTACHMENT}便
*/
public interface TABLE {
/**
@ -67,6 +69,11 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
*
*/
public static final String DATA = "data";
/**
*
*/
public static final String ATTACHMENT = "note_attachment";
}
/**
@ -136,6 +143,26 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
DataColumns.DATA5 + " TEXT NOT NULL DEFAULT ''" +
")";
/**
* (note_attachment) SQL
* <p>
* 便
*
* - _id: Android ContentProvider
* - note_id: ID
* - type: 0=1=
* - file_path:
* - created_time:
*/
private static final String CREATE_ATTACHMENT_TABLE_SQL =
"CREATE TABLE " + TABLE.ATTACHMENT + "(" +
"_id INTEGER PRIMARY KEY," +
"note_id INTEGER NOT NULL," +
"type INTEGER NOT NULL DEFAULT 0," +
"file_path TEXT NOT NULL," +
"created_time INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)" +
")";
/**
* data NOTE_ID SQL
* <p>
@ -403,6 +430,20 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
Log.d(TAG, "data table has been created");
}
/**
* (note_attachment)
*
* @param db
*/
public void createAttachmentTable(SQLiteDatabase db) {
// 执行创建表SQL
db.execSQL(CREATE_ATTACHMENT_TABLE_SQL);
// 创建note_id索引以提高查询性能
db.execSQL("CREATE INDEX IF NOT EXISTS attachment_note_id_index ON " +
TABLE.ATTACHMENT + "(note_id);");
Log.d(TAG, "attachment table has been created");
}
/**
* data SQL
* <p>
@ -428,7 +469,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
* @param context
* @return NotesDatabaseHelper
*/
static synchronized NotesDatabaseHelper getInstance(Context context) {
public static synchronized NotesDatabaseHelper getInstance(Context context) {
if (mInstance == null) {
mInstance = new NotesDatabaseHelper(context);
}
@ -438,7 +479,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
/**
*
* <p>
* note data
* note data
*
* @param db
*/
@ -448,6 +489,8 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
createNoteTable(db);
// 创建数据表
createDataTable(db);
// 创建附件表
createAttachmentTable(db);
}
/**
@ -486,13 +529,31 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
oldVersion++;
}
// 4. 如果表结构在v3升级中发生较大变更需要重建触发器以确保兼容性
// 4. 从版本4升级到版本5支持图片插入功能
if (oldVersion == 4) {
upgradeToV5(db);
oldVersion++;
}
// 5. 从版本5升级到版本6添加附件表
if (oldVersion == 5) {
upgradeToV6(db);
oldVersion++;
}
// 6. 从版本6升级到版本7修复附件表主键列名
if (oldVersion == 6) {
upgradeToV7(db);
oldVersion++;
}
// 7. 如果表结构在v3升级中发生较大变更需要重建触发器以确保兼容性
if (reCreateTriggers) {
reCreateNoteTableTriggers(db);
reCreateDataTableTriggers(db);
}
// 5. 最终版本校验:确保所有升级步骤已执行完毕
// 8. 最终版本校验:确保所有升级步骤已执行完毕
if (oldVersion != newVersion) {
throw new IllegalStateException("Upgrade notes database to version " + newVersion
+ "fails");
@ -555,4 +616,126 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.VERSION
+ " INTEGER NOT NULL DEFAULT 0");
}
/**
* 5
* <p>
*
*
* <p>
*
* - data 使 DATA3DATA4DATA5
* - MIME_TYPE
* -
*
* @param db
*/
private void upgradeToV5(SQLiteDatabase db) {
// 为图片数据添加查询索引,提高图片查询性能
db.execSQL("CREATE INDEX IF NOT EXISTS image_data_index ON " + TABLE.DATA + "(" + DataColumns.MIME_TYPE + ") WHERE "
+ DataColumns.MIME_TYPE + " LIKE 'image/%'");
Log.d(TAG, "Database upgraded to version 5 (image support with index)");
}
/**
* 6
* <p>
*
* 便
*
* @param db
*/
private void upgradeToV6(SQLiteDatabase db) {
// 创建附件表
createAttachmentTable(db);
Log.d(TAG, "Database upgraded to version 6 (attachment table added)");
}
/**
* 7
* <p>
*
* "id" "_id"Android ContentProvider
*
* @param db
*/
private void upgradeToV7(SQLiteDatabase db) {
// 检查附件表是否存在
Cursor cursor = db.rawQuery("SELECT name FROM sqlite_master WHERE type='table' AND name='note_attachment'", null);
boolean tableExists = cursor != null && cursor.getCount() > 0;
if (cursor != null) {
cursor.close();
}
if (tableExists) {
// 检查当前表结构中的主键列名
cursor = db.rawQuery("PRAGMA table_info(note_attachment)", null);
boolean hasIdColumn = false;
boolean hasUnderscoreIdColumn = false;
if (cursor != null) {
while (cursor.moveToNext()) {
String columnName = cursor.getString(1); // 列名在索引1
if ("id".equals(columnName)) {
hasIdColumn = true;
} else if ("_id".equals(columnName)) {
hasUnderscoreIdColumn = true;
}
}
cursor.close();
}
if (hasIdColumn && !hasUnderscoreIdColumn) {
// 需要将id列重命名为_id
Log.d(TAG, "Starting migration: renaming 'id' column to '_id' in note_attachment table");
// 使用事务确保迁移的原子性
db.beginTransaction();
try {
// 1. 创建临时表
db.execSQL("CREATE TABLE note_attachment_temp (" +
"_id INTEGER PRIMARY KEY," +
"note_id INTEGER NOT NULL," +
"type INTEGER NOT NULL DEFAULT 0," +
"file_path TEXT NOT NULL," +
"created_time INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)" +
")");
// 2. 复制数据
db.execSQL("INSERT INTO note_attachment_temp (_id, note_id, type, file_path, created_time) " +
"SELECT id, note_id, type, file_path, created_time FROM note_attachment");
// 3. 删除原表
db.execSQL("DROP TABLE note_attachment");
// 4. 重命名临时表
db.execSQL("ALTER TABLE note_attachment_temp RENAME TO note_attachment");
// 5. 重新创建索引
db.execSQL("CREATE INDEX IF NOT EXISTS attachment_note_id_index ON note_attachment(note_id)");
db.setTransactionSuccessful();
Log.d(TAG, "Successfully migrated note_attachment table: renamed 'id' to '_id'");
} catch (Exception e) {
Log.e(TAG, "Failed to migrate note_attachment table", e);
throw e;
} finally {
db.endTransaction();
}
} else if (hasUnderscoreIdColumn) {
Log.d(TAG, "Table already has correct '_id' column, no migration needed");
} else {
Log.w(TAG, "Unexpected table structure for note_attachment, recreating table");
// 如果表结构异常,重新创建表
db.execSQL("DROP TABLE IF EXISTS note_attachment");
createAttachmentTable(db);
}
} else {
// 表不存在,直接创建新表
createAttachmentTable(db);
Log.d(TAG, "Created note_attachment table with correct column names");
}
Log.d(TAG, "Database upgraded to version 7 (attachment table column name fixed)");
}
}

@ -32,6 +32,7 @@ import android.util.Log;
import net.micode.notes.R;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.Notes.AttachmentColumns;
import net.micode.notes.data.NotesDatabaseHelper.TABLE;
@ -86,6 +87,16 @@ public class NotesProvider extends ContentProvider {
*/
private static final int URI_SEARCH_SUGGEST = 6;
/**
* URI
*/
private static final int URI_ATTACHMENT = 7;
/**
* URI
*/
private static final int URI_ATTACHMENT_ITEM = 8;
/**
* UriMatcher
*/
@ -99,6 +110,8 @@ public class NotesProvider extends ContentProvider {
mMatcher.addURI(Notes.AUTHORITY, "search", URI_SEARCH); // content://micode_notes/search
mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, URI_SEARCH_SUGGEST); // 搜索建议
mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", URI_SEARCH_SUGGEST); // 带参数的搜索建议
mMatcher.addURI(Notes.AUTHORITY, "attachment", URI_ATTACHMENT); // content://micode_notes/attachment
mMatcher.addURI(Notes.AUTHORITY, "attachment/#", URI_ATTACHMENT_ITEM); // content://micode_notes/attachment/1
}
/**
@ -166,6 +179,15 @@ public class NotesProvider extends ContentProvider {
c = db.query(TABLE.DATA, projection, DataColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs, null, null, sortOrder);
break;
case URI_ATTACHMENT:
c = db.query(TABLE.ATTACHMENT, projection, selection, selectionArgs, null, null,
sortOrder);
break;
case URI_ATTACHMENT_ITEM:
id = uri.getPathSegments().get(1);
c = db.query(TABLE.ATTACHMENT, projection, AttachmentColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs, null, null, sortOrder);
break;
case URI_SEARCH:
case URI_SEARCH_SUGGEST:
if (sortOrder != null || projection != null) {
@ -225,6 +247,14 @@ public class NotesProvider extends ContentProvider {
}
insertedId = dataId = db.insert(TABLE.DATA, null, values);
break;
case URI_ATTACHMENT:
if (values.containsKey(AttachmentColumns.NOTE_ID)) {
noteId = values.getAsLong(AttachmentColumns.NOTE_ID);
} else {
Log.d(TAG, "Wrong attachment format without note id:" + values.toString());
}
insertedId = db.insert(TABLE.ATTACHMENT, null, values);
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
@ -284,6 +314,14 @@ public class NotesProvider extends ContentProvider {
DataColumns.ID + "=" + id + parseSelection(selection), selectionArgs);
deleteData = true;
break;
case URI_ATTACHMENT:
count = db.delete(TABLE.ATTACHMENT, selection, selectionArgs);
break;
case URI_ATTACHMENT_ITEM:
id = uri.getPathSegments().get(1);
count = db.delete(TABLE.ATTACHMENT,
Notes.AttachmentColumns.ID + "=" + id + parseSelection(selection), selectionArgs);
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
@ -331,6 +369,14 @@ public class NotesProvider extends ContentProvider {
+ parseSelection(selection), selectionArgs);
updateData = true;
break;
case URI_ATTACHMENT:
count = db.update(TABLE.ATTACHMENT, values, selection, selectionArgs);
break;
case URI_ATTACHMENT_ITEM:
id = uri.getPathSegments().get(1);
count = db.update(TABLE.ATTACHMENT, values, "id=" + id
+ parseSelection(selection), selectionArgs);
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}

@ -29,8 +29,13 @@ import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.DataConstants;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.Notes.TextNote;
import net.micode.notes.data.AttachmentManager;
import net.micode.notes.tool.ResourceParser.NoteBgResources;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
/**
* WorkingNote - 使
@ -46,6 +51,10 @@ public class WorkingNote {
* Note
*/
private Note mNote;
/**
*
*/
private AttachmentManager mAttachmentManager;
/**
* ID
*/
@ -145,6 +154,7 @@ public class WorkingNote {
mModifiedDate = System.currentTimeMillis();
mFolderId = folderId;
mNote = new Note();
mAttachmentManager = new AttachmentManager(context);
mNoteId = 0;
mIsDeleted = false;
mMode = 0;
@ -158,6 +168,7 @@ public class WorkingNote {
mFolderId = folderId;
mIsDeleted = false;
mNote = new Note();
mAttachmentManager = new AttachmentManager(context);
loadNote();
}
@ -379,6 +390,61 @@ public class WorkingNote {
return mWidgetType;
}
/**
*
* @param type Notes.ATTACHMENT_TYPE_GALLERY Notes.ATTACHMENT_TYPE_CAMERA
* @param sourceFile
* @return ID-1
*/
public long addAttachment(int type, File sourceFile) {
if (mAttachmentManager == null || mNoteId <= 0) {
return -1;
}
return mAttachmentManager.addAttachment(mNoteId, type, sourceFile);
}
/**
*
* @param attachmentId ID
* @return
*/
public boolean deleteAttachment(long attachmentId) {
if (mAttachmentManager == null) {
return false;
}
return mAttachmentManager.deleteAttachment(attachmentId);
}
/**
*
* @return
*/
public int deleteAllAttachments() {
if (mAttachmentManager == null) {
return 0;
}
return mAttachmentManager.deleteAttachmentsByNoteId(mNoteId);
}
/**
*
* @return
*/
public List<AttachmentManager.Attachment> getAttachments() {
if (mAttachmentManager == null) {
return new ArrayList<>();
}
return mAttachmentManager.getAttachmentsByNoteId(mNoteId);
}
/**
*
* @return
*/
public AttachmentManager getAttachmentManager() {
return mAttachmentManager;
}
public interface NoteSettingChangedListener {
/**
* Called when the background color of current note has just changed

@ -249,7 +249,7 @@ public class BackupUtils {
Notes.CONTENT_NOTE_URI,
NOTE_PROJECTION,
"(" + NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER + " AND "
+ NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER + ") OR "
+ NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLDER + ") OR "
+ NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER, null, null);
if (folderCursor != null) {
@ -300,60 +300,9 @@ public class BackupUtils {
return STATE_SYSTEM_ERROR;
}
// 第一部分:导出文件夹及其中的笔记
// 查询所有文件夹(排除回收站,但包含通话记录文件夹)
Cursor folderCursor = mContext.getContentResolver().query(
Notes.CONTENT_NOTE_URI,
NOTE_PROJECTION,
"(" + NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER + " AND "
+ NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLDER + ") OR "
+ NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER, null, null);
if (folderCursor != null) {
if (folderCursor.moveToFirst()) {
do {
// 输出文件夹名称
String folderName = "";
if(folderCursor.getLong(NOTE_COLUMN_ID) == Notes.ID_CALL_RECORD_FOLDER) {
folderName = mContext.getString(R.string.call_record_folder_name);
} else {
folderName = folderCursor.getString(NOTE_COLUMN_SNIPPET);
}
if (!TextUtils.isEmpty(folderName)) {
ps.println(String.format(getFormat(FORMAT_FOLDER_NAME), folderName));
}
// 导出该文件夹下的所有笔记
String folderId = folderCursor.getString(NOTE_COLUMN_ID);
exportFolderToText(folderId, ps);
} while (folderCursor.moveToNext());
}
folderCursor.close();
}
// 第二部分:导出根目录下的笔记(不属于任何文件夹的笔记)
Cursor noteCursor = mContext.getContentResolver().query(
Notes.CONTENT_NOTE_URI,
NOTE_PROJECTION,
NoteColumns.TYPE + "=" + +Notes.TYPE_NOTE + " AND " + NoteColumns.PARENT_ID
+ "=0", null, null);
if (noteCursor != null) {
if (noteCursor.moveToFirst()) {
do {
ps.println(String.format(getFormat(FORMAT_NOTE_DATE), DateFormat.format(
mContext.getString(R.string.format_datetime_mdhm),
noteCursor.getLong(NOTE_COLUMN_MODIFIED_DATE))));
// 导出该笔记的内容
String noteId = noteCursor.getString(NOTE_COLUMN_ID);
exportNoteToText(noteId, ps);
} while (noteCursor.moveToNext());
}
noteCursor.close();
}
// 关闭输出流
ps.close();
return STATE_SUCCESS;
}
/**

@ -19,6 +19,7 @@ package net.micode.notes.tool;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
@ -66,7 +67,7 @@ public class ElderModeUtils {
// 只增大用户输入内容的字体
if (id == R.id.tv_title || // 笔记列表中的标题(用户输入内容)
id == R.id.note_edit_view) { // 编辑界面中的内容(用户输入)
textView.setTextSize(textView.getTextSize() * ELDER_MODE_FONT_SCALE);
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textView.getTextSize() * ELDER_MODE_FONT_SCALE);
}
} else if (view instanceof ViewGroup) {
// 递归处理ViewGroup中的所有子View
@ -84,9 +85,10 @@ public class ElderModeUtils {
* @param view View
*/
public static void clearElderMode(Context context, View view) {
if (!isElderModeEnabled(context)) {
return;
}
// 移除这个检查,因为清除操作应该在老年人模式禁用时执行
// if (!isElderModeEnabled(context)) {
// return;
// }
// 只对用户输入的内容应用字体恢复,系统信息保持不变
if (view instanceof TextView) {
@ -96,7 +98,7 @@ public class ElderModeUtils {
// 只恢复用户输入内容的字体
if (id == R.id.tv_title || // 笔记列表中的标题(用户输入内容)
id == R.id.note_edit_view) { // 编辑界面中的内容(用户输入)
textView.setTextSize(textView.getTextSize() / ELDER_MODE_FONT_SCALE);
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textView.getTextSize() / ELDER_MODE_FONT_SCALE);
}
} else if (view instanceof ViewGroup) {
// 递归处理ViewGroup中的所有子View

@ -0,0 +1,148 @@
package net.micode.notes.tool;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import java.util.ArrayList;
import java.util.List;
/**
*
*/
public class PermissionHelper {
/**
*
* @param context
* @param permission
* @return
*/
public static boolean isPermissionGranted(Context context, String permission) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return true;
}
return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED;
}
/**
*
* @param context
* @param permissions
* @return
*/
public static boolean arePermissionsGranted(Context context, String[] permissions) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return true;
}
for (String permission : permissions) {
if (!isPermissionGranted(context, permission)) {
return false;
}
}
return true;
}
/**
*
* @param activity
* @param permissions
* @param requestCode
*/
public static void requestPermissions(Activity activity, String[] permissions, int requestCode) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return;
}
ActivityCompat.requestPermissions(activity, permissions, requestCode);
}
/**
*
* @param activity
* @param permission
* @param requestCode
*/
public static void requestPermission(Activity activity, String permission, int requestCode) {
requestPermissions(activity, new String[]{permission}, requestCode);
}
/**
*
* @param requestCode
* @param permissions
* @param grantResults
* @param listener
*/
public static void handlePermissionResult(int requestCode, String[] permissions, int[] grantResults,
OnPermissionResultListener listener) {
if (listener == null) {
return;
}
List<String> granted = new ArrayList<>();
List<String> denied = new ArrayList<>();
for (int i = 0; i < permissions.length; i++) {
if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
granted.add(permissions[i]);
} else {
denied.add(permissions[i]);
}
}
if (denied.isEmpty()) {
listener.onAllGranted(requestCode, granted);
} else {
listener.onDenied(requestCode, granted, denied);
}
}
/**
*
* @param activity
* @param permission
* @return
*/
public static boolean shouldShowRequestPermissionRationale(Activity activity, String permission) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return false;
}
return ActivityCompat.shouldShowRequestPermissionRationale(activity, permission);
}
/**
*
*/
public interface OnPermissionResultListener {
/**
*
* @param requestCode
* @param grantedPermissions
*/
void onAllGranted(int requestCode, List<String> grantedPermissions);
/**
*
* @param requestCode
* @param grantedPermissions
* @param deniedPermissions
*/
void onDenied(int requestCode, List<String> grantedPermissions, List<String> deniedPermissions);
}
/**
*
*/
public static class Permissions {
public static final String CAMERA = android.Manifest.permission.CAMERA;
public static final String READ_EXTERNAL_STORAGE = android.Manifest.permission.READ_EXTERNAL_STORAGE;
public static final String WRITE_EXTERNAL_STORAGE = android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
// Android 13+ 需要单独请求媒体权限
public static final String READ_MEDIA_IMAGES = android.Manifest.permission.READ_MEDIA_IMAGES;
}
}

@ -0,0 +1,97 @@
package net.micode.notes.ui;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.DialogInterface;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import net.micode.notes.R;
import java.io.File;
/**
*
*/
public class CameraPreviewDialogFragment extends DialogFragment {
public static final String TAG = "CameraPreviewDialogFragment";
public interface OnCameraPreviewListener {
void onConfirm(File photoFile);
void onRetake();
}
private OnCameraPreviewListener mListener;
private File mPhotoFile;
public CameraPreviewDialogFragment() {
// Required empty public constructor
}
public void setPhotoFile(File photoFile) {
mPhotoFile = photoFile;
}
public void setOnCameraPreviewListener(OnCameraPreviewListener listener) {
mListener = listener;
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
Dialog dialog = new Dialog(getActivity());
dialog.setContentView(R.layout.dialog_camera_preview);
return dialog;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.dialog_camera_preview, container, false);
ImageView previewImage = view.findViewById(R.id.preview_image);
Button btnRetake = view.findViewById(R.id.btn_retake);
Button btnConfirm = view.findViewById(R.id.btn_confirm);
// 加载预览图片
if (mPhotoFile != null && mPhotoFile.exists()) {
// 使用BitmapFactory解码图片避免OOM
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 4; // 降低采样率,减少内存占用
options.inJustDecodeBounds = false;
Bitmap bitmap = BitmapFactory.decodeFile(mPhotoFile.getAbsolutePath(), options);
if (bitmap != null) {
previewImage.setImageBitmap(bitmap);
}
}
btnRetake.setOnClickListener(v -> {
if (mListener != null) {
mListener.onRetake();
}
dismiss();
});
btnConfirm.setOnClickListener(v -> {
if (mListener != null && mPhotoFile != null) {
mListener.onConfirm(mPhotoFile);
}
dismiss();
});
return view;
}
@Override
public void onDismiss(DialogInterface dialog) {
super.onDismiss(dialog);
mListener = null;
mPhotoFile = null;
}
}

@ -0,0 +1,78 @@
package net.micode.notes.ui;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import net.micode.notes.R;
/**
*
*/
public class ImageInsertDialogFragment extends DialogFragment {
public static final String TAG = "ImageInsertDialogFragment";
public interface OnImageInsertListener {
void onGallerySelected();
void onCameraSelected();
}
private OnImageInsertListener mListener;
public ImageInsertDialogFragment() {
// Required empty public constructor
}
public void setOnImageInsertListener(OnImageInsertListener listener) {
mListener = listener;
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
// 使用自定义布局而不是AlertDialog
Dialog dialog = new Dialog(getActivity());
dialog.setContentView(R.layout.dialog_image_insert);
return dialog;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.dialog_image_insert, container, false);
Button btnGallery = view.findViewById(R.id.btn_gallery);
Button btnCamera = view.findViewById(R.id.btn_camera);
Button btnCancel = view.findViewById(R.id.btn_cancel);
btnGallery.setOnClickListener(v -> {
if (mListener != null) {
mListener.onGallerySelected();
}
dismiss();
});
btnCamera.setOnClickListener(v -> {
if (mListener != null) {
mListener.onCameraSelected();
}
dismiss();
});
btnCancel.setOnClickListener(v -> dismiss());
return view;
}
@Override
public void onDismiss(DialogInterface dialog) {
super.onDismiss(dialog);
mListener = null;
}
}

@ -28,7 +28,10 @@ import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Paint;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.preference.PreferenceManager;
import android.text.Spannable;
import android.text.SpannableString;
@ -47,6 +50,7 @@ import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
@ -65,13 +69,29 @@ import net.micode.notes.ui.DateTimePickerDialog.OnDateTimeSetListener;
import net.micode.notes.ui.NoteEditText.OnTextViewChangeListener;
import net.micode.notes.widget.NoteWidgetProvider_2x;
import net.micode.notes.widget.NoteWidgetProvider_4x;
import net.micode.notes.data.AttachmentManager;
import net.micode.notes.tool.PermissionHelper;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import androidx.annotation.NonNull;
import androidx.core.content.FileProvider;
/**
* Activity - 便
* OnClickListener, NoteSettingChangedListener, OnTextViewChangeListener
@ -151,17 +171,27 @@ public class NoteEditActivity extends Activity implements OnClickListener,
private String mUserQuery; // 用户搜索查询(用于高亮显示)
private Pattern mPattern; // 用于高亮搜索结果的模式
// 附件相关
private LinearLayout mAttachmentContainer; // 附件容器
private static final int REQUEST_GALLERY = 1001;
private static final int REQUEST_CAMERA = 1002;
private static final int REQUEST_PERMISSION_CAMERA = 1003;
private static final int REQUEST_PERMISSION_STORAGE = 1004;
private static final int REQUEST_CAMERA_PREVIEW = 1005;
private String mCurrentPhotoPath; // 相机拍照临时文件路径
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setContentView(R.layout.note_edit); // 设置布局文件
initResources(); // 初始化资源
// 初始化Activity状态如果初始化失败则结束Activity
if (savedInstanceState == null && !initActivityState(getIntent())) {
finish();
return;
}
initResources(); // 初始化资源
}
/**
@ -210,18 +240,32 @@ public class NoteEditActivity extends Activity implements OnClickListener,
finish();
return false;
} else {
// 加载笔记
mWorkingNote = WorkingNote.load(this, noteId);
if (mWorkingNote == null) {
Log.e(TAG, "load note failed with note id" + noteId);
finish();
return false;
}
// 在后台线程加载笔记避免阻塞UI线程
new AsyncTask<Long, Void, WorkingNote>() {
@Override
protected WorkingNote doInBackground(Long... params) {
long id = params[0];
try {
return WorkingNote.load(NoteEditActivity.this, id);
} catch (Exception e) {
Log.e(TAG, "load note failed with note id" + id, e);
return null;
}
}
@Override
protected void onPostExecute(WorkingNote result) {
if (result != null) {
mWorkingNote = result;
setupWorkingNoteAndInitializeUI(Intent.ACTION_VIEW);
} else {
Log.e(TAG, "load note failed");
finish();
}
}
}.execute(noteId);
return true;
}
// 隐藏软键盘
getWindow().setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN
| WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
}
// 处理新建或编辑笔记的请求
else if(TextUtils.equals(Intent.ACTION_INSERT_OR_EDIT, intent.getAction())) {
@ -245,45 +289,88 @@ public class NoteEditActivity extends Activity implements OnClickListener,
// 检查是否已存在相同的通话记录笔记
if ((noteId = DataUtils.getNoteIdByPhoneNumberAndCallDate(getContentResolver(),
phoneNumber, callDate)) > 0) {
mWorkingNote = WorkingNote.load(this, noteId);
if (mWorkingNote == null) {
Log.e(TAG, "load call note failed with note id" + noteId);
finish();
return false;
}
// 在后台线程加载通话记录笔记
new AsyncTask<Long, Void, WorkingNote>() {
@Override
protected WorkingNote doInBackground(Long... params) {
long id = params[0];
try {
return WorkingNote.load(NoteEditActivity.this, id);
} catch (Exception e) {
Log.e(TAG, "load call note failed with note id" + id, e);
return null;
}
}
@Override
protected void onPostExecute(WorkingNote result) {
if (result != null) {
mWorkingNote = result;
setupWorkingNoteAndInitializeUI(Intent.ACTION_INSERT_OR_EDIT);
} else {
Log.e(TAG, "load call note failed");
finish();
}
}
}.execute(noteId);
} else {
// 创建新的通话记录笔记
// 创建新的通话记录笔记无需数据库操作直接在UI线程处理
mWorkingNote = WorkingNote.createEmptyNote(this, folderId, widgetId,
widgetType, bgResId);
mWorkingNote.convertToCallNote(phoneNumber, callDate);
setupWorkingNoteAndInitializeUI(Intent.ACTION_INSERT_OR_EDIT);
}
} else {
// 创建普通笔记
// 创建普通笔记无需数据库操作直接在UI线程处理
mWorkingNote = WorkingNote.createEmptyNote(this, folderId, widgetId, widgetType,
bgResId);
setupWorkingNoteAndInitializeUI(Intent.ACTION_INSERT_OR_EDIT);
}
// 显示软键盘
getWindow().setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
| WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
} else {
Log.e(TAG, "Intent not specified action, should not support");
finish();
return false;
}
// 设置笔记设置变化监听器
mWorkingNote.setOnSettingStatusChangedListener(this);
return true;
}
/**
* WorkingNoteUI
*/
private void setupWorkingNoteAndInitializeUI(String action) {
if (mWorkingNote != null) {
// 设置笔记设置变化监听器
mWorkingNote.setOnSettingStatusChangedListener(this);
// 根据不同的操作类型设置软键盘模式
if (TextUtils.equals(Intent.ACTION_VIEW, action)) {
// 隐藏软键盘
getWindow().setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN
| WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
} else if (TextUtils.equals(Intent.ACTION_INSERT_OR_EDIT, action)) {
// 显示软键盘
getWindow().setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
| WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
}
// 初始化UI界面
initNoteScreen();
// 加载附件
loadAttachments();
}
}
@Override
protected void onResume() {
super.onResume();
initNoteScreen(); // 初始化笔记界面
// 每次恢复时应用老年人模式
ElderModeUtils.applyElderMode(this, findViewById(android.R.id.content));
// 只有当mWorkingNote已经初始化完成时才初始化界面
if (mWorkingNote != null) {
initNoteScreen(); // 初始化笔记界面(已包含老年人模式应用)
}
}
/**
@ -319,6 +406,9 @@ public class NoteEditActivity extends Activity implements OnClickListener,
// 显示闹钟提醒头部
showAlertHeader();
// 在设置字体外观后应用老年人模式,确保老年人模式的字体设置不被覆盖
ElderModeUtils.applyElderMode(this, mNoteEditor);
}
/**
@ -442,6 +532,9 @@ public class NoteEditActivity extends Activity implements OnClickListener,
mEditTextList = (LinearLayout) findViewById(R.id.note_edit_list);
// 初始化附件容器
mAttachmentContainer = (LinearLayout) findViewById(R.id.note_attachment_container);
// 应用老年人模式
ElderModeUtils.applyElderMode(this, findViewById(android.R.id.content));
}
@ -630,6 +723,9 @@ public class NoteEditActivity extends Activity implements OnClickListener,
mWorkingNote.setCheckListMode(mWorkingNote.getCheckListMode() == 0 ?
TextNote.MODE_CHECK_LIST : 0);
break;
case R.id.menu_insert_image:
handleInsertImage(); // 插入图片
break;
case R.id.menu_share:
getWorkingText(); // 获取工作文本
sendTo(this, mWorkingNote.getContent()); // 分享笔记
@ -1049,4 +1145,293 @@ public class NoteEditActivity extends Activity implements OnClickListener,
private void showToast(int resId, int duration) {
Toast.makeText(this, resId, duration).show();
}
// ==================== 附件相关方法 ====================
/**
*
*/
private void loadAttachments() {
if (mWorkingNote == null || mWorkingNote.getNoteId() <= 0) {
return;
}
List<AttachmentManager.Attachment> attachments = mWorkingNote.getAttachments();
if (attachments.isEmpty()) {
mAttachmentContainer.setVisibility(View.GONE);
return;
}
mAttachmentContainer.setVisibility(View.VISIBLE);
mAttachmentContainer.removeAllViews();
for (AttachmentManager.Attachment attachment : attachments) {
addAttachmentView(attachment);
}
}
/**
*
* @param attachment
*/
private void addAttachmentView(AttachmentManager.Attachment attachment) {
View view = LayoutInflater.from(this).inflate(R.layout.image_item, mAttachmentContainer, false);
ImageView imageView = view.findViewById(R.id.image_view);
ImageButton deleteButton = view.findViewById(R.id.btn_delete);
// 使用Glide加载图片
Glide.with(this)
.load(new File(attachment.filePath))
.override(800, 800)
.centerCrop()
.transition(DrawableTransitionOptions.withCrossFade())
.into(imageView);
// 长按删除
view.setOnLongClickListener(v -> {
new AlertDialog.Builder(this)
.setTitle(R.string.delete_image)
.setMessage(R.string.confirm_delete_image)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
if (mWorkingNote.deleteAttachment(attachment.id)) {
mAttachmentContainer.removeView(view);
if (mAttachmentContainer.getChildCount() == 0) {
mAttachmentContainer.setVisibility(View.GONE);
}
showToast(R.string.image_deleted);
}
})
.setNegativeButton(android.R.string.cancel, null)
.show();
return true;
});
// 删除按钮点击
deleteButton.setOnClickListener(v -> view.performLongClick());
mAttachmentContainer.addView(view);
}
/**
*
*/
private void handleInsertImage() {
ImageInsertDialogFragment dialog = new ImageInsertDialogFragment();
dialog.setOnImageInsertListener(new ImageInsertDialogFragment.OnImageInsertListener() {
@Override
public void onGallerySelected() {
requestGalleryPermission();
}
@Override
public void onCameraSelected() {
requestCameraPermission();
}
});
dialog.show(getFragmentManager(), ImageInsertDialogFragment.TAG);
}
/**
*
*/
private void requestGalleryPermission() {
String[] permissions;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
permissions = new String[]{PermissionHelper.Permissions.READ_MEDIA_IMAGES};
} else {
permissions = new String[]{PermissionHelper.Permissions.READ_EXTERNAL_STORAGE};
}
if (PermissionHelper.arePermissionsGranted(this, permissions)) {
openGallery();
} else {
PermissionHelper.requestPermissions(this, permissions, REQUEST_PERMISSION_STORAGE);
}
}
/**
*
*/
private void requestCameraPermission() {
String[] permissions = {PermissionHelper.Permissions.CAMERA};
if (PermissionHelper.arePermissionsGranted(this, permissions)) {
openCamera();
} else {
PermissionHelper.requestPermissions(this, permissions, REQUEST_PERMISSION_CAMERA);
}
}
/**
*
*/
private void openGallery() {
Intent intent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
intent.setType("image/*");
startActivityForResult(intent, REQUEST_GALLERY);
}
/**
*
*/
private void openCamera() {
Intent takePictureIntent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE);
File photoFile = createImageFile();
if (photoFile != null) {
mCurrentPhotoPath = photoFile.getAbsolutePath();
try {
Uri photoURI = FileProvider.getUriForFile(this,
getApplicationContext().getPackageName() + ".fileprovider",
photoFile);
takePictureIntent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, photoURI);
startActivityForResult(takePictureIntent, REQUEST_CAMERA);
} catch (Exception e) {
Log.e(TAG, "Failed to get URI from FileProvider", e);
}
} else {
Log.e(TAG, "Failed to create image file");
}
}
/**
*
* @return null
*/
private File createImageFile() {
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date());
String imageFileName = "JPEG_" + timeStamp + "_";
File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
try {
File image = File.createTempFile(imageFileName, ".jpg", storageDir);
return image;
} catch (IOException e) {
Log.e(TAG, "Failed to create image file", e);
return null;
}
}
/**
*
*/
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
PermissionHelper.handlePermissionResult(requestCode, permissions, grantResults,
new PermissionHelper.OnPermissionResultListener() {
@Override
public void onAllGranted(int requestCode, List<String> grantedPermissions) {
if (requestCode == REQUEST_PERMISSION_STORAGE) {
openGallery();
} else if (requestCode == REQUEST_PERMISSION_CAMERA) {
openCamera();
}
}
@Override
public void onDenied(int requestCode, List<String> grantedPermissions, List<String> deniedPermissions) {
showToast(R.string.permission_denied);
}
});
}
/**
* Activity/
*/
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode != RESULT_OK) {
return;
}
if (requestCode == REQUEST_GALLERY && data != null) {
Uri selectedImage = data.getData();
if (selectedImage != null) {
processSelectedImage(selectedImage, Notes.ATTACHMENT_TYPE_GALLERY);
}
} else if (requestCode == REQUEST_CAMERA) {
if (mCurrentPhotoPath != null) {
File photoFile = new File(mCurrentPhotoPath);
if (photoFile.exists()) {
showCameraPreviewDialog(photoFile);
}
}
}
}
/**
*
* @param imageUri URI
* @param type
*/
private void processSelectedImage(Uri imageUri, int type) {
try {
File sourceFile = new File(imageUri.getPath());
if (!sourceFile.exists()) {
// 如果直接路径不存在尝试通过ContentResolver获取
sourceFile = getFileFromUri(imageUri);
}
if (sourceFile != null && sourceFile.exists()) {
long attachmentId = mWorkingNote.addAttachment(type, sourceFile);
if (attachmentId > 0) {
loadAttachments();
showToast(R.string.image_added);
} else {
showToast(R.string.failed_to_add_image);
}
}
} catch (Exception e) {
Log.e(TAG, "Failed to process image", e);
showToast(R.string.failed_to_add_image);
}
}
/**
* URI
* @param uri URI
* @return null
*/
private File getFileFromUri(Uri uri) {
// 简化实现对于content:// URI直接复制到临时文件
try {
InputStream inputStream = getContentResolver().openInputStream(uri);
if (inputStream == null) return null;
File tempFile = File.createTempFile("temp_image", ".jpg", getCacheDir());
FileOutputStream outputStream = new FileOutputStream(tempFile);
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.close();
inputStream.close();
return tempFile;
} catch (IOException e) {
Log.e(TAG, "Failed to get file from URI", e);
return null;
}
}
/**
*
* @param photoFile
*/
private void showCameraPreviewDialog(File photoFile) {
CameraPreviewDialogFragment dialog = new CameraPreviewDialogFragment();
dialog.setPhotoFile(photoFile);
dialog.setOnCameraPreviewListener(new CameraPreviewDialogFragment.OnCameraPreviewListener() {
@Override
public void onConfirm(File photoFile) {
// 确认使用照片,添加到附件
processSelectedImage(Uri.fromFile(photoFile), Notes.ATTACHMENT_TYPE_CAMERA);
}
@Override
public void onRetake() {
// 重拍,重新打开相机
openCamera();
}
});
dialog.show(getFragmentManager(), CameraPreviewDialogFragment.TAG);
}
}

@ -27,6 +27,7 @@ import android.widget.TextView;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.tool.DataUtils;
import net.micode.notes.tool.ElderModeUtils;
import net.micode.notes.tool.ResourceParser.NoteItemBgResources;
/**
@ -126,6 +127,9 @@ public class NotesListItem extends LinearLayout {
// 根据数据类型和位置设置背景
setBackground(data);
// 在设置完所有内容后应用老年人模式,确保字体大小正确
ElderModeUtils.applyElderMode(context, mTitle);
}
/**

@ -85,7 +85,7 @@ public class NotesPreferenceActivity extends PreferenceActivity {
mReceiver = new GTaskReceiver(); // 创建广播接收器
IntentFilter filter = new IntentFilter();
filter.addAction(GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME); // 注册同步服务广播
registerReceiver(mReceiver, filter); // 注册广播接收器
registerReceiver(mReceiver, filter, Context.RECEIVER_NOT_EXPORTED); // 注册广播接收器,标记为非导出
mOriAccounts = null;
// 添加设置界面的头部视图

Loading…
Cancel
Save