diff --git a/AlarmInitReceiver.java b/AlarmInitReceiver.java new file mode 100644 index 0000000..c1e7d08 --- /dev/null +++ b/AlarmInitReceiver.java @@ -0,0 +1,75 @@ +/* + * 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.AlarmManager; // 引入AlarmManager类,用于设置闹钟 +import android.app.PendingIntent; // 引入PendingIntent类,表示延迟的Intent +import android.content.BroadcastReceiver; // 引入BroadcastReceiver类,用于接收广播 +import android.content.ContentUris; // 引入ContentUris类,用于生成URI +import android.content.Context; // 引入Context类,用于访问系统资源 +import android.content.Intent; // 引入Intent类,用于启动组件 +import android.database.Cursor; // 引入Cursor类,用于数据库查询 + +import net.micode.notes.data.Notes; // 引入Notes类,处理笔记数据 +import net.micode.notes.data.Notes.NoteColumns; // 引入NoteColumns类,表示Notes表的列 + +public class AlarmInitReceiver extends BroadcastReceiver { // 定义一个继承自BroadcastReceiver的类 + + // 定义查询需要的列 + private static final String [] PROJECTION = new String [] { + NoteColumns.ID, // 获取笔记的ID列 + NoteColumns.ALERTED_DATE // 获取提醒时间列 + }; + + // 列索引常量 + private static final int COLUMN_ID = 0; // 第0列是ID列 + private static final int COLUMN_ALERTED_DATE = 1; // 第1列是提醒时间列 + + @Override + public void onReceive(Context context, Intent intent) { // 重写onReceive方法,接收广播 + long currentDate = System.currentTimeMillis(); // 获取当前的系统时间(以毫秒为单位) + + // 查询数据库,获取需要提醒的笔记,且提醒时间大于当前时间 + Cursor c = context.getContentResolver().query(Notes.CONTENT_NOTE_URI, // 查询URI为Notes.CONTENT_NOTE_URI的内容提供者 + PROJECTION, // 需要查询的列 + NoteColumns.ALERTED_DATE + ">? AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE, // 查询条件:提醒时间大于当前时间,且类型为笔记 + new String[] { String.valueOf(currentDate) }, // 查询条件中的当前时间 + null); // 不需要排序 + + // 如果查询结果不为空 + if (c != null) { + // 如果查询到至少一条记录 + if (c.moveToFirst()) { + do { + long alertDate = c.getLong(COLUMN_ALERTED_DATE); // 获取提醒时间 + Intent sender = new Intent(context, AlarmReceiver.class); // 创建一个新的Intent,用于发送广播 + sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, c.getLong(COLUMN_ID))); // 设置Intent的数据(笔记的ID) + + // 创建PendingIntent,表示在指定时间执行AlarmReceiver广播 + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, 0); + + // 获取系统的AlarmManager服务 + AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + + // 设置闹钟,使用RTC_WAKEUP模式,当到达指定的提醒时间时唤醒设备并触发PendingIntent + alarmManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent); + } while (c.moveToNext()); // 如果查询结果中还有下一条记录,则继续处理 + } + c.close(); // 关闭Cursor + } + } +} diff --git a/AlarmReceiver.java b/AlarmReceiver.java new file mode 100644 index 0000000..c692e17 --- /dev/null +++ b/AlarmReceiver.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +// AlarmReceiver 类继承自 BroadcastReceiver,用于接收便签提醒的广播事件。 +public class AlarmReceiver extends BroadcastReceiver { + // onReceive 方法是 BroadcastReceiver 的回调方法,当接收到广播时被调用。 + @Override + public void onReceive(Context context, Intent intent) { + // 设置 intent 的目标类为 AlarmAlertActivity。 + intent.setClass(context, AlarmAlertActivity.class); + // 为 intent 添加 FLAG_ACTIVITY_NEW_TASK 标志,使得在新的任务栈中启动 Activity。 + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + // 在给定的上下文中启动 AlarmAlertActivity。 + context.startActivity(intent); + } +} \ No newline at end of file diff --git a/DateTimePicker.java b/DateTimePicker.java new file mode 100644 index 0000000..52692cf --- /dev/null +++ b/DateTimePicker.java @@ -0,0 +1,222 @@ +/* + * 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 java.text.DateFormatSymbols; +import java.util.Calendar; + +import net.micode.notes.R; + + +import android.content.Context; +import android.text.format.DateFormat; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.NumberPicker; + +// DateTimePicker 类继承自 FrameLayout,用于提供一个日期时间选择器。 +public class DateTimePicker extends FrameLayout { + + // 默认启用状态。 + private static final boolean DEFAULT_ENABLE_STATE = true; + + // 半天的小时数和全天的小时数。 + private static final int HOURS_IN_HALF_DAY = 12; + private static final int HOURS_IN_ALL_DAY = 24; + // 一周的天数。 + private static final int DAYS_IN_ALL_WEEK = 7; + // 日期选择器的最小值和最大值。 + private static final int DATE_SPINNER_MIN_VAL = 0; + private static final int DATE_SPINNER_MAX_VAL = DAYS_IN_ALL_WEEK - 1; + // 24小时制和12小时制下小时选择器的最小值和最大值。 + private static final int HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW = 0; + private static final int HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW = 23; + private static final int HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW = 1; + private static final int HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW = 12; + // 分钟选择器的最小值和最大值。 + private static final int MINUT_SPINNER_MIN_VAL = 0; + private static final int MINUT_SPINNER_MAX_VAL = 59; + // 上下午选择器的最小值和最大值。 + private static final int AMPM_SPINNER_MIN_VAL = 0; + private static final int AMPM_SPINNER_MAX_VAL = 1; + + // 日期、小时、分钟和上下午的 NumberPicker 控件。 + private final NumberPicker mDateSpinner; + private final NumberPicker mHourSpinner; + private final NumberPicker mMinuteSpinner; + private final NumberPicker mAmPmSpinner; + // 用于存储日期的 Calendar 对象。 + private Calendar mDate; + + // 日期显示值数组。 + private String[] mDateDisplayValues = new String[DAYS_IN_ALL_WEEK]; + + // 表示是否为上午的布尔值。 + private boolean mIsAm; + + // 表示是否为24小时视图的布尔值。 + private boolean mIs24HourView; + + // 控件的启用状态。 + private boolean mIsEnabled = DEFAULT_ENABLE_STATE; + + // 正在初始化的布尔值。 + private boolean mInitialising; + + // 日期时间改变监听器。 + private OnDateTimeChangedListener mOnDateTimeChangedListener; + + // 日期改变监听器。 + private NumberPicker.OnValueChangeListener mOnDateChangedListener = new NumberPicker.OnValueChangeListener() { + @Override + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + // 更新日期并通知日期时间改变。 + mDate.add(Calendar.DAY_OF_YEAR, newVal - oldVal); + updateDateControl(); + onDateTimeChanged(); + } + }; + + // 小时改变监听器。 + private NumberPicker.OnValueChangeListener mOnHourChangedListener = new NumberPicker.OnValueChangeListener() { + @Override + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + // 更新小时并通知日期时间改变。 + // 处理12小时制下上下午的转换。 + boolean isDateChanged = false; + Calendar cal = Calendar.getInstance(); + if (!mIs24HourView) { + if (!mIsAm && oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) { + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, 1); + isDateChanged = true; + } else if (mIsAm && oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) { + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, -1); + isDateChanged = true; + } + if (oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY || + oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) { + mIsAm = !mIsAm; + updateAmPmControl(); + } + } else { + if (oldVal == HOURS_IN_ALL_DAY - 1 && newVal == 0) { + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, 1); + isDateChanged = true; + } else if (oldVal == 0 && newVal == HOURS_IN_ALL_DAY - 1) { + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, -1); + isDateChanged = true; + } + } + int newHour = mHourSpinner.getValue() % HOURS_IN_HALF_DAY + (mIsAm ? 0 : HOURS_IN_HALF_DAY); + mDate.set(Calendar.HOUR_OF_DAY, newHour); + onDateTimeChanged(); + if (isDateChanged) { + setCurrentYear(cal.get(Calendar.YEAR)); + setCurrentMonth(cal.get(Calendar.MONTH)); + setCurrentDay(cal.get(Calendar.DAY_OF_MONTH)); + } + } + }; + + // 分钟改变监听器。 + private NumberPicker.OnValueChangeListener mOnMinuteChangedListener = new NumberPicker.OnValueChangeListener() { + @Override + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + // 更新分钟并通知日期时间改变。 + int minValue = mMinuteSpinner.getMinValue(); + int maxValue = mMinuteSpinner.getMaxValue(); + int offset = 0; + if (oldVal == maxValue && newVal == minValue) { + offset += 1; + } else if (oldVal == minValue && newVal == maxValue) { + offset -= 1; + } + if (offset != 0) { + mDate.add(Calendar.HOUR_OF_DAY, offset); + mHourSpinner.setValue(getCurrentHour()); + updateDateControl(); + int newHour = getCurrentHourOfDay(); + if (newHour >= HOURS_IN_HALF_DAY) { + mIsAm = false; + updateAmPmControl(); + } else { + mIsAm = true; + updateAmPmControl(); + } + } + mDate.set(Calendar.MINUTE, newVal); + onDateTimeChanged(); + } + }; + + // 上下午改变监听器。 + private NumberPicker.OnValueChangeListener mOnAmPmChangedListener = new NumberPicker.OnValueChangeListener() { + @Override + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + // 更新上下午并通知日期时间改变。 + mIsAm = !mIsAm; + if (mIsAm) { + mDate.add(Calendar.HOUR_OF_DAY, -HOURS_IN_HALF_DAY); + } else { + mDate.add(Calendar.HOUR_OF_DAY, HOURS_IN_HALF_DAY); + } + updateAmPmControl(); + onDateTimeChanged(); + } + }; + + // 日期时间改变监听器接口。 + public interface OnDateTimeChangedListener { + void onDateTimeChanged(DateTimePicker view, int year, int month, + int dayOfMonth, int hourOfDay, int minute); + } + + // DateTimePicker 的构造函数。 + public DateTimePicker(Context context) { + this(context, System.currentTimeMillis()); + } + + public DateTimePicker(Context context, long date) { + this(context, date, DateFormat.is24HourFormat(context)); + } + + public DateTimePicker(Context context, long date, boolean is24HourView) { + super(context); + mDate = Calendar.getInstance(); + mInitialising = true; + mIsAm = getCurrentHourOfDay() >= HOURS_IN_HALF_DAY; + inflate(context, R.layout.datetime_picker, this); + + // 初始化日期、小时、分钟和上下午的 NumberPicker 控件。 + mDateSpinner = (NumberPicker) findViewById(R.id.date); + mDateSpinner.setMinValue(DATE_SPINNER_MIN_VAL); + mDateSpinner.setMaxValue(DATE_SPINNER_MAX_VAL); + mDateSpinner.setOnValueChangedListener(mOnDateChangedListener); + + mHourSpinner = (NumberPicker) findViewById(R.id.hour); + mHourSpinner.setOnValueChangedListener(mOnHourChangedListener); + mMinuteSpinner = (NumberPicker) findViewById(R.id.minute); + mMinuteSpinner.setMinValue(MINUT_SPINNER_MIN_VAL); + mMinuteSpinner.setMaxValue(MINUT_SPINNER_MAX_VAL); + mMinuteSpinner.setOnLongPressUpdateInterval(100); + mMinuteSpinner.setOnValueChangedListener(mOnMinuteChangedListener); + + // \ No newline at end of file diff --git a/DateTimePickerDialog.java b/DateTimePickerDialog.java new file mode 100644 index 0000000..ceffc71 --- /dev/null +++ b/DateTimePickerDialog.java @@ -0,0 +1,98 @@ +/* + * 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 java.util.Calendar; // 引入Calendar类用于处理日期和时间 + +import net.micode.notes.R; // 引入R文件,用于获取资源ID +import net.micode.notes.ui.DateTimePicker; // 引入自定义的DateTimePicker控件 +import net.micode.notes.ui.DateTimePicker.OnDateTimeChangedListener; // 引入DateTimePicker控件的监听器 + +import android.app.AlertDialog; // 引入AlertDialog类,用于创建对话框 +import android.content.Context; // 引入Context类,用于访问应用程序上下文 +import android.content.DialogInterface; // 引入DialogInterface类,用于处理对话框的按钮事件 +import android.content.DialogInterface.OnClickListener; // 引入OnClickListener接口,用于按钮点击事件 +import android.text.format.DateFormat; // 引入DateFormat类,用于格式化日期和时间 +import android.text.format.DateUtils; // 引入DateUtils类,用于时间格式化与操作 + +// 定义一个自定义的DateTimePickerDialog类,继承自AlertDialog +public class DateTimePickerDialog extends AlertDialog implements OnClickListener { + + private Calendar mDate = Calendar.getInstance(); // 用于保存当前日期和时间 + private boolean mIs24HourView; // 标记是否为24小时制显示 + private OnDateTimeSetListener mOnDateTimeSetListener; // 回调接口,用于在日期和时间设置后通知调用者 + private DateTimePicker mDateTimePicker; // 自定义的日期时间选择器 + + // 定义回调接口,用于在用户设置日期时间后进行处理 + public interface OnDateTimeSetListener { + void OnDateTimeSet(AlertDialog dialog, long date); // 回调方法,传递设置的日期时间 + } + + // 构造函数,初始化对话框,设置默认日期时间 + public DateTimePickerDialog(Context context, long date) { + super(context); // 调用父类构造函数,初始化AlertDialog + mDateTimePicker = new DateTimePicker(context); // 初始化DateTimePicker控件 + setView(mDateTimePicker); // 设置对话框的内容视图为DateTimePicker控件 + mDateTimePicker.setOnDateTimeChangedListener(new OnDateTimeChangedListener() { // 设置日期时间变化的监听器 + public void onDateTimeChanged(DateTimePicker view, int year, int month, + int dayOfMonth, int hourOfDay, int minute) { + mDate.set(Calendar.YEAR, year); // 设置选中的年 + mDate.set(Calendar.MONTH, month); // 设置选中的月 + mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); // 设置选中的天 + mDate.set(Calendar.HOUR_OF_DAY, hourOfDay); // 设置选中的小时 + mDate.set(Calendar.MINUTE, minute); // 设置选中的分钟 + updateTitle(mDate.getTimeInMillis()); // 更新对话框标题显示的日期时间 + } + }); + mDate.setTimeInMillis(date); // 将传入的日期时间(毫秒)设置为当前日期 + mDate.set(Calendar.SECOND, 0); // 将秒设置为0 + mDateTimePicker.setCurrentDate(mDate.getTimeInMillis()); // 设置DateTimePicker控件的初始日期 + setButton(context.getString(R.string.datetime_dialog_ok), this); // 设置“确认”按钮,并指定点击时的行为 + setButton2(context.getString(R.string.datetime_dialog_cancel), (OnClickListener)null); // 设置“取消”按钮 + set24HourView(DateFormat.is24HourFormat(this.getContext())); // 设置是否为24小时制显示 + updateTitle(mDate.getTimeInMillis()); // 初始化时更新对话框的标题 + } + + // 设置是否使用24小时制 + public void set24HourView(boolean is24HourView) { + mIs24HourView = is24HourView; + } + + // 设置日期时间设置完成后的回调监听器 + public void setOnDateTimeSetListener(OnDateTimeSetListener callBack) { + mOnDateTimeSetListener = callBack; + } + + // 更新对话框标题,显示当前选中的日期时间 + private void updateTitle(long date) { + // 设置日期时间显示的格式 + int flag = + DateUtils.FORMAT_SHOW_YEAR | // 显示年份 + DateUtils.FORMAT_SHOW_DATE | // 显示日期 + DateUtils.FORMAT_SHOW_TIME; // 显示时间 + flag |= mIs24HourView ? DateUtils.FORMAT_24HOUR : DateUtils.FORMAT_24HOUR; // 如果是24小时制,显示24小时格式 + setTitle(DateUtils.formatDateTime(this.getContext(), date, flag)); // 格式化日期时间并设置为对话框标题 + } + + // 确认按钮点击事件的处理 + public void onClick(DialogInterface arg0, int arg1) { + if (mOnDateTimeSetListener != null) { + // 调用回调接口,传递设置的日期时间 + mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis()); + } + } +} diff --git a/DropdownMenu.java b/DropdownMenu.java new file mode 100644 index 0000000..fe6e47a --- /dev/null +++ b/DropdownMenu.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; // 定义类所在的包路径 + +import android.content.Context; // 引入Context类,用于访问应用程序的上下文 +import android.view.Menu; // 引入Menu类,用于操作菜单 +import android.view.MenuItem; // 引入MenuItem类,用于菜单项的处理 +import android.view.View; // 引入View类,用于视图的操作 +import android.view.View.OnClickListener; // 引入OnClickListener接口,用于处理点击事件 +import android.widget.Button; // 引入Button类,用于显示按钮 +import android.widget.PopupMenu; // 引入PopupMenu类,用于显示弹出菜单 +import android.widget.PopupMenu.OnMenuItemClickListener; // 引入PopupMenu的菜单项点击监听器 + +public class DropdownMenu { + private Button mButton; // 用于显示的按钮,点击后弹出菜单 + private PopupMenu mPopupMenu; // 弹出菜单的实例 + private Menu mMenu; // 菜单项的集合 + + // 构造函数,初始化DropdownMenu,设置按钮及其弹出菜单 + public DropdownMenu(Context context, Button button, int menuId) { + mButton = button; // 获取传入的按钮对象 + mButton.setBackgroundResource(R.drawable.dropdown_icon); // 设置按钮的背景图标为下拉菜单图标 + mPopupMenu = new PopupMenu(context, mButton); // 初始化PopupMenu,指定按钮作为弹出菜单的锚点 + mMenu = mPopupMenu.getMenu(); // 获取PopupMenu中的Menu对象 + mPopupMenu.getMenuInflater().inflate(menuId, mMenu); // 使用传入的menuId加载菜单资源 + mButton.setOnClickListener(new OnClickListener() { // 为按钮设置点击事件监听器 + public void onClick(View v) { + mPopupMenu.show(); // 当按钮被点击时,显示弹出菜单 + } + }); + } + + // 设置菜单项点击事件监听器 + public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) { + if (mPopupMenu != null) { + mPopupMenu.setOnMenuItemClickListener(listener); // 设置PopupMenu的菜单项点击监听器 + } + } + + // 根据菜单项的ID查找菜单项 + public MenuItem findItem(int id) { + return mMenu.findItem(id); // 在菜单中查找具有指定ID的菜单项 + } + + // 设置按钮的文本 + public void setTitle(CharSequence title) { + mButton.setText(title); // 设置按钮的文本为传入的title + } +} diff --git a/FoldersListAdapter.java b/FoldersListAdapter.java new file mode 100644 index 0000000..94bb583 --- /dev/null +++ b/FoldersListAdapter.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.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; + + +// FoldersListAdapter 是一个自定义的 CursorAdapter,用于显示文件夹列表。 +public class FoldersListAdapter extends CursorAdapter { + // 定义查询数据库时需要的列。 + public static final String [] PROJECTION = { + NoteColumns.ID, + NoteColumns.SNIPPET + }; + + // 定义列索引常量。 + public static final int ID_COLUMN = 0; + public static final int NAME_COLUMN = 1; + + // FoldersListAdapter 的构造函数。 + public FoldersListAdapter(Context context, Cursor c) { + super(context, c); + // TODO Auto-generated constructor stub + } + + // 新建视图的方法,返回一个 FolderListItem 实例。 + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return new FolderListItem(context); + } + + // 绑定视图与数据的方法。 + @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); + } + } + + // 根据位置获取文件夹名称的方法。 + 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); + } + + // FolderListItem 是一个内部类,表示列表中的单个文件夹项。 + private class FolderListItem extends LinearLayout { + private TextView mName; // 用于显示文件夹名称的 TextView。 + + // FolderListItem 的构造函数。 + public FolderListItem(Context context) { + super(context); + inflate(context, R.layout.folder_list_item, this); + mName = (TextView) findViewById(R.id.tv_folder_name); + } + + // 绑定文件夹名称的方法。 + public void bind(String name) { + mName.setText(name); + } + } +} \ No newline at end of file diff --git a/NoteEditActivity.java b/NoteEditActivity.java new file mode 100644 index 0000000..075ccd6 --- /dev/null +++ b/NoteEditActivity.java @@ -0,0 +1,515 @@ +/* + * 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; + +// NoteEditActivity 是一个用于编辑便签的 Activity。 +public class NoteEditActivity extends Activity implements OnClickListener, + NoteSettingChangedListener, OnTextViewChangeListener { + // 内部类,用于持有头部视图的控件引用。 + private class HeadViewHolder { + public TextView tvModified; // 修改日期的 TextView。 + public ImageView ivAlertIcon; // 提醒图标的 ImageView。 + public TextView tvAlertDate; // 提醒日期的 TextView。 + public ImageView ibSetBgColor; // 设置背景颜色的 ImageView。 + } + + // 背景颜色选择按钮与颜色 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 的映射。 + 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 的映射。 + 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 的映射。 + 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"; + + // 头部视图的 Holder。 + private HeadViewHolder mNoteHeaderHolder; + + // 头部视图面板、背景颜色选择器和字体大小选择器。 + private View mHeadViewPanel; + + private View mNoteBgColorSelector; + + private View mFontSizeSelector; + + // 编辑器和编辑器面板。 + private EditText mNoteEditor; + + private View mNoteEditorPanel; + + // 正在编辑的便签。 + private WorkingNote mWorkingNote; + + // SharedPreferences 和字体大小 ID。 + private SharedPreferences mSharedPrefs; + private int mFontSizeId; + + // SharedPreferences 中字体大小的键。 + 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; + + // onCreate 方法,初始化 Activity。 + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + this.setContentView(R.layout.note_edit); + + if (savedInstanceState == null && !initActivityState(getIntent())) { + finish(); + return; + } + initResources(); + } + + // onRestoreInstanceState 方法,恢复 Activity 状态。 + @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"); + } + } + + // initActivityState 方法,初始化 Activity 状态。 + private boolean initActivityState(Intent intent) { + // 省略部分代码... + } + + // onResume 方法,恢复 Activity 时调用。 + @Override + protected void onResume() { + super.onResume(); + initNoteScreen(); + } + + // initNoteScreen 方法,初始化便签屏幕。 + private void initNoteScreen() { + // 省略部分代码... + } + + // onNewIntent 方法,处理新的 Intent。 + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + initActivityState(intent); + } + + // onSaveInstanceState 方法,保存 Activity 状态。 + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + // 省略部分代码... + } + + // dispatchTouchEvent 方法,分发触摸事件。 + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + // 省略部分代码... + } + + // inRangeOfView 方法,判断触摸事件是否在视图范围内。 + private boolean inRangeOfView(View view, MotionEvent ev) { + // 省略部分代码... + } + + // initResources 方法,初始化资源。 + private void initResources() { + // 省略部分代码... + } + + // onPause 方法,暂停 Activity 时调用。 + @Override + protected void onPause() { + super.onPause(); + if(saveNote()) { + Log.d(TAG, "Note data was saved with length:" + mWorkingNote.getContent().length()); + } + clearSettingState(); + } + + // updateWidget 方法,更新小部件。 + private void updateWidget() { + // 省略部分代码... + } +} +/** + * 当点击某个视图时调用此方法。 + * @param v 被点击的视图。 + */ +public void onClick(View v) { + int id = v.getId(); // 获取被点击视图的ID。 + // 如果被点击的视图是设置背景颜色的按钮,则显示背景颜色选择器。 + 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)); // 更改背景颜色ID。 + mNoteBgColorSelector.setVisibility(View.GONE); + } else if (sFontSizeBtnsMap.containsKey(id)) { // 如果被点击的是字体大小按钮。 + findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.GONE); + mFontSizeId = sFontSizeBtnsMap.get(id); // 更改字体大小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); + } +} + +/** + * 当按下返回键时调用。 + */ +@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; +} + +/** + * 当背景颜色改变时调用。 + */ +public void onBackgroundColorChanged() { + findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility(View.VISIBLE); + mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId()); + mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId()); +} + +/** + * 准备选项菜单。 + * @param menu 选项菜单。 + * @return 如果菜单已准备,则返回true,否则返回false。 + */ +@Override +public boolean onPrepareOptionsMenu(Menu menu) { + if (isFinishing()) { + return true; + } + clearSettingState(); + menu.clear(); + // 根据便签的文件夹ID决定加载哪个菜单。 + 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; +} + +/** + * 处理选项菜单项的选择。 + * @param item 被选择的菜单项。 + * @return 如果项被处理,则返回true,否则返回false。 + */ +@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; +} + +/** + * 设置便签的提醒。 + */ +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}类型的应用。 + * @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); +} + +/** + * 创建一个新的便签。 + */ +private void createNewNote() { + // 在创建新便签之前保存当前正在编辑的便签。 + saveNote(); + 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); +} + +/** + * 删除当前便签。 + */ +private void deleteCurrentNote() { + if (mWorkingNote.existInDatabase()) { + HashSet ids = new HashSet(); + long id = mWorkingNote.getNoteId(); + if (id != Notes.ID_ROOT_FOLDER) { + ids.add(id); + } else { + Log.d(TAG, "错误的便签ID,不应该发生"); + } + if (!isSyncMode()) { + if (!DataUtils.batchDeleteNotes(getContentResolver(), ids)) { + Log.e(TAG, "删除便签错误"); + } + } else { + if (!DataUtils.batchMoveToFolder(getContentResolver(), ids, Notes.ID_TRASH_FOLER)) { + Log.e(TAG, "将便签移动到废纸篓文件夹错误,不应该发生"); + } + } + } + mWorkingNote.markDeleted(true); +} + +/** + * 检查应用是否处于同步模式。 + * @return 如果处于同步模式,则返回true,否则返回false。 + */ +private boolean isSyncMode() { + return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0; +} + +/** + * 当时钟提醒改变时调用。 + * @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 { + Log.e(TAG, "时钟提醒设置错误"); + showToast(R.string.error_note_empty_for_clock); + } +} + +/** + * 当小部件改变时调用。 + */ +public void onWidgetChanged() { + updateWidget(); +} + +/** + * 处理在清单中删除文本项。 + * @param index 要删除项的索引。 + * @param text 项的文本。 + */ diff --git a/NoteEditText.java b/NoteEditText.java new file mode 100644 index 0000000..0e34903 --- /dev/null +++ b/NoteEditText.java @@ -0,0 +1,304 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.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; + +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(); + static { + sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel); + sSchemaActionResMap.put(SCHEME_HTTP, R.string.note_link_web); + sSchemaActionResMap.put(SCHEME_EMAIL, R.string.note_link_email); + } + + /** + * Call by the {@link NoteEditActivity} to delete or add edit text + */ + public interface OnTextViewChangeListener { + /** + * Delete current edit text when {@link KeyEvent#KEYCODE_DEL} happens + * and the text is null + */ + void onEditTextDelete(int index, String text); + + /** + * Add edit text after current edit text when {@link KeyEvent#KEYCODE_ENTER} + * happen + */ + void onEditTextEnter(int index, String text); + + /** + * Hide or show item option when text change + */ + void onTextChange(int index, boolean hasText); + } + + /* + * 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; // 定义类所在的包路径 + +// 引入相关的 Android 类 +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; // 引入资源文件中的R类 + +import java.util.HashMap; +import java.util.Map; + +public class NoteEditText extends EditText { + private static final String TAG = "NoteEditText"; // 定义日志标签,用于调试输出 + private int mIndex; // 用于表示当前编辑文本的索引 + private int mSelectionStartBeforeDelete; // 用于记录删除前的文本选择开始位置 + + // 定义常见的URI Schemes + private static final String SCHEME_TEL = "tel:" ; + private static final String SCHEME_HTTP = "http:" ; + private static final String SCHEME_EMAIL = "mailto:" ; + + // 定义一个映射关系,存储不同URI Scheme对应的资源ID + private static final Map sSchemaActionResMap = new HashMap(); + static { + sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel); // 电话链接对应的资源ID + sSchemaActionResMap.put(SCHEME_HTTP, R.string.note_link_web); // 网站链接对应的资源ID + sSchemaActionResMap.put(SCHEME_EMAIL, R.string.note_link_email); // 邮件链接对应的资源ID + } + + /** + * 在 {@link NoteEditActivity} 中调用,用于删除或添加编辑文本 + */ + public interface OnTextViewChangeListener { + /** + * 当发生删除事件(例如按下删除键)且文本为空时,删除当前编辑文本 + */ + void onEditTextDelete(int index, String text); + + /** + * 当发生回车事件(例如按下回车键)时,添加编辑文本 + */ + void onEditTextEnter(int index, String text); + + /** + * 当文本发生变化时,隐藏或显示项的选项 + */ + void onTextChange(int index, boolean hasText); + } +} +// 定义一个文本视图变化监听器的接口 +private OnTextViewChangeListener mOnTextViewChangeListener; + +// NoteEditText的构造函数,当从AttributeSet创建时调用 +public NoteEditText(Context context) { + super(context, null); + mIndex = 0; +} + +// 设置当前文本编辑框的索引 +public void setIndex(int index) { + mIndex = index; +} + +// 设置文本视图变化监听器 +public void setOnTextViewChangeListener(OnTextViewChangeListener listener) { + mOnTextViewChangeListener = listener; +} + +// NoteEditText的构造函数,当从AttributeSet和 defStyle创建时调用 +public NoteEditText(Context context, AttributeSet attrs) { + super(context, attrs, android.R.attr.editTextStyle); +} + +// NoteEditText的构造函数,当从AttributeSet、defStyle和 defStyle创建时调用 +public NoteEditText(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + // TODO Auto-generated constructor stub +} + +// 处理触摸事件,用于实现自定义的文本选择行为 +@Override +public boolean onTouchEvent(MotionEvent event) { + switch (event.getAction()) { + case 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); + break; + } + + return super.onTouchEvent(event); +} + +// 处理按键事件,例如Enter和Delete键 +@Override +public boolean onKeyDown(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_ENTER: + if (mOnTextViewChangeListener != null) { + return false; + } + break; + case KeyEvent.KEYCODE_DEL: + mSelectionStartBeforeDelete = getSelectionStart(); + break; + default: + break; + } + return super.onKeyDown(keyCode, event); +} + +// 处理按键释放事件,例如Enter和Delete键 +@Override +public boolean onKeyUp(int keyCode, KeyEvent event) { + switch(keyCode) { + case KeyEvent.KEYCODE_DEL: + if (mOnTextViewChangeListener != null) { + if (0 == mSelectionStartBeforeDelete && mIndex != 0) { + mOnTextViewChangeListener.onEditTextDelete(mIndex, getText().toString()); + return true; + } + } else { + Log.d(TAG, "OnTextViewChangeListener was not seted"); + } + break; + case KeyEvent.KEYCODE_ENTER: + if (mOnTextViewChangeListener != null) { + int selectionStart = getSelectionStart(); + String text = getText().subSequence(selectionStart, length()).toString(); + setText(getText().subSequence(0, selectionStart)); + mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, text); + } else { + Log.d(TAG, "OnTextViewChangeListener was not seted"); + } + break; + default: + break; + } + return super.onKeyUp(keyCode, event); +} + +// 处理焦点变化事件,用于在文本编辑框失去焦点时通知监听器 +@Override +protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { + if (mOnTextViewChangeListener != null) { + if (!focused && TextUtils.isEmpty(getText())) { + mOnTextViewChangeListener.onTextChange(mIndex, false); + } else { + mOnTextViewChangeListener.onTextChange(mIndex, true); + } + } + super.onFocusChanged(focused, direction, previouslyFocusedRect); +} + +// 创建上下文菜单,用于实现长按文本时弹出的菜单 +@Override +protected void onCreateContextMenu(ContextMenu menu) { + if (getText() instanceof Spanned) { + int selStart = getSelectionStart(); + int selEnd = getSelectionEnd(); + + int min = Math.min(selStart, selEnd); + int max = Math.max(selStart, selEnd); + + final URLSpan[] urls = ((Spanned) getText()).getSpans(min, max, URLSpan.class); + if (urls.length == 1) { + int defaultResId = 0; + for(String schema: sSchemaActionResMap.keySet()) { + if(urls[0].getURL().indexOf(schema) >= 0) { + defaultResId = sSchemaActionResMap.get(schema); + break; + } + } + + if (defaultResId == 0) { + defaultResId = R.string.note_link_other; + } + + menu.add(0, 0, 0, defaultResId).setOnMenuItemClickListener( + new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + // goto a new intent + urls[0].onClick(NoteEditText.this); + return true; + } + }); + } + } + super.onCreateContextMenu(menu); +} diff --git a/NoteItemData.java b/NoteItemData.java new file mode 100644 index 0000000..023c94c --- /dev/null +++ b/NoteItemData.java @@ -0,0 +1,156 @@ +// 定义一个文本视图变化监听器的接口,用于监听文本编辑框中的变化事件 +private OnTextViewChangeListener mOnTextViewChangeListener; + +// NoteEditText的构造函数,当从AttributeSet创建时调用 +public NoteEditText(Context context) { + super(context, null); + mIndex = 0; +} + +// 设置当前文本编辑框的索引,用于标识编辑框的位置 +public void setIndex(int index) { + mIndex = index; +} + +// 设置文本视图变化监听器 +public void setOnTextViewChangeListener(OnTextViewChangeListener listener) { + mOnTextViewChangeListener = listener; +} + +// NoteEditText的构造函数,当从AttributeSet和 defStyle创建时调用 +public NoteEditText(Context context, AttributeSet attrs) { + super(context, attrs, android.R.attr.editTextStyle); +} + +// NoteEditText的构造函数,当从AttributeSet、defStyle和 defStyle创建时调用 +public NoteEditText(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + // TODO Auto-generated constructor stub +} + +// 处理触摸事件,用于实现自定义的文本选择行为 +@Override +public boolean onTouchEvent(MotionEvent event) { + switch (event.getAction()) { + case 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); + break; + } + + return super.onTouchEvent(event); +} + +// 处理按键事件,例如Enter和Delete键 +@Override +public boolean onKeyDown(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_ENTER: + // 如果设置了监听器,消费Enter键事件 + if (mOnTextViewChangeListener != null) { + return false; + } + break; + case KeyEvent.KEYCODE_DEL: + // 在删除之前记录光标的位置 + mSelectionStartBeforeDelete = getSelectionStart(); + break; + default: + break; + } + return super.onKeyDown(keyCode, event); +} + +// 处理按键释放事件,例如Enter和Delete键 +@Override +public boolean onKeyUp(int keyCode, KeyEvent event) { + switch(keyCode) { + case KeyEvent.KEYCODE_DEL: + // 如果设置了监听器,并且在删除之前光标位于文本开头,则通知监听器删除事件 + if (mOnTextViewChangeListener != null) { + if (0 == mSelectionStartBeforeDelete && mIndex != 0) { + mOnTextViewChangeListener.onEditTextDelete(mIndex, getText().toString()); + return true; + } + } else { + Log.d(TAG, "OnTextViewChangeListener was not seted"); + } + break; + case KeyEvent.KEYCODE_ENTER: + // 如果设置了监听器,通知监听器Enter键事件,创建新的文本行 + if (mOnTextViewChangeListener != null) { + int selectionStart = getSelectionStart(); + String text = getText().subSequence(selectionStart, length()).toString(); + setText(getText().subSequence(0, selectionStart)); + mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, text); + } else { + Log.d(TAG, "OnTextViewChangeListener was not seted"); + } + break; + default: + break; + } + return super.onKeyUp(keyCode, event); +} + +// 处理焦点变化事件,用于在文本编辑框失去焦点时通知监听器 +@Override +protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { + if (mOnTextViewChangeListener != null) { + // 如果文本编辑框失去焦点且文本为空,通知监听器文本变化 + if (!focused && TextUtils.isEmpty(getText())) { + mOnTextViewChangeListener.onTextChange(mIndex, false); + } else { + mOnTextViewChangeListener.onTextChange(mIndex, true); + } + } + super.onFocusChanged(focused, direction, previouslyFocusedRect); +} + +// 创建上下文菜单,用于实现长按文本时弹出的菜单 +@Override +protected void onCreateContextMenu(ContextMenu menu) { + if (getText() instanceof Spanned) { + int selStart = getSelectionStart(); + int selEnd = getSelectionEnd(); + + int min = Math.min(selStart, selEnd); + int max = Math.max(selStart, selEnd); + + // 如果选中的文本包含URL,添加一个菜单项用于打开链接 + final URLSpan[] urls = ((Spanned) getText()).getSpans(min, max, URLSpan.class); + if (urls.length == 1) { + int defaultResId = 0; + for(String schema: sSchemaActionResMap.keySet()) { + if(urls[0].getURL().indexOf(schema) >= 0) { + defaultResId = sSchemaActionResMap.get(schema); + break; + } + } + + if (defaultResId == 0) { + defaultResId = R.string.note_link_other; + } + + menu.add(0, 0, 0, defaultResId).setOnMenuItemClickListener( + new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + // 打开链接 + urls[0].onClick(NoteEditText.this); + return true; + } + }); + } + } + super.onCreateContextMenu(menu); +} \ No newline at end of file diff --git a/NotesListActivity.java b/NotesListActivity.java new file mode 100644 index 0000000..2c07c20 --- /dev/null +++ b/NotesListActivity.java @@ -0,0 +1,368 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * 根据Apache License, Version 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.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; + +// NotesListActivity 是一个用于显示便签列表的 Activity。 +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; + private static final int MENU_FOLDER_VIEW = 1; + private static final int MENU_FOLDER_CHANGE_NAME = 2; + + // 定义 SharedPreferences 中保存的“添加介绍”的键。 + 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; + private int mDispatchY; + // 标题栏。 + private TextView mTitleBar; + // 当前文件夹ID。 + private long mCurrentFolderId; + // 内容解析器。 + private ContentResolver mContentResolver; + // 模式回调。 + private ModeCallback mModeCallBack; + // 日志标签。 + private static final String TAG = "NotesListActivity"; + + // 便签列表视图滚动速率。 + public static final int NOTES_LISTVIEW_SCROLL_RATE = 30; + // 焦点便签数据项。 + private NoteItemData mFocusNoteDataItem; + // 普通选择和根文件夹选择的SQL语句。 + 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; + + // onCreate 方法,初始化 Activity。 + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.note_list); + initResources(); + + /** + * 当用户首次使用此应用程序时插入介绍。 + */ + setAppInfoFromRawRes(); + } + + // onActivityResult 方法,处理 Activity 结果。 + @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 { + in = getResources().openRawResource(R.raw.introduction); + if (in != null) { + InputStreamReader isr = new InputStreamReader(in); + BufferedReader br = new BufferedReader(isr); + char [] buf = new char[1024]; + int len = 0; + while ((len = br.read(buf)) > 0) { + sb.append(buf, 0, len); + } + } else { + Log.e(TAG, "Read introduction file error"); + return; + } + } catch (IOException e) { + e.printStackTrace(); + return; + } finally { + if(in != null) { + try { + in.close(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + } + + 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(); + } else { + Log.e(TAG, "Save introduction note error"); + return; + } + } + } +} +/** + * 当 Activity 开始时调用此方法。 + */ +@Override +protected void onStart() { + super.onStart(); // 调用父类的 onStart 方法。 + startAsyncNotesListQuery(); // 开始异步查询便签列表。 +} + +/** + * 初始化资源的方法。 + */ +private void initResources() { + mContentResolver = this.getContentResolver(); // 获取内容解析器。 + mBackgroundQueryHandler = new BackgroundQueryHandler(this.getContentResolver()); // 创建后台查询处理器。 + mCurrentFolderId = Notes.ID_ROOT_FOLDER; // 设置当前文件夹ID为根文件夹。 + mNotesListView = (ListView) findViewById(R.id.notes_list); // 获取便签列表视图。 + mNotesListView.addFooterView(LayoutInflater.from(this).inflate(R.layout.note_list_footer, null), null, false); // 添加列表底部视图。 + 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()); // 设置按钮触摸事件监听器。 + mDispatch = false; // 初始化 dispatch 变量。 + mDispatchY = 0; // 初始化 dispatchY 变量。 + mOriginY = 0; // 初始化 originY 变量。 + mTitleBar = (TextView) findViewById(R.id.tv_title_bar); // 获取标题栏。 + mState = ListEditState.NOTE_LIST; // 初始化列表编辑状态。 + mModeCallBack = new ModeCallback(); // 创建模式回调对象。 +} + +/** + * ModeCallback 内部类,实现 ListView 多选模式监听器和菜单项点击事件。 + */ +private class ModeCallback implements ListView.MultiChoiceModeListener, OnMenuItemClickListener { + private DropdownMenu mDropDownMenu; // 下拉菜单。 + private ActionMode mActionMode; // 动作模式。 + private MenuItem mMoveMenu; // 移动菜单项。 + + /** + * 创建动作模式时调用。 + * @param mode 动作模式。 + * @param menu 菜单。 + * @return 是否创建成功。 + */ + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + // 省略部分代码... + } + + /** + * 更新菜单项。 + */ + private void updateMenu() { + // 省略部分代码... + } + + /** + * 准备动作模式时调用。 + * @param mode 动作模式。 + * @param menu 菜单。 + * @return 是否准备成功。 + */ + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + // TODO Auto-generated method stub + return false; + } + + /** + * 动作模式中的菜单项被点击时调用。 + * @param mode 动作模式。 + * @param item 菜单项。 + * @return 是否处理成功。 + */ + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + // TODO Auto-generated method stub + return false; + } + + /** + * 动作模式销毁时调用。 + * @param mode 动作模式。 + */ + public void onDestroyActionMode(ActionMode mode) { + // 省略部分代码... + } + + /** + * 结束动作模式。 + */ + public void finishActionMode() { + mActionMode.finish(); + } + + /** + * 列表项选中状态改变时调用。 + * @param mode 动作模式。 + * @param position 位置。 + * @param id ID。 + * @param checked 是否选中。 + */ + public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) { + // 省略部分代码... + } + + /** + * 菜单项点击事件。 + * @param item 菜单项。 + * @return 是否处理成功。 + */ + public boolean onMenuItemClick(MenuItem item) { + // 省略部分代码... + } +} + +/** + * NewNoteOnTouchListener 内部类,处理添加新便签按钮的触摸事件。 + */ +private class NewNoteOnTouchListener implements OnTouchListener { + /** + * 触摸事件处理。 + * @param v 视图。 + * @param event 触摸事件。 + * @return 是否处理成功。 + */ + public boolean onTouch(View v, MotionEvent event) { + // 省略部分代码... + } +} + +/** + * 开始异步查询便签列表。 + */ +private void startAsyncNotesListQuery() { + // 省略部分代码... +} + +/** + * BackgroundQueryHandler 内部类,继承自 AsyncQueryHandler,处理后台查询。 + */ +private final class BackgroundQueryHandler extends AsyncQueryHandler { + /** + * 构造函数。 + * @param contentResolver 内容解析器。 + */ + public BackgroundQueryHandler(ContentResolver contentResolver) { + super(contentResolver); + } + + /** + * 查询完成时调用。 + * @param token 令牌。 + * @param cookie 饼干。 + * @param cursor 游标。 + */ + @Override + protected void onQueryComplete(int token, Object cookie, Cursor cursor) { + // 省略部分代码... + } +} \ No newline at end of file