diff --git a/other/MainActivity.java b/other/MainActivity.java new file mode 100644 index 0000000..0b82cd7 --- /dev/null +++ b/other/MainActivity.java @@ -0,0 +1,36 @@ +package net.micode.notes; + +import android.os.Bundle; + +import androidx.activity.EdgeToEdge; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.graphics.Insets; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; + +public class MainActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // 启用边缘到边缘显示支持,允许内容延伸到屏幕刘海、状态栏和导航栏区域 + EdgeToEdge.enable(this); + + // 设置Activity的布局为activity_main.xml + setContentView(R.layout.activity_main); + + // 为ID为main的根视图设置窗口插入监听,处理系统窗口装饰区域 + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { + // 获取系统栏(状态栏、导航栏等)的插入区域 + Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); + + // 设置根视图的内边距,确保内容不会被系统栏遮挡 + // 四个方向的内边距分别对应左、上、右、下系统栏的尺寸 + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); + + // 返回处理后的窗口插入对象 + return insets; + }); + } +} \ No newline at end of file diff --git a/test/ExampleUnitTest.java b/test/ExampleUnitTest.java new file mode 100644 index 0000000..a99315d --- /dev/null +++ b/test/ExampleUnitTest.java @@ -0,0 +1,25 @@ +package net.micode.notes; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * 示例本地单元测试类 + * 功能:演示JUnit框架在Android项目中的基本用法 + * 运行环境:开发主机(非Android设备) + * 测试范围:不依赖Android系统API的纯Java逻辑 + */ +public class ExampleUnitTest { + /** + * 加法运算测试方法 + * 测试目标:验证2 + 2的结果是否等于4 + * 断言类型:assertEquals(预期值,实际值) + */ + @Test + public void addition_isCorrect() { + // 验证整数加法运算的正确性 + // 若2 + 2不等于4,测试将失败并抛出AssertionError + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/ui/DropdownMenu.java b/ui/DropdownMenu.java new file mode 100644 index 0000000..b452595 --- /dev/null +++ b/ui/DropdownMenu.java @@ -0,0 +1,93 @@ +/* + * 版权所有 (c) 2010-2011,The MiCode Open Source Community (www.micode.net) + * + * 本软件根据 Apache 许可证 2.0 版("许可证")发布; + * 除非符合许可证,否则不得使用此文件。 + * 您可以在以下网址获取许可证副本: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 除非法律要求或书面同意,软件 + * 根据许可证分发的内容按"原样"提供, + * 不附带任何明示或暗示的保证或条件。 + * 请参阅许可证,了解有关权限和限制的具体语言。 + */ + +package net.micode.notes.ui; + +import android.content.Context; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.PopupMenu; +import android.widget.PopupMenu.OnMenuItemClickListener; + +import net.micode.notes.R; + +/** + * 下拉菜单组件(封装PopupMenu功能) + * 功能: + * 1. 将按钮与PopupMenu关联,点击按钮显示下拉菜单 + * 2. 支持菜单项点击事件监听 + * 3. 提供查找菜单项和设置按钮标题的接口 + */ +public class DropdownMenu { + private Button mButton; // 触发下拉菜单的按钮 + private PopupMenu mPopupMenu; // 弹出式菜单 + private Menu mMenu; // 菜单项集合 + + /** + * 构造方法:初始化下拉菜单 + * @param context 上下文 + * @param button 触发菜单的按钮 + * @param menuId 菜单布局资源ID + */ + public DropdownMenu(Context context, Button button, int menuId) { + mButton = button; + // 设置按钮背景为下拉图标(自定义样式) + mButton.setBackgroundResource(R.drawable.dropdown_icon); + + // 创建PopupMenu并关联到按钮 + mPopupMenu = new PopupMenu(context, mButton); + mMenu = mPopupMenu.getMenu(); + + // 从资源加载菜单项 + mPopupMenu.getMenuInflater().inflate(menuId, mMenu); + + // 设置按钮点击事件:点击时显示菜单 + mButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + mPopupMenu.show(); + } + }); + } + + /** + * 设置菜单项点击事件监听器 + * @param listener 菜单项点击监听器 + */ + public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) { + if (mPopupMenu != null) { + mPopupMenu.setOnMenuItemClickListener(listener); + } + } + + /** + * 查找指定ID的菜单项 + * @param id 菜单项ID + * @return 对应的MenuItem对象 + */ + public MenuItem findItem(int id) { + return mMenu.findItem(id); + } + + /** + * 设置按钮的标题文本 + * @param title 标题文本 + */ + public void setTitle(CharSequence title) { + mButton.setText(title); + } +} \ No newline at end of file diff --git a/ui/FoldersListAdapter.java b/ui/FoldersListAdapter.java new file mode 100644 index 0000000..929a46b --- /dev/null +++ b/ui/FoldersListAdapter.java @@ -0,0 +1,124 @@ +/* + * 版权所有 (c) 2010-2011,The MiCode Open Source Community (www.micode.net) + * + * 本软件根据 Apache 许可证 2.0 版("许可证")发布; + * 除非符合许可证,否则不得使用此文件。 + * 您可以在以下网址获取许可证副本: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 除非法律要求或书面同意,软件 + * 根据许可证分发的内容按"原样"提供, + * 不附带任何明示或暗示的保证或条件。 + * 请参阅许可证,了解有关权限和限制的具体语言。 + */ + +package net.micode.notes.ui; + +import android.content.Context; +import android.database.Cursor; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CursorAdapter; +import android.widget.LinearLayout; +import android.widget.TextView; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; + +/** + * 文件夹列表适配器(用于显示笔记文件夹列表) + * 功能: + * 1. 将数据库中的文件夹数据映射到列表项视图 + * 2. 处理特殊文件夹(如根文件夹)的显示逻辑 + * 3. 提供获取文件夹名称的接口方法 + */ +public class FoldersListAdapter extends CursorAdapter { + // 查询投影:定义从数据库查询的字段 + public static final String [] PROJECTION = { + NoteColumns.ID, // 文件夹ID + NoteColumns.SNIPPET // 文件夹名称(此处Snippet用作文件夹名称) + }; + + // 列索引常量(用于快速访问Cursor中的数据) + public static final int ID_COLUMN = 0; + public static final int NAME_COLUMN = 1; + + /** + * 构造方法:初始化适配器 + * @param context 上下文 + * @param c 包含文件夹数据的Cursor + */ + public FoldersListAdapter(Context context, Cursor c) { + super(context, c, 0); // 调用父类构造方法,传递Cursor + } + + /** + * 创建新的列表项视图 + * @param context 上下文 + * @param cursor 当前数据Cursor + * @param parent 父视图组 + * @return 新创建的列表项视图 + */ + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return new FolderListItem(context); // 创建自定义文件夹列表项视图 + } + + /** + * 将数据绑定到已创建的视图 + * @param view 要绑定数据的视图 + * @param context 上下文 + * @param cursor 包含数据的Cursor + */ + @Override + public void bindView(View view, Context context, Cursor cursor) { + if (view instanceof FolderListItem) { + // 获取文件夹名称(特殊处理根文件夹) + String folderName = (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) + ? context.getString(R.string.menu_move_parent_folder) // 根文件夹显示特定名称 + : cursor.getString(NAME_COLUMN); // 普通文件夹显示数据库中的名称 + + ((FolderListItem) view).bind(folderName); // 绑定文件夹名称到视图 + } + } + + /** + * 获取指定位置的文件夹名称 + * @param context 上下文 + * @param position 列表位置 + * @return 文件夹名称 + */ + public String getFolderName(Context context, int position) { + Cursor cursor = (Cursor) getItem(position); + return (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) + ? context.getString(R.string.menu_move_parent_folder) + : cursor.getString(NAME_COLUMN); + } + + /** + * 文件夹列表项视图(自定义LinearLayout) + * 功能: + * 1. 加载列表项布局 + * 2. 提供绑定文件夹名称的方法 + */ + private class FolderListItem extends LinearLayout { + private TextView mName; // 文件夹名称显示文本框 + + public FolderListItem(Context context) { + super(context); + // 加载列表项布局并添加到当前视图 + inflate(context, R.layout.folder_list_item, this); + mName = findViewById(R.id.tv_folder_name); // 初始化名称文本框 + } + + /** + * 绑定文件夹名称到视图 + * @param name 文件夹名称 + */ + public void bind(String name) { + mName.setText(name); // 设置文件夹名称 + } + } +} \ No newline at end of file diff --git a/ui/NoteEditActivity.java b/ui/NoteEditActivity.java new file mode 100644 index 0000000..97e799b --- /dev/null +++ b/ui/NoteEditActivity.java @@ -0,0 +1,364 @@ +/* + * 版权所有 (c) 2010-2011,The MiCode Open Source Community (www.micode.net) + * + * 本软件根据 Apache 许可证 2.0 版("许可证")发布; + * 除非符合许可证,否则不得使用此文件。 + * 您可以在以下网址获取许可证副本: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 除非法律要求或书面同意,软件 + * 根据许可证分发的内容按"原样"提供, + * 不附带任何明示或暗示的保证或条件。 + * 请参阅许可证,了解有关权限和限制的具体语言。 + */ + +package net.micode.notes.ui; + +import android.app.Activity; +import android.app.AlarmManager; +import android.app.AlertDialog; +import android.app.PendingIntent; +import android.app.SearchManager; +import android.appwidget.AppWidgetManager; +import android.content.ContentUris; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.Paint; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.TextUtils; +import android.text.format.DateUtils; +import android.text.style.BackgroundColorSpan; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.WindowManager; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.LinearLayout; +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.TextNote; +import net.micode.notes.model.WorkingNote; +import net.micode.notes.model.WorkingNote.NoteSettingChangedListener; +import net.micode.notes.tool.DataUtils; +import net.micode.notes.tool.ResourceParser; +import net.micode.notes.tool.ResourceParser.TextAppearanceResources; +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 java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 笔记编辑活动(核心界面逻辑) + * 功能: + * 1. 支持新建/编辑文本笔记、待办事项、通话记录笔记 + * 2. 实现笔记内容编辑、格式设置(背景颜色、字体大小) + * 3. 处理提醒设置、桌面小部件更新、快捷方式创建 + * 4. 支持列表模式(待办事项)和普通文本模式切换 + * 5. 集成搜索高亮、上下文菜单、多编辑项管理 + */ +public class NoteEditActivity extends Activity implements OnClickListener, + NoteSettingChangedListener, OnTextViewChangeListener { + + // 头部视图ViewHolder(优化UI组件访问) + private class HeadViewHolder { + public TextView tvModified; // 最后修改时间 + public ImageView ivAlertIcon; // 提醒图标 + public TextView tvAlertDate; // 提醒时间 + public ImageView ibSetBgColor; // 设置背景颜色按钮 + } + + // 背景颜色选择按钮映射(视图ID -> 资源ID) + private static final Map sBgSelectorBtnsMap = new HashMap<>(); + static { + sBgSelectorBtnsMap.put(R.id.iv_bg_yellow, ResourceParser.YELLOW); + sBgSelectorBtnsMap.put(R.id.iv_bg_red, ResourceParser.RED); + sBgSelectorBtnsMap.put(R.id.iv_bg_blue, ResourceParser.BLUE); + sBgSelectorBtnsMap.put(R.id.iv_bg_green, ResourceParser.GREEN); + sBgSelectorBtnsMap.put(R.id.iv_bg_white, ResourceParser.WHITE); + } + + // 背景颜色选中状态映射(资源ID -> 选中视图ID) + private static final Map sBgSelectorSelectionMap = new HashMap<>(); + static { + sBgSelectorSelectionMap.put(ResourceParser.YELLOW, R.id.iv_bg_yellow_select); + sBgSelectorSelectionMap.put(ResourceParser.RED, R.id.iv_bg_red_select); + sBgSelectorSelectionMap.put(ResourceParser.BLUE, R.id.iv_bg_blue_select); + sBgSelectorSelectionMap.put(ResourceParser.GREEN, R.id.iv_bg_green_select); + sBgSelectorSelectionMap.put(ResourceParser.WHITE, R.id.iv_bg_white_select); + } + + // 字体大小按钮映射(视图ID -> 资源ID) + private static final Map sFontSizeBtnsMap = new HashMap<>(); + static { + sFontSizeBtnsMap.put(R.id.ll_font_large, ResourceParser.TEXT_LARGE); + sFontSizeBtnsMap.put(R.id.ll_font_small, ResourceParser.TEXT_SMALL); + sFontSizeBtnsMap.put(R.id.ll_font_normal, ResourceParser.TEXT_MEDIUM); + sFontSizeBtnsMap.put(R.id.ll_font_super, ResourceParser.TEXT_SUPER); + } + + // 字体大小选中状态映射(资源ID -> 选中视图ID) + private static final Map sFontSelectorSelectionMap = new HashMap<>(); + static { + sFontSelectorSelectionMap.put(ResourceParser.TEXT_LARGE, R.id.iv_large_select); + sFontSelectorSelectionMap.put(ResourceParser.TEXT_SMALL, R.id.iv_small_select); + sFontSelectorSelectionMap.put(ResourceParser.TEXT_MEDIUM, R.id.iv_medium_select); + sFontSelectorSelectionMap.put(ResourceParser.TEXT_SUPER, R.id.iv_super_select); + } + + private static final String TAG = "NoteEditActivity"; + private HeadViewHolder mNoteHeaderHolder; // 头部视图持有者 + private View mHeadViewPanel; // 头部布局 + private View mNoteBgColorSelector; // 背景颜色选择面板 + private View mFontSizeSelector; // 字体大小选择面板 + private EditText mNoteEditor; // 普通文本编辑框 + private View mNoteEditorPanel; // 文本编辑背景面板 + private WorkingNote mWorkingNote; // 笔记数据模型 + private SharedPreferences mSharedPrefs; // 共享偏好(存储字体大小等设置) + private int mFontSizeId; // 当前字体大小资源ID + private static final String PREFERENCE_FONT_SIZE = "pref_font_size"; // 字体大小偏好键 + private static final int SHORTCUT_ICON_TITLE_MAX_LEN = 10; // 快捷方式标题最大长度 + public static final String TAG_CHECKED = "\u221A"; // 待办事项选中标记 + public static final String TAG_UNCHECKED = "\u25A1"; // 待办事项未选中标记 + private LinearLayout mEditTextList; // 列表模式下的编辑项容器 + private String mUserQuery; // 搜索查询词(用于高亮显示) + private Pattern mPattern; // 搜索匹配模式 + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.note_edit); // 加载编辑界面布局 + + // 恢复因内存不足被销毁的活动状态 + if (savedInstanceState == null && !initActivityState(getIntent())) { + finish(); + return; + } + initResources(); // 初始化UI组件和资源 + } + + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + if (savedInstanceState != null && savedInstanceState.containsKey(Intent.EXTRA_UID)) { + // 从已保存的状态中恢复笔记ID + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.putExtra(Intent.EXTRA_UID, savedInstanceState.getLong(Intent.EXTRA_UID)); + if (!initActivityState(intent)) { + finish(); + } + } + } + + /** + * 初始化活动状态(处理打开/新建笔记逻辑) + * @param intent 启动意图 + * @return 是否初始化成功 + */ + private boolean initActivityState(Intent intent) { + mWorkingNote = null; + if (TextUtils.equals(intent.getAction(), Intent.ACTION_VIEW)) { + // 打开已有笔记 + long noteId = intent.getLongExtra(Intent.EXTRA_UID, 0); + mUserQuery = intent.getStringExtra(SearchManager.USER_QUERY); // 处理搜索结果跳转 + + // 检查笔记是否存在且可见 + if (!DataUtils.visibleInNoteDatabase(getContentResolver(), noteId, Notes.TYPE_NOTE)) { + showToast(R.string.error_note_not_exist); + startActivity(new Intent(this, NotesListActivity.class)); + finish(); + return false; + } + mWorkingNote = WorkingNote.load(this, noteId); // 加载笔记数据 + if (mWorkingNote == null) { + Log.e(TAG, "加载笔记失败:" + noteId); + finish(); + return false; + } + // 隐藏软键盘(查看模式) + getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN); + } else if (TextUtils.equals(intent.getAction(), Intent.ACTION_INSERT_OR_EDIT)) { + // 新建笔记 + long folderId = intent.getLongExtra(Notes.INTENT_EXTRA_FOLDER_ID, 0); + int widgetId = intent.getIntExtra(Notes.INTENT_EXTRA_WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); + int widgetType = intent.getIntExtra(Notes.INTENT_EXTRA_WIDGET_TYPE, Notes.TYPE_WIDGET_INVALIDE); + int bgResId = intent.getIntExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, ResourceParser.getDefaultBgId(this)); + + // 处理通话记录笔记(特殊类型) + String phoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER); + long callDate = intent.getLongExtra(Notes.INTENT_EXTRA_CALL_DATE, 0); + if (callDate != 0 && phoneNumber != null) { + long noteId = DataUtils.getNoteIdByPhoneNumberAndCallDate(getContentResolver(), phoneNumber, callDate); + if (noteId > 0) { + mWorkingNote = WorkingNote.load(this, noteId); // 加载已有通话记录笔记 + } else { + // 创建新的通话记录笔记 + mWorkingNote = WorkingNote.createEmptyNote(this, folderId, widgetId, widgetType, bgResId); + mWorkingNote.convertToCallNote(phoneNumber, callDate); + } + } else { + // 创建普通空笔记 + mWorkingNote = WorkingNote.createEmptyNote(this, folderId, widgetId, widgetType, bgResId); + } + // 显示软键盘(编辑模式) + getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); + } else { + Log.e(TAG, "不支持的意图操作"); + finish(); + return false; + } + // 注册笔记设置变化监听器 + mWorkingNote.setOnSettingStatusChangedListener(this); + return true; + } + + @Override + protected void onResume() { + super.onResume(); + initNoteScreen(); // 初始化界面显示 + } + + /** + * 初始化界面显示(加载笔记内容和设置) + */ + private void initNoteScreen() { + // 应用字体大小设置 + mNoteEditor.setTextAppearance(this, TextAppearanceResources.getTexAppearanceResource(mFontSizeId)); + // 根据笔记类型切换显示模式 + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + switchToListMode(mWorkingNote.getContent()); // 列表模式(待办事项) + } else { + // 普通文本模式,高亮搜索词 + mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery)); + mNoteEditor.setSelection(mNoteEditor.getText().length()); // 光标移至末尾 + } + // 更新背景颜色显示 + for (Integer bgId : sBgSelectorSelectionMap.keySet()) { + findViewById(sBgSelectorSelectionMap.get(bgId)).setVisibility(View.GONE); + } + mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId()); + mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId()); + // 更新修改时间和提醒状态 + mNoteHeaderHolder.tvModified.setText(DateUtils.formatDateTime(this, + mWorkingNote.getModifiedDate(), + DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NUMERIC_DATE | + DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_YEAR)); + showAlertHeader(); // 显示提醒时间 + } + + /** + * 显示提醒时间和图标 + */ + private void showAlertHeader() { + if (mWorkingNote.hasClockAlert()) { + long currentTime = System.currentTimeMillis(); + if (currentTime > mWorkingNote.getAlertDate()) { + mNoteHeaderHolder.tvAlertDate.setText(R.string.note_alert_expired); // 提醒已过期 + } else { + // 显示相对时间(如“10分钟后”) + mNoteHeaderHolder.tvAlertDate.setText(DateUtils.getRelativeTimeSpanString( + mWorkingNote.getAlertDate(), currentTime, DateUtils.MINUTE_IN_MILLIS)); + } + mNoteHeaderHolder.tvAlertDate.setVisibility(View.VISIBLE); + mNoteHeaderHolder.ivAlertIcon.setVisibility(View.VISIBLE); + } else { + mNoteHeaderHolder.tvAlertDate.setVisibility(View.GONE); + mNoteHeaderHolder.ivAlertIcon.setVisibility(View.GONE); + } + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + initActivityState(intent); // 处理新的启动意图(如从搜索结果重新打开) + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + // 保存未存入数据库的新笔记(生成ID) + if (!mWorkingNote.existInDatabase()) { + saveNote(); + } + outState.putLong(Intent.EXTRA_UID, mWorkingNote.getNoteId()); // 保存笔记ID + } + + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + // 点击外部区域隐藏颜色/字体选择面板 + if ((mNoteBgColorSelector.getVisibility() == View.VISIBLE || mFontSizeSelector.getVisibility() == View.VISIBLE) && + !inRangeOfView(mNoteBgColorSelector, ev) && !inRangeOfView(mFontSizeSelector, ev)) { + mNoteBgColorSelector.setVisibility(View.GONE); + mFontSizeSelector.setVisibility(View.GONE); + return true; + } + return super.dispatchTouchEvent(ev); + } + + /** + * 判断触摸点是否在指定视图范围内 + * @param view 目标视图 + * @param ev 触摸事件 + * @return 是否在范围内 + */ + private boolean inRangeOfView(View view, MotionEvent ev) { + int[] location = new int[2]; + view.getLocationOnScreen(location); + int x = location[0], y = location[1]; + return ev.getX() >= x && ev.getX() <= x + view.getWidth() && + ev.getY() >= y && ev.getY() <= y + view.getHeight(); + } + + /** + * 初始化UI组件和资源 + */ + private void initResources() { + // 头部视图组件初始化 + mHeadViewPanel = findViewById(R.id.note_title); + mNoteHeaderHolder = new HeadViewHolder(); + mNoteHeaderHolder.tvModified = findViewById(R.id.tv_modified_date); + mNoteHeaderHolder.ivAlertIcon = findViewById(R.id.iv_alert_icon); + mNoteHeaderHolder.tvAlertDate = findViewById(R.id.tv_alert_date); + mNoteHeaderHolder.ibSetBgColor = findViewById(R.id.btn_set_bg_color); + mNoteHeaderHolder.ibSetBgColor.setOnClickListener(this); // 设置背景颜色按钮点击监听 + + // 文本编辑组件初始化 + mNoteEditor = findViewById(R.id.note_edit_view); + mNoteEditorPanel = findViewById(R.id.sv_note_edit); + mNoteBgColorSelector = findViewById(R.id.note_bg_color_selector); + // 注册背景颜色选择按钮点击监听 + for (int btnId : sBgSelectorBtnsMap.keySet()) { + findViewById(btnId).setOnClickListener(this); + } + + // 字体大小选择面板初始化 + mFontSizeSelector = findViewById(R.id.font_size_selector); + for (int sizeId : sFontSizeBtnsMap.keySet()) { + findViewById(sizeId).setOnClickListener(this); + } + // 加载字体大小偏好设置 + mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); + mFontSizeId = mSharedPrefs.getInt(PREFERENCE_FONT_SIZE, ResourceParser.BG_DEFAULT_FONT_SIZE); + // \ No newline at end of file diff --git a/ui/NoteEditText.java b/ui/NoteEditText.java new file mode 100644 index 0000000..b95f1fe --- /dev/null +++ b/ui/NoteEditText.java @@ -0,0 +1,245 @@ +/* + * 版权所有 (c) 2010-2011,The MiCode Open Source Community (www.micode.net) + * + * 本软件根据 Apache 许可证 2.0 版("许可证")发布; + * 除非符合许可证,否则不得使用此文件。 + * 您可以在以下网址获取许可证副本: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 除非法律要求或书面同意,软件 + * 根据许可证分发的内容按"原样"提供, + * 不附带任何明示或暗示的保证或条件。 + * 请参阅许可证,了解有关权限和限制的具体语言。 + */ + +package net.micode.notes.ui; + +import android.content.Context; +import android.graphics.Rect; +import android.text.Layout; +import android.text.Selection; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.style.URLSpan; +import android.util.AttributeSet; +import android.util.Log; +import android.view.ContextMenu; +import android.view.KeyEvent; +import android.view.MenuItem; +import android.view.MenuItem.OnMenuItemClickListener; +import android.view.MotionEvent; +import android.widget.EditText; + +import net.micode.notes.R; + +import java.util.HashMap; +import java.util.Map; + +/** + * 自定义笔记编辑文本组件(继承自EditText) + * 功能: + * 1. 处理文本输入时的特殊按键事件(删除、换行) + * 2. 支持文本变化监听,通知上层组件更新 + * 3. 识别文本中的链接(电话、网址、邮箱),并提供上下文菜单操作 + * 4. 优化触摸事件处理,确保光标位置准确 + */ +public class NoteEditText extends EditText { + private static final String TAG = "NoteEditText"; + private int mIndex; // 当前编辑文本在列表中的索引 + private int mSelectionStartBeforeDelete; // 删除操作前的光标位置 + + // 支持的链接协议及对应的菜单文本资源 + private static final String SCHEME_TEL = "tel:"; + private static final String SCHEME_HTTP = "http:"; + private static final String SCHEME_EMAIL = "mailto:"; + private static final Map sSchemaActionResMap = new HashMap() {{ + put(SCHEME_TEL, R.string.note_link_tel); // 电话链接 + put(SCHEME_HTTP, R.string.note_link_web); // 网页链接 + put(SCHEME_EMAIL, R.string.note_link_email); // 邮箱链接 + }}; + + // 文本变化监听器接口 + public interface OnTextViewChangeListener { + /** + * 当按下删除键且文本为空时触发(删除当前编辑项) + * @param index 当前编辑项索引 + * @param text 当前文本内容 + */ + void onEditTextDelete(int index, String text); + + /** + * 当按下回车键时触发(添加新编辑项) + * @param index 当前编辑项索引+1 + * @param text 新编辑项的文本内容 + */ + void onEditTextEnter(int index, String text); + + /** + * 当文本内容变化时触发(更新界面状态) + * @param index 当前编辑项索引 + * @param hasText 是否有文本内容 + */ + void onTextChange(int index, boolean hasText); + } + + private OnTextViewChangeListener mOnTextViewChangeListener; // 监听器实例 + + public NoteEditText(Context context) { + super(context, null); + mIndex = 0; // 初始化索引为0 + } + + /** + * 设置编辑项索引(用于标识在列表中的位置) + * @param index 索引值 + */ + public void setIndex(int index) { + mIndex = index; + } + + /** + * 设置文本变化监听器 + * @param listener 监听器实例 + */ + public void setOnTextViewChangeListener(OnTextViewChangeListener listener) { + mOnTextViewChangeListener = listener; + } + + // 构造方法(支持XML布局初始化) + public NoteEditText(Context context, AttributeSet attrs) { + super(context, attrs, android.R.attr.editTextStyle); + } + + public NoteEditText(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + /** + * 触摸事件处理:确保点击位置正确设置光标 + * @param event 触摸事件 + * @return 事件处理结果 + */ + @Override + public boolean onTouchEvent(MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_DOWN) { + // 计算触摸点在文本中的坐标 + int x = (int) event.getX(); + int y = (int) event.getY(); + x -= getTotalPaddingLeft(); + y -= getTotalPaddingTop(); + x += getScrollX(); + y += getScrollY(); + + // 获取文本布局和行号 + Layout layout = getLayout(); + int line = layout.getLineForVertical(y); + int off = layout.getOffsetForHorizontal(line, x); + Selection.setSelection(getText(), off); // 设置光标位置 + } + return super.onTouchEvent(event); + } + + /** + * 按键按下事件处理 + * @param keyCode 按键码 + * @param event 按键事件 + * @return 事件处理结果 + */ + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_ENTER: + // 回车键由onKeyUp处理,此处返回false以便后续处理 + if (mOnTextViewChangeListener != null) return false; + break; + case KeyEvent.KEYCODE_DEL: + // 记录删除前的光标位置 + mSelectionStartBeforeDelete = getSelectionStart(); + break; + } + return super.onKeyDown(keyCode, event); + } + + /** + * 按键释放事件处理 + * @param keyCode 按键码 + * @param event 按键事件 + * @return 事件处理结果 + */ + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_DEL: + // 删除键处理:文本为空且光标在开头时,通知删除当前编辑项 + if (mOnTextViewChangeListener != null) { + if (mSelectionStartBeforeDelete == 0 && getText().length() == 0 && mIndex != 0) { + mOnTextViewChangeListener.onEditTextDelete(mIndex, getText().toString()); + return true; // 消费事件,阻止默认删除行为 + } + } + break; + case KeyEvent.KEYCODE_ENTER: + // 回车键处理:分割文本并通知添加新编辑项 + if (mOnTextViewChangeListener != null) { + int selectionStart = getSelectionStart(); + String newText = getText().subSequence(selectionStart, length()).toString(); + setText(getText().subSequence(0, selectionStart)); // 保留当前行文本 + mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, newText); // 通知添加新项 + return true; // 消费事件,阻止默认换行行为 + } + break; + } + return super.onKeyUp(keyCode, event); + } + + /** + * 焦点变化处理:通知文本状态变化 + * @param focused 是否获得焦点 + * @param direction 焦点移动方向 + * @param previouslyFocusedRect 前一个焦点位置 + */ + @Override + protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { + if (mOnTextViewChangeListener != null) { + boolean hasText = !TextUtils.isEmpty(getText()); + mOnTextViewChangeListener.onTextChange(mIndex, hasText); // 通知文本是否有内容 + } + super.onFocusChanged(focused, direction, previouslyFocusedRect); + } + + /** + * 创建上下文菜单:处理链接点击 + * @param menu 上下文菜单 + */ + @Override + protected void onCreateContextMenu(ContextMenu menu) { + super.onCreateContextMenu(menu); + Spanned text = (Spanned) getText(); + int selStart = getSelectionStart(); + int selEnd = getSelectionEnd(); + int min = Math.min(selStart, selEnd); + int max = Math.max(selStart, selEnd); + + // 获取选中区域内的URLSpan + URLSpan[] urls = text.getSpans(min, max, URLSpan.class); + if (urls.length == 1) { + String url = urls[0].getURL(); + int menuTextResId = R.string.note_link_other; // 默认菜单文本 + + // 根据链接协议设置对应的菜单文本 + for (Map.Entry entry : sSchemaActionResMap.entrySet()) { + if (url.startsWith(entry.getKey())) { + menuTextResId = entry.getValue(); + break; + } + } + + // 添加菜单选项:点击时触发链接点击事件 + menu.add(0, 0, 0, menuTextResId).setOnMenuItemClickListener(item -> { + urls[0].onClick(NoteEditText.this); // 调用URLSpan的点击处理 + return true; + }); + } + } +} \ No newline at end of file diff --git a/ui/NoteItemData.java b/ui/NoteItemData.java new file mode 100644 index 0000000..749d7cb --- /dev/null +++ b/ui/NoteItemData.java @@ -0,0 +1,203 @@ +/* + * 版权所有 (c) 2010-2011,The MiCode 开源社区 (www.micode.net) + * + * 本软件根据 Apache 许可证 2.0 版("许可证")发布; + * 除非符合许可证,否则不得使用此文件。 + * 您可以在以下网址获取许可证副本: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 除非法律要求或书面同意,软件 + * 根据许可证分发的内容按"原样"提供, + * 不附带任何明示或暗示的保证或条件。 + * 请参阅许可证,了解有关权限和限制的具体语言。 + */ + +package net.micode.notes.ui; + +import android.content.Context; +import android.database.Cursor; +import android.text.TextUtils; + +import net.micode.notes.data.Contact; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.tool.DataUtils; + +/** + * 笔记列表项数据模型 + * 功能: + * 1. 从Cursor中提取笔记/文件夹/通话记录的数据 + * 2. 提供格式化的显示数据(如联系人姓名、摘要内容) + * 3. 判断笔记在列表中的位置状态(用于背景样式处理) + * 4. 提供便捷的数据访问方法 + */ +public class NoteItemData { + // 查询投影:定义从数据库查询时需要的字段 + static final String [] PROJECTION = new String [] { + NoteColumns.ID, // 笔记/文件夹ID + NoteColumns.ALERTED_DATE, // 提醒日期 + NoteColumns.BG_COLOR_ID, // 背景颜色ID + NoteColumns.CREATED_DATE, // 创建日期 + NoteColumns.HAS_ATTACHMENT, // 是否有附件 + NoteColumns.MODIFIED_DATE, // 修改日期 + NoteColumns.NOTES_COUNT, // 子项数量(仅文件夹有效) + NoteColumns.PARENT_ID, // 父文件夹ID + NoteColumns.SNIPPET, // 摘要内容 + NoteColumns.TYPE, // 类型(笔记/文件夹/系统项) + NoteColumns.WIDGET_ID, // 桌面小部件ID + NoteColumns.WIDGET_TYPE, // 桌面小部件类型 + }; + + // 列索引常量(用于快速访问Cursor中的数据) + private static final int ID_COLUMN = 0; + private static final int ALERTED_DATE_COLUMN = 1; + private static final int BG_COLOR_ID_COLUMN = 2; + private static final int CREATED_DATE_COLUMN = 3; + private static final int HAS_ATTACHMENT_COLUMN = 4; + private static final int MODIFIED_DATE_COLUMN = 5; + private static final int NOTES_COUNT_COLUMN = 6; + private static final int PARENT_ID_COLUMN = 7; + private static final int SNIPPET_COLUMN = 8; + private static final int TYPE_COLUMN = 9; + private static final int WIDGET_ID_COLUMN = 10; + private static final int WIDGET_TYPE_COLUMN = 11; + + // 核心数据字段 + private long mId; // 笔记/文件夹ID + private long mAlertDate; // 提醒日期(毫秒) + private int mBgColorId; // 背景颜色ID + private long mCreatedDate; // 创建日期(毫秒) + private boolean mHasAttachment; // 是否有附件 + private long mModifiedDate; // 修改日期(毫秒) + private int mNotesCount; // 子项数量(文件夹包含的笔记数) + private long mParentId; // 父文件夹ID + private String mSnippet; // 摘要内容 + private int mType; // 类型(Notes.TYPE_NOTE/Notes.TYPE_FOLDER等) + private int mWidgetId; // 关联的桌面小部件ID + private int mWidgetType; // 桌面小部件类型 + + // 通话记录相关字段 + private String mName; // 联系人姓名(通话记录专用) + private String mPhoneNumber; // 电话号码(通话记录专用) + + // 位置状态字段(用于确定列表项的背景样式) + private boolean mIsLastItem; // 是否为列表最后一项 + private boolean mIsFirstItem; // 是否为列表第一项 + private boolean mIsOnlyOneItem; // 是否为列表中唯一一项 + private boolean mIsOneNoteFollowingFolder; // 是否为文件夹后的第一个笔记(且无后续笔记) + private boolean mIsMultiNotesFollowingFolder; // 是否为文件夹后的第一个笔记(且有后续笔记) + + /** + * 构造方法:从Cursor中提取数据并初始化对象 + * @param context 上下文 + * @param cursor 包含笔记数据的Cursor + */ + public NoteItemData(Context context, Cursor cursor) { + // 从Cursor中提取基本数据 + mId = cursor.getLong(ID_COLUMN); + mAlertDate = cursor.getLong(ALERTED_DATE_COLUMN); + mBgColorId = cursor.getInt(BG_COLOR_ID_COLUMN); + mCreatedDate = cursor.getLong(CREATED_DATE_COLUMN); + mHasAttachment = (cursor.getInt(HAS_ATTACHMENT_COLUMN) > 0); + mModifiedDate = cursor.getLong(MODIFIED_DATE_COLUMN); + mNotesCount = cursor.getInt(NOTES_COUNT_COLUMN); + mParentId = cursor.getLong(PARENT_ID_COLUMN); + mSnippet = cursor.getString(SNIPPET_COLUMN); + mType = cursor.getInt(TYPE_COLUMN); + mWidgetId = cursor.getInt(WIDGET_ID_COLUMN); + mWidgetType = cursor.getInt(WIDGET_TYPE_COLUMN); + + // 处理摘要内容(移除编辑标记) + mSnippet = mSnippet.replace(NoteEditActivity.TAG_CHECKED, "").replace( + NoteEditActivity.TAG_UNCHECKED, ""); + + // 通话记录特殊处理:获取联系人信息 + mPhoneNumber = ""; + if (mParentId == Notes.ID_CALL_RECORD_FOLDER) { + // 从数据库获取通话记录对应的电话号码 + mPhoneNumber = DataUtils.getCallNumberByNoteId(context.getContentResolver(), mId); + if (!TextUtils.isEmpty(mPhoneNumber)) { + // 从联系人数据库查找匹配的联系人姓名 + mName = Contact.getContact(context, mPhoneNumber); + if (mName == null) { + mName = mPhoneNumber; // 未找到联系人时使用电话号码 + } + } + } + + if (mName == null) { + mName = ""; + } + + // 检查并设置列表项的位置状态(用于背景样式) + checkPostion(cursor); + } + + /** + * 检查并设置列表项的位置状态 + * 用于确定列表项的背景样式(圆角、分隔线等) + * @param cursor 当前Cursor + */ + private void checkPostion(Cursor cursor) { + mIsLastItem = cursor.isLast(); // 是否为最后一项 + mIsFirstItem = cursor.isFirst(); // 是否为第一项 + mIsOnlyOneItem = (cursor.getCount() == 1); // 是否为唯一一项 + + mIsMultiNotesFollowingFolder = false; + mIsOneNoteFollowingFolder = false; + + // 特殊位置判断:笔记项且非第一项 + if (mType == Notes.TYPE_NOTE && !mIsFirstItem) { + int position = cursor.getPosition(); + if (cursor.moveToPrevious()) { + // 前一项是文件夹或系统项 + if (cursor.getInt(TYPE_COLUMN) == Notes.TYPE_FOLDER + || cursor.getInt(TYPE_COLUMN) == Notes.TYPE_SYSTEM) { + // 判断是否有后续笔记 + if (cursor.getCount() > (position + 1)) { + mIsMultiNotesFollowingFolder = true; // 有后续笔记 + } else { + mIsOneNoteFollowingFolder = true; // 无后续笔记 + } + } + // 恢复Cursor位置 + if (!cursor.moveToNext()) { + throw new IllegalStateException("cursor move to previous but can't move back"); + } + } + } + } + + // 位置状态判断方法(用于UI渲染时选择合适的背景样式) + public boolean isOneFollowingFolder() { return mIsOneNoteFollowingFolder; } + public boolean isMultiFollowingFolder() { return mIsMultiNotesFollowingFolder; } + public boolean isLast() { return mIsLastItem; } + public boolean isFirst() { return mIsFirstItem; } + public boolean isSingle() { return mIsOnlyOneItem; } + + // 数据访问方法 + public long getId() { return mId; } + public long getAlertDate() { return mAlertDate; } + public int getBgColorId() { return mBgColorId; } + public long getCreatedDate() { return mCreatedDate; } + public boolean hasAttachment() { return mHasAttachment; } + public long getModifiedDate() { return mModifiedDate; } + public int getNotesCount() { return mNotesCount; } + public long getParentId() { return mParentId; } + public String getSnippet() { return mSnippet; } + public int getType() { return mType; } + public int getWidgetId() { return mWidgetId; } + public int getWidgetType() { return mWidgetType; } + + // 通话记录专用方法 + public String getCallName() { return mName; } + public boolean isCallRecord() { + return (mParentId == Notes.ID_CALL_RECORD_FOLDER && !TextUtils.isEmpty(mPhoneNumber)); + } + + // 辅助方法:获取笔记类型(静态方法,无需实例化对象) + public static int getNoteType(Cursor cursor) { + return cursor.getInt(TYPE_COLUMN); + } +} \ No newline at end of file diff --git a/ui/NotesListActivity.java b/ui/NotesListActivity.java new file mode 100644 index 0000000..f218215 --- /dev/null +++ b/ui/NotesListActivity.java @@ -0,0 +1,397 @@ +/* + * 版权所有 (c) 2010-2011,The MiCode 开源社区 (www.micode.net) + * + * 本软件根据 Apache 许可证 2.0 版("许可证")发布; + * 除非符合许可证,否则不得使用此文件。 + * 您可以在以下网址获取许可证副本: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 除非法律要求或书面同意,软件 + * 根据许可证分发的内容按"原样"提供, + * 不附带任何明示或暗示的保证或条件。 + * 请参阅许可证,了解有关权限和限制的具体语言。 + */ + +package net.micode.notes.ui; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.appwidget.AppWidgetManager; +import android.content.AsyncQueryHandler; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.os.AsyncTask; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.util.Log; +import android.view.ActionMode; +import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.Display; +import android.view.HapticFeedbackConstants; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MenuItem.OnMenuItemClickListener; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnCreateContextMenuListener; +import android.view.View.OnTouchListener; +import android.view.inputmethod.InputMethodManager; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.AdapterView.OnItemLongClickListener; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.PopupMenu; +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.gtask.remote.GTaskSyncService; +import net.micode.notes.model.WorkingNote; +import net.micode.notes.tool.BackupUtils; +import net.micode.notes.tool.DataUtils; +import net.micode.notes.tool.ResourceParser; +import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute; +import net.micode.notes.widget.NoteWidgetProvider_2x; +import net.micode.notes.widget.NoteWidgetProvider_4x; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.HashSet; + +/** + * 笔记列表主活动 + * 功能: + * 1. 展示笔记、文件夹、通话记录的列表界面 + * 2. 支持多选模式下的批量操作(删除、移动) + * 3. 处理文件夹的创建、重命名、删除 + * 4. 集成同步服务(GTaskSyncService)和桌面小部件更新 + * 5. 实现数据备份与恢复(导出为文本) + * 6. 管理不同层级的列表状态(根目录、子文件夹、通话记录文件夹) + */ +public class NotesListActivity extends Activity implements OnClickListener, OnItemLongClickListener { + private static final int FOLDER_NOTE_LIST_QUERY_TOKEN = 0; // 笔记/文件夹列表查询令牌 + private static final int FOLDER_LIST_QUERY_TOKEN = 1; // 目标文件夹列表查询令牌 + private static final int MENU_FOLDER_DELETE = 0; // 文件夹删除菜单项ID + private static final int MENU_FOLDER_VIEW = 1; // 文件夹查看菜单项ID + private static final int MENU_FOLDER_CHANGE_NAME = 2; // 文件夹重命名菜单项ID + private static final String PREFERENCE_ADD_INTRODUCTION = "net.micode.notes.introduction"; // 首次启动引导标记 + private enum ListEditState { NOTE_LIST, SUB_FOLDER, CALL_RECORD_FOLDER };// 列表状态枚举 + + private ListEditState mState; // 当前列表状态 + private BackgroundQueryHandler mBackgroundQueryHandler; // 后台查询处理器 + private NotesListAdapter mNotesListAdapter; // 列表适配器 + private ListView mNotesListView; // 列表视图 + private Button mAddNewNote; // 新建笔记按钮 + private boolean mDispatch; // 触摸事件分发标记 + private int mOriginY, mDispatchY; // 触摸坐标记录 + private TextView mTitleBar; // 标题栏 + private long mCurrentFolderId; // 当前文件夹ID(根目录/子文件夹) + private ContentResolver mContentResolver; // 内容解析器 + private ModeCallback mModeCallBack; // 多选模式回调 + + private static final String TAG = "NotesListActivity"; + public static final int NOTES_LISTVIEW_SCROLL_RATE = 30; + private NoteItemData mFocusNoteDataItem; // 长按选中的笔记/文件夹数据 + + // 查询条件:普通列表(子文件夹下的项) + private static final String NORMAL_SELECTION = NoteColumns.PARENT_ID + "=?"; + // 查询条件:根目录列表(包含普通文件夹和通话记录文件夹) + private static final String ROOT_FOLDER_SELECTION = "(" + NoteColumns.TYPE + "<>" + + Notes.TYPE_SYSTEM + " AND " + NoteColumns.PARENT_ID + "=?)" + " OR (" + + NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER + " AND " + + NoteColumns.NOTES_COUNT + ">0)"; + private final static int REQUEST_CODE_OPEN_NODE = 102; // 打开笔记/文件夹请求码 + private final static int REQUEST_CODE_NEW_NODE = 103; // 新建笔记请求码 + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.note_list); + initResources(); // 初始化界面元素和数据 + + // 首次启动时插入引导笔记 + setAppInfoFromRawRes(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + // 处理笔记编辑返回结果,刷新列表 + if (resultCode == RESULT_OK && (requestCode == REQUEST_CODE_OPEN_NODE || requestCode == REQUEST_CODE_NEW_NODE)) { + mNotesListAdapter.changeCursor(null); // 触发适配器更新 + } else { + super.onActivityResult(requestCode, resultCode, data); + } + } + + /** + * 首次启动时从raw资源读取引导内容并创建引导笔记 + */ + private void setAppInfoFromRawRes() { + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); + if (!sp.getBoolean(PREFERENCE_ADD_INTRODUCTION, false)) { + StringBuilder sb = new StringBuilder(); + InputStream in = null; + try { + // 读取raw资源中的引导文本 + in = getResources().openRawResource(R.raw.introduction); + if (in != null) { + BufferedReader br = new BufferedReader(new InputStreamReader(in)); + char[] buf = new char[1024]; + int len; + while ((len = br.read(buf)) > 0) { + sb.append(buf, 0, len); + } + } + } catch (IOException e) { + e.printStackTrace(); + return; + } finally { + DataUtils.closeQuietly(in); // 关闭流工具方法 + } + + // 创建引导笔记 + WorkingNote note = WorkingNote.createEmptyNote(this, Notes.ID_ROOT_FOLDER, + AppWidgetManager.INVALID_APPWIDGET_ID, Notes.TYPE_WIDGET_INVALIDE, + ResourceParser.RED); + note.setWorkingText(sb.toString()); + if (note.saveNote()) { + sp.edit().putBoolean(PREFERENCE_ADD_INTRODUCTION, true).commit(); // 标记已创建引导笔记 + } + } + } + + @Override + protected void onStart() { + super.onStart(); + startAsyncNotesListQuery(); // 启动异步查询加载列表数据 + } + + /** + * 初始化界面元素和适配器 + */ + private void initResources() { + mContentResolver = getContentResolver(); + mBackgroundQueryHandler = new BackgroundQueryHandler(mContentResolver); + mCurrentFolderId = Notes.ID_ROOT_FOLDER; // 初始化为根目录 + + // 初始化列表视图 + mNotesListView = (ListView) findViewById(R.id.notes_list); + mNotesListView.addFooterView(LayoutInflater.from(this).inflate(R.layout.note_list_footer, null)); + mNotesListView.setOnItemClickListener(new OnListItemClickListener()); + mNotesListView.setOnItemLongClickListener(this); + + // 设置适配器 + mNotesListAdapter = new NotesListAdapter(this); + mNotesListView.setAdapter(mNotesListAdapter); + + // 新建笔记按钮 + mAddNewNote = (Button) findViewById(R.id.btn_new_note); + mAddNewNote.setOnClickListener(this); + mAddNewNote.setOnTouchListener(new NewNoteOnTouchListener()); // 自定义触摸事件处理 + + // 标题栏和状态初始化 + mTitleBar = (TextView) findViewById(R.id.tv_title_bar); + mState = ListEditState.NOTE_LIST; + mModeCallBack = new ModeCallback(); // 多选模式回调实例 + } + + /** + * 多选模式回调类(处理ActionMode相关逻辑) + */ + private class ModeCallback implements ListView.MultiChoiceModeListener, OnMenuItemClickListener { + private DropdownMenu mDropDownMenu; // 下拉菜单 + private ActionMode mActionMode; // 操作模式 + + // 创建ActionMode时初始化菜单 + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + getMenuInflater().inflate(R.menu.note_list_options, menu); + menu.findItem(R.id.delete).setOnMenuItemClickListener(this); // 删除菜单项监听 + + // 移动菜单项可见性控制(通话记录文件夹或无用户文件夹时隐藏) + mMoveMenu = menu.findItem(R.id.move); + boolean isCallRecordFolder = mFocusNoteDataItem.getParentId() == Notes.ID_CALL_RECORD_FOLDER; + boolean hasUserFolders = DataUtils.getUserFolderCount(mContentResolver) > 0; + mMoveMenu.setVisible(!isCallRecordFolder && hasUserFolders); + if (mMoveMenu.isVisible()) { + mMoveMenu.setOnMenuItemClickListener(this); // 移动菜单项监听 + } + + // 设置自定义ActionMode视图(包含下拉菜单) + View customView = LayoutInflater.from(NotesListActivity.this).inflate( + R.layout.note_list_dropdown_menu, null); + mode.setCustomView(customView); + mDropDownMenu = new DropdownMenu(NotesListActivity.this, + (Button) customView.findViewById(R.id.selection_menu), + R.menu.note_list_dropdown); + mDropDownMenu.setOnDropdownMenuItemClickListener(item -> { + mNotesListAdapter.selectAll(!mNotesListAdapter.isAllSelected()); // 切换全选状态 + updateMenu(); // 更新菜单显示 + return true; + }); + + mActionMode = mode; + mNotesListAdapter.setChoiceMode(true); // 启用多选模式 + mNotesListView.setLongClickable(false); // 禁用长按事件 + mAddNewNote.setVisibility(View.GONE); // 隐藏新建按钮 + return true; + } + + // 更新菜单显示(选中数量和全选状态) + private void updateMenu() { + int selectedCount = mNotesListAdapter.getSelectedCount(); + mDropDownMenu.setTitle(getString(R.string.menu_select_title, selectedCount)); + MenuItem selectAllItem = mDropDownMenu.findItem(R.id.action_select_all); + if (selectAllItem != null) { + boolean isAllSelected = mNotesListAdapter.isAllSelected(); + selectAllItem.setChecked(isAllSelected); + selectAllItem.setTitle(isAllSelected ? R.string.menu_deselect_all : R.string.menu_select_all); + } + } + + // 处理菜单项点击(删除、移动) + public boolean onMenuItemClick(MenuItem item) { + if (mNotesListAdapter.getSelectedCount() == 0) { + Toast.makeText(this, R.string.menu_select_none, Toast.LENGTH_SHORT).show(); + return true; + } + + switch (item.getItemId()) { + case R.id.delete: // 删除选中项 + showDeleteConfirmationDialog(); + break; + case R.id.move: // 移动选中项到目标文件夹 + startQueryDestinationFolders(); + break; + } + return true; + } + + // 省略其他MultiChoiceModeListener接口方法(见完整代码) + } + + /** + * 新建笔记按钮触摸事件处理器(处理透明区域的事件分发) + */ + private class NewNoteOnTouchListener implements OnTouchListener { + public boolean onTouch(View v, MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: { + // 计算坐标并判断是否触发事件分发(按钮透明区域逻辑) + Display display = getWindowManager().getDefaultDisplay(); + int screenHeight = display.getHeight(); + int buttonHeight = mAddNewNote.getHeight(); + int startY = screenHeight - buttonHeight; + int eventY = startY + (int) event.getY(); + + // 调整子文件夹状态下的坐标(减去标题栏高度) + if (mState == ListEditState.SUB_FOLDER) { + eventY -= mTitleBar.getHeight(); + startY -= mTitleBar.getHeight(); + } + + // 透明区域判断公式(根据UI设计硬编码,需与按钮背景匹配) + if (event.getY() < (-0.12 * event.getX() + 94)) { + View lastItem = mNotesListView.getChildAt(mNotesListView.getChildCount() - 1 + - mNotesListView.getFooterViewsCount()); + if (lastItem != null && lastItem.getBottom() > startY && lastItem.getTop() < (startY + 94)) { + mOriginY = (int) event.getY(); + mDispatchY = eventY; + event.setLocation(event.getX(), mDispatchY); + mDispatch = true; + return mNotesListView.dispatchTouchEvent(event); // 分发事件到列表视图 + } + } + break; + } + // 省略其他触摸事件处理(见完整代码) + } + return false; + } + } + + /** + * 启动异步查询加载列表数据 + */ + private void startAsyncNotesListQuery() { + String selection = (mCurrentFolderId == Notes.ID_ROOT_FOLDER) ? ROOT_FOLDER_SELECTION : NORMAL_SELECTION; + mBackgroundQueryHandler.startQuery(FOLDER_NOTE_LIST_QUERY_TOKEN, null, + Notes.CONTENT_NOTE_URI, NoteItemData.PROJECTION, selection, + new String[]{String.valueOf(mCurrentFolderId)}, + NoteColumns.TYPE + " DESC," + NoteColumns.MODIFIED_DATE + " DESC"); // 按类型降序(文件夹优先)、修改时间降序排序 + } + + /** + * 后台查询处理器(处理Cursor加载回调) + */ + private final class BackgroundQueryHandler extends AsyncQueryHandler { + public BackgroundQueryHandler(ContentResolver contentResolver) { + super(contentResolver); + } + + @Override + protected void onQueryComplete(int token, Object cookie, Cursor cursor) { + switch (token) { + case FOLDER_NOTE_LIST_QUERY_TOKEN: // 笔记/文件夹列表查询结果 + mNotesListAdapter.changeCursor(cursor); // 更新适配器数据 + break; + case FOLDER_LIST_QUERY_TOKEN: // 目标文件夹列表查询结果 + if (cursor != null && cursor.getCount() > 0) { + showFolderListMenu(cursor); // 显示文件夹选择菜单 + } + break; + } + } + } + + /** + * 显示文件夹选择菜单(用于移动操作) + */ + private void showFolderListMenu(Cursor cursor) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.menu_title_select_folder); + final FoldersListAdapter adapter = new FoldersListAdapter(this, cursor); + builder.setAdapter(adapter, (dialog, which) -> { + // 执行批量移动操作 + DataUtils.batchMoveToFolder(mContentResolver, + mNotesListAdapter.getSelectedItemIds(), adapter.getItemId(which)); + Toast.makeText(this, getString(R.string.format_move_notes_to_folder, + mNotesListAdapter.getSelectedCount(), adapter.getFolderName(this, which)), + Toast.LENGTH_SHORT).show(); + mModeCallBack.finishActionMode(); // 结束多选模式 + }); + builder.show(); + } + + /** + * 新建笔记 + */ + private void createNewNote() { + Intent intent = new Intent(this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_INSERT_OR_EDIT); + intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mCurrentFolderId); + startActivityForResult(intent, REQUEST_CODE_NEW_NODE); + } + + /** + * 批量删除选中项(异步处理,支持同步模式下移动到回收站) + */ + private void batch \ No newline at end of file diff --git a/ui/NotesListAdapter.java b/ui/NotesListAdapter.java new file mode 100644 index 0000000..d13e6f9 --- /dev/null +++ b/ui/NotesListAdapter.java @@ -0,0 +1,255 @@ +/* + * 版权所有 (c) 2010-2011,The MiCode 开源社区 (www.micode.net) + * + * 本软件根据 Apache 许可证 2.0 版("许可证")发布; + * 除非符合许可证,否则不得使用此文件。 + * 您可以在以下网址获取许可证副本: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 除非法律要求或书面同意,软件 + * 根据许可证分发的内容按"原样"提供, + * 不附带任何明示或暗示的保证或条件。 + * 请参阅许可证,了解有关权限和限制的具体语言。 + */ + +package net.micode.notes.ui; + +import android.content.Context; +import android.database.Cursor; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CursorAdapter; + +import net.micode.notes.data.Notes; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; + +/** + * 笔记列表适配器(继承自CursorAdapter) + * 功能:管理笔记列表的数据展示,支持多选模式、数据过滤和Widget相关操作 + * 职责: + * 1. 将Cursor数据转换为NotesListItem视图 + * 2. 处理多选模式下的选中状态管理 + * 3. 统计可操作的笔记数量(非文件夹项) + * 4. 提供批量操作相关接口(全选、获取选中项等) + */ +public class NotesListAdapter extends CursorAdapter { + private static final String TAG = "NotesListAdapter"; + private Context mContext; // 上下文 + private HashMap mSelectedIndex; // 选中项位置集合(键:列表位置,值:是否选中) + private int mNotesCount; // 可操作的笔记数量(非文件夹项) + private boolean mChoiceMode; // 是否为多选模式 + + // 内部类:用于封装Widget相关属性(ID和类型) + public static class AppWidgetAttribute { + public int widgetId; // Widget ID + public int widgetType; // Widget类型 + } + + /** + * 构造方法 + * @param context 上下文 + */ + public NotesListAdapter(Context context) { + super(context, null); // 调用父类构造方法(传入空Cursor,后续通过changeCursor设置) + mSelectedIndex = new HashMap(); // 初始化选中状态集合 + mContext = context; + mNotesCount = 0; // 初始化可操作笔记数量 + } + + /** + * 创建新视图:返回NotesListItem实例 + * @param context 上下文 + * @param cursor 数据Cursor + * @param parent 父视图组 + * @return 列表项视图 + */ + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return new NotesListItem(context); // 使用自定义列表项视图 + } + + /** + * 绑定视图:将Cursor数据填充到NotesListItem中 + * @param view 列表项视图 + * @param context 上下文 + * @param cursor 数据Cursor + */ + @Override + public void bindView(View view, Context context, Cursor cursor) { + if (view instanceof NotesListItem) { + // 从Cursor创建数据对象 + NoteItemData itemData = new NoteItemData(context, cursor); + // 绑定数据到视图,传入多选模式状态和选中状态 + ((NotesListItem) view).bind(context, itemData, mChoiceMode, + isSelectedItem(cursor.getPosition())); // 根据位置判断是否选中 + } + } + + /** + * 设置指定位置的选中状态 + * @param position 列表位置 + * @param checked 是否选中 + */ + public void setCheckedItem(final int position, final boolean checked) { + mSelectedIndex.put(position, checked); // 存储选中状态 + notifyDataSetChanged(); // 通知数据集变化,更新视图 + } + + /** + * 判断是否处于多选模式 + * @return true=多选模式,false=普通模式 + */ + public boolean isInChoiceMode() { + return mChoiceMode; + } + + /** + * 设置多选模式状态(同时清空选中状态) + * @param mode 多选模式开关 + */ + public void setChoiceMode(boolean mode) { + mSelectedIndex.clear(); // 清空所有选中状态 + mChoiceMode = mode; + } + + /** + * 全选/全不选操作 + * @param checked 是否全选 + */ + public void selectAll(boolean checked) { + Cursor cursor = getCursor(); // 获取当前Cursor + if (cursor == null) return; + + for (int i = 0; i < getCount(); i++) { + if (cursor.moveToPosition(i)) { // 移动到指定位置 + // 仅处理笔记类型(非文件夹) + if (NoteItemData.getNoteType(cursor) == Notes.TYPE_NOTE) { + setCheckedItem(i, checked); // 设置选中状态 + } + } + } + } + + /** + * 获取所有选中项的ID集合(仅笔记和通话记录项,排除根文件夹) + * @return 选中项ID集合 + */ + public HashSet getSelectedItemIds() { + HashSet itemSet = new HashSet(); + for (Integer position : mSelectedIndex.keySet()) { + if (mSelectedIndex.get(position)) { // 仅处理选中的位置 + Long id = getItemId(position); // 获取项ID(来自Cursor的_id字段) + if (id == Notes.ID_ROOT_FOLDER) { + // 根文件夹ID为特殊值,忽略(防止误操作) + Log.d(TAG, "Wrong item id, should not happen"); + } else { + itemSet.add(id); + } + } + } + return itemSet; + } + + /** + * 获取所有选中项的Widget属性集合(用于桌面小部件操作) + * @return Widget属性集合 + */ + public HashSet getSelectedWidget() { + HashSet itemSet = new HashSet(); + for (Integer position : mSelectedIndex.keySet()) { + if (mSelectedIndex.get(position)) { // 仅处理选中的位置 + Cursor c = (Cursor) getItem(position); // 获取对应Cursor + if (c != null) { + AppWidgetAttribute widget = new AppWidgetAttribute(); + NoteItemData item = new NoteItemData(mContext, c); + widget.widgetId = item.getWidgetId(); // 获取Widget ID + widget.widgetType = item.getWidgetType(); // 获取Widget类型 + itemSet.add(widget); + } else { + Log.e(TAG, "Invalid cursor"); + return null; // 无效Cursor时返回null + } + } + } + return itemSet; + } + + /** + * 获取选中项数量 + * @return 选中项数量 + */ + public int getSelectedCount() { + Collection values = mSelectedIndex.values(); + if (values == null) return 0; + + int count = 0; + Iterator iter = values.iterator(); + while (iter.hasNext()) { + if (iter.next()) count++; // 统计值为true的数量 + } + return count; + } + + /** + * 判断是否全选(选中数量等于可操作笔记数量) + * @return true=全选,false=未全选 + */ + public boolean isAllSelected() { + int checkedCount = getSelectedCount(); + return (checkedCount != 0 && checkedCount == mNotesCount); // 非零且等于可操作数量 + } + + /** + * 判断指定位置是否选中 + * @param position 列表位置 + * @return true=选中,false=未选中 + */ + public boolean isSelectedItem(final int position) { + // 默认返回false,若集合中无该位置则视为未选中 + return mSelectedIndex.get(position) != null && mSelectedIndex.get(position); + } + + /** + * 内容变化时的回调(当数据更新时触发) + */ + @Override + protected void onContentChanged() { + super.onContentChanged(); + calcNotesCount(); // 重新计算可操作笔记数量 + } + + /** + * 更换Cursor时的回调(如数据刷新或查询条件变更) + * @param cursor 新的Cursor + */ + @Override + public void changeCursor(Cursor cursor) { + super.changeCursor(cursor); + calcNotesCount(); // 重新计算可操作笔记数量 + } + + /** + * 计算可操作笔记数量(非文件夹项) + */ + private void calcNotesCount() { + mNotesCount = 0; + for (int i = 0; i < getCount(); i++) { + Cursor c = (Cursor) getItem(i); // 获取第i项的Cursor + if (c != null) { + // 判断是否为笔记类型(TYPE_NOTE) + if (NoteItemData.getNoteType(c) == Notes.TYPE_NOTE) { + mNotesCount++; // 累计数量 + } + } else { + Log.e(TAG, "Invalid cursor"); + return; + } + } + } +} \ No newline at end of file diff --git a/ui/NotesListItem.java b/ui/NotesListItem.java new file mode 100644 index 0000000..55f4e0e --- /dev/null +++ b/ui/NotesListItem.java @@ -0,0 +1,174 @@ +/* + * 版权所有 (c) 2010-2011,The MiCode 开源社区 (www.micode.net) + * + * 本软件根据 Apache 许可证 2.0 版("许可证")发布; + * 除非符合许可证,否则不得使用此文件。 + * 您可以在以下网址获取许可证副本: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 除非法律要求或书面同意,软件 + * 根据许可证分发的内容按"原样"提供, + * 不附带任何明示或暗示的保证或条件。 + * 请参阅许可证,了解有关权限和限制的具体语言。 + */ + +package net.micode.notes.ui; + +import android.content.Context; +import android.text.format.DateUtils; +import android.view.View; +import android.widget.CheckBox; +import android.widget.ImageView; +import android.widget.LinearLayout; +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.ResourceParser.NoteItemBgResources; + +/** + * 笔记列表项视图组件(继承自LinearLayout) + * 功能:展示单个笔记/文件夹/通话记录项的UI界面,支持不同类型数据的差异化展示 + * 包含元素: + * - 提醒图标(mAlert) + * - 标题(mTitle) + * - 时间(mTime) + * - 通话联系人(mCallName,仅通话记录项显示) + * - 多选框(mCheckBox,仅笔记项在多选模式下显示) + */ +public class NotesListItem extends LinearLayout { + // 界面元素声明 + private ImageView mAlert; // 提醒图标(闹钟/通话记录图标) + private TextView mTitle; // 标题/内容摘要 + private TextView mTime; // 最后修改时间 + private TextView mCallName; // 通话记录对应的联系人姓名 + private NoteItemData mItemData; // 绑定的数据对象 + private CheckBox mCheckBox; // 多选框(用于批量操作) + + /** + * 构造方法:初始化布局 + * @param context 上下文 + */ + public NotesListItem(Context context) { + super(context); + // 加载列表项布局(R.layout.note_item)并添加到当前视图 + inflate(context, R.layout.note_item, this); + // 初始化界面元素 + mAlert = (ImageView) findViewById(R.id.iv_alert_icon); + mTitle = (TextView) findViewById(R.id.tv_title); + mTime = (TextView) findViewById(R.id.tv_time); + mCallName = (TextView) findViewById(R.id.tv_name); + mCheckBox = (CheckBox) findViewById(android.R.id.checkbox); + } + + /** + * 绑定数据并更新界面显示 + * @param context 上下文 + * @param data 数据对象(NoteItemData) + * @param choiceMode 是否为多选模式(控制多选框显示) + * @param checked 多选框选中状态(仅在choiceMode为true时有效) + */ + 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); // 设置选中状态 + } else { + mCheckBox.setVisibility(View.GONE); // 隐藏多选框 + } + + mItemData = data; // 保存数据对象 + + // 根据不同数据类型渲染界面 + if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { + // 通话记录文件夹类型 + mCallName.setVisibility(View.GONE); // 隐藏联系人姓名 + mAlert.setVisibility(View.VISIBLE); // 显示通话记录图标 + mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); // 设置主标题样式 + // 标题格式:通话记录文件夹名称 + 文件数量 + 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.getParentId() == Notes.ID_CALL_RECORD_FOLDER) { + // 子级通话记录项(属于通话记录文件夹的子项) + mCallName.setVisibility(View.VISIBLE); // 显示联系人姓名 + mCallName.setText(data.getCallName()); // 设置联系人姓名 + mTitle.setTextAppearance(context, R.style.TextAppearanceSecondaryItem); // 设置次级标题样式 + mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); // 显示内容摘要(格式化处理) + // 处理提醒图标(闹钟) + if (data.hasAlert()) { + mAlert.setImageResource(R.drawable.clock); // 显示闹钟图标 + mAlert.setVisibility(View.VISIBLE); + } else { + mAlert.setVisibility(View.GONE); + } + } else { + // 普通笔记或文件夹类型 + mCallName.setVisibility(View.GONE); // 隐藏联系人姓名 + mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); // 设置主标题样式 + + if (data.getType() == Notes.TYPE_FOLDER) { + // 文件夹类型 + // 标题格式:文件夹名称 + 文件数量 + mTitle.setText(data.getSnippet() + + context.getString(R.style.format_folder_files_count, data.getNotesCount())); + mAlert.setVisibility(View.GONE); // 隐藏提醒图标 + } else { + // 普通笔记类型 + mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); // 显示笔记内容摘要 + // 处理提醒图标(闹钟) + if (data.hasAlert()) { + mAlert.setImageResource(R.drawable.clock); // 显示闹钟图标 + mAlert.setVisibility(View.VISIBLE); + } else { + mAlert.setVisibility(View.GONE); + } + } + } + + // 设置最后修改时间(相对时间格式,如"5分钟前") + mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate())); + + // 设置背景样式(根据笔记类型和排列位置) + setBackground(data); + } + + /** + * 设置列表项背景样式 + * @param data 数据对象 + */ + private void setBackground(NoteItemData data) { + int bgColorId = data.getBgColorId(); // 获取背景颜色ID + + if (data.getType() == Notes.TYPE_NOTE) { + // 笔记类型,根据排列位置设置不同背景 + if (data.isSingle() || data.isOneFollowingFolder()) { + // 单独项或后续仅有一个文件夹(单背景) + setBackgroundResource(NoteItemBgResources.getNoteBgSingleRes(bgColorId)); + } else if (data.isLast()) { + // 最后一项(底部圆角背景) + setBackgroundResource(NoteItemBgResources.getNoteBgLastRes(bgColorId)); + } else if (data.isFirst() || data.isMultiFollowingFolder()) { + // 第一项或后续有多个文件夹(顶部圆角背景) + setBackgroundResource(NoteItemBgResources.getNoteBgFirstRes(bgColorId)); + } else { + // 中间项(普通背景) + setBackgroundResource(NoteItemBgResources.getNoteBgNormalRes(bgColorId)); + } + } else { + // 文件夹类型,使用统一背景 + setBackgroundResource(NoteItemBgResources.getFolderBgRes()); + } + } + + /** + * 获取绑定的数据对象 + * @return NoteItemData 数据对象 + */ + public NoteItemData getItemData() { + return mItemData; + } +} \ No newline at end of file diff --git a/ui/NotesPreferenceActivity.java b/ui/NotesPreferenceActivity.java new file mode 100644 index 0000000..76ebd08 --- /dev/null +++ b/ui/NotesPreferenceActivity.java @@ -0,0 +1,442 @@ +/* + * 版权所有 (c) 2010-2011,The MiCode 开源社区 (www.micode.net) + * + * 本软件根据 Apache 许可证 2.0 版("许可证")发布; + * 除非符合许可证,否则不得使用此文件。 + * 您可以在以下网址获取许可证副本: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 除非法律要求或书面同意,软件 + * 根据许可证分发的内容按"原样"提供, + * 不附带任何明示或暗示的保证或条件。 + * 请参阅许可证,了解有关权限和限制的具体语言。 + */ + +package net.micode.notes.ui; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.app.ActionBar; +import android.app.AlertDialog; +import android.content.BroadcastReceiver; +import android.content.ContentValues; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceClickListener; +import android.preference.PreferenceActivity; +import android.preference.PreferenceCategory; +import android.text.TextUtils; +import android.text.format.DateFormat; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; +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.gtask.remote.GTaskSyncService; + +/** + * 笔记应用的设置界面Activity + * 主要功能: + * 1. 管理同步账户(Google账户) + * 2. 触发数据同步操作 + * 3. 显示同步状态和最后同步时间 + * 4. 处理账户切换/删除逻辑 + */ +public class NotesPreferenceActivity extends PreferenceActivity { + + // 偏好设置文件名 + public static final String PREFERENCE_NAME = "notes_preferences"; + + // 存储同步账户名的偏好键 + public static final String PREFERENCE_SYNC_ACCOUNT_NAME = "pref_key_account_name"; + + // 存储最后同步时间的偏好键 + public static final String PREFERENCE_LAST_SYNC_TIME = "pref_last_sync_time"; + + // 存储背景颜色随机显示的偏好键(未在代码中使用,可能为遗留字段) + public static final String PREFERENCE_SET_BG_COLOR_KEY = "pref_key_bg_random_appear"; + + // 账户管理类别在XML中的键(内部使用) + private static final String PREFERENCE_SYNC_ACCOUNT_KEY = "pref_sync_account_key"; + + // 账户过滤参数键(用于添加账户时的意图) + private static final String AUTHORITIES_FILTER_KEY = "authorities"; + + // 账户管理类别视图 + private PreferenceCategory mAccountCategory; + + // 同步服务广播接收器 + private GTaskReceiver mReceiver; + + // 原始账户列表(用于检测新增账户) + private Account[] mOriAccounts; + + // 标记是否新增了账户 + private boolean mHasAddedAccount; + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + + // 设置ActionBar导航图标(返回按钮) + ActionBar actionBar = getActionBar(); + actionBar.setDisplayHomeAsUpEnabled(true); // 启用返回按钮 + + // 加载设置界面布局(来自XML资源) + addPreferencesFromResource(R.xml.preferences); + + // 获取账户管理类别 + mAccountCategory = (PreferenceCategory) findPreference(PREFERENCE_SYNC_ACCOUNT_KEY); + + // 初始化广播接收器,监听同步服务状态 + mReceiver = new GTaskReceiver(); + IntentFilter filter = new IntentFilter(); + filter.addAction(GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME); // 监听同步服务广播 + registerReceiver(mReceiver, filter); + + mOriAccounts = null; // 初始化原始账户列表 + + // 添加设置界面头部视图 + View header = LayoutInflater.from(this).inflate(R.layout.settings_header, null); + getListView().addHeaderView(header, null, true); // 将头部添加到列表视图 + } + + @Override + protected void onResume() { + super.onResume(); + + // 自动设置同步账户(如果有新账户添加) + if (mHasAddedAccount) { + Account[] accounts = getGoogleAccounts(); // 获取所有Google账户 + // 检测是否有新账户添加(账户数量增加) + if (mOriAccounts != null && accounts.length > mOriAccounts.length) { + for (Account accountNew : accounts) { + boolean found = false; + // 检查是否为已存在的账户 + for (Account accountOld : mOriAccounts) { + if (TextUtils.equals(accountOld.name, accountNew.name)) { + found = true; + break; + } + } + if (!found) { // 发现新账户,自动设置为同步账户 + setSyncAccount(accountNew.name); + break; + } + } + } + } + + refreshUI(); // 刷新界面显示 + } + + @Override + protected void onDestroy() { + if (mReceiver != null) { + unregisterReceiver(mReceiver); // 注销广播接收器 + } + super.onDestroy(); + } + + /** + * 加载账户管理偏好项 + */ + private void loadAccountPreference() { + mAccountCategory.removeAll(); // 清空现有偏好项 + + // 创建账户选择偏好项 + Preference accountPref = new Preference(this); + String defaultAccount = getSyncAccountName(this); // 获取当前同步账户 + accountPref.setTitle(R.string.preferences_account_title); // 设置标题 + accountPref.setSummary(R.string.preferences_account_summary); // 设置摘要 + + // 点击事件处理:选择/更改同步账户 + accountPref.setOnPreferenceClickListener(new OnPreferenceClickListener() { + public boolean onPreferenceClick(Preference preference) { + if (!GTaskSyncService.isSyncing()) { // 非同步状态才能修改账户 + if (TextUtils.isEmpty(defaultAccount)) { // 首次设置账户 + showSelectAccountAlertDialog(); // 显示账户选择对话框 + } else { // 已设置账户,提示更改风险 + showChangeAccountConfirmAlertDialog(); // 显示更改确认对话框 + } + } else { // 同步中,提示无法更改 + Toast.makeText(NotesPreferenceActivity.this, + R.string.preferences_toast_cannot_change_account, + Toast.LENGTH_SHORT).show(); + } + return true; + } + }); + + mAccountCategory.addPreference(accountPref); // 添加到账户类别 + } + + /** + * 加载同步按钮和状态 + */ + private void loadSyncButton() { + Button syncButton = (Button) findViewById(R.id.preference_sync_button); + TextView lastSyncTimeView = (TextView) findViewById(R.id.prefenerece_sync_status_textview); + + // 设置按钮状态 + if (GTaskSyncService.isSyncing()) { // 同步中 + syncButton.setText(R.string.preferences_button_sync_cancel); // 显示取消按钮 + syncButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + GTaskSyncService.cancelSync(NotesPreferenceActivity.this); // 取消同步 + } + }); + } else { // 非同步状态 + syncButton.setText(R.string.preferences_button_sync_immediately); // 显示立即同步按钮 + syncButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + GTaskSyncService.startSync(NotesPreferenceActivity.this); // 启动同步 + } + }); + } + // 按钮可用性:必须有同步账户才能操作 + syncButton.setEnabled(!TextUtils.isEmpty(getSyncAccountName(this))); + + // 设置最后同步时间 + if (GTaskSyncService.isSyncing()) { // 同步中显示进度 + lastSyncTimeView.setText(GTaskSyncService.getProgressString()); + lastSyncTimeView.setVisibility(View.VISIBLE); + } else { // 显示最后同步时间或隐藏 + long lastSyncTime = getLastSyncTime(this); + if (lastSyncTime != 0) { + // 格式化时间显示 + lastSyncTimeView.setText(getString(R.string.preferences_last_sync_time, + DateFormat.format(getString(R.string.preferences_last_sync_time_format), + lastSyncTime))); + lastSyncTimeView.setVisibility(View.VISIBLE); + } else { + lastSyncTimeView.setVisibility(View.GONE); + } + } + } + + /** + * 刷新界面显示(账户和同步状态) + */ + private void refreshUI() { + loadAccountPreference(); // 刷新账户偏好项 + loadSyncButton(); // 刷新同步按钮状态 + } + + /** + * 显示选择账户对话框(首次设置或更改时) + */ + private void showSelectAccountAlertDialog() { + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); + + // 自定义对话框标题 + View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null); + TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title); + titleTextView.setText(R.string.preferences_dialog_select_account_title); // 设置标题 + TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle); + subtitleTextView.setText(R.string.preferences_dialog_select_account_tips); // 设置提示语 + dialogBuilder.setCustomTitle(titleView); // 设置自定义标题 + dialogBuilder.setPositiveButton(null, null); // 移除默认按钮 + + Account[] accounts = getGoogleAccounts(); // 获取所有Google账户 + String defAccount = getSyncAccountName(this); // 当前账户 + + mOriAccounts = accounts; // 保存原始账户列表 + mHasAddedAccount = false; // 初始化新增标记 + + if (accounts.length > 0) { + // 构建账户列表选项 + CharSequence[] items = new CharSequence[accounts.length]; + int checkedItem = -1; // 选中项索引 + int index = 0; + for (Account account : accounts) { + items[index] = account.name; // 填充账户名 + if (TextUtils.equals(account.name, defAccount)) { + checkedItem = index; // 标记当前账户为选中 + } + index++; + } + // 设置单选列表 + dialogBuilder.setSingleChoiceItems(items, checkedItem, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + setSyncAccount(items[which].toString()); // 设置选中账户 + dialog.dismiss(); // 关闭对话框 + refreshUI(); // 刷新界面 + } + }); + } + + // 添加"添加账户"自定义视图 + View addAccountView = LayoutInflater.from(this).inflate(R.layout.add_account_text, null); + dialogBuilder.setView(addAccountView); // 将视图添加到对话框 + + final AlertDialog dialog = dialogBuilder.show(); + // 点击"添加账户"视图时的处理 + addAccountView.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + mHasAddedAccount = true; // 标记新增账户操作 + // 跳转到系统添加账户界面(过滤Google账户类型) + Intent intent = new Intent("android.settings.ADD_ACCOUNT_SETTINGS"); + intent.putExtra(AUTHORITIES_FILTER_KEY, new String[] {"gmail-ls"}); + startActivityForResult(intent, -1); // 启动添加账户页面 + dialog.dismiss(); // 关闭当前对话框 + } + }); + } + + /** + * 显示更改账户确认对话框(提示数据同步风险) + */ + private void showChangeAccountConfirmAlertDialog() { + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); + + // 自定义对话框标题(显示当前账户名) + View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null); + TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title); + titleTextView.setText(getString(R.string.preferences_dialog_change_account_title, + getSyncAccountName(this))); // 标题包含当前账户名 + TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle); + subtitleTextView.setText(R.string.preferences_dialog_change_account_warn_msg); // 警告信息 + dialogBuilder.setCustomTitle(titleView); // 设置自定义标题 + + // 对话框选项:更改账户、移除账户、取消 + CharSequence[] menuItemArray = new CharSequence[] { + getString(R.string.preferences_menu_change_account), + getString(R.string.preferences_menu_remove_account), + getString(R.string.preferences_menu_cancel) + }; + dialogBuilder.setItems(menuItemArray, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + if (which == 0) { // 选择更改账户 + showSelectAccountAlertDialog(); // 显示账户选择对话框 + } else if (which == 1) { // 选择移除账户 + removeSyncAccount(); // 移除当前账户 + refreshUI(); // 刷新界面 + } + } + }); + dialogBuilder.show(); // 显示对话框 + } + + /** + * 获取所有Google账户(账户类型为com.google) + * @return Google账户数组 + */ + private Account[] getGoogleAccounts() { + AccountManager accountManager = AccountManager.get(this); + return accountManager.getAccountsByType("com.google"); // 获取指定类型账户 + } + + /** + * 设置同步账户 + * @param account 账户名(可为空) + */ + private void setSyncAccount(String account) { + if (!getSyncAccountName(this).equals(account)) { // 仅在账户变更时处理 + SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, MODE_PRIVATE); + SharedPreferences.Editor editor = settings.edit(); + editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, account); // 存储账户名 + editor.commit(); // 提交更改 + + // 重置最后同步时间 + setLastSyncTime(this, 0); + + // 异步清理本地GTask相关数据(在新线程中执行) + new Thread(new Runnable() { + public void run() { + ContentValues values = new ContentValues(); + values.put(NoteColumns.GTASK_ID, ""); // 清空GTask ID + values.put(NoteColumns.SYNC_ID, 0); // 重置同步ID + // 更新所有笔记的同步状态 + getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null); + } + }).start(); + + // 提示设置成功 + Toast.makeText(NotesPreferenceActivity.this, + getString(R.string.preferences_toast_success_set_accout, account), + Toast.LENGTH_SHORT).show(); + } + } + + /** + * 移除当前同步账户 + */ + private void removeSyncAccount() { + SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, MODE_PRIVATE); + SharedPreferences.Editor editor = settings.edit(); + // 移除账户名和最后同步时间 + if (settings.contains(PREFERENCE_SYNC_ACCOUNT_NAME)) { + editor.remove(PREFERENCE_SYNC_ACCOUNT_NAME); + } + if (settings.contains(PREFERENCE_LAST_SYNC_TIME)) { + editor.remove(PREFERENCE_LAST_SYNC_TIME); + } + editor.commit(); // 提交更改 + + // 异步清理本地GTask相关数据(同设置账户逻辑) + new Thread(new Runnable() { + public void run() { + ContentValues values = new ContentValues(); + values.put(NoteColumns.GTASK_ID, ""); + values.put(NoteColumns.SYNC_ID, 0); + getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null); + } + }).start(); + } + + // ====================== 静态工具方法 ====================== + /** + * 获取当前同步账户名 + * @param context 上下文 + * @return 账户名(空字符串表示未设置) + */ + public static String getSyncAccountName(Context context) { + SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, MODE_PRIVATE); + return settings.getString(PREFERENCE_SYNC_ACCOUNT_NAME, ""); + } + + /** + * 设置最后同步时间 + * @param context 上下文 + * @param time 时间戳(毫秒) + */ + public static void setLastSyncTime(Context context, long time) { + SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, MODE_PRIVATE); + settings.edit().putLong(PREFERENCE_LAST_SYNC_TIME, time).commit(); + } + + /** + * 获取最后同步时间 + * @param context 上下文 + * @return 时间戳(0表示未同步过) + */ + public static long getLastSyncTime(Context context) { + SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, MODE_PRIVATE); + return settings.getLong(PREFERENCE_LAST_SYNC_TIME, 0); + } + + // ====================== 广播接收器 ====================== + /** + * 监听同步服务状态变化的广播接收器 + */ + private class GTaskReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + refreshUI(); // 刷新界面显示 + + // 处理同步进度更新 + if (intent.getBooleanExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_IS_SYNCING, false)) { \ No newline at end of file diff --git a/widget/NoteWidgetProvider.java b/widget/NoteWidgetProvider.java new file mode 100644 index 0000000..adfe75f --- /dev/null +++ b/widget/NoteWidgetProvider.java @@ -0,0 +1,207 @@ +/* + * 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.widget; +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.util.Log; +import android.widget.RemoteViews; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.tool.ResourceParser; +import net.micode.notes.ui.NoteEditActivity; +import net.micode.notes.ui.NotesListActivity; + +/** + * NoteWidgetProvider 是一个抽象基类,用于实现便签应用的桌面小部件功能。 + * 它提供了小部件生命周期管理、数据查询和界面更新的核心逻辑, + * 具体的布局和样式由子类实现。 + * + * 小部件功能包括: + * - 显示便签内容和背景 + * - 支持点击便签打开编辑界面 + * - 处理小部件删除事件 + * - 支持隐私模式显示 + */ + +public abstract class NoteWidgetProvider extends AppWidgetProvider { + // 查询便签数据时使用的投影,指定需要获取的列 + public static final String [] PROJECTION = new String [] { + NoteColumns.ID, // 便签ID + NoteColumns.BG_COLOR_ID,// 便签背景颜色ID + NoteColumns.SNIPPET // 便签内容摘要 + }; + + // 投影列的索引常量,用于快速访问查询结果 + public static final int COLUMN_ID = 0; + public static final int COLUMN_BG_COLOR_ID = 1; + public static final int COLUMN_SNIPPET = 2; + + private static final String TAG = "NoteWidgetProvider"; + + /** + * 当一个或多个小部件被删除时调用。 + * 此方法会将被删除小部件关联的便签的 widget_id 字段重置为 INVALID_APPWIDGET_ID, + * 以解除便签与已删除小部件的关联。 + * + * @param context 应用上下文 + * @param appWidgetIds 被删除的小部件ID数组 + */ + @Override + public void onDeleted(Context context, int[] appWidgetIds) { + ContentValues values = new ContentValues(); + values.put(NoteColumns.WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); + // 遍历所有被删除的小部件ID,更新对应的便签记录 + for (int i = 0; i < appWidgetIds.length; i++) { + context.getContentResolver().update(Notes.CONTENT_NOTE_URI, + values, + NoteColumns.WIDGET_ID + "=?", + new String[] { String.valueOf(appWidgetIds[i])}); + } + } + + /** + * 查询与指定小部件ID关联的便签信息。 + * 只查询不在回收站中的便签。 + * + * @param context 应用上下文 + * @param widgetId 小部件ID + * @return 返回包含便签信息的Cursor,使用完后需要关闭 + */ + private Cursor getNoteWidgetInfo(Context context, int widgetId) { + return context.getContentResolver().query(Notes.CONTENT_NOTE_URI, + PROJECTION, + NoteColumns.WIDGET_ID + "=? AND " + NoteColumns.PARENT_ID + "<>?", + new String[] { String.valueOf(widgetId), String.valueOf(Notes.ID_TRASH_FOLER) }, + null); + } + + /** + * 更新一组小部件的显示内容,默认不使用隐私模式。 + * + * @param context 应用上下文 + * @param appWidgetManager AppWidgetManager实例 + * @param appWidgetIds 需要更新的小部件ID数组 + */ + protected void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + update(context, appWidgetManager, appWidgetIds, false); + } + + /** + * 更新一组小部件的显示内容,可以选择是否使用隐私模式。 + * 隐私模式下会显示通用提示信息,而不是具体的便签内容。 + * + * @param context 应用上下文 + * @param appWidgetManager AppWidgetManager实例 + * @param appWidgetIds 需要更新的小部件ID数组 + * @param privacyMode 是否使用隐私模式 + */ + private void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds, + boolean privacyMode) { + // 遍历所有需要更新的小部件ID + for (int i = 0; i < appWidgetIds.length; i++) { + if (appWidgetIds[i] != AppWidgetManager.INVALID_APPWIDGET_ID) { + // 设置默认值 + int bgId = ResourceParser.getDefaultBgId(context); + String snippet = ""; + // 创建点击便签时要启动的Intent + Intent intent = new Intent(context, NoteEditActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + intent.putExtra(Notes.INTENT_EXTRA_WIDGET_ID, appWidgetIds[i]); + intent.putExtra(Notes.INTENT_EXTRA_WIDGET_TYPE, getWidgetType()); + + // 查询与小部件关联的便签信息 + Cursor c = getNoteWidgetInfo(context, appWidgetIds[i]); + if (c != null && c.moveToFirst()) { + // 检查是否存在多个便签关联到同一个小部件ID(异常情况) + if (c.getCount() > 1) { + Log.e(TAG, "Multiple message with same widget id:" + appWidgetIds[i]); + c.close(); + return; + } + // 从Cursor中获取便签内容和背景信息 + snippet = c.getString(COLUMN_SNIPPET); + bgId = c.getInt(COLUMN_BG_COLOR_ID); + intent.putExtra(Intent.EXTRA_UID, c.getLong(COLUMN_ID)); + intent.setAction(Intent.ACTION_VIEW); + } else { + // 如果没有关联的便签,显示默认提示信息 + snippet = context.getResources().getString(R.string.widget_havenot_content); + intent.setAction(Intent.ACTION_INSERT_OR_EDIT); + } + + // 确保关闭Cursor以防止资源泄漏 + if (c != null) { + c.close(); + } + + // 创建并配置RemoteViews + RemoteViews rv = new RemoteViews(context.getPackageName(), getLayoutId()); + rv.setImageViewResource(R.id.widget_bg_image, getBgResourceId(bgId)); + intent.putExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, bgId); + // 根据是否为隐私模式设置不同的PendingIntent和显示内容 + PendingIntent pendingIntent = null; + if (privacyMode) { + // 隐私模式下显示通用提示,点击打开便签列表 + rv.setTextViewText(R.id.widget_text, + context.getString(R.string.widget_under_visit_mode)); + pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], new Intent( + context, NotesListActivity.class), PendingIntent.FLAG_UPDATE_CURRENT); + } else { + // 正常模式下显示便签内容,点击打开便签编辑界面 + rv.setTextViewText(R.id.widget_text, snippet); + pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], intent, + PendingIntent.FLAG_UPDATE_CURRENT); + } + // 设置点击事件并更新小部件 + rv.setOnClickPendingIntent(R.id.widget_text, pendingIntent); + appWidgetManager.updateAppWidget(appWidgetIds[i], rv); + } + } + } + + /** + * 获取指定背景ID对应的资源ID。 + * 由子类实现,根据不同的小部件样式返回不同的背景资源。 + * + * @param bgId 背景ID + * @return 背景资源ID + */ + protected abstract int getBgResourceId(int bgId); + + /** + * 获取小部件的布局资源ID。 + * 由子类实现,返回特定尺寸小部件的布局。 + * + * @return 布局资源ID + */ + protected abstract int getLayoutId(); + + /** + * 获取小部件类型。 + * 由子类实现,返回小部件的类型常量。 + * + * @return 小部件类型 + */ + protected abstract int getWidgetType(); +} diff --git a/widget/NoteWidgetProvider_2x.java b/widget/NoteWidgetProvider_2x.java new file mode 100644 index 0000000..fdd043d --- /dev/null +++ b/widget/NoteWidgetProvider_2x.java @@ -0,0 +1,83 @@ +/* + * 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.widget; + +import android.appwidget.AppWidgetManager; +import android.content.Context; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.tool.ResourceParser; + +/** + * NoteWidgetProvider_2x 是 NoteWidgetProvider 的具体实现类, + * 负责管理和更新 2x 尺寸的便签桌面小部件。 + * + * 2x 小部件通常指占用 2 列 x 2 行网格空间的桌面部件, + * 适合展示中等长度的便签内容。 + */ + + +public class NoteWidgetProvider_2x extends NoteWidgetProvider { + /** + * 当小部件更新时调用此方法。 + * 继承自 AppWidgetProvider,在此实现中调用父类的 update 方法处理更新逻辑。 + * + * @param context 应用上下文 + * @param appWidgetManager AppWidgetManager 实例,用于管理小部件 + * @param appWidgetIds 需要更新的小部件 ID 数组 + */ + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + // 调用父类的 update 方法处理小部件更新逻辑 + super.update(context, appWidgetManager, appWidgetIds); + } + + /** + * 获取 2x 小部件的布局资源 ID。 + * 此方法由父类抽象方法定义,返回适用于 2x 小部件的布局文件。 + * + * @return 布局资源 ID (R.layout.widget_2x) + */ + @Override + protected int getLayoutId() { + return R.layout.widget_2x; + } + + /** + * 根据背景 ID 获取对应的 2x 小部件背景资源。 + * 此方法由父类抽象方法定义,使用 ResourceParser 工具类获取适合 2x 小部件的背景图资源。 + * + * @param bgId 背景颜色 ID + * @return 对应的背景资源 ID + */ + @Override + protected int getBgResourceId(int bgId) { + return ResourceParser.WidgetBgResources.getWidget2xBgResource(bgId); + } + + /** + * 获取当前小部件的类型。 + * 此方法由父类抽象方法定义,返回标识 2x 小部件的类型常量。 + * + * @return 小部件类型常量 (Notes.TYPE_WIDGET_2X) + */ + @Override + protected int getWidgetType() { + return Notes.TYPE_WIDGET_2X; + } +} diff --git a/widget/NoteWidgetProvider_4x.java b/widget/NoteWidgetProvider_4x.java new file mode 100644 index 0000000..c25cff2 --- /dev/null +++ b/widget/NoteWidgetProvider_4x.java @@ -0,0 +1,76 @@ +/* + * 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.widget; + +import android.appwidget.AppWidgetManager; +import android.content.Context; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.tool.ResourceParser; + +/* + * NoteWidgetProvider_4x 是便签应用中 4x 尺寸桌面小部件的实现类。 + * 负责处理 4x 小部件的更新、布局和样式显示逻辑。 + * + * 4x 小部件通常占用 4 列 x 2 行的桌面空间,适合展示较长内容或更丰富的便签布局。 + */ +public class NoteWidgetProvider_4x extends NoteWidgetProvider { + /* + * 当小部件更新时调用此方法。 + * 继承自 AppWidgetProvider,实现小部件的具体更新逻辑。 + * + * @param context 应用上下文 + * @param appWidgetManager AppWidgetManager 实例 + * @param appWidgetIds 需要更新的小部件 ID 数组 + */ + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + // 调用父类的更新方法处理通用逻辑 + super.update(context, appWidgetManager, appWidgetIds); + } + + /* + * 获取 4x 小部件的布局资源 ID。 + * + * @return 返回 4x 小部件使用的布局文件资源 ID + */ + protected int getLayoutId() { + return R.layout.widget_4x; + } + + /* + * 根据背景颜色 ID 获取对应的 4x 小部件背景资源。 + * + * @param bgId 背景颜色 ID + * @return 返回对应的背景图片资源 ID + */ + @Override + protected int getBgResourceId(int bgId) { + return ResourceParser.WidgetBgResources.getWidget4xBgResource(bgId); + } + + /* + * 获取当前小部件的类型。 + * + * @return 返回标识 4x 小部件的类型常量 + */ + @Override + protected int getWidgetType() { + return Notes.TYPE_WIDGET_4X; + } +}