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 @@ + + + + + + @@ -147,10 +155,10 @@ + android:name=".ui.TrashActivity" + android:label="@string/trash_folder_name" + android:launchMode="singleTop" + android:theme="@android:style/Theme.Holo.Light" > + * 定义回收站操作过程中可能出现的各种异常 + *

+ */ +public class TrashException extends Exception { + private static final String TAG = "TrashException"; + + /** + * 异常类型枚举 + */ + public enum ErrorType { + INVALID_NOTE_ID("Invalid note ID"), + SYSTEM_FOLDER_ERROR("Cannot operate on system folder"), + NOTE_NOT_FOUND("Note not found"), + DATABASE_ERROR("Database operation failed"), + ALREADY_IN_TRASH("Note is already in trash"), + NOT_IN_TRASH("Note is not in trash"), + EMPTY_TRASH("Trash is already empty"), + UNKNOWN_ERROR("Unknown error occurred"); + + private final String message; + + ErrorType(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + } + + private ErrorType errorType; + private long noteId; + + public TrashException(ErrorType errorType) { + super(errorType.getMessage()); + this.errorType = errorType; + this.noteId = -1; + Log.e(TAG, "TrashException: " + errorType.getMessage()); + } + + public TrashException(ErrorType errorType, long noteId) { + super(errorType.getMessage() + " (ID: " + noteId + ")"); + this.errorType = errorType; + this.noteId = noteId; + Log.e(TAG, "TrashException: " + errorType.getMessage() + " (ID: " + noteId + ")"); + } + + public TrashException(ErrorType errorType, Throwable cause) { + super(errorType.getMessage(), cause); + this.errorType = errorType; + this.noteId = -1; + Log.e(TAG, "TrashException: " + errorType.getMessage(), cause); + } + + public ErrorType getErrorType() { + return errorType; + } + + public long getNoteId() { + return noteId; + } +} diff --git a/src/main/java/net/micode/notes/data/TrashManager.java b/src/main/java/net/micode/notes/data/TrashManager.java new file mode 100644 index 0000000..6c15ba4 --- /dev/null +++ b/src/main/java/net/micode/notes/data/TrashManager.java @@ -0,0 +1,433 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.data; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.util.Log; + +import net.micode.notes.data.Notes.NoteColumns; + +/** + * 回收站管理类 + *

+ * 负责回收站相关的所有操作,包括: + * - 移动便签到回收站 + * - 从回收站恢复便签 + * - 清空回收站 + * - 永久删除便签 + * - 查询回收站内容 + * - 自动清理过期便签 + *

+ */ +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 + " 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 + ">() { protected HashSet doInBackground(Void... unused) { HashSet widgets = mNotesListAdapter.getSelectedWidget(); - if (!isSyncMode()) { - // if not synced, delete notes directly - if (DataUtils.batchDeleteNotes(mContentResolver, mNotesListAdapter - .getSelectedItemIds())) { - } else { - Log.e(TAG, "Delete notes error, should not happens"); - } - } else { - // in sync mode, we'll move the deleted note into the trash - // folder - if (!DataUtils.batchMoveToFolder(mContentResolver, mNotesListAdapter - .getSelectedItemIds(), Notes.ID_TRASH_FOLER)) { - Log.e(TAG, "Move notes to trash folder error, should not happens"); - } + // Always move notes to trash folder for better user experience + if (!DataUtils.batchMoveToFolder(mContentResolver, mNotesListAdapter + .getSelectedItemIds(), Notes.ID_TRASH_FOLER)) { + Log.e(TAG, "Move notes to trash folder error, should not happens"); } return widgets; } @@ -567,18 +557,15 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt Log.e(TAG, "Wrong folder id, should not happen " + folderId); return; } - + HashSet ids = new HashSet(); ids.add(folderId); HashSet widgets = DataUtils.getFolderNoteWidget(mContentResolver, folderId); - if (!isSyncMode()) { - // if not synced, delete folder directly - DataUtils.batchDeleteNotes(mContentResolver, ids); - } else { - // in sync mode, we'll move the deleted folder into the trash folder - DataUtils.batchMoveToFolder(mContentResolver, ids, Notes.ID_TRASH_FOLER); - } + + // Always move folder to trash folder for better user experience + DataUtils.batchMoveToFolder(mContentResolver, ids, Notes.ID_TRASH_FOLER); + if (widgets != null) { for (AppWidgetAttribute widget : widgets) { if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID @@ -858,9 +845,16 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt createNewNote(); } else if (item.getItemId() == R.id.menu_search) { onSearchRequested(); + } else if (item.getItemId() == R.id.menu_trash) { + startTrashActivity(); } return true; } + + private void startTrashActivity() { + Intent intent = new Intent(this, TrashActivity.class); + startActivity(intent); + } @Override public boolean onSearchRequested() { diff --git a/src/main/java/net/micode/notes/ui/TrashActivity.java b/src/main/java/net/micode/notes/ui/TrashActivity.java new file mode 100644 index 0000000..35bb8a3 --- /dev/null +++ b/src/main/java/net/micode/notes/ui/TrashActivity.java @@ -0,0 +1,355 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.AsyncQueryHandler; +import android.content.ContentResolver; +import android.content.DialogInterface; +import android.database.Cursor; +import android.os.Bundle; +import android.util.Log; +import android.view.ActionMode; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.data.TrashException; +import net.micode.notes.data.TrashManager; + +/** + * 回收站Activity + *

+ * 显示回收站中的所有便签,支持恢复、永久删除和清空回收站操作 + *

+ */ +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 selectedIds = mTrashListAdapter.getSelectedItemIds(); + long[] ids = new long[selectedIds.size()]; + int i = 0; + for (Long id : selectedIds) { + ids[i++] = id; + } + return ids; + } + + private String getErrorMessage(TrashException e) { + switch (e.getErrorType()) { + case INVALID_NOTE_ID: + return getString(R.string.error_note_not_exist); + case NOT_IN_TRASH: + return getString(R.string.toast_restore_failed); + case NOTE_NOT_FOUND: + return getString(R.string.error_note_not_exist); + case DATABASE_ERROR: + return getString(R.string.toast_restore_failed); + default: + return getString(R.string.toast_restore_failed); + } + } + + @Override + protected void onResume() { + super.onResume(); + startAsyncTrashQuery(); + } + + @Override + public void onBackPressed() { + super.onBackPressed(); + finish(); + } +} diff --git a/src/main/java/net/micode/notes/ui/TrashCleanupReceiver.java b/src/main/java/net/micode/notes/ui/TrashCleanupReceiver.java new file mode 100644 index 0000000..8979392 --- /dev/null +++ b/src/main/java/net/micode/notes/ui/TrashCleanupReceiver.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import net.micode.notes.data.TrashManager; + +/** + * 回收站自动清理广播接收器 + *

+ * 在应用启动时自动清理回收站中过期的便签(超过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 @@ + + + + + + + + + + + + + + diff --git a/src/main/res/menu/note_list.xml b/src/main/res/menu/note_list.xml index 42ea736..a2e8b6e 100644 --- a/src/main/res/menu/note_list.xml +++ b/src/main/res/menu/note_list.xml @@ -36,4 +36,8 @@ + + diff --git a/src/main/res/menu/trash_options.xml b/src/main/res/menu/trash_options.xml new file mode 100644 index 0000000..a8a6b5e --- /dev/null +++ b/src/main/res/menu/trash_options.xml @@ -0,0 +1,22 @@ + + + + + + + + + + diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 6eb7e88..2d1f2a6 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -177,5 +177,26 @@ %1$s results for \"%2$s\" + + + Trash + Trash is empty + Restore + Permanently delete + Empty trash + Trash + Permanently delete + Are you sure you want to permanently delete this note? + Are you sure you want to permanently delete %d notes? + Empty trash + Are you sure you want to empty trash? All notes will be permanently deleted. + Note restored + Failed to restore + Restored %d notes + Note permanently deleted + Failed to delete + Permanently deleted %d notes + Trash emptied + Failed to empty trash