diff --git a/src/ui/MemoryBottleDialog.java b/src/ui/MemoryBottleDialog.java new file mode 100644 index 0000000..897b312 --- /dev/null +++ b/src/ui/MemoryBottleDialog.java @@ -0,0 +1,569 @@ +/* + * 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.app.Dialog; +import android.appwidget.AppWidgetManager; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.os.AsyncTask; +import android.preference.PreferenceManager; +import android.text.InputType; +import android.text.TextUtils; +import android.view.Gravity; +import android.view.View; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.EditText; +import android.widget.Toast; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.data.Notes.TextNote; +import net.micode.notes.model.WorkingNote; +import net.micode.notes.tool.DataUtils; +import net.micode.notes.tool.ResourceParser; + +import java.lang.ref.WeakReference; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.Random; +import java.util.HashSet; + +public class MemoryBottleDialog extends Dialog implements View.OnClickListener { + private static final String PREF_MEMORY_FOLDER_ID = "pref_memory_bottle_folder_id"; + private static final List sAllEntries = new ArrayList<>(); + private static final List sRemainingEntries = new ArrayList<>(); + private static final Random sRandom = new Random(); + private static long sFolderId = Long.MIN_VALUE; + + private final Activity mActivity; + private Button mAddButton; + private Button mBrowseButton; + private long mMemoryFolderId = -1; + private boolean mEntriesLoaded; + private boolean mLoading; + private boolean mBrowseLoading; + private LoadTask mLoadTask; + private BrowseTask mBrowseTask; + private PendingAction mPendingAction = PendingAction.NONE; + + public MemoryBottleDialog(Activity activity) { + super(activity, android.R.style.Theme_Light_NoTitleBar); + mActivity = activity; + } + + @Override + protected void onCreate(android.os.Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.memory_bottle); + getWindow().setLayout(WindowManager.LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.MATCH_PARENT); + initResources(); + } + + @Override + public void dismiss() { + if (mLoadTask != null) { + mLoadTask.cancel(true); + mLoadTask = null; + } + if (mBrowseTask != null) { + mBrowseTask.cancel(true); + mBrowseTask = null; + } + super.dismiss(); + } + + private void initResources() { + mAddButton = (Button) findViewById(R.id.btn_memory_add); + mBrowseButton = (Button) findViewById(R.id.btn_memory_browse); + mAddButton.setOnClickListener(this); + mBrowseButton.setOnClickListener(this); + updateButtonState(); + } + + @Override + public void onClick(View v) { + switch (v.getId()) { + case R.id.btn_memory_add: + requestAction(PendingAction.ADD); + break; + case R.id.btn_memory_browse: + requestAction(PendingAction.BROWSE); + break; + default: + break; + } + } + + private void requestAction(PendingAction action) { + if (mLoading || mBrowseLoading) { + Toast.makeText(mActivity, R.string.memory_bottle_loading, Toast.LENGTH_SHORT).show(); + return; + } + if (mMemoryFolderId <= 0) { + mPendingAction = action; + Toast.makeText(mActivity, R.string.memory_bottle_loading, Toast.LENGTH_SHORT).show(); + startLoadTask(action == PendingAction.BROWSE); + return; + } + if (action == PendingAction.BROWSE && !mEntriesLoaded) { + mPendingAction = action; + Toast.makeText(mActivity, R.string.memory_bottle_loading, Toast.LENGTH_SHORT).show(); + startLoadTask(true); + return; + } + if (action == PendingAction.ADD) { + showAddDialog(); + } else if (action == PendingAction.BROWSE) { + browseMemory(); + } + } + + private void startLoadTask(boolean loadEntries) { + if (mLoadTask != null) { + return; + } + setLoading(true); + mLoadTask = new LoadTask(this, loadEntries); + mLoadTask.execute(); + } + + private void setLoading(boolean loading) { + mLoading = loading; + updateButtonState(); + } + + private void setBrowseLoading(boolean loading) { + mBrowseLoading = loading; + updateButtonState(); + } + + private void updateButtonState() { + boolean enabled = !(mLoading || mBrowseLoading); + if (mAddButton != null) { + mAddButton.setEnabled(enabled); + } + if (mBrowseButton != null) { + mBrowseButton.setEnabled(enabled); + } + } + + private void showAddDialog() { + final EditText editText = new EditText(mActivity); + int padding = (int) (mActivity.getResources().getDisplayMetrics().density * 16); + editText.setPadding(padding, padding, padding, padding); + editText.setGravity(Gravity.TOP | Gravity.START); + editText.setMinLines(4); + editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE); + editText.setHint(R.string.memory_bottle_add_hint); + + AlertDialog.Builder builder = new AlertDialog.Builder(mActivity); + builder.setTitle(R.string.memory_bottle_add_title); + builder.setView(editText); + builder.setPositiveButton(android.R.string.ok, null); + builder.setNegativeButton(android.R.string.cancel, null); + final AlertDialog dialog = builder.show(); + dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + String content = editText.getText().toString().trim(); + if (TextUtils.isEmpty(content)) { + Toast.makeText(mActivity, R.string.memory_bottle_empty_input, + Toast.LENGTH_SHORT).show(); + return; + } + if (createMemoryNote(content)) { + dialog.dismiss(); + } + } + }); + } + + private boolean createMemoryNote(String content) { + if (mMemoryFolderId <= 0) { + Toast.makeText(mActivity, R.string.memory_bottle_folder_error, Toast.LENGTH_SHORT).show(); + return false; + } + WorkingNote note = WorkingNote.createEmptyNote(mActivity, mMemoryFolderId, + AppWidgetManager.INVALID_APPWIDGET_ID, Notes.TYPE_WIDGET_INVALIDE, + ResourceParser.getDefaultBgId(mActivity)); + note.setWorkingText(content); + if (!note.saveNote()) { + Toast.makeText(mActivity, R.string.memory_bottle_save_failed, Toast.LENGTH_SHORT).show(); + return false; + } + long noteId = note.getNoteId(); + long createdDate = queryNoteCreatedDate(noteId); + MemoryEntry entry = new MemoryEntry(noteId, createdDate, content); + sAllEntries.add(entry); + sRemainingEntries.add(entry); + mEntriesLoaded = true; + Toast.makeText(mActivity, R.string.memory_bottle_save_success, Toast.LENGTH_SHORT).show(); + return true; + } + + private long queryNoteCreatedDate(long noteId) { + Cursor cursor = mActivity.getContentResolver().query( + ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), + new String[] { NoteColumns.CREATED_DATE }, + null, null, null); + if (cursor == null) { + return System.currentTimeMillis(); + } + try { + if (cursor.moveToFirst()) { + return cursor.getLong(0); + } + } finally { + cursor.close(); + } + return System.currentTimeMillis(); + } + + private void browseMemory() { + if (sAllEntries.isEmpty()) { + Toast.makeText(mActivity, R.string.memory_bottle_empty, Toast.LENGTH_SHORT).show(); + return; + } + if (sRemainingEntries.isEmpty()) { + showBrowseFinishedDialog(); + return; + } + showRandomEntry(); + } + + private void showBrowseFinishedDialog() { + new AlertDialog.Builder(mActivity) + .setMessage(R.string.memory_bottle_browse_done) + .setPositiveButton(R.string.memory_bottle_restart, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + resetRemainingEntries(); + showRandomEntry(); + } + }) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } + + private void resetRemainingEntries() { + sRemainingEntries.clear(); + sRemainingEntries.addAll(sAllEntries); + } + + private void showRandomEntry() { + int index = sRandom.nextInt(sRemainingEntries.size()); + MemoryEntry entry = sRemainingEntries.remove(index); + startBrowseTask(entry); + } + + private void startBrowseTask(MemoryEntry entry) { + if (mBrowseTask != null) { + return; + } + setBrowseLoading(true); + mBrowseTask = new BrowseTask(this, entry); + mBrowseTask.execute(); + } + + private String formatEntryMessage(long createdDate, String content) { + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()); + String date = format.format(new Date(createdDate)); + return mActivity.getString(R.string.memory_bottle_entry_format, date, content); + } + + private long ensureMemoryFolder() { + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mActivity); + long storedId = sp.getLong(PREF_MEMORY_FOLDER_ID, Long.MIN_VALUE); + if (storedId > 0 && DataUtils.visibleInNoteDatabase(mActivity.getContentResolver(), + storedId, Notes.TYPE_FOLDER)) { + return storedId; + } + String folderName = mActivity.getString(R.string.memory_bottle_folder_name); + long folderId = queryFolderIdByName(folderName); + if (folderId > 0) { + sp.edit().putLong(PREF_MEMORY_FOLDER_ID, folderId).commit(); + return folderId; + } + folderId = createMemoryFolder(folderName); + if (folderId > 0) { + sp.edit().putLong(PREF_MEMORY_FOLDER_ID, folderId).commit(); + return folderId; + } + return -1; + } + + private long queryFolderIdByName(String name) { + ContentResolver resolver = mActivity.getContentResolver(); + Cursor cursor = resolver.query( + Notes.CONTENT_NOTE_URI, + new String[] { NoteColumns.ID }, + NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>?" + + " AND " + NoteColumns.SNIPPET + "=?", + new String[] { String.valueOf(Notes.TYPE_FOLDER), + String.valueOf(Notes.ID_TRASH_FOLER), name }, + null); + if (cursor == null) { + return 0; + } + try { + if (cursor.moveToFirst()) { + return cursor.getLong(0); + } + } finally { + cursor.close(); + } + return 0; + } + + private long createMemoryFolder(String name) { + ContentValues values = new ContentValues(); + values.put(NoteColumns.SNIPPET, name); + values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); + values.put(NoteColumns.PARENT_ID, Notes.ID_ROOT_FOLDER); + values.put(NoteColumns.LOCAL_MODIFIED, 1); + android.net.Uri uri = mActivity.getContentResolver().insert(Notes.CONTENT_NOTE_URI, values); + if (uri == null) { + return 0; + } + try { + return Long.parseLong(uri.getPathSegments().get(1)); + } catch (NumberFormatException e) { + return 0; + } + } + + private String queryNoteContent(ContentResolver resolver, long noteId) { + Cursor cursor = resolver.query( + Notes.CONTENT_DATA_URI, + new String[] { DataColumns.CONTENT }, + DataColumns.NOTE_ID + "=? AND " + DataColumns.MIME_TYPE + "=?", + new String[] { String.valueOf(noteId), TextNote.CONTENT_ITEM_TYPE }, + null); + if (cursor == null) { + return ""; + } + try { + if (cursor.moveToFirst()) { + return cursor.getString(0); + } + } finally { + cursor.close(); + } + return ""; + } + + private List loadEntriesFromDatabase(long folderId) { + List entries = new ArrayList<>(); + ContentResolver resolver = mActivity.getContentResolver(); + Cursor cursor = resolver.query( + Notes.CONTENT_NOTE_URI, + new String[] { NoteColumns.ID, NoteColumns.CREATED_DATE }, + NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "=?", + new String[] { String.valueOf(Notes.TYPE_NOTE), String.valueOf(folderId) }, + NoteColumns.CREATED_DATE + " DESC"); + if (cursor == null) { + return entries; + } + try { + while (cursor.moveToNext()) { + long noteId = cursor.getLong(0); + long createdDate = cursor.getLong(1); + entries.add(new MemoryEntry(noteId, createdDate, "")); + } + } finally { + cursor.close(); + } + return entries; + } + + private static final class MemoryEntry { + private final long id; + private final long createdDate; + private final String content; + + private MemoryEntry(long id, long createdDate, String content) { + this.id = id; + this.createdDate = createdDate; + this.content = content; + } + } + + private static final class LoadResult { + private final long folderId; + private final List entries; + private final boolean loadedEntries; + + private LoadResult(long folderId, List entries, boolean loadedEntries) { + this.folderId = folderId; + this.entries = entries; + this.loadedEntries = loadedEntries; + } + } + + private static final class LoadTask extends AsyncTask { + private final WeakReference mRef; + private final boolean mLoadEntries; + + private LoadTask(MemoryBottleDialog dialog, boolean loadEntries) { + mRef = new WeakReference<>(dialog); + mLoadEntries = loadEntries; + } + + @Override + protected LoadResult doInBackground(Void... params) { + MemoryBottleDialog dialog = mRef.get(); + if (dialog == null) { + return null; + } + long folderId = dialog.ensureMemoryFolder(); + List entries = new ArrayList<>(); + if (folderId > 0 && mLoadEntries) { + entries = dialog.loadEntriesFromDatabase(folderId); + } + return new LoadResult(folderId, entries, mLoadEntries); + } + + @Override + protected void onPostExecute(LoadResult result) { + MemoryBottleDialog dialog = mRef.get(); + if (dialog == null || !dialog.isShowing()) { + return; + } + dialog.mLoadTask = null; + dialog.setLoading(false); + if (result == null || result.folderId <= 0) { + Toast.makeText(dialog.mActivity, R.string.memory_bottle_folder_error, + Toast.LENGTH_SHORT).show(); + dialog.mPendingAction = PendingAction.NONE; + return; + } + dialog.mMemoryFolderId = result.folderId; + sFolderId = result.folderId; + if (result.loadedEntries) { + sAllEntries.clear(); + sAllEntries.addAll(result.entries); + sRemainingEntries.clear(); + sRemainingEntries.addAll(result.entries); + dialog.mEntriesLoaded = true; + } else if (sFolderId == result.folderId) { + dialog.mEntriesLoaded = !sAllEntries.isEmpty(); + } + PendingAction pending = dialog.mPendingAction; + dialog.mPendingAction = PendingAction.NONE; + if (pending == PendingAction.ADD) { + dialog.showAddDialog(); + } else if (pending == PendingAction.BROWSE) { + dialog.browseMemory(); + } + } + } + + private static final class BrowseResult { + private final MemoryEntry entry; + private final String content; + + private BrowseResult(MemoryEntry entry, String content) { + this.entry = entry; + this.content = content; + } + } + + private static final class BrowseTask extends AsyncTask { + private final WeakReference mRef; + private final MemoryEntry mEntry; + + private BrowseTask(MemoryBottleDialog dialog, MemoryEntry entry) { + mRef = new WeakReference<>(dialog); + mEntry = entry; + } + + @Override + protected BrowseResult doInBackground(Void... params) { + MemoryBottleDialog dialog = mRef.get(); + if (dialog == null) { + return null; + } + String content = mEntry.content; + if (TextUtils.isEmpty(content)) { + content = dialog.queryNoteContent(dialog.mActivity.getContentResolver(), mEntry.id); + } + if (TextUtils.isEmpty(content)) { + content = dialog.mActivity.getString(R.string.memory_bottle_missing_content); + } + return new BrowseResult(mEntry, content); + } + + @Override + protected void onPostExecute(BrowseResult result) { + MemoryBottleDialog dialog = mRef.get(); + if (dialog == null || !dialog.isShowing()) { + return; + } + dialog.mBrowseTask = null; + dialog.setBrowseLoading(false); + if (result == null) { + return; + } + String message = dialog.formatEntryMessage(result.entry.createdDate, result.content); + new AlertDialog.Builder(dialog.mActivity) + .setTitle(R.string.memory_bottle_title) + .setMessage(message) + .setPositiveButton(R.string.memory_bottle_close, null) + .setNegativeButton(R.string.memory_bottle_delete, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int which) { + dialog.deleteMemoryEntry(result.entry); + } + }) + .show(); + } + } + private void deleteMemoryEntry(MemoryEntry entry) { + if (mMemoryFolderId <= 0) { + Toast.makeText(mActivity, R.string.memory_bottle_folder_error, Toast.LENGTH_SHORT).show(); + return; + } + HashSet ids = new HashSet(); + ids.add(entry.id); + if (DataUtils.batchMoveToTrash(mActivity.getContentResolver(), ids, mMemoryFolderId)) { + sAllEntries.remove(entry); + sRemainingEntries.remove(entry); + Toast.makeText(mActivity, R.string.memory_bottle_delete_success, Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(mActivity, R.string.memory_bottle_delete_failed, Toast.LENGTH_SHORT).show(); + } + } + private enum PendingAction { + NONE, + ADD, + BROWSE + } +} diff --git a/src/ui/NotesListAdapter.java b/src/ui/NotesListAdapter.java index 98106be..5159005 100644 --- a/src/ui/NotesListAdapter.java +++ b/src/ui/NotesListAdapter.java @@ -31,80 +31,89 @@ import java.util.HashSet; import java.util.Iterator; -public class NotesListAdapter extends CursorAdapter {//Adapter作为Activity和ListView之间的适配器,将Cursor数据转换为ListView可以理解的视图格式 - private static final String TAG = "NotesListAdapter"; // 日志标签 - private Context mContext; // 上下文对象 - private HashMap mSelectedIndex; // 选中项索引映射 - private int mNotesCount; // 笔记数量(排除文件夹) - private boolean mChoiceMode; // 是否处于选择模式 - - public static class AppWidgetAttribute { // 小部件属性类,用于存储小部件的ID和类型 - public int widgetId; // 小部件ID - public int widgetType; // 小部件类型 +public class NotesListAdapter extends CursorAdapter { + private static final String TAG = "NotesListAdapter"; + private Context mContext; + private HashMap mSelectedIndex; + private int mNotesCount; + private boolean mChoiceMode; + + private boolean mIncludeFolders; + + public static class AppWidgetAttribute { + public int widgetId; + public int widgetType; }; - - public NotesListAdapter(Context context) { // 构造函数,创建NotesListAdapter实例 + public NotesListAdapter(Context context) { super(context, null); - mSelectedIndex = new HashMap(); // 初始化选中项映射 + mSelectedIndex = new HashMap(); mContext = context; mNotesCount = 0; } @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { - return new NotesListItem(context); // 创建新的NotesListItem + return new NotesListItem(context); } @Override public void bindView(View view, Context context, Cursor cursor) { if (view instanceof NotesListItem) { - NoteItemData itemData = new NoteItemData(context, cursor); // 创建数据模型 - ((NotesListItem) view).bind(context, itemData, mChoiceMode, // 绑定数据到视图 + NoteItemData itemData = new NoteItemData(context, cursor); + ((NotesListItem) view).bind(context, itemData, mChoiceMode, isSelectedItem(cursor.getPosition())); } } public void setCheckedItem(final int position, final boolean checked) { - mSelectedIndex.put(position, checked); // 更新选中状态 - notifyDataSetChanged(); // 通知数据变化,刷新UI + mSelectedIndex.put(position, checked); + notifyDataSetChanged(); } public boolean isInChoiceMode() { - return mChoiceMode; // 检查是否处于选择模式 + return mChoiceMode; } public void setChoiceMode(boolean mode) { - mSelectedIndex.clear(); // 清除之前的选中状态 - mChoiceMode = mode; // 更新模式标志 + mSelectedIndex.clear(); + mChoiceMode = mode; + mIncludeFolders = false; + } + public void setChoiceMode(boolean mode, boolean includeFolders) { + mSelectedIndex.clear(); + mChoiceMode = mode; + mIncludeFolders = includeFolders; } public void selectAll(boolean checked) { Cursor cursor = getCursor(); for (int i = 0; i < getCount(); i++) { if (cursor.moveToPosition(i)) { - if (NoteItemData.getNoteType(cursor) == Notes.TYPE_NOTE) { // 只处理普通笔记,跳过文件夹 - setCheckedItem(i, checked); // 设置选中状态 + int type = NoteItemData.getNoteType(cursor); + if (type == Notes.TYPE_NOTE || (mIncludeFolders && type == Notes.TYPE_FOLDER)) { + setCheckedItem(i, checked); } } } } - public HashSet getSelectedItemIds() {// 获取选中项的ID集合 + public HashSet getSelectedItemIds() { HashSet itemSet = new HashSet(); for (Integer position : mSelectedIndex.keySet()) { if (mSelectedIndex.get(position) == true) { - Long id = getItemId(position); // 获取选中项的ID + Long id = getItemId(position); if (id == Notes.ID_ROOT_FOLDER) { Log.d(TAG, "Wrong item id, should not happen"); } else { - itemSet.add(id); // 添加到集合 + itemSet.add(id); } } } + return itemSet; } - public HashSet getSelectedWidget() {// 获取选中项的小部件属性集合 + public HashSet getSelectedWidget() { HashSet itemSet = new HashSet(); for (Integer position : mSelectedIndex.keySet()) { if (mSelectedIndex.get(position) == true) { @@ -112,10 +121,12 @@ public class NotesListAdapter extends CursorAdapter {//Adapter作为Activity和L if (c != null) { AppWidgetAttribute widget = new AppWidgetAttribute(); NoteItemData item = new NoteItemData(mContext, c); - widget.widgetId = item.getWidgetId(); // 获取小部件ID - widget.widgetType = item.getWidgetType(); // 获取小部件类型 + widget.widgetId = item.getWidgetId(); + widget.widgetType = item.getWidgetType(); itemSet.add(widget); - // Don't close cursor here, only the adapter could close it + /** + * Don't close cursor here, only the adapter could close it + */ } else { Log.e(TAG, "Invalid cursor"); return null; @@ -125,7 +136,7 @@ public class NotesListAdapter extends CursorAdapter {//Adapter作为Activity和L return itemSet; } - public int getSelectedCount() {// 获取选中项数量 + public int getSelectedCount() { Collection values = mSelectedIndex.values(); if (null == values) { return 0; @@ -137,39 +148,39 @@ public class NotesListAdapter extends CursorAdapter {//Adapter作为Activity和L count++; } } - return count; // 返回选中项数量 + return count; } - public boolean isAllSelected() {// 检查是否所有笔记都被选中 + public boolean isAllSelected() { int checkedCount = getSelectedCount(); - return (checkedCount != 0 && checkedCount == mNotesCount); + return (checkedCount != 0 && checkedCount == mNotesCount); } - public boolean isSelectedItem(final int position) {// 检查指定位置的项是否被选中 + public boolean isSelectedItem(final int position) { if (null == mSelectedIndex.get(position)) { return false; } - return mSelectedIndex.get(position); // 检查指定位置的项是否被选中 + return mSelectedIndex.get(position); } @Override protected void onContentChanged() { super.onContentChanged(); - calcNotesCount(); // 当内容变化时,重新计算笔记数量 + calcNotesCount(); } @Override public void changeCursor(Cursor cursor) { super.changeCursor(cursor); - calcNotesCount(); // 当游标变化时,重新计算笔记数量 + calcNotesCount(); } - private void calcNotesCount() {// 计算普通笔记的数量 + private void calcNotesCount() { mNotesCount = 0; for (int i = 0; i < getCount(); i++) { Cursor c = (Cursor) getItem(i); if (c != null) { - if (NoteItemData.getNoteType(c) == Notes.TYPE_NOTE) { // 只统计普通笔记,排除文件夹 + if (NoteItemData.getNoteType(c) == Notes.TYPE_NOTE) { mNotesCount++; } } else { diff --git a/src/ui/NotesListItem.java b/src/ui/NotesListItem.java index 694f325..b41f8e7 100644 --- a/src/ui/NotesListItem.java +++ b/src/ui/NotesListItem.java @@ -31,16 +31,13 @@ import net.micode.notes.tool.ResourceParser.NoteItemBgResources; public class NotesListItem extends LinearLayout { - private ImageView mAlert; // 提醒图标(时钟或通话记录图标) - private TextView mTitle; // 笔记/文件夹标题或内容摘要 - private TextView mTime; // 最后修改时间 - private TextView mCallName; // 通话记录来电者名称(仅通话记录显示) - private NoteItemData mItemData; // 当前列表项的数据模型 - private CheckBox mCheckBox; // 选择模式下的复选框(用于批量操作) + private ImageView mAlert; + private TextView mTitle; + private TextView mTime; + private TextView mCallName; + private NoteItemData mItemData; + private CheckBox mCheckBox; - /** - * 构造函数,初始化视图组件 - */ public NotesListItem(Context context) { super(context); inflate(context, R.layout.note_item, this); @@ -51,13 +48,7 @@ public class NotesListItem extends LinearLayout { mCheckBox = (CheckBox) findViewById(android.R.id.checkbox); } - /** - * 绑定数据到视图,根据数据类型动态调整UI样式 - * @param data 笔记数据模型 - * @param checked 选择模式下是否选中 - */ public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked) { - // 处理选择模式显示 if (choiceMode && data.getType() == Notes.TYPE_NOTE) { mCheckBox.setVisibility(View.VISIBLE); mCheckBox.setChecked(checked); @@ -66,7 +57,6 @@ public class NotesListItem extends LinearLayout { } mItemData = data; - // 处理通话记录文件夹 if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { mCallName.setVisibility(View.GONE); mAlert.setVisibility(View.VISIBLE); @@ -74,18 +64,14 @@ public class NotesListItem extends LinearLayout { mTitle.setText(context.getString(R.string.call_record_folder_name) + context.getString(R.string.format_folder_files_count, data.getNotesCount())); mAlert.setImageResource(R.drawable.call_record); - } - // 处理回收站文件夹 - else if (data.getId() == Notes.ID_TRASH_FOLER) { + } else if (data.getId() == Notes.ID_TRASH_FOLER) { mCallName.setVisibility(View.GONE); mAlert.setVisibility(View.VISIBLE); mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); mTitle.setText(context.getString(R.string.trash_folder_name) + context.getString(R.string.format_folder_files_count, data.getNotesCount())); - mAlert.setImageResource(R.drawable.trash); - } - // 处理通话记录笔记 - else if (data.getParentId() == Notes.ID_CALL_RECORD_FOLDER) { + mAlert.setImageResource(android.R.drawable.ic_menu_delete); + } else if (data.getParentId() == Notes.ID_CALL_RECORD_FOLDER) { mCallName.setVisibility(View.VISIBLE); mCallName.setText(data.getCallName()); mTitle.setTextAppearance(context,R.style.TextAppearanceSecondaryItem); @@ -96,9 +82,14 @@ public class NotesListItem extends LinearLayout { } else { mAlert.setVisibility(View.GONE); } - } - // 处理普通文件夹和笔记 - else { + } else if (data.getId() == Notes.ID_TRASH_FOLER) { //为回收站添加图标和显示逻辑 + mCallName.setVisibility(View.GONE); + mAlert.setVisibility(View.VISIBLE); + mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); + mTitle.setText(context.getString(R.string.trash_folder_name) + + context.getString(R.string.format_folder_files_count, data.getNotesCount())); + mAlert.setImageResource(R.drawable.baseline_restore_from_trash_24); + } else { mCallName.setVisibility(View.GONE); mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); @@ -117,20 +108,14 @@ public class NotesListItem extends LinearLayout { } } } - // 设置最后修改时间(相对时间格式) mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate())); - // 根据数据类型和位置设置背景 setBackground(data); } - /** - * 根据笔记类型和位置设置不同的背景资源 - */ private void setBackground(NoteItemData data) { int id = data.getBgColorId(); if (data.getType() == Notes.TYPE_NOTE) { - // 根据笔记在列表中的位置设置不同背景 if (data.isSingle() || data.isOneFollowingFolder()) { setBackgroundResource(NoteItemBgResources.getNoteBgSingleRes(id)); } else if (data.isLast()) { @@ -141,14 +126,10 @@ public class NotesListItem extends LinearLayout { setBackgroundResource(NoteItemBgResources.getNoteBgNormalRes(id)); } } else { - // 文件夹使用统一背景 setBackgroundResource(NoteItemBgResources.getFolderBgRes()); } } - /** - * 获取当前列表项的数据模型 - */ public NoteItemData getItemData() { return mItemData; } diff --git a/src/ui/TrashManager.java b/src/ui/TrashManager.java new file mode 100644 index 0000000..e900b8e --- /dev/null +++ b/src/ui/TrashManager.java @@ -0,0 +1,192 @@ +/* + * 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.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.os.AsyncTask; +import android.util.Log; + +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.tool.DataUtils; +import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute; + +import java.util.HashSet; + +public class TrashManager { + private static final String TAG = "TrashManager"; + private final Context mContext; + private final ContentResolver mResolver; + private final Callback mCallback; + + public interface Callback { + void onWidgetsNeedUpdate(HashSet widgets); + + void onListChanged(); + + void onActionModeFinished(); + + void onRestoreInvalid(); + } + + public TrashManager(Context context, ContentResolver resolver, Callback callback) { + mContext = context; + mResolver = resolver; + mCallback = callback; + } + + public void cleanupExpiredTrash() { + long expireTime = System.currentTimeMillis() - 24L * 60L * 60L * 1000L; + mResolver.delete(Notes.CONTENT_NOTE_URI, + NoteColumns.PARENT_ID + "=? AND " + NoteColumns.MODIFIED_DATE + " ids, + final HashSet widgets, final long originFolderId) { + new AsyncTask>() { + protected HashSet doInBackground(Void... unused) { + if (inTrash) { + if (!DataUtils.batchDeleteNotes(mResolver, ids)) { + Log.e(TAG, "Delete notes error, should not happens"); + } + } else { + if (!DataUtils.batchMoveToTrash(mResolver, ids, originFolderId)) { + Log.e(TAG, "Move notes to trash folder error"); + } + } + return widgets; + } + + @Override + protected void onPostExecute(HashSet resultWidgets) { + if (mCallback != null) { + mCallback.onWidgetsNeedUpdate(resultWidgets); + mCallback.onListChanged(); + mCallback.onActionModeFinished(); + } + } + }.execute(); + } + + public void restoreSelected(final HashSet ids, final HashSet widgets) { + new AsyncTask() { + protected Boolean doInBackground(Void... params) { + boolean hasInvalid = false; + long now = System.currentTimeMillis(); + for (long id : ids) { + Cursor cursor = mResolver.query( + ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id), + new String[] { NoteColumns.ORIGIN_PARENT_ID, NoteColumns.TYPE }, + null, null, null); + if (cursor == null) { + continue; + } + long originParent = Notes.ID_ROOT_FOLDER; + int type = Notes.TYPE_NOTE; + try { + if (cursor.moveToFirst()) { + originParent = cursor.getLong(0); + type = cursor.getInt(1); + } + } finally { + cursor.close(); + } + long targetParent = resolveRestoreParent(originParent); + if (targetParent != originParent) { + hasInvalid = true; + } + ContentValues values = new ContentValues(); + values.put(NoteColumns.PARENT_ID, targetParent); + values.put(NoteColumns.ORIGIN_PARENT_ID, 0); + values.put(NoteColumns.LOCAL_MODIFIED, 1); + values.put(NoteColumns.MODIFIED_DATE, now); + mResolver.update(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id), + values, null, null); + if (type == Notes.TYPE_FOLDER) { + restoreNotesForFolder(id, now); + } + } + return hasInvalid; + } + + @Override + protected void onPostExecute(Boolean hasInvalid) { + if (mCallback != null) { + if (hasInvalid != null && hasInvalid) { + mCallback.onRestoreInvalid(); + } + mCallback.onWidgetsNeedUpdate(widgets); + mCallback.onListChanged(); + mCallback.onActionModeFinished(); + } + } + }.execute(); + } + + public HashSet moveFolderToTrash(long folderId, long originFolderId) { + HashSet widgets = DataUtils.getFolderNoteWidget(mResolver, folderId); + DataUtils.moveNotesToTrashForFolder(mResolver, folderId); + HashSet ids = new HashSet(); + ids.add(folderId); + if (!DataUtils.batchMoveToTrash(mResolver, ids, originFolderId)) { + Log.e(TAG, "Move folder to trash error"); + } + return widgets; + } + + private void restoreNotesForFolder(long folderId, long now) { + ContentValues values = new ContentValues(); + values.put(NoteColumns.PARENT_ID, folderId); + values.put(NoteColumns.ORIGIN_PARENT_ID, 0); + values.put(NoteColumns.LOCAL_MODIFIED, 1); + values.put(NoteColumns.MODIFIED_DATE, now); + mResolver.update(Notes.CONTENT_NOTE_URI, values, + NoteColumns.PARENT_ID + "=? AND " + NoteColumns.ORIGIN_PARENT_ID + "=?", + new String[] { String.valueOf(Notes.ID_TRASH_FOLER), String.valueOf(folderId) }); + } + + private long resolveRestoreParent(long originParentId) { + if (originParentId == Notes.ID_ROOT_FOLDER || originParentId == Notes.ID_CALL_RECORD_FOLDER) { + return originParentId; + } + if (originParentId <= 0) { + return Notes.ID_ROOT_FOLDER; + } + Cursor cursor = mResolver.query( + ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, originParentId), + new String[] { NoteColumns.ID, NoteColumns.PARENT_ID }, + NoteColumns.PARENT_ID + "<>?", + new String[] { String.valueOf(Notes.ID_TRASH_FOLER) }, + null); + if (cursor == null) { + return Notes.ID_ROOT_FOLDER; + } + try { + if (cursor.moveToFirst()) { + return originParentId; + } + } finally { + cursor.close(); + } + return Notes.ID_ROOT_FOLDER; + } +}