diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
index ed21bae..fe700b6 100644
--- a/src/main/AndroidManifest.xml
+++ b/src/main/AndroidManifest.xml
@@ -112,6 +112,14 @@
+
+ * 负责回收站相关的所有操作,包括: + * - 移动便签到回收站 + * - 从回收站恢复便签 + * - 清空回收站 + * - 永久删除便签 + * - 查询回收站内容 + * - 自动清理过期便签 + *
+ */ +public class TrashManager { + private static final String TAG = "TrashManager"; + + private static final long TRASH_AUTO_CLEANUP_DAYS = 30; // 回收站自动清理天数:30天 + + private Context mContext; + private NotesDatabaseHelper mDatabaseHelper; + + private static TrashManager sInstance; + + private TrashManager(Context context) { + mContext = context.getApplicationContext(); + mDatabaseHelper = NotesDatabaseHelper.getInstance(context); + } + + public static synchronized TrashManager getInstance(Context context) { + if (sInstance == null) { + sInstance = new TrashManager(context); + } + return sInstance; + } + + /** + * 移动便签到回收站 + * @param noteId 便签ID + * @return 是否成功 + * @throws TrashException 当操作失败时抛出异常 + */ + public boolean moveToTrash(long noteId) throws TrashException { + if (noteId <= 0) { + throw new TrashException(TrashException.ErrorType.INVALID_NOTE_ID, noteId); + } + + if (noteId == Notes.ID_ROOT_FOLDER || noteId == Notes.ID_CALL_RECORD_FOLDER) { + throw new TrashException(TrashException.ErrorType.SYSTEM_FOLDER_ERROR, noteId); + } + + if (isInTrash(noteId)) { + throw new TrashException(TrashException.ErrorType.ALREADY_IN_TRASH, noteId); + } + + try { + ContentValues values = new ContentValues(); + values.put(NoteColumns.PARENT_ID, Notes.ID_TRASH_FOLER); + values.put(NoteColumns.LOCAL_MODIFIED, 1); + values.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); + + int result = mContext.getContentResolver().update( + Notes.CONTENT_NOTE_URI, + values, + NoteColumns.ID + "=?", + new String[]{String.valueOf(noteId)}); + + if (result == 0) { + throw new TrashException(TrashException.ErrorType.NOTE_NOT_FOUND, noteId); + } + return true; + } catch (Exception e) { + if (e instanceof TrashException) { + throw (TrashException) e; + } + throw new TrashException(TrashException.ErrorType.DATABASE_ERROR, e); + } + } + + /** + * 批量移动便签到回收站 + * @param noteIds 便签ID集合 + * @return 是否成功 + * @throws TrashException 当操作失败时抛出异常 + */ + public boolean batchMoveToTrash(long[] noteIds) throws TrashException { + if (noteIds == null || noteIds.length == 0) { + return true; + } + + boolean allSuccess = true; + for (long noteId : noteIds) { + try { + moveToTrash(noteId); + } catch (TrashException e) { + Log.e(TAG, "Failed to move note " + noteId + " to trash: " + e.getMessage()); + allSuccess = false; + } + } + return allSuccess; + } + + /** + * 从回收站恢复便签 + * @param noteId 便签ID + * @return 是否成功 + * @throws TrashException 当操作失败时抛出异常 + */ + public boolean restoreFromTrash(long noteId) throws TrashException { + if (noteId <= 0) { + throw new TrashException(TrashException.ErrorType.INVALID_NOTE_ID, noteId); + } + + if (!isInTrash(noteId)) { + throw new TrashException(TrashException.ErrorType.NOT_IN_TRASH, noteId); + } + + try { + Cursor cursor = mContext.getContentResolver().query( + Notes.CONTENT_NOTE_URI, + new String[]{NoteColumns.ORIGIN_PARENT_ID}, + NoteColumns.ID + "=?", + new String[]{String.valueOf(noteId)}, + null); + + long targetParentId = Notes.ID_ROOT_FOLDER; + if (cursor != null && cursor.moveToFirst()) { + targetParentId = cursor.getLong(0); + if (targetParentId == 0 || targetParentId == Notes.ID_TRASH_FOLER) { + targetParentId = Notes.ID_ROOT_FOLDER; + } + cursor.close(); + } + + ContentValues values = new ContentValues(); + values.put(NoteColumns.PARENT_ID, targetParentId); + values.put(NoteColumns.LOCAL_MODIFIED, 1); + values.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); + + int result = mContext.getContentResolver().update( + Notes.CONTENT_NOTE_URI, + values, + NoteColumns.ID + "=?", + new String[]{String.valueOf(noteId)}); + + if (result == 0) { + throw new TrashException(TrashException.ErrorType.NOTE_NOT_FOUND, noteId); + } + return true; + } catch (Exception e) { + if (e instanceof TrashException) { + throw (TrashException) e; + } + throw new TrashException(TrashException.ErrorType.DATABASE_ERROR, e); + } + } + + /** + * 批量从回收站恢复便签 + * @param noteIds 便签ID集合 + * @return 是否成功 + * @throws TrashException 当操作失败时抛出异常 + */ + public boolean batchRestoreFromTrash(long[] noteIds) throws TrashException { + if (noteIds == null || noteIds.length == 0) { + return true; + } + + boolean allSuccess = true; + for (long noteId : noteIds) { + try { + restoreFromTrash(noteId); + } catch (TrashException e) { + Log.e(TAG, "Failed to restore note " + noteId + ": " + e.getMessage()); + allSuccess = false; + } + } + return allSuccess; + } + + /** + * 永久删除便签(从回收站彻底删除) + * @param noteId 便签ID + * @return 是否成功 + * @throws TrashException 当操作失败时抛出异常 + */ + public boolean permanentlyDelete(long noteId) throws TrashException { + if (noteId <= 0) { + throw new TrashException(TrashException.ErrorType.INVALID_NOTE_ID, noteId); + } + + try { + int result = mContext.getContentResolver().delete( + Notes.CONTENT_NOTE_URI, + NoteColumns.ID + "=?", + new String[]{String.valueOf(noteId)}); + + if (result == 0) { + throw new TrashException(TrashException.ErrorType.NOTE_NOT_FOUND, noteId); + } + return true; + } catch (Exception e) { + if (e instanceof TrashException) { + throw (TrashException) e; + } + throw new TrashException(TrashException.ErrorType.DATABASE_ERROR, e); + } + } + + /** + * 批量永久删除便签 + * @param noteIds 便签ID集合 + * @return 是否成功 + * @throws TrashException 当操作失败时抛出异常 + */ + public boolean batchPermanentlyDelete(long[] noteIds) throws TrashException { + if (noteIds == null || noteIds.length == 0) { + return true; + } + + boolean allSuccess = true; + for (long noteId : noteIds) { + try { + permanentlyDelete(noteId); + } catch (TrashException e) { + Log.e(TAG, "Failed to permanently delete note " + noteId + ": " + e.getMessage()); + allSuccess = false; + } + } + return allSuccess; + } + + /** + * 清空回收站 + * @return 是否成功 + * @throws TrashException 当操作失败时抛出异常 + */ + public boolean emptyTrash() throws TrashException { + try { + int result = mContext.getContentResolver().delete( + Notes.CONTENT_NOTE_URI, + NoteColumns.PARENT_ID + "=?", + new String[]{String.valueOf(Notes.ID_TRASH_FOLER)}); + + if (result == 0) { + throw new TrashException(TrashException.ErrorType.EMPTY_TRASH); + } + + Log.d(TAG, "Empty trash: deleted " + result + " notes"); + return true; + } catch (Exception e) { + if (e instanceof TrashException) { + throw (TrashException) e; + } + throw new TrashException(TrashException.ErrorType.DATABASE_ERROR, e); + } + } + + /** + * 获取回收站中的便签数量 + * @return 便签数量 + */ + public int getTrashCount() { + Cursor cursor = null; + try { + cursor = mContext.getContentResolver().query( + Notes.CONTENT_NOTE_URI, + new String[]{"COUNT(*)"}, + NoteColumns.PARENT_ID + "=?", + new String[]{String.valueOf(Notes.ID_TRASH_FOLER)}, + null); + + if (cursor != null && cursor.moveToFirst()) { + return cursor.getInt(0); + } + } catch (Exception e) { + Log.e(TAG, "Get trash count failed: " + e.getMessage()); + } finally { + if (cursor != null) { + cursor.close(); + } + } + return 0; + } + + /** + * 检查便签是否在回收站中 + * @param noteId 便签ID + * @return 是否在回收站中 + */ + public boolean isInTrash(long noteId) { + if (noteId <= 0) { + return false; + } + + Cursor cursor = null; + try { + cursor = mContext.getContentResolver().query( + Notes.CONTENT_NOTE_URI, + new String[]{NoteColumns.PARENT_ID}, + NoteColumns.ID + "=?", + new String[]{String.valueOf(noteId)}, + null); + + if (cursor != null && cursor.moveToFirst()) { + long parentId = cursor.getLong(0); + return parentId == Notes.ID_TRASH_FOLER; + } + } catch (Exception e) { + Log.e(TAG, "Check if note in trash failed: " + e.getMessage()); + } finally { + if (cursor != null) { + cursor.close(); + } + } + return false; + } + + /** + * 获取回收站中便签的原始父文件夹ID + * @param noteId 便签ID + * @return 原始父文件夹ID,如果不在回收站返回-1 + */ + public long getOriginParentId(long noteId) { + if (noteId <= 0) { + return -1; + } + + Cursor cursor = null; + try { + cursor = mContext.getContentResolver().query( + Notes.CONTENT_NOTE_URI, + new String[]{NoteColumns.ORIGIN_PARENT_ID}, + NoteColumns.ID + "=?", + new String[]{String.valueOf(noteId)}, + null); + + if (cursor != null && cursor.moveToFirst()) { + return cursor.getLong(0); + } + } catch (Exception e) { + Log.e(TAG, "Get origin parent id failed: " + e.getMessage()); + } finally { + if (cursor != null) { + cursor.close(); + } + } + return -1; + } + + /** + * 自动清理回收站中过期的便签 + * 删除在回收站中超过30天的便签 + * @return 删除的便签数量 + */ + public int autoCleanupExpiredNotes() { + long expireTime = System.currentTimeMillis() - (TRASH_AUTO_CLEANUP_DAYS * 24 * 60 * 60 * 1000L); + + try { + int result = mContext.getContentResolver().delete( + Notes.CONTENT_NOTE_URI, + NoteColumns.PARENT_ID + "=? AND " + NoteColumns.MODIFIED_DATE + "", + new String[]{ + String.valueOf(Notes.ID_TRASH_FOLER), + String.valueOf(expireTime) + }); + + if (result > 0) { + Log.d(TAG, "Auto cleanup: deleted " + result + " expired notes from trash"); + } + return result; + } catch (Exception e) { + Log.e(TAG, "Auto cleanup failed: " + e.getMessage()); + return 0; + } + } + + /** + * 获取回收站中过期的便签数量 + * @return 过期便签数量 + */ + public int getExpiredNotesCount() { + long expireTime = System.currentTimeMillis() - (TRASH_AUTO_CLEANUP_DAYS * 24 * 60 * 60 * 1000L); + + Cursor cursor = null; + try { + cursor = mContext.getContentResolver().query( + Notes.CONTENT_NOTE_URI, + new String[]{"COUNT(*)"}, + NoteColumns.PARENT_ID + "=? AND " + NoteColumns.MODIFIED_DATE + "", + new String[]{ + String.valueOf(Notes.ID_TRASH_FOLER), + String.valueOf(expireTime) + }, + null); + + if (cursor != null && cursor.moveToFirst()) { + return cursor.getInt(0); + } + } catch (Exception e) { + Log.e(TAG, "Get expired notes count failed: " + e.getMessage()); + } finally { + if (cursor != null) { + cursor.close(); + } + } + return 0; + } +} diff --git a/src/main/java/net/micode/notes/ui/NotesListActivity.java b/src/main/java/net/micode/notes/ui/NotesListActivity.java index 3f63999..65aa7f5 100644 --- a/src/main/java/net/micode/notes/ui/NotesListActivity.java +++ b/src/main/java/net/micode/notes/ui/NotesListActivity.java @@ -529,20 +529,10 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt new AsyncTask+ * 显示回收站中的所有便签,支持恢复、永久删除和清空回收站操作 + *
+ */ +public class TrashActivity extends Activity { + private static final String TAG = "TrashActivity"; + + private static final int TRASH_LIST_QUERY_TOKEN = 0; + + private ListView mTrashListView; + private NotesListAdapter mTrashListAdapter; + private BackgroundQueryHandler mBackgroundQueryHandler; + private ContentResolver mContentResolver; + private TrashManager mTrashManager; + private ModeCallback mModeCallback; + private TextView mEmptyView; + private TextView mTitleBar; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.trash_list); + + initResources(); + startAsyncTrashQuery(); + } + + private void initResources() { + mContentResolver = getContentResolver(); + mBackgroundQueryHandler = new BackgroundQueryHandler(mContentResolver); + mTrashManager = TrashManager.getInstance(this); + mModeCallback = new ModeCallback(); + + mTrashListView = (ListView) findViewById(R.id.trash_list); + mEmptyView = (TextView) findViewById(R.id.empty_view); + mTitleBar = (TextView) findViewById(R.id.tv_title_bar); + + mTrashListView.setEmptyView(mEmptyView); + mTrashListView.setOnItemClickListener(new OnTrashItemClickListener()); + mTrashListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL); + mTrashListView.setMultiChoiceModeListener(mModeCallback); + + mTrashListAdapter = new NotesListAdapter(this); + mTrashListView.setAdapter(mTrashListAdapter); + + mTitleBar.setText(R.string.trash_folder_name); + } + + private void startAsyncTrashQuery() { + String selection = NoteColumns.PARENT_ID + "=?"; + mBackgroundQueryHandler.startQuery( + TRASH_LIST_QUERY_TOKEN, + null, + Notes.CONTENT_NOTE_URI, + NoteItemData.PROJECTION, + selection, + new String[]{String.valueOf(Notes.ID_TRASH_FOLER)}, + NoteColumns.MODIFIED_DATE + " DESC"); + } + + private class BackgroundQueryHandler extends AsyncQueryHandler { + public BackgroundQueryHandler(ContentResolver contentResolver) { + super(contentResolver); + } + + @Override + protected void onQueryComplete(int token, Object cookie, Cursor cursor) { + if (token == TRASH_LIST_QUERY_TOKEN) { + mTrashListAdapter.changeCursor(cursor); + updateEmptyView(); + } + } + } + + private void updateEmptyView() { + int count = mTrashManager.getTrashCount(); + if (count == 0) { + mEmptyView.setText(R.string.trash_empty); + mEmptyView.setVisibility(View.VISIBLE); + } else { + mEmptyView.setVisibility(View.GONE); + } + } + + private class OnTrashItemClickListener implements OnItemClickListener { + @Override + public void onItemClick(AdapterView> parent, View view, int position, long id) { + if (view instanceof NotesListItem) { + NoteItemData item = ((NotesListItem) view).getItemData(); + if (mTrashListAdapter.isInChoiceMode()) { + if (item.getType() == Notes.TYPE_NOTE) { + position = position - mTrashListView.getHeaderViewsCount(); + mModeCallback.onItemCheckedStateChanged(null, position, id, + !mTrashListAdapter.isSelectedItem(position)); + } + } else { + showNoteDetailDialog(item); + } + } + } + } + + private void showNoteDetailDialog(final NoteItemData item) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(item.getSnippet()); + builder.setMessage(item.getSnippet()); + + builder.setPositiveButton(R.string.menu_restore, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + restoreNote(item.getId()); + } + }); + + builder.setNegativeButton(R.string.menu_permanently_delete, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + permanentlyDeleteNote(item.getId()); + } + }); + + builder.setNeutralButton(android.R.string.cancel, null); + builder.show(); + } + + private void restoreNote(long noteId) { + try { + mTrashManager.restoreFromTrash(noteId); + Toast.makeText(this, R.string.toast_restore_success, Toast.LENGTH_SHORT).show(); + startAsyncTrashQuery(); + } catch (TrashException e) { + Toast.makeText(this, getErrorMessage(e), Toast.LENGTH_SHORT).show(); + } + } + + private void permanentlyDeleteNote(long noteId) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.alert_title_permanently_delete); + builder.setMessage(R.string.alert_message_permanently_delete); + builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + try { + mTrashManager.permanentlyDelete(noteId); + Toast.makeText(TrashActivity.this, R.string.toast_delete_success, Toast.LENGTH_SHORT).show(); + startAsyncTrashQuery(); + } catch (TrashException e) { + Toast.makeText(TrashActivity.this, getErrorMessage(e), Toast.LENGTH_SHORT).show(); + } + } + }); + builder.setNegativeButton(android.R.string.cancel, null); + builder.show(); + } + + private class ModeCallback implements ListView.MultiChoiceModeListener { + private ActionMode mActionMode; + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + getMenuInflater().inflate(R.menu.trash_options, menu); + mActionMode = mode; + mTrashListAdapter.setChoiceMode(true); + return true; + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + int selectedCount = mTrashListAdapter.getSelectedCount(); + if (selectedCount == 0) { + Toast.makeText(TrashActivity.this, R.string.menu_select_none, Toast.LENGTH_SHORT).show(); + return true; + } + + int itemId = item.getItemId(); + if (itemId == R.id.menu_restore) { + restoreSelectedNotes(); + } else if (itemId == R.id.menu_permanently_delete) { + permanentlyDeleteSelectedNotes(); + } else if (itemId == R.id.menu_empty_trash) { + emptyTrash(); + } + return true; + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + mTrashListAdapter.setChoiceMode(false); + mActionMode = null; + } + + @Override + public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) { + mTrashListAdapter.setCheckedItem(position, checked); + if (mActionMode != null) { + int count = mTrashListAdapter.getSelectedCount(); + mActionMode.setTitle(getResources().getString(R.string.menu_select_title, count)); + } + } + + public void finishActionMode() { + if (mActionMode != null) { + mActionMode.finish(); + } + } + } + + private void restoreSelectedNotes() { + long[] selectedIds = getSelectedNoteIds(); + if (selectedIds.length > 0) { + try { + mTrashManager.batchRestoreFromTrash(selectedIds); + Toast.makeText(this, getString(R.string.toast_restore_multiple, selectedIds.length), + Toast.LENGTH_SHORT).show(); + mModeCallback.finishActionMode(); + startAsyncTrashQuery(); + } catch (TrashException e) { + Toast.makeText(this, getErrorMessage(e), Toast.LENGTH_SHORT).show(); + } + } + } + + private void permanentlyDeleteSelectedNotes() { + final long[] selectedIds = getSelectedNoteIds(); + if (selectedIds.length > 0) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.alert_title_permanently_delete); + builder.setMessage(getString(R.string.alert_message_permanently_delete_multiple, selectedIds.length)); + builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + try { + mTrashManager.batchPermanentlyDelete(selectedIds); + Toast.makeText(TrashActivity.this, + getString(R.string.toast_delete_multiple, selectedIds.length), + Toast.LENGTH_SHORT).show(); + mModeCallback.finishActionMode(); + startAsyncTrashQuery(); + } catch (TrashException e) { + Toast.makeText(TrashActivity.this, getErrorMessage(e), Toast.LENGTH_SHORT).show(); + } + } + }); + builder.setNegativeButton(android.R.string.cancel, null); + builder.show(); + } + } + + private void emptyTrash() { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.alert_title_empty_trash); + builder.setMessage(R.string.alert_message_empty_trash); + builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + try { + mTrashManager.emptyTrash(); + Toast.makeText(TrashActivity.this, R.string.toast_empty_trash_success, + Toast.LENGTH_SHORT).show(); + startAsyncTrashQuery(); + } catch (TrashException e) { + Toast.makeText(TrashActivity.this, getErrorMessage(e), Toast.LENGTH_SHORT).show(); + } + } + }); + builder.setNegativeButton(android.R.string.cancel, null); + builder.show(); + } + + private long[] getSelectedNoteIds() { + java.util.HashSet+ * 在应用启动时自动清理回收站中过期的便签(超过30天) + *
+ */ +public class TrashCleanupReceiver extends BroadcastReceiver { + private static final String TAG = "TrashCleanupReceiver"; + + @Override + public void onReceive(Context context, Intent intent) { + if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) { + Log.d(TAG, "Boot completed, starting trash cleanup"); + + try { + TrashManager trashManager = TrashManager.getInstance(context); + int deletedCount = trashManager.autoCleanupExpiredNotes(); + + if (deletedCount > 0) { + Log.d(TAG, "Auto cleanup: deleted " + deletedCount + " expired notes"); + } else { + Log.d(TAG, "Auto cleanup: no expired notes found"); + } + } catch (Exception e) { + Log.e(TAG, "Auto cleanup failed: " + e.getMessage(), e); + } + } + } +} diff --git a/src/main/res/layout/trash_list.xml b/src/main/res/layout/trash_list.xml new file mode 100644 index 0000000..4db8bd0 --- /dev/null +++ b/src/main/res/layout/trash_list.xml @@ -0,0 +1,45 @@ + +