You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
git-test/src/main/java/net/micode/notes/ui/NoteEditActivity.java

1163 lines
45 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/*
* 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.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;
/**
* 笔记编辑活动类,负责处理笔记的创建、编辑和保存操作。
* <p>
* 该类实现了笔记编辑界面的所有功能,包括文本编辑、背景颜色设置、字体大小调整、
* 提醒设置、分享等功能,同时支持普通文本模式和清单模式的切换。
* </p>
*/
public class NoteEditActivity extends Activity implements OnClickListener,
NoteSettingChangedListener, OnTextViewChangeListener {
/**
* 头部视图持有者类,用于缓存头部视图的控件引用
*/
private class HeadViewHolder {
public TextView tvModified;
public ImageView ivAlertIcon;
public TextView tvAlertDate;
public ImageView ibSetBgColor;
}
/** 背景颜色选择按钮映射表将按钮ID映射到颜色ID */
private static final Map<Integer, Integer> sBgSelectorBtnsMap = new HashMap<Integer, Integer>();
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<Integer, Integer> sBgSelectorSelectionMap = new HashMap<Integer, Integer>();
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<Integer, Integer> sFontSizeBtnsMap = new HashMap<Integer, Integer>();
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<Integer, Integer> sFontSelectorSelectionMap = new HashMap<Integer, Integer>();
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;
/** 字体大小ID */
private int mFontSizeId;
/** 字体大小偏好设置键 */
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 = String.valueOf('\u221A');
/** 未选中标记 */
public static final String TAG_UNCHECKED = String.valueOf('\u25A1');
/** 清单模式下的编辑文本列表 */
private LinearLayout mEditTextList;
/** 用户查询字符串 */
private String mUserQuery;
/** 查询正则表达式模式 */
private Pattern mPattern;
/**
* 活动创建时调用,初始化界面和状态
*
* @param savedInstanceState 保存的实例状态
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setContentView(R.layout.note_edit);
if (savedInstanceState == null && !initActivityState(getIntent())) {
finish();
return;
}
initResources();
}
/**
* Current activity may be killed when the memory is low. Once it is killed, for another time
* user load this activity, we should restore the former state
*/
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
if (savedInstanceState != null && savedInstanceState.containsKey(Intent.EXTRA_UID)) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.putExtra(Intent.EXTRA_UID, savedInstanceState.getLong(Intent.EXTRA_UID));
if (!initActivityState(intent)) {
finish();
return;
}
Log.d(TAG, "Restoring from killed activity");
}
}
/**
* 初始化活动状态根据传入的Intent处理不同的操作
*
* @param intent 包含操作信息的Intent
* @return 初始化是否成功
*/
private boolean initActivityState(Intent intent) {
/**
* If the user specified the {@link Intent#ACTION_VIEW} but not provided with id,
* then jump to the NotesListActivity
*/
mWorkingNote = null;
if (TextUtils.equals(Intent.ACTION_VIEW, intent.getAction())) {
long noteId = intent.getLongExtra(Intent.EXTRA_UID, 0);
mUserQuery = "";
/**
* Starting from the searched result
*/
if (intent.hasExtra(SearchManager.EXTRA_DATA_KEY)) {
noteId = Long.parseLong(intent.getStringExtra(SearchManager.EXTRA_DATA_KEY));
mUserQuery = intent.getStringExtra(SearchManager.USER_QUERY);
}
if (!DataUtils.visibleInNoteDatabase(getContentResolver(), noteId, Notes.TYPE_NOTE)) {
Intent jump = new Intent(this, NotesListActivity.class);
startActivity(jump);
showToast(R.string.error_note_not_exist);
finish();
return false;
} else {
mWorkingNote = WorkingNote.load(this, noteId);
if (mWorkingNote == null) {
Log.e(TAG, "load note failed with note id" + noteId);
finish();
return false;
}
}
getWindow().setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN
| WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
} else if(TextUtils.equals(Intent.ACTION_INSERT_OR_EDIT, intent.getAction())) {
// New note
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));
// Parse call-record note
String phoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER);
long callDate = intent.getLongExtra(Notes.INTENT_EXTRA_CALL_DATE, 0);
if (callDate != 0 && phoneNumber != null) {
if (TextUtils.isEmpty(phoneNumber)) {
Log.w(TAG, "The call record number is null");
}
long noteId = 0;
if ((noteId = DataUtils.getNoteIdByPhoneNumberAndCallDate(getContentResolver(),
phoneNumber, callDate)) > 0) {
mWorkingNote = WorkingNote.load(this, noteId);
if (mWorkingNote == null) {
Log.e(TAG, "load call note failed with note id" + noteId);
finish();
return false;
}
} 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_ADJUST_RESIZE
| WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
} else {
Log.e(TAG, "Intent not specified action, should not support");
finish();
return false;
}
mWorkingNote.setOnSettingStatusChangedListener(this);
return true;
}
/**
* 活动恢复时调用,初始化笔记屏幕
*/
@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 id : sBgSelectorSelectionMap.keySet()) {
findViewById(sBgSelectorSelectionMap.get(id)).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));
/**
* TODO: Add the menu for setting alert. Currently disable it because the DateTimePicker
* is not ready
*/
showAlertHeader();
}
/**
* 显示提醒头部信息,包括提醒图标和提醒时间
*/
private void showAlertHeader() {
if (mWorkingNote.hasClockAlert()) {
long time = System.currentTimeMillis();
if (time > mWorkingNote.getAlertDate()) {
mNoteHeaderHolder.tvAlertDate.setText(R.string.note_alert_expired);
} else {
mNoteHeaderHolder.tvAlertDate.setText(DateUtils.getRelativeTimeSpanString(
mWorkingNote.getAlertDate(), time, 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);
};
}
/**
* 处理活动收到的新Intent
*
* @param intent 新的Intent对象
*/
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
initActivityState(intent);
}
/**
* 保存活动状态,当活动可能被销毁时调用
*
* @param outState 用于保存状态的Bundle对象
*/
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
/**
* For new note without note id, we should firstly save it to
* generate a id. If the editing note is not worth saving, there
* is no id which is equivalent to create new note
*/
if (!mWorkingNote.existInDatabase()) {
saveNote();
}
outState.putLong(Intent.EXTRA_UID, mWorkingNote.getNoteId());
Log.d(TAG, "Save working note id: " + mWorkingNote.getNoteId() + " onSaveInstanceState");
}
/**
* 分发触摸事件,当点击外部区域时关闭颜色选择器和字体大小选择器
*
* @param ev 触摸事件对象
* @return 事件是否被消费
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mNoteBgColorSelector.getVisibility() == View.VISIBLE
&& !inRangeOfView(mNoteBgColorSelector, ev)) {
mNoteBgColorSelector.setVisibility(View.GONE);
return true;
}
if (mFontSizeSelector.getVisibility() == View.VISIBLE
&& !inRangeOfView(mFontSizeSelector, ev)) {
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];
int y = location[1];
if (ev.getX() < x
|| ev.getX() > (x + view.getWidth())
|| ev.getY() < y
|| ev.getY() > (y + view.getHeight())) {
return false;
}
return true;
}
/**
* 初始化资源,包括视图、事件监听器等
*/
private void initResources() {
mHeadViewPanel = findViewById(R.id.note_title);
mNoteHeaderHolder = new HeadViewHolder();
mNoteHeaderHolder.tvModified = (TextView) findViewById(R.id.tv_modified_date);
mNoteHeaderHolder.ivAlertIcon = (ImageView) findViewById(R.id.iv_alert_icon);
mNoteHeaderHolder.tvAlertDate = (TextView) findViewById(R.id.tv_alert_date);
mNoteHeaderHolder.ibSetBgColor = (ImageView) findViewById(R.id.btn_set_bg_color);
mNoteHeaderHolder.ibSetBgColor.setOnClickListener(this);
mNoteEditor = (EditText) findViewById(R.id.note_edit_view);
mNoteEditorPanel = findViewById(R.id.sv_note_edit);
mNoteBgColorSelector = findViewById(R.id.note_bg_color_selector);
for (int id : sBgSelectorBtnsMap.keySet()) {
ImageView iv = (ImageView) findViewById(id);
iv.setOnClickListener(this);
}
mFontSizeSelector = findViewById(R.id.font_size_selector);
for (int id : sFontSizeBtnsMap.keySet()) {
View view = findViewById(id);
view.setOnClickListener(this);
};
mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
mFontSizeId = mSharedPrefs.getInt(PREFERENCE_FONT_SIZE, ResourceParser.BG_DEFAULT_FONT_SIZE);
/**
* HACKME: Fix bug of store the resource id in shared preference.
* The id may larger than the length of resources, in this case,
* return the {@link ResourceParser#BG_DEFAULT_FONT_SIZE}
*/
if(mFontSizeId >= TextAppearanceResources.getResourcesSize()) {
mFontSizeId = ResourceParser.BG_DEFAULT_FONT_SIZE;
}
mEditTextList = (LinearLayout) findViewById(R.id.note_edit_list);
}
/**
* 活动暂停时调用,保存笔记并清除设置状态
*/
@Override
protected void onPause() {
super.onPause();
if(saveNote()) {
Log.d(TAG, "Note data was saved with length:" + mWorkingNote.getContent().length());
}
clearSettingState();
}
/**
* 更新小组件,根据笔记的小组件类型发送更新广播
*/
private void updateWidget() {
Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
if (mWorkingNote.getWidgetType() == Notes.TYPE_WIDGET_2X) {
intent.setClass(this, NoteWidgetProvider_2x.class);
} else if (mWorkingNote.getWidgetType() == Notes.TYPE_WIDGET_4X) {
intent.setClass(this, NoteWidgetProvider_4x.class);
} else {
Log.e(TAG, "Unspported widget type");
return;
}
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] {
mWorkingNote.getWidgetId()
});
sendBroadcast(intent);
setResult(RESULT_OK, intent);
}
/**
* 处理视图的点击事件
*
* @param v 被点击的视图对象
*/
public void onClick(View v) {
int id = v.getId();
if (id == R.id.btn_set_bg_color) {
mNoteBgColorSelector.setVisibility(View.VISIBLE);
findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility(
- View.VISIBLE);
} else if (sBgSelectorBtnsMap.containsKey(id)) {
findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility(
View.GONE);
mWorkingNote.setBgColorId(sBgSelectorBtnsMap.get(id));
mNoteBgColorSelector.setVisibility(View.GONE);
} else if (sFontSizeBtnsMap.containsKey(id)) {
findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.GONE);
mFontSizeId = sFontSizeBtnsMap.get(id);
mSharedPrefs.edit().putInt(PREFERENCE_FONT_SIZE, mFontSizeId).commit();
findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.VISIBLE);
if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) {
getWorkingText();
switchToListMode(mWorkingNote.getContent());
} else {
mNoteEditor.setTextAppearance(this,
TextAppearanceResources.getTexAppearanceResource(mFontSizeId));
}
mFontSizeSelector.setVisibility(View.GONE);
}
}
/**
* 处理返回按钮点击事件
* <p>
* 先尝试清除设置状态,如果成功则不执行返回操作,否则保存笔记并执行返回
* </p>
*/
@Override
public void onBackPressed() {
if(clearSettingState()) {
return;
}
saveNote();
super.onBackPressed();
}
/**
* 清除设置状态,隐藏所有设置选择器
*
* @return 如果有设置选择器被隐藏则返回true否则返回false
*/
private boolean clearSettingState() {
if (mNoteBgColorSelector.getVisibility() == View.VISIBLE) {
mNoteBgColorSelector.setVisibility(View.GONE);
return true;
} else if (mFontSizeSelector.getVisibility() == View.VISIBLE) {
mFontSizeSelector.setVisibility(View.GONE);
return true;
}
return false;
}
/**
* 背景颜色改变时调用的回调方法
* <p>
* 显示当前选中的背景颜色,并更新头部面板和编辑面板的背景资源
* </p>
*/
public void onBackgroundColorChanged() {
findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility(
View.VISIBLE);
mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId());
mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId());
}
/**
* 准备选项菜单,在菜单显示前调用
* <p>
* 根据笔记类型和当前状态动态配置菜单项,包括加载不同的菜单资源、
* 设置菜单标题和控制菜单项的可见性
* </p>
*
* @param menu 要准备的菜单对象
* @return 是否成功准备菜单
*/
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
if (isFinishing()) {
return true;
}
clearSettingState();
menu.clear();
if (mWorkingNote.getFolderId() == Notes.ID_CALL_RECORD_FOLDER) {
getMenuInflater().inflate(R.menu.call_note_edit, menu);
} else {
getMenuInflater().inflate(R.menu.note_edit, menu);
}
if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) {
menu.findItem(R.id.menu_list_mode).setTitle(R.string.menu_normal_mode);
} else {
menu.findItem(R.id.menu_list_mode).setTitle(R.string.menu_list_mode);
}
if (mWorkingNote.hasClockAlert()) {
menu.findItem(R.id.menu_alert).setVisible(false);
} else {
menu.findItem(R.id.menu_delete_remind).setVisible(false);
}
return true;
}
/**
* 处理菜单选项选择事件
* <p>
* 该方法处理各种菜单选项的点击事件,包括创建新笔记、删除笔记、设置字体大小、
* 切换列表模式、分享笔记、发送到桌面、设置提醒等功能
* </p>
* @param item 被选择的菜单项
* @return 总是返回true表示事件已被处理
*/
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_new_note:
createNewNote();
break;
case R.id.menu_delete:
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(getString(R.string.alert_title_delete));
builder.setIcon(android.R.drawable.ic_dialog_alert);
builder.setMessage(getString(R.string.alert_message_delete_note));
builder.setPositiveButton(android.R.string.ok,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
deleteCurrentNote();
finish();
}
});
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
break;
case R.id.menu_font_size:
mFontSizeSelector.setVisibility(View.VISIBLE);
findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.VISIBLE);
break;
case R.id.menu_list_mode:
mWorkingNote.setCheckListMode(mWorkingNote.getCheckListMode() == 0 ?
TextNote.MODE_CHECK_LIST : 0);
break;
case R.id.menu_share:
getWorkingText();
sendTo(this, mWorkingNote.getContent());
break;
case R.id.menu_send_to_desktop:
sendToDesktop();
break;
case R.id.menu_alert:
setReminder();
break;
case R.id.menu_delete_remind:
mWorkingNote.setAlertDate(0, false);
break;
default:
break;
}
return true;
}
/**
* 设置笔记的提醒时间
* <p>
* 该方法显示一个日期时间选择对话框,让用户选择提醒时间,
* 并将选择的时间设置到当前编辑的笔记中
* </p>
*/
private void setReminder() {
DateTimePickerDialog d = new DateTimePickerDialog(this, System.currentTimeMillis());
d.setOnDateTimeSetListener(new OnDateTimeSetListener() {
public void OnDateTimeSet(AlertDialog dialog, long date) {
mWorkingNote.setAlertDate(date , true);
}
});
d.show();
}
/**
* 分享笔记到支持{@link Intent#ACTION_SEND}动作和{@text/plain}类型的应用
* <p>
* 该方法创建一个分享意图,将笔记内容发送给其他支持文本分享的应用程序
* </p>
* @param context 上下文对象
* @param info 要分享的笔记内容
*/
private void sendTo(Context context, String info) {
Intent intent = new Intent(Intent.ACTION_SEND);
intent.putExtra(Intent.EXTRA_TEXT, info);
intent.setType("text/plain");
context.startActivity(intent);
}
/**
* 创建新笔记
* <p>
* 该方法先保存当前编辑的笔记然后结束当前活动并启动一个新的NoteEditActivity
* 来创建新笔记,确保笔记保存在同一文件夹中
* </p>
*/
private void createNewNote() {
// Firstly, save current editing notes
saveNote();
// For safety, start a new NoteEditActivity
finish();
Intent intent = new Intent(this, NoteEditActivity.class);
intent.setAction(Intent.ACTION_INSERT_OR_EDIT);
intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mWorkingNote.getFolderId());
startActivity(intent);
}
/**
* 删除当前编辑的笔记
* <p>
* 该方法根据笔记是否存在于数据库中进行处理:
* 1. 如果笔记存在将其ID添加到待删除列表
* 2. 如果不是同步模式,直接删除笔记
* 3. 如果是同步模式,将笔记移动到垃圾桶
* </p>
*/
private void deleteCurrentNote() {
if (mWorkingNote.existInDatabase()) {
HashSet<Long> ids = new HashSet<Long>();
long id = mWorkingNote.getNoteId();
if (id != Notes.ID_ROOT_FOLDER) {
ids.add(id);
} else {
Log.d(TAG, "Wrong note id, should not happen");
}
if (!isSyncMode()) {
if (!DataUtils.batchDeleteNotes(getContentResolver(), ids)) {
Log.e(TAG, "Delete Note error");
}
} else {
if (!DataUtils.batchMoveToFolder(getContentResolver(), ids, Notes.ID_TRASH_FOLER)) {
Log.e(TAG, "Move notes to trash folder error, should not happens");
}
}
}
mWorkingNote.markDeleted(true);
}
/**
* 检查是否启用了同步模式
* <p>
* 该方法通过检查同步账户名称是否存在来判断是否启用了同步模式
* </p>
* @return 如果启用了同步模式返回true否则返回false
*/
private boolean isSyncMode() {
return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0;
}
/**
* 处理时钟提醒的变化
* <p>
* 该方法用于设置或取消笔记的时钟提醒,
* 如果笔记未保存,会先保存笔记,然后根据参数设置或取消闹钟
* </p>
* @param date 提醒时间的时间戳
* @param set 如果为true则设置提醒为false则取消提醒
*/
public void onClockAlertChanged(long date, boolean set) {
// 用户可能会为未保存的笔记设置时钟,所以在设置提醒时钟之前,我们应该先保存笔记
if (!mWorkingNote.existInDatabase()) {
saveNote();
}
if (mWorkingNote.getNoteId() > 0) {
Intent intent = new Intent(this, AlarmReceiver.class);
intent.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mWorkingNote.getNoteId()));
PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0);
AlarmManager alarmManager = ((AlarmManager) getSystemService(ALARM_SERVICE));
showAlertHeader();
if(!set) {
alarmManager.cancel(pendingIntent);
} else {
alarmManager.set(AlarmManager.RTC_WAKEUP, date, pendingIntent);
}
} else {
/**
* There is the condition that user has input nothing (the note is
* not worthy saving), we have no note id, remind the user that he
* should input something
*/
Log.e(TAG, "Clock alert setting error");
showToast(R.string.error_note_empty_for_clock);
}
}
/**
* 更新小部件
* <p>
* 该方法调用updateWidget()方法来更新桌面小部件的内容
* </p>
*/
public void onWidgetChanged() {
updateWidget();
}
/**
* 处理编辑文本的删除操作
* <p>
* 该方法处理用户在编辑文本时按下删除键的操作,特别是在列表模式下:
* 1. 检查是否只有一个编辑文本项,如果是则直接返回
* 2. 将索引大于当前删除项的所有编辑文本项的索引减1
* 3. 从列表中移除指定索引的视图
* 4. 将被删除的文本追加到前一个或第一个编辑文本项中
* 5. 设置焦点并将光标移动到追加文本的开始位置
* </p>
* @param index 要删除的编辑文本项的索引
* @param text 被删除的文本内容
*/
public void onEditTextDelete(int index, String text) {
int childCount = mEditTextList.getChildCount();
if (childCount == 1) {
return;
}
for (int i = index + 1; i < childCount; i++) {
((NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text))
.setIndex(i - 1);
}
mEditTextList.removeViewAt(index);
NoteEditText edit = null;
if(index == 0) {
edit = (NoteEditText) mEditTextList.getChildAt(0).findViewById(
R.id.et_edit_text);
} else {
edit = (NoteEditText) mEditTextList.getChildAt(index - 1).findViewById(
R.id.et_edit_text);
}
int length = edit.length();
edit.append(text);
edit.requestFocus();
edit.setSelection(length);
}
/**
* 处理编辑文本的回车操作
* <p>
* 该方法处理用户在编辑文本时按下回车键的操作,在指定索引位置插入新的编辑文本项
* </p>
* @param index 插入新编辑文本项的索引位置
* @param text 新编辑文本项的内容
*/
public void onEditTextEnter(int index, String text) {
// 不应该发生的情况,用于调试检查
if(index > mEditTextList.getChildCount()) {
Log.e(TAG, "Index out of mEditTextList boundrary, should not happen");
}
View view = getListItem(text, index);
mEditTextList.addView(view, index);
NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text);
edit.requestFocus();
edit.setSelection(0);
for (int i = index + 1; i < mEditTextList.getChildCount(); i++) {
((NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text))
.setIndex(i);
}
}
/**
* 切换到列表模式
* <p>
* 该方法将笔记从普通文本模式切换到列表模式,
* 将文本内容按行分割并创建对应的列表项,最后添加一个空的列表项用于继续输入
* </p>
* @param text 当前笔记的文本内容
*/
private void switchToListMode(String text) {
mEditTextList.removeAllViews();
String[] items = text.split("\n");
int index = 0;
for (String item : items) {
if(!TextUtils.isEmpty(item)) {
mEditTextList.addView(getListItem(item, index));
index++;
}
}
mEditTextList.addView(getListItem("", index));
mEditTextList.getChildAt(index).findViewById(R.id.et_edit_text).requestFocus();
mNoteEditor.setVisibility(View.GONE);
mEditTextList.setVisibility(View.VISIBLE);
}
/**
* 获取带有高亮查询结果的文本
* <p>
* 该方法在完整文本中查找用户查询的内容,并为匹配项添加背景高亮效果
* </p>
* @param fullText 完整的文本内容
* @param userQuery 用户查询的内容
* @return 带有高亮查询结果的Spannable对象
*/
private Spannable getHighlightQueryResult(String fullText, String userQuery) {
SpannableString spannable = new SpannableString(fullText == null ? "" : fullText);
if (!TextUtils.isEmpty(userQuery)) {
mPattern = Pattern.compile(userQuery);
Matcher m = mPattern.matcher(fullText);
int start = 0;
while (m.find(start)) {
spannable.setSpan(
new BackgroundColorSpan(this.getResources().getColor(
R.color.user_query_highlight)), m.start(), m.end(),
Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
start = m.end();
}
}
return spannable;
}
/**
* 获取列表模式下的列表项视图
* <p>
* 该方法创建并配置列表模式下的列表项视图,包括:
* 1. 加载列表项布局
* 2. 设置编辑文本的字体大小
* 3. 配置复选框的选中状态变化监听器
* 4. 根据项目文本的标签设置初始选中状态和文本样式
* 5. 设置文本变化监听器和索引
* 6. 应用查询高亮效果
* </p>
* @param item 列表项的文本内容
* @param index 列表项的索引
* @return 配置好的列表项视图
*/
private View getListItem(String item, int index) {
View view = LayoutInflater.from(this).inflate(R.layout.note_edit_list_item, null);
final NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text);
edit.setTextAppearance(this, TextAppearanceResources.getTexAppearanceResource(mFontSizeId));
CheckBox cb = ((CheckBox) view.findViewById(R.id.cb_edit_item));
cb.setOnCheckedChangeListener(new OnCheckedChangeListener() {
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
edit.setPaintFlags(edit.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
} else {
edit.setPaintFlags(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG);
}
}
});
if (item.startsWith(TAG_CHECKED)) {
cb.setChecked(true);
edit.setPaintFlags(edit.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
item = item.substring(TAG_CHECKED.length(), item.length()).trim();
} else if (item.startsWith(TAG_UNCHECKED)) {
cb.setChecked(false);
edit.setPaintFlags(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG);
item = item.substring(TAG_UNCHECKED.length(), item.length()).trim();
}
edit.setOnTextViewChangeListener(this);
edit.setIndex(index);
edit.setText(getHighlightQueryResult(item, mUserQuery));
return view;
}
/**
* 处理文本变化事件
* <p>
* 该方法在列表模式下处理编辑文本的内容变化事件,
* 根据文本是否为空来控制对应列表项中复选框的显示或隐藏
* </p>
* @param index 编辑文本项的索引
* @param hasText 编辑文本是否包含内容
*/
public void onTextChange(int index, boolean hasText) {
if (index >= mEditTextList.getChildCount()) {
Log.e(TAG, "Wrong index, should not happen");
return;
}
if(hasText) {
mEditTextList.getChildAt(index).findViewById(R.id.cb_edit_item).setVisibility(View.VISIBLE);
} else {
mEditTextList.getChildAt(index).findViewById(R.id.cb_edit_item).setVisibility(View.GONE);
}
}
/**
* 处理检查列表模式的变化
* <p>
* 该方法在笔记的普通文本模式和检查列表模式之间切换时被调用,
* 根据新的模式执行相应的视图切换和数据处理操作
* </p>
* @param oldMode 旧的模式
* @param newMode 新的模式
*/
public void onCheckListModeChanged(int oldMode, int newMode) {
if (newMode == TextNote.MODE_CHECK_LIST) {
switchToListMode(mNoteEditor.getText().toString());
} else {
if (!getWorkingText()) {
mWorkingNote.setWorkingText(mWorkingNote.getContent().replace(TAG_UNCHECKED + " ",
""));
}
mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery));
mEditTextList.setVisibility(View.GONE);
mNoteEditor.setVisibility(View.VISIBLE);
}
}
/**
* 获取当前工作笔记的文本内容
* <p>
* 该方法根据当前笔记的模式(普通文本或检查列表)获取相应的文本内容:
* 1. 如果是检查列表模式,遍历所有列表项,收集文本内容并添加相应的标签(已完成或未完成)
* 2. 如果是普通文本模式,直接获取编辑框的文本内容
* </p>
* @return 是否包含已完成的列表项
*/
private boolean getWorkingText() {
boolean hasChecked = false;
if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < mEditTextList.getChildCount(); i++) {
View view = mEditTextList.getChildAt(i);
NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text);
if (!TextUtils.isEmpty(edit.getText())) {
if (((CheckBox) view.findViewById(R.id.cb_edit_item)).isChecked()) {
sb.append(TAG_CHECKED).append(" ").append(edit.getText()).append("\n");
hasChecked = true;
} else {
sb.append(TAG_UNCHECKED).append(" ").append(edit.getText()).append("\n");
}
}
}
mWorkingNote.setWorkingText(sb.toString());
} else {
mWorkingNote.setWorkingText(mNoteEditor.getText().toString());
}
return hasChecked;
}
/**
* 保存当前工作笔记
* <p>
* 该方法首先获取当前工作笔记的最新文本内容然后调用mWorkingNote的saveNote方法将其保存到数据库中。
* 如果保存成功会设置返回结果为RESULT_OK用于标识创建/编辑状态,以便在返回列表视图时正确定位。
* </p>
* @return 保存是否成功
*/
private boolean saveNote() {
getWorkingText();
boolean saved = mWorkingNote.saveNote();
if (saved) {
/**
* There are two modes from List view to edit view, open one note,
* create/edit a node. Opening node requires to the original
* position in the list when back from edit view, while creating a
* new node requires to the top of the list. This code
* {@link #RESULT_OK} is used to identify the create/edit state
*/
setResult(RESULT_OK);
}
return saved;
}
/**
* 将当前编辑的笔记发送到桌面创建快捷方式
* <p>
* 该方法执行以下操作:
* 1. 在发送前确保当前编辑的笔记已保存在数据库中,如果是新笔记则先保存
* 2. 创建一个快捷方式Intent包含打开该笔记的相关信息
* 3. 设置快捷方式的名称、图标等属性
* 4. 发送广播来创建桌面快捷方式
* 5. 如果笔记为空或保存失败,显示错误提示
* </p>
*/
private void sendToDesktop() {
// 在发送到桌面之前,确保当前编辑的笔记已存在于数据库中。对于新笔记,首先保存它
if (!mWorkingNote.existInDatabase()) {
saveNote();
}
if (mWorkingNote.getNoteId() > 0) {
Intent sender = new Intent();
Intent shortcutIntent = new Intent(this, NoteEditActivity.class);
shortcutIntent.setAction(Intent.ACTION_VIEW);
shortcutIntent.putExtra(Intent.EXTRA_UID, mWorkingNote.getNoteId());
sender.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
sender.putExtra(Intent.EXTRA_SHORTCUT_NAME,
makeShortcutIconTitle(mWorkingNote.getContent()));
sender.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
Intent.ShortcutIconResource.fromContext(this, R.drawable.icon_app));
sender.putExtra("duplicate", true);
sender.setAction("com.android.launcher.action.INSTALL_SHORTCUT");
showToast(R.string.info_note_enter_desktop);
sendBroadcast(sender);
} else {
/**
* There is the condition that user has input nothing (the note is
* not worthy saving), we have no note id, remind the user that he
* should input something
*/
Log.e(TAG, "Send to desktop error");
showToast(R.string.error_note_empty_for_send_to_desktop);
}
}
/**
* 生成快捷方式图标的标题
* <p>
* 该方法将笔记内容转换为适合作为快捷方式标题的格式:
* 1. 移除检查列表模式下的状态标签(已完成和未完成标签)
* 2. 如果内容长度超过最大限制,则截断为指定长度
* </p>
* @param content 笔记的原始内容
* @return 处理后的快捷方式标题
*/
private String makeShortcutIconTitle(String content) {
content = content.replace(TAG_CHECKED, "");
content = content.replace(TAG_UNCHECKED, "");
return content.length() > SHORTCUT_ICON_TITLE_MAX_LEN ? content.substring(0,
SHORTCUT_ICON_TITLE_MAX_LEN) : content;
}
/**
* 显示短时长的Toast提示信息
* <p>
* 该方法是showToast(int resId, int duration)的便捷重载版本,
* 默认使用Toast.LENGTH_SHORT作为显示时长
* </p>
* @param resId 提示信息的字符串资源ID
*/
private void showToast(int resId) {
showToast(resId, Toast.LENGTH_SHORT);
}
/**
* 显示Toast提示信息
* <p>
* 该方法使用Android系统的Toast机制显示指定的提示信息
* </p>
* @param resId 提示信息的字符串资源ID
* @param duration 显示时长可以是Toast.LENGTH_SHORT或Toast.LENGTH_LONG
*/
private void showToast(int resId, int duration) {
Toast.makeText(this, resId, duration).show();
}
}