From 1fbc2e9fee438e4038d6668f63d36a4fc89ec08d Mon Sep 17 00:00:00 2001 From: yolo <2915594363@qq.com> Date: Thu, 12 Dec 2024 20:36:41 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/AlarmAlertActivity.java | 166 +++++++ ui/AlarmInitReceiver.java | 59 +++ ui/AlarmReceiver.java | 20 + ui/DateTimePicker.java | 488 ++++++++++++++++++++ ui/DateTimePickerDialog.java | 91 ++++ ui/DropdownMenu.java | 58 +++ ui/FoldersListAdapter.java | 132 ++++++ ui/NoteEditActivity.java | 851 +++++++++++++++++++++++++++++++++++ 8 files changed, 1865 insertions(+) create mode 100644 ui/AlarmAlertActivity.java create mode 100644 ui/AlarmInitReceiver.java create mode 100644 ui/AlarmReceiver.java create mode 100644 ui/DateTimePicker.java create mode 100644 ui/DateTimePickerDialog.java create mode 100644 ui/DropdownMenu.java create mode 100644 ui/FoldersListAdapter.java create mode 100644 ui/NoteEditActivity.java diff --git a/ui/AlarmAlertActivity.java b/ui/AlarmAlertActivity.java new file mode 100644 index 0000000..4affc83 --- /dev/null +++ b/ui/AlarmAlertActivity.java @@ -0,0 +1,166 @@ +package net.micode.notes.ui; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.DialogInterface.OnDismissListener; +import android.content.Intent; +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.media.RingtoneManager; +import android.net.Uri; +import android.os.Bundle; +import android.os.PowerManager; +import android.provider.Settings; +import android.view.Window; +import android.view.WindowManager; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.tool.DataUtils; + +import java.io.IOException; + +// 闹钟提醒活动类,用于显示闹钟提醒并播放提醒声音 +public class AlarmAlertActivity extends Activity implements OnClickListener, OnDismissListener { + + // 笔记ID + private long mNoteId; + // 笔记片段 + private String mSnippet; + // 预览片段的最大长度 + private static final int SNIPPET_PREW_MAX_LEN = 60; + // 媒体播放器,用于播放提醒声音 + MediaPlayer mPlayer; + + // 活动创建时调用 + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // 设置无标题栏 + requestWindowFeature(Window.FEATURE_NO_TITLE); + final Window win = getWindow(); + // 设置在锁屏时也显示 + win.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); + // 如果屏幕未点亮,添加保持屏幕亮起等相关标志 + if (!isScreenOn()) { + win.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON + | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON + | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON + | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR); + } + + Intent intent = getIntent(); + try { + // 从意图中获取笔记ID + mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1)); + // 根据笔记ID获取笔记片段 + mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId); + // 如果片段长度超过最大预览长度,截断并添加提示信息 + mSnippet = mSnippet.length() > SNIPPET_PREW_MAX_LEN? mSnippet.substring(0, + SNIPPET_PREW_MAX_LEN) + getResources().getString(R.string.notelist_string_info) + : mSnippet; + } catch (IllegalArgumentException e) { + e.printStackTrace(); + return; + } + + mPlayer = new MediaPlayer(); + // 如果笔记在数据库中可见(类型为笔记且不在回收站),则显示操作对话框并播放提醒声音 + if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) { + showActionDialog(); + playAlarmSound(); + } else { + // 否则结束活动 + finish(); + } + } + + // 判断屏幕是否点亮 + private boolean isScreenOn() { + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + return pm.isScreenOn(); + } + + // 播放提醒声音 + private void playAlarmSound() { + // 获取默认闹钟铃声的Uri + Uri url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM); + int silentModeStreams = Settings.System.getInt(getContentResolver(), + Settings.System.MODE_RINGER_STREAMS_AFFECTED, 0); + // 根据静音模式设置音频流类型 + if ((silentModeStreams & (1 << AudioManager.STREAM_ALARM))!= 0) { + mPlayer.setAudioStreamType(silentModeStreams); + } else { + mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM); + } + try { + // 设置媒体播放器的数据源为铃声Uri + mPlayer.setDataSource(this, url); + // 准备播放 + mPlayer.prepare(); + // 设置循环播放 + mPlayer.setLooping(true); + // 开始播放 + mPlayer.start(); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + } catch (SecurityException e) { + e.printStackTrace(); + } catch (IllegalStateException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + // 显示操作对话框 + private void showActionDialog() { + AlertDialog.Builder dialog = new AlertDialog.Builder(this); + // 设置对话框标题为应用名称 + dialog.setTitle(R.string.app_name); + // 设置对话框消息为笔记片段 + dialog.setMessage(mSnippet); + // 设置确定按钮 + dialog.setPositiveButton(R.string.notealert_ok, this); + // 如果屏幕点亮,设置进入按钮 + if (isScreenOn()) { + dialog.setNegativeButton(R.string.notealert_enter, this); + } + // 显示对话框并设置对话框关闭监听器 + dialog.show().setOnDismissListener(this); + } + + // 对话框按钮点击事件处理 + public void onClick(DialogInterface dialog, int which) { + switch (which) { + case DialogInterface.BUTTON_NEGATIVE: + // 点击进入按钮,启动笔记编辑活动并传递笔记ID + Intent intent = new Intent(this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.putExtra(Intent.EXTRA_UID, mNoteId); + startActivity(intent); + break; + default: + break; + } + } + + // 对话框关闭事件处理 + public void onDismiss(DialogInterface dialog) { + // 停止播放提醒声音并结束活动 + stopAlarmSound(); + finish(); + } + + // 停止播放提醒声音 + private void stopAlarmSound() { + if (mPlayer!= null) { + mPlayer.stop(); + mPlayer.release(); + mPlayer = null; + } + } +} \ No newline at end of file diff --git a/ui/AlarmInitReceiver.java b/ui/AlarmInitReceiver.java new file mode 100644 index 0000000..f7277ce --- /dev/null +++ b/ui/AlarmInitReceiver.java @@ -0,0 +1,59 @@ +package net.micode.notes.ui; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.ContentUris; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; + +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; + +// 闹钟初始化接收器类,用于在系统启动或其他合适时机初始化闹钟提醒 +public class AlarmInitReceiver extends BroadcastReceiver { + + // 查询笔记ID和提醒日期的投影 + private static final String[] PROJECTION = new String[]{ + NoteColumns.ID, + NoteColumns.ALERTED_DATE + }; + // 投影中ID列的索引 + private static final int COLUMN_ID = 0; + // 投影中提醒日期列的索引 + private static final int COLUMN_ALERTED_DATE = 1; + + // 当接收到广播时调用 + @Override + public void onReceive(Context context, Intent intent) { + // 获取当前时间 + long currentDate = System.currentTimeMillis(); + // 查询数据库中提醒日期大于当前时间且类型为笔记的记录 + Cursor c = context.getContentResolver().query(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); + // 创建一个意图,用于启动AlarmReceiver + Intent sender = new Intent(context, AlarmReceiver.class); + // 设置意图的数据为当前笔记的Uri + sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, c.getLong(COLUMN_ID))); + // 创建一个PendingIntent,用于发送广播 + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, 0); + // 获取AlarmManager系统服务 + AlarmManager alermManager = (AlarmManager) context + .getSystemService(Context.ALARM_SERVICE); + // 设置闹钟,在提醒日期触发广播 + alermManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent); + } while (c.moveToNext()); + } + c.close(); + } + } +} \ No newline at end of file diff --git a/ui/AlarmReceiver.java b/ui/AlarmReceiver.java new file mode 100644 index 0000000..e181d78 --- /dev/null +++ b/ui/AlarmReceiver.java @@ -0,0 +1,20 @@ +package net.micode.notes.ui; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +// 闹钟接收者类,用于接收闹钟触发的广播并启动闹钟提醒活动 +public class AlarmReceiver extends BroadcastReceiver { + + // 当接收到广播时调用 + @Override + public void onReceive(Context context, Intent intent) { + // 设置意图的目标活动为AlarmAlertActivity,即闹钟提醒页面 + intent.setClass(context, AlarmAlertActivity.class); + // 添加新任务标志,确保活动在新的任务栈中启动 + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + // 启动活动,显示闹钟提醒 + context.startActivity(intent); + } +} \ No newline at end of file diff --git a/ui/DateTimePicker.java b/ui/DateTimePicker.java new file mode 100644 index 0000000..b8a869b --- /dev/null +++ b/ui/DateTimePicker.java @@ -0,0 +1,488 @@ +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; + +// 日期时间选择器类,继承自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小时制小时选择器的最小值 + private static final int HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW = 0; + // 24小时制小时选择器的最大值 + private static final int HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW = 23; + // 12小时制小时选择器的最小值 + private static final int HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW = 1; + // 12小时制小时选择器的最大值 + 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; + // AM/PM选择器的最小值 + private static final int AMPM_SPINNER_MIN_VAL = 0; + // AM/PM选择器的最大值 + private static final int AMPM_SPINNER_MAX_VAL = 1; + + // 日期选择器组件 + private final NumberPicker mDateSpinner; + // 小时选择器组件 + private final NumberPicker mHourSpinner; + // 分钟选择器组件 + private final NumberPicker mMinuteSpinner; + // AM/PM选择器组件 + private final NumberPicker mAmPmSpinner; + + // 当前日期时间 + 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) { + boolean isDateChanged = false; + Calendar cal = Calendar.getInstance(); + if (!mIs24HourView) { + // 12小时制下,处理上午/下午切换和日期跨越的情况 + 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 { + // 24小时制下,处理日期跨越的情况 + 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(); + } + }; + + // AM/PM选择器值改变监听器 + private NumberPicker.OnValueChangeListener mOnAmPmChangedListener = new NumberPicker.OnValueChangeListener() { + @Override + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + mIsAm =!mIsAm; + // 根据AM/PM切换更新小时数 + 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); + } + + // 构造函数,使用当前时间初始化 + public DateTimePicker(Context context) { + this(context, System.currentTimeMillis()); + } + + // 构造函数,使用指定时间初始化 + public DateTimePicker(Context context, long date) { + this(context, date, DateFormat.is24HourFormat(context)); + } + + // 构造函数,使用指定时间和24小时制视图状态初始化 + 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); + + 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); + + String[] stringsForAmPm = new DateFormatSymbols().getAmPmStrings(); + mAmPmSpinner = (NumberPicker) findViewById(R.id.amPm); + mAmPmSpinner.setMinValue(AMPM_SPINNER_MIN_VAL); + mAmPmSpinner.setMaxValue(AMPM_SPINNER_MAX_VAL); + mAmPmSpinner.setDisplayedValues(stringsForAmPm); + mAmPmSpinner.setOnValueChangedListener(mOnAmPmChangedListener); + + // 更新控件到初始状态 + updateDateControl(); + updateHourControl(); + updateAmPmControl(); + set24HourView(is24HourView); + // 设置为当前时间 + setCurrentDate(date); + setEnabled(isEnabled()); + // 设置内容描述 + mInitialising = false; + } + + // 设置启用状态 + @Override + public void setEnabled(boolean enabled) { + if (mIsEnabled == enabled) { + return; + } + super.setEnabled(enabled); + mDateSpinner.setEnabled(enabled); + mMinuteSpinner.setEnabled(enabled); + mHourSpinner.setEnabled(enabled); + mAmPmSpinner.setEnabled(enabled); + mIsEnabled = enabled; + } + + // 获取启用状态 + @Override + public boolean isEnabled() { + return mIsEnabled; + } + + // 获取当前日期时间的毫秒数 + public long getCurrentDateInTimeMillis() { + return mDate.getTimeInMillis(); + } + + // 设置当前日期时间(以毫秒为单位) + public void setCurrentDate(long date) { + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(date); + setCurrentDate(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH), + cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE)); + } + + // 设置当前日期时间 + public void setCurrentDate(int year, int month, + int dayOfMonth, int hourOfDay, int minute) { + setCurrentYear(year); + setCurrentMonth(month); + setCurrentDay(dayOfMonth); + setCurrentHour(hourOfDay); + setCurrentMinute(minute); + } + + // 获取当前年份 + public int getCurrentYear() { + return mDate.get(Calendar.YEAR); + } + + // 设置当前年份 + public void setCurrentYear(int year) { + if (!mInitialising && year == getCurrentYear()) { + return; + } + mDate.set(Calendar.YEAR, year); + updateDateControl(); + onDateTimeChanged(); + } + + // 获取当前月份(0-11) + public int getCurrentMonth() { + return mDate.get(Calendar.MONTH); + } + + // 设置当前月份(0-11) + public void setCurrentMonth(int month) { + if (!mInitialising && month == getCurrentMonth()) { + return; + } + mDate.set(Calendar.MONTH, month); + updateDateControl(); + onDateTimeChanged(); + } + + // 获取当前日期(1-31) + public int getCurrentDay() { + return mDate.get(Calendar.DAY_OF_MONTH); + } + + // 设置当前日期(1-31) + public void setCurrentDay(int dayOfMonth) { + if (!mInitialising && dayOfMonth == getCurrentDay()) { + return; + } + mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); + updateDateControl(); + onDateTimeChanged(); + } + + // 获取当前小时(24小时制,0-23) + public int getCurrentHourOfDay() { + return mDate.get(Calendar.HOUR_OF_DAY); + } + + // 获取当前小时(12小时制或24小时制转换后的小时数) + private int getCurrentHour() { + if (mIs24HourView) { + return getCurrentHourOfDay(); + } else { + int hour = getCurrentHourOfDay(); + if (hour > HOURS_IN_HALF_DAY) { + return hour - HOURS_IN_HALF_DAY; + } else { + return hour == 0? HOURS_IN_HALF_DAY : hour; + } + } + } + + // 设置当前小时(24小时制,0-23) + public void setCurrentHour(int hourOfDay) { + if (!mInitialising && hourOfDay == getCurrentHourOfDay()) { + return; + } + mDate.set(Calendar.HOUR_OF_DAY, hourOfDay); + if (!mIs24HourView) { + if (hourOfDay >= HOURS_IN_HALF_DAY) { + mIsAm = false; + if (hourOfDay > HOURS_IN_HALF_DAY) { + hourOfDay -= HOURS_IN_HALF_DAY; + } + } else { + mIsAm = true; + if (hourOfDay == 0) { + hourOfDay = HOURS_IN_HALF_DAY; + } + } + updateAmPmControl(); + } + mHourSpinner.setValue(hourOfDay); + onDateTimeChanged(); + } + + // 获取当前分钟 + public int getCurrentMinute() { + return mDate.get(Calendar.MINUTE); + } + + // 设置当前分钟 + public void setCurrentMinute(int minute) { + if (!mInitialising && minute == getCurrentMinute()) { + return; + } + mMinuteSpinner.setValue(minute); + mDate.set(Calendar.MINUTE, minute); + onDateTimeChanged(); + } + + // 判断是否为24小时制视图 + public boolean is24HourView() { + return mIs24HourView; + } + + // 设置是否为24小时制视图 + public void set24HourView(boolean is24HourView) { + if (mIs24HourView == is24HourView) { + return; + } + mIs24HourView = is24HourView; + mAmPmSpinner.setVisibility(is24HourView? View.GONE : View.VISIBLE); + int hour = getCurrentHourOfDay(); + updateHourControl(); + setCurrentHour(hour); + updateAmPmControl(); + } + + // 更新日期选择器的显示值 + private void updateDateControl() { + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, -DAYS_IN_ALL_WEEK / 2 - 1); + mDateSpinner.setDisplayedValues(null); + for (int i = 0; i < DAYS_IN_ALL_WEEK; ++i) { + cal.add(Calendar.DAY_OF_YEAR, 1); + mDateDisplayValues[i] = (String) DateFormat.format("MM.dd EEEE", cal); + } + mDateSpinner.setDisplayedValues(mDateDisplayValues); + mDateSpinner.setValue(DAYS_IN_ALL_WEEK / 2); + mDateSpinner.invalidate(); + } + + + private void updateAmPmControl() { + if (mIs24HourView) { + mAmPmSpinner.setVisibility(View.GONE); + } else { + int index = mIsAm ? Calendar.AM : Calendar.PM; + mAmPmSpinner.setValue(index); + mAmPmSpinner.setVisibility(View.VISIBLE); + } + } + + private void updateHourControl() { + if (mIs24HourView) { + mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW); + mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW); + } else { + mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW); + mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW); + } + } + + /** + * Set the callback that indicates the 'Set' button has been pressed. + * @param callback the callback, if null will do nothing + */ + public void setOnDateTimeChangedListener(OnDateTimeChangedListener callback) { + mOnDateTimeChangedListener = callback; + } + + private void onDateTimeChanged() { + if (mOnDateTimeChangedListener != null) { + mOnDateTimeChangedListener.onDateTimeChanged(this, getCurrentYear(), + getCurrentMonth(), getCurrentDay(), getCurrentHourOfDay(), getCurrentMinute()); + } + } +} + // 更新AM/PM选择器的显示状态和值 + private void updateAmPmControl() { + // 如果是24小时制视图,隐藏AM/PM选择器 + if (mIs24HourView) { + mAmPmSpinner.setVisibility(View.GONE); + } else { + // 根据是否为上午,设置AM/PM选择器的值并显示 + int index = mIsAm? Calendar.AM : Calendar.PM; + mAmPmSpinner.setValue(index); + mAmPmSpinner.setVisibility(View.VISIBLE); + } + } + + // 更新小时选择器的最小值和最大值 + private void updateHourControl() { + // 如果是24小时制视图,设置小时选择器的范围为0-23 + if (mIs24HourView) { + mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW); + mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW); + } else { + // 如果是12小时制视图,设置小时选择器的范围为1-12 + mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW); + mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW); + } + } + + /** + * 设置当日期时间改变时的回调函数,用于在用户操作日期时间选择器后通知外部 + * @param callback 回调函数,如果为null则不执行任何操作 + */ + public void setOnDateTimeChangedListener(OnDateTimeChangedListener callback) { + mOnDateTimeChangedListener = callback; + } + + // 当日期时间发生改变时调用此方法,如果设置了回调函数,则触发回调 + private void onDateTimeChanged() { + if (mOnDateTimeChangedListener!= null) { + mOnDateTimeChangedListener.onDateTimeChanged(this, getCurrentYear(), + getCurrentMonth(), getCurrentDay(), getCurrentHourOfDay(), getCurrentMinute()); + } + } +} \ No newline at end of file diff --git a/ui/DateTimePickerDialog.java b/ui/DateTimePickerDialog.java new file mode 100644 index 0000000..2a35172 --- /dev/null +++ b/ui/DateTimePickerDialog.java @@ -0,0 +1,91 @@ +package net.micode.notes.ui; + +import java.util.Calendar; + +import net.micode.notes.R; +import net.micode.notes.ui.DateTimePicker; +import net.micode.notes.ui.DateTimePicker.OnDateTimeChangedListener; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.text.format.DateFormat; +import android.text.format.DateUtils; + +// 日期时间选择对话框类,继承自AlertDialog,用于显示一个包含日期时间选择器的对话框 +public class DateTimePickerDialog extends AlertDialog implements OnClickListener { + + // 当前日期时间 + private Calendar mDate = Calendar.getInstance(); + // 是否为24小时制视图 + private boolean mIs24HourView; + // 日期时间设置监听器 + private OnDateTimeSetListener mOnDateTimeSetListener; + // 日期时间选择器组件 + private DateTimePicker mDateTimePicker; + + // 日期时间设置监听器接口 + public interface OnDateTimeSetListener { + void OnDateTimeSet(AlertDialog dialog, long date); + } + + // 构造函数,使用指定日期初始化对话框 + public DateTimePickerDialog(Context context, long date) { + super(context); + // 创建日期时间选择器 + mDateTimePicker = new DateTimePicker(context); + // 将日期时间选择器设置为对话框的视图 + setView(mDateTimePicker); + // 设置日期时间选择器的监听器,当日期时间改变时更新内部的日期变量并更新对话框标题 + 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); + mDateTimePicker.setCurrentDate(mDate.getTimeInMillis()); + // 设置对话框的确定按钮和点击监听器 + setButton(context.getString(R.string.datetime_dialog_ok), this); + // 设置对话框的取消按钮和点击监听器(这里传入null表示使用默认行为) + setButton2(context.getString(R.string.datetime_dialog_cancel), (OnClickListener) null); + // 设置对话框的24小时制视图状态 + set24HourView(DateFormat.is24HourFormat(this.getContext())); + // 更新对话框标题 + updateTitle(mDate.getTimeInMillis()); + } + + // 设置对话框的24小时制视图状态 + public void set24HourView(boolean is24HourView) { + mIs24HourView = is24HourView; + } + + // 设置日期时间设置监听器 + public void setOnDateTimeSetListener(OnDateTimeSetListener callBack) { + mOnDateTimeSetListener = callBack; + } + + // 更新对话框标题,根据传入的日期和当前的24小时制视图状态格式化日期时间字符串 + 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; + setTitle(DateUtils.formatDateTime(this.getContext(), date, flag)); + } + + // 确定按钮点击事件处理,当点击确定按钮时,如果设置了日期时间设置监听器,则触发监听器并传递当前日期时间 + public void onClick(DialogInterface arg0, int arg1) { + if (mOnDateTimeSetListener!= null) { + mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis()); + } + } +} \ No newline at end of file diff --git a/ui/DropdownMenu.java b/ui/DropdownMenu.java new file mode 100644 index 0000000..5b9a448 --- /dev/null +++ b/ui/DropdownMenu.java @@ -0,0 +1,58 @@ +package net.micode.notes.ui; + +import android.content.Context; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.PopupMenu; +import android.widget.PopupMenu.OnMenuItemClickListener; + +import net.micode.notes.R; + +// 下拉菜单类,用于创建和管理一个与按钮关联的下拉菜单 +public class DropdownMenu { + + // 关联的按钮 + private Button mButton; + // 弹出式菜单对象 + private PopupMenu mPopupMenu; + // 菜单对象 + private Menu mMenu; + + // 构造函数,初始化下拉菜单 + public DropdownMenu(Context context, Button button, int menuId) { + mButton = button; + // 设置按钮的背景资源为下拉图标 + mButton.setBackgroundResource(R.drawable.dropdown_icon); + // 创建与按钮关联的弹出式菜单 + mPopupMenu = new PopupMenu(context, mButton); + mMenu = mPopupMenu.getMenu(); + // 填充菜单资源 + mPopupMenu.getMenuInflater().inflate(menuId, mMenu); + // 设置按钮的点击事件,点击按钮时显示弹出式菜单 + mButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + mPopupMenu.show(); + } + }); + } + + // 设置下拉菜单菜单项的点击监听器 + public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) { + if (mPopupMenu!= null) { + mPopupMenu.setOnMenuItemClickListener(listener); + } + } + + // 根据ID查找菜单项 + public MenuItem findItem(int id) { + return mMenu.findItem(id); + } + + // 设置按钮的文本(即下拉菜单的标题) + public void setTitle(CharSequence title) { + mButton.setText(title); + } +} \ No newline at end of file diff --git a/ui/FoldersListAdapter.java b/ui/FoldersListAdapter.java new file mode 100644 index 0000000..d71c9d9 --- /dev/null +++ b/ui/FoldersListAdapter.java @@ -0,0 +1,132 @@ +package net.micode.notes.ui; + +import android.content.Context; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.PopupMenu; +import android.widget.PopupMenu.OnMenuItemClickListener; + +import net.micode.notes.R; + +// 下拉菜单类,用于创建和管理与按钮关联的下拉菜单 +public class DropdownMenu { + + // 关联的按钮 + private Button mButton; + // 弹出式菜单对象 + private PopupMenu mPopupMenu; + // 菜单对象 + private Menu mMenu; + + // 构造函数,初始化下拉菜单相关组件 + public DropdownMenu(Context context, Button button, int menuId) { + mButton = button; + // 设置按钮的背景为下拉图标 + mButton.setBackgroundResource(R.drawable.dropdown_icon); + // 创建与按钮关联的弹出式菜单 + mPopupMenu = new PopupMenu(context, mButton); + mMenu = mPopupMenu.getMenu(); + // 填充菜单资源 + mPopupMenu.getMenuInflater().inflate(menuId, mMenu); + // 设置按钮的点击事件,点击时显示弹出式菜单 + mButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + mPopupMenu.show(); + } + }); + } + + // 设置下拉菜单菜单项的点击监听器 + public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) { + if (mPopupMenu!= null) { + mPopupMenu.setOnMenuItemClickListener(listener); + } + } + + // 根据ID查找菜单项 + public MenuItem findItem(int id) { + return mMenu.findItem(id); + } + + // 设置按钮的文本(即下拉菜单的标题) + public void setTitle(CharSequence title) { + mButton.setText(title); + } +} + +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; + +// 文件夹列表适配器类,用于将文件夹数据适配到列表视图中 +public class FoldersListAdapter extends CursorAdapter { + + // 查询文件夹数据的投影,包括ID和片段(名称) + public static final String[] PROJECTION = { + NoteColumns.ID, + NoteColumns.SNIPPET + }; + // 投影中ID列的索引 + public static final int ID_COLUMN = 0; + // 投影中名称列的索引 + public static final int NAME_COLUMN = 1; + + // 构造函数,初始化父类 + 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); + } + + // 绑定视图数据,根据文件夹ID设置显示的名称 + @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); + } + + // 文件夹列表项内部类,继承自LinearLayout,用于显示文件夹名称 + private class FolderListItem extends LinearLayout { + private TextView mName; + + // 构造函数,初始化视图并获取名称文本视图 + 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/ui/NoteEditActivity.java b/ui/NoteEditActivity.java new file mode 100644 index 0000000..d4fc62c --- /dev/null +++ b/ui/NoteEditActivity.java @@ -0,0 +1,851 @@ +package net.micode.notes.ui; + +import android.app.Activity; +import android.app.AlarmManager; +import android.app.AlertDialog; +import android.app.PendingIntent; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.graphics.Paint; +import android.os.Bundle; +import android.text.Editable; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.TextUtils; +import android.text.style.BackgroundColorSpan; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.Nullable; +import androidx.appcompat.widget.Toolbar; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.tool.DataUtils; +import net.micode.notes.ui.DateTimePickerDialog; +import net.micode.notes.ui.DateTimePickerDialog.OnDateTimeSetListener; +import net.micode.notes.ui.TextAppearanceResources; +import net.micode.notes.ui.WorkingNote; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +// 笔记编辑活动类,用于编辑笔记的内容、设置等操作 +public class NoteEditActivity extends Activity implements View.OnClickListener, + NoteSettingChangedListener, OnTextViewChangeListener { + + // 笔记头部视图持有者类,用于方便管理笔记头部的视图组件 + private class HeadViewHolder { + public TextView tvModified; // 最后修改日期文本视图 + public ImageView ivAlertIcon; // 提醒图标 + public TextView tvAlertDate; // 提醒日期文本视图 + public ImageView ibSetBgColor; // 设置背景颜色按钮 + } + + // 背景颜色选择按钮与颜色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); + } + + // 背景颜色选择按钮与选中状态图标的映射 + 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"; + + // 笔记头部视图持有者实例 + 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; + + // 活动创建时调用 + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.note_edit); + // 初始化活动状态,如果初始化失败则结束活动 + if (savedInstanceState == null &&!initActivityState(getIntent())) { + finish(); + return; + } + initResources(); + } + + // 当活动从被杀死状态恢复时调用 + @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"); + } + } + + // 初始化活动状态,根据传入的意图确定是查看、新建还是编辑笔记 + private boolean initActivityState(Intent intent) { + mWorkingNote = null; + // 如果是查看笔记操作 + if (TextUtils.equals(Intent.ACTION_VIEW, intent.getAction())) { + long noteId = intent.getLongExtra(Intent.EXTRA_UID, 0); + mUserQuery = ""; + // 如果是从搜索结果进入 + 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())) { + long folderId = intent.getLongExtra(Notes.INTENT_EXTRA_FOLDER_ID, 0); + int widgetId = intent.getIntExtra(Notes.INTENT_EXTRA_WIDGET_ID, + AppWidgetManager.INVALID_APPWIDGET_ID); + int widgetType = intent.getIntExtra(Notes.INTENT_EXTRA_WIDGET_TYPE, + Notes.TYPE_WIDGET_INVALIDE); + int bgResId = intent.getIntExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, + ResourceParser.getDefaultBgId(this)); + String phoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER); + long callDate = intent.getLongExtra(Notes.INTENT_EXTRA_CALL_DATE, 0); + if (callDate!= 0 && phoneNumber!= null) { + 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)); + 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); + } + } + + // 当有新的意图到达时调用,重新初始化活动状态 + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + initActivityState(intent); + } + + // 保存活动状态,在保存前如果笔记未保存则先保存 + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + if (!mWorkingNote.existInDatabase()) { + saveNote(); + } + outState.putLong(Intent.EXTRA_UID, mWorkingNote.getNoteId()); + Log.d(TAG, "Save working note id: " + mWorkingNote.getNoteId() + " onSaveInstanceState"); + } + + // 分发触摸事件,用于处理背景颜色选择器和字体大小选择器的显示和隐藏 + @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); + } + + // 判断触摸点是否在视图范围内 + 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); + 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); + + 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); + } + + 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); + } + } + + @Override + public void onBackPressed() { + if(clearSettingState()) { + return; + } + + saveNote(); + super.onBackPressed(); + } + + 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()); + } + + @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; + } + + @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(); + } + + /** + * Share note to apps that support {@link Intent#ACTION_SEND} action + * and {@text/plain} type + */ + 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() { + // 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); + } + + 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, "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); + } + + private boolean isSyncMode() { + return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0; + } + + public void onClockAlertChanged(long date, boolean set) { + /** + * User could set clock to an unsaved note, so before setting the + * alert clock, we should save the note first + */ + 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); + } + } + + public void onWidgetChanged() { + updateWidget(); + } + + 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); + } + + public void onEditTextEnter(int index, String text) { + /** + * Should not happen, check for debug + */ + 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); + } + } + + 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); + } + + 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; + } + + 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; + } + + 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); + } + } + + 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); + } + } + + 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; + } + + 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; + } + + private void sendToDesktop() { + /** + * Before send message to home, we should make sure that current + * editing note is exists in databases. So, for new note, firstly + * save it + */ + 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); + } + } + + 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; + } + + private void showToast(int resId) { + showToast(resId, Toast.LENGTH_SHORT); + } + + private void showToast(int resId, int duration) { + Toast.makeText(this, resId, duration).show(); + } +}