Merge branch 'cdy' into develop

# Conflicts:
#	src/Notes-master/src/net/micode/notes/gtask/data/MetaData.java
develop
dcy 1 year ago
commit 6817cff036

@ -40,20 +40,29 @@ import net.micode.notes.tool.DataUtils;
import java.io.IOException;
// AlarmAlertActivity类继承自Activity实现了OnClickListener和OnDismissListener接口用于处理闹钟提醒的展示、声音播放以及相关交互逻辑
public class AlarmAlertActivity extends Activity implements OnClickListener, OnDismissListener {
// 用于存储提醒关联的笔记的ID
private long mNoteId;
// 用于存储笔记内容的摘要信息,最大展示长度有限制
private String mSnippet;
// 定义摘要信息的最大展示长度为60个字符
private static final int SNIPPET_PREW_MAX_LEN = 60;
// 用于播放闹钟提醒声音的MediaPlayer对象
MediaPlayer mPlayer;
// Activity被创建时调用的方法进行一些初始化操作
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 设置Activity无标题栏去除默认的标题显示
requestWindowFeature(Window.FEATURE_NO_TITLE);
final Window win = getWindow();
// 设置窗口属性使得在屏幕锁定时也能显示该Activity
win.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
// 判断屏幕是否处于开启状态,如果屏幕未开启,添加一系列屏幕相关的标志位,以确保屏幕按期望的方式显示和处理
if (!isScreenOn()) {
win.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
| WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
@ -64,9 +73,12 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD
Intent intent = getIntent();
try {
// 从传入的Intent中获取数据提取出笔记的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,
// 如果摘要内容长度超过最大展示长度,进行截断处理,并添加提示信息
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) {
@ -74,7 +86,9 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD
return;
}
// 创建MediaPlayer对象用于后续播放闹钟声音
mPlayer = new MediaPlayer();
// 判断笔记是否在笔记数据库中可见如果可见则展示操作对话框并播放闹钟声音否则结束该Activity
if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) {
showActionDialog();
playAlarmSound();
@ -83,18 +97,20 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD
}
}
// 判断屏幕是否处于开启状态的方法通过获取PowerManager服务来检查屏幕状态
private boolean isScreenOn() {
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
return pm.isScreenOn();
}
// 播放闹钟提醒声音的方法获取默认的闹钟铃声Uri并根据系统设置配置音频流类型然后加载并播放声音同时设置为循环播放
private void playAlarmSound() {
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) {
if ((silentModeStreams & (1 << AudioManager.STREAM_ALARM))!= 0) {
mPlayer.setAudioStreamType(silentModeStreams);
} else {
mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM);
@ -119,6 +135,7 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD
}
}
// 展示操作对话框的方法创建一个AlertDialog设置标题、消息内容、正按钮以及根据屏幕是否开启来决定是否设置负按钮并设置对话框消失时的监听器
private void showActionDialog() {
AlertDialog.Builder dialog = new AlertDialog.Builder(this);
dialog.setTitle(R.string.app_name);
@ -130,6 +147,7 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD
dialog.show().setOnDismissListener(this);
}
// 处理对话框按钮点击事件的方法,根据点击的按钮进行相应的逻辑处理,点击负按钮时跳转到笔记编辑页面查看对应的笔记
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case DialogInterface.BUTTON_NEGATIVE:
@ -143,13 +161,15 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD
}
}
// 处理对话框消失事件的方法停止正在播放的闹钟声音并结束当前Activity
public void onDismiss(DialogInterface dialog) {
stopAlarmSound();
finish();
}
// 停止闹钟声音播放的方法释放MediaPlayer资源将其置为null
private void stopAlarmSound() {
if (mPlayer != null) {
if (mPlayer!= null) {
mPlayer.stop();
mPlayer.release();
mPlayer = null;

@ -28,37 +28,55 @@ import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
// AlarmInitReceiver类继承自BroadcastReceiver用于接收系统广播并执行与闹钟初始化相关的操作比如根据条件查询数据库中的笔记信息为符合条件的笔记设置对应的闹钟提醒
public class AlarmInitReceiver extends BroadcastReceiver {
// 定义一个字符串数组用于指定从数据库查询时要获取的列名这里包含笔记的ID和提醒日期两列
private static final String [] PROJECTION = new String [] {
NoteColumns.ID,
NoteColumns.ALERTED_DATE
NoteColumns.ID,
NoteColumns.ALERTED_DATE
};
// 定义常量表示查询结果中笔记ID所在列的索引值为0方便后续从游标中获取对应数据
private static final int COLUMN_ID = 0;
// 定义常量表示查询结果中提醒日期所在列的索引值为1方便后续从游标中获取对应数据
private static final int COLUMN_ALERTED_DATE = 1;
// 重写BroadcastReceiver的onReceive方法当该广播接收器接收到相应广播时会执行此方法内的逻辑
@Override
public void onReceive(Context context, Intent intent) {
// 获取当前系统时间的毫秒数,用于后续作为查询条件去筛选数据库中的记录
long currentDate = System.currentTimeMillis();
// 通过ContentResolver执行数据库查询操作从指定的UriNotes.CONTENT_NOTE_URI中查询数据按照PROJECTION定义的列进行查询
// 使用提供的查询条件筛选出提醒日期大于当前时间且类型为笔记Notes.TYPE_NOTE的记录查询参数使用当前时间的字符串表示形式
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!= null) {
// 将游标移动到第一条查询结果记录位置,如果有结果则开始遍历处理
if (c.moveToFirst()) {
do {
// 从游标中获取提醒日期对应的列数据,得到该笔记的提醒时间(以毫秒为单位的时间戳形式)
long alertDate = c.getLong(COLUMN_ALERTED_DATE);
// 创建一个新的Intent用于启动AlarmReceiver这个广播接收器传递与当前笔记相关的信息
Intent sender = new Intent(context, AlarmReceiver.class);
// 为Intent设置数据将当前笔记的ID附加到对应的Uri上以便接收方可以识别是哪个笔记的闹钟相关操作
sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, c.getLong(COLUMN_ID)));
// 创建一个PendingIntent用于在合适的时候触发指定的广播这里是AlarmReceiver设置相关的上下文、请求码、Intent以及标志位
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, 0);
// 获取系统的AlarmManager服务用于设置闹钟提醒相关的操作
AlarmManager alermManager = (AlarmManager) context
.getSystemService(Context.ALARM_SERVICE);
// 通过AlarmManager设置闹钟提醒使用RTC_WAKEUP模式在指定的绝对时间唤醒设备来触发提醒传入提醒时间和对应的PendingIntent
alermManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent);
} while (c.moveToNext());
}
// 关闭游标,释放相关资源,避免内存泄漏等问题
c.close();
}
}

@ -20,11 +20,18 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
// AlarmReceiver类继承自BroadcastReceiver它的主要作用是接收广播并在接收到广播后进行相应的页面跳转操作以展示闹钟提醒相关的界面
public class AlarmReceiver extends BroadcastReceiver {
// 重写BroadcastReceiver的onReceive方法当该广播接收器接收到广播时此方法内的逻辑将会被执行
@Override
public void onReceive(Context context, Intent intent) {
// 将传入的Intent的目标类设置为AlarmAlertActivity.class也就是当这个Intent被启动时将会启动AlarmAlertActivity这个Activity
// 通常用于根据接收到的广播信息跳转到对应的展示界面,此处就是跳转到闹钟提醒相关的界面
intent.setClass(context, AlarmAlertActivity.class);
// 为Intent添加标志位FLAG_ACTIVITY_NEW_TASK这是因为从广播接收器中启动Activity需要这个标志位来确保Activity能够在新的任务栈中被正确启动
// 否则可能会出现启动失败等问题,特别是在应用处于后台或者未运行的一些场景下
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
// 通过传入的上下文对象启动设置好的Intent从而启动对应的Activity实现从接收到广播到展示闹钟提醒界面的流程
context.startActivity(intent);
}
}

@ -28,85 +28,128 @@ import android.view.View;
import android.widget.FrameLayout;
import android.widget.NumberPicker;
// DateTimePicker类继承自FrameLayout它是一个自定义的视图组件用于实现日期和时间的选择功能支持24小时制和12小时制含AM/PM的显示与操作并且可以响应各种时间组件值变化的事件。
public class DateTimePicker extends FrameLayout {
// 默认的启用状态初始化为true表示默认启用该日期时间选择器
private static final boolean DEFAULT_ENABLE_STATE = true;
// 半天包含的小时数用于12小时制相关的时间计算和判断
private static final int HOURS_IN_HALF_DAY = 12;
// 一整天包含的小时数用于24小时制相关的时间计算和判断等操作
private static final int HOURS_IN_ALL_DAY = 24;
// 一周包含的天数,用于日期选择器相关的范围设置等操作
private static final int DAYS_IN_ALL_WEEK = 7;
// 日期选择器NumberPicker的最小值通常从0开始表示一周中的第一天等情况
private static final int DATE_SPINNER_MIN_VAL = 0;
// 日期选择器NumberPicker的最大值设置为一周天数减1对应一周中的最后一天
private static final int DATE_SPINNER_MAX_VAL = DAYS_IN_ALL_WEEK - 1;
// 24小时制视图下小时选择器NumberPicker的最小值即0时
private static final int HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW = 0;
// 24小时制视图下小时选择器NumberPicker的最大值即23时
private static final int HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW = 23;
// 12小时制视图下小时选择器NumberPicker的最小值即1时通常12小时制显示从1开始
private static final int HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW = 1;
// 12小时制视图下小时选择器NumberPicker的最大值即12时
private static final int HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW = 12;
// 分钟选择器NumberPicker的最小值即0分
private static final int MINUT_SPINNER_MIN_VAL = 0;
// 分钟选择器NumberPicker的最大值即59分表示一分钟的最大数值
private static final int MINUT_SPINNER_MAX_VAL = 59;
// AM/PM选择器NumberPicker的最小值通常0表示AM
private static final int AMPM_SPINNER_MIN_VAL = 0;
// AM/PM选择器NumberPicker的最大值通常1表示PM
private static final int AMPM_SPINNER_MAX_VAL = 1;
// 用于选择日期的NumberPicker组件用户可以通过它滚动选择具体的日期以周为范围参考
private final NumberPicker mDateSpinner;
// 用于选择小时的NumberPicker组件根据设置的24小时制或12小时制显示不同的范围值供用户选择小时数
private final NumberPicker mHourSpinner;
// 用于选择分钟的NumberPicker组件范围是0到59分供用户选择具体的分钟数
private final NumberPicker mMinuteSpinner;
// 用于选择上午AM或下午PM的NumberPicker组件仅在12小时制下可见并起作用
private final NumberPicker mAmPmSpinner;
// 用于存储当前选择的日期时间信息的Calendar对象方便进行各种时间相关的计算和设置操作
private Calendar mDate;
// 用于存储要在日期选择器中显示的一周日期的字符串数组,每个元素对应一周中的一天
private String[] mDateDisplayValues = new String[DAYS_IN_ALL_WEEK];
// 用于标记当前时间是否处于上午AM初始值根据当前小时数判断大于等于12小时则为false即下午PM
private boolean mIsAm;
// 用于标记当前是否处于24小时制视图初始值根据系统设置来确定通过调用DateFormat.is24HourFormat(context)获取)
private boolean mIs24HourView;
// 用于标记该日期时间选择器是否启用初始值为默认启用状态DEFAULT_ENABLE_STATE
private boolean mIsEnabled = DEFAULT_ENABLE_STATE;
// 用于标记是否正在进行初始化操作初始化为true在初始化完成后会设置为false用于避免一些不必要的重复操作或逻辑判断干扰
private boolean mInitialising;
// 定义一个接口类型的成员变量,用于设置当日期时间发生变化时的回调监听器,外部类可以实现该接口来响应时间变化事件
private OnDateTimeChangedListener mOnDateTimeChangedListener;
// 日期选择器NumberPicker的值变化监听器当日期选择器的值发生改变时会触发此监听器内的逻辑
// 主要作用是根据选择的日期变化来更新内部的日期数据mDate并调用相关方法更新界面显示以及通知日期时间变化的监听器如果有设置
private NumberPicker.OnValueChangeListener mOnDateChangedListener = new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
// 根据新选择的日期与旧日期的差值调整内部存储的日期mDate实现日期的增减操作
mDate.add(Calendar.DAY_OF_YEAR, newVal - oldVal);
// 更新日期选择器的显示相关内容,比如更新显示的一周日期字符串等
updateDateControl();
// 通知日期时间发生了变化,触发设置的回调监听器(如果有)执行相应逻辑
onDateTimeChanged();
}
};
// 小时选择器NumberPicker的值变化监听器当小时选择器的值发生改变时会触发此监听器内的逻辑
// 会根据是否是24小时制以及新旧小时值的情况处理日期的变化跨天等情况、AM/PM的切换12小时制下以及更新内部日期时间数据和通知相关监听器等操作
private NumberPicker.OnValueChangeListener mOnHourChangedListener = new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
boolean isDateChanged = false;
Calendar cal = Calendar.getInstance();
// 如果不是24小时制即12小时制的情况
if (!mIs24HourView) {
// 从上午切换到下午11点到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) {
}
// 从下午切换到上午12点到11点且回退到上一天的情况
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;
}
// 处理11点到12点或者12点到11点切换时AM/PM状态的改变并更新对应的AM/PM控制显示
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;
mIsAm =!mIsAm;
updateAmPmControl();
}
} else {
// 24小时制下从23时切换到0时跨天的情况
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) {
}
// 24小时制下从0时切换到23时回退一天的情况
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);
// 根据是否是12小时制以及当前的AM/PM状态计算出正确的小时数并设置到内部的日期对象mDate
int newHour = mHourSpinner.getValue() % HOURS_IN_HALF_DAY + (mIsAm? 0 : HOURS_IN_HALF_DAY);
mDate.set(Calendar.HOUR_OF_DAY, newHour);
// 通知日期时间发生了变化,触发设置的回调监听器(如果有)执行相应逻辑
onDateTimeChanged();
// 如果日期发生了变化更新当前的年、月、日信息到内部的日期对象mDate
if (isDateChanged) {
setCurrentYear(cal.get(Calendar.YEAR));
setCurrentMonth(cal.get(Calendar.MONTH));
@ -115,18 +158,23 @@ public class DateTimePicker extends FrameLayout {
}
};
// 分钟选择器NumberPicker的值变化监听器当分钟选择器的值发生改变时会触发此监听器内的逻辑
// 会处理分钟变化导致的小时变化跨小时等情况、AM/PM的切换影响到跨12点时以及更新内部日期时间数据和通知相关监听器等操作
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;
// 分钟从最大值变为最小值如59分变为0分可能跨小时的情况
if (oldVal == maxValue && newVal == minValue) {
offset += 1;
} else if (oldVal == minValue && newVal == maxValue) {
}
// 分钟从最小值变为最大值如0分变为59分可能跨小时的情况
else if (oldVal == minValue && newVal == maxValue) {
offset -= 1;
}
if (offset != 0) {
if (offset!= 0) {
mDate.add(Calendar.HOUR_OF_DAY, offset);
mHourSpinner.setValue(getCurrentHour());
updateDateControl();
@ -144,10 +192,12 @@ public class DateTimePicker extends FrameLayout {
}
};
// AM/PM选择器NumberPicker的值变化监听器当AM/PM选择器的值发生改变时会触发此监听器内的逻辑
// 主要作用是根据AM/PM的切换来调整内部日期时间数据增减12小时更新AM/PM控制显示以及通知日期时间变化的监听器如果有执行相应逻辑
private NumberPicker.OnValueChangeListener mOnAmPmChangedListener = new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
mIsAm = !mIsAm;
mIsAm =!mIsAm;
if (mIsAm) {
mDate.add(Calendar.HOUR_OF_DAY, -HOURS_IN_HALF_DAY);
} else {
@ -158,39 +208,51 @@ public class DateTimePicker extends FrameLayout {
}
};
// 定义一个接口用于外部类实现当日期时间在DateTimePicker中发生变化时会回调此接口的方法传递相关的日期时间参数
public interface OnDateTimeChangedListener {
void onDateTimeChanged(DateTimePicker view, int year, int month,
int dayOfMonth, int hourOfDay, int minute);
int dayOfMonth, int hourOfDay, int minute);
}
// 构造函数使用默认的当前时间System.currentTimeMillis()来初始化DateTimePicker调用另一个构造函数传递当前时间和系统默认的时间显示格式24小时制或12小时制来完成初始化
public DateTimePicker(Context context) {
this(context, System.currentTimeMillis());
}
// 构造函数使用指定的时间date参数来初始化DateTimePicker调用另一个构造函数传递指定时间和系统默认的时间显示格式24小时制或12小时制来完成初始化
public DateTimePicker(Context context, long date) {
this(context, date, DateFormat.is24HourFormat(context));
}
// 主要的构造函数完成DateTimePicker的初始化工作包括加载布局、初始化各个NumberPicker组件及其监听器、设置初始时间、设置启用状态以及一些初始的显示控制等操作
public DateTimePicker(Context context, long date, boolean is24HourView) {
super(context);
// 获取一个默认的Calendar实例用于存储和操作当前的日期时间信息
mDate = Calendar.getInstance();
mInitialising = true;
// 根据当前小时数判断初始的AM/PM状态大于等于12小时则为下午PM即mIsAm为false
mIsAm = getCurrentHourOfDay() >= HOURS_IN_HALF_DAY;
// 加载指定的布局文件R.layout.datetime_picker到该FrameLayout中用于显示日期时间选择的各个组件
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);
// 获取布局中的小时选择器NumberPicker组件并设置值变化监听器
mHourSpinner = (NumberPicker) findViewById(R.id.hour);
mHourSpinner.setOnValueChangedListener(mOnHourChangedListener);
// 获取布局中的分钟选择器NumberPicker组件设置最小值、最大值、长按更新间隔等并设置值变化监听器
mMinuteSpinner = (NumberPicker) findViewById(R.id.minute);
mMinuteSpinner.setMinValue(MINUT_SPINNER_MIN_VAL);
mMinuteSpinner.setMaxValue(MINUT_SPINNER_MAX_VAL);
mMinuteSpinner.setOnLongPressUpdateInterval(100);
mMinuteSpinner.setOnValueChangedListener(mOnMinuteChangedListener);
// 获取用于显示AM/PM的字符串数组根据系统语言环境等获取本地化的AM/PM表示并设置到AM/PM选择器NumberPicker组件中包括设置最小值、最大值、显示值以及值变化监听器
String[] stringsForAmPm = new DateFormatSymbols().getAmPmStrings();
mAmPmSpinner = (NumberPicker) findViewById(R.id.amPm);
mAmPmSpinner.setMinValue(AMPM_SPINNER_MIN_VAL);
@ -198,288 +260,22 @@ public class DateTimePicker extends FrameLayout {
mAmPmSpinner.setDisplayedValues(stringsForAmPm);
mAmPmSpinner.setOnValueChangedListener(mOnAmPmChangedListener);
// update controls to initial state
// 更新各个相关的控制显示使其处于初始状态比如日期选择器显示的一周日期字符串、小时选择器的范围显示、AM/PM选择器的初始可见性等
updateDateControl();
updateHourControl();
updateAmPmControl();
// 设置是否为24小时制视图根据传入的参数决定并相应地调整相关组件的显示和值范围等
set24HourView(is24HourView);
// set to current time
// 设置当前显示的日期时间为传入的指定日期date参数通过设置年、月、日、小时、分钟等各个部分来完成
setCurrentDate(date);
// 设置该日期时间选择器的启用状态根据当前的启用状态mIsEnabled来决定
setEnabled(isEnabled());
// set the content descriptions
// 初始化完成将标记设置为false表示不再处于初始化阶段
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;
}
/**
* Get the current date in millis
*
* @return the current date in millis
*/
public long getCurrentDateInTimeMillis() {
return mDate.getTimeInMillis();
}
/**
* Set the current date
*
* @param date The current date in millis
*/
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));
}
/**
* Set the current date
*
* @param year The current year
* @param month The current month
* @param dayOfMonth The current dayOfMonth
* @param hourOfDay The current hourOfDay
* @param minute The current minute
*/
public void setCurrentDate(int year, int month,
int dayOfMonth, int hourOfDay, int minute) {
setCurrentYear(year);
setCurrentMonth(month);
setCurrentDay(dayOfMonth);
setCurrentHour(hourOfDay);
setCurrentMinute(minute);
}
/**
* Get current year
*
* @return The current year
*/
public int getCurrentYear() {
return mDate.get(Calendar.YEAR);
}
/**
* Set current year
*
* @param year The current year
*/
public void setCurrentYear(int year) {
if (!mInitialising && year == getCurrentYear()) {
return;
}
mDate.set(Calendar.YEAR, year);
updateDateControl();
onDateTimeChanged();
}
/**
* Get current month in the year
*
* @return The current month in the year
*/
public int getCurrentMonth() {
return mDate.get(Calendar.MONTH);
}
/**
* Set current month in the year
*
* @param month The month in the year
*/
public void setCurrentMonth(int month) {
if (!mInitialising && month == getCurrentMonth()) {
return;
}
mDate.set(Calendar.MONTH, month);
updateDateControl();
onDateTimeChanged();
}
/**
* Get current day of the month
*
* @return The day of the month
*/
public int getCurrentDay() {
return mDate.get(Calendar.DAY_OF_MONTH);
}
/**
* Set current day of the month
*
* @param dayOfMonth The day of the month
*/
public void setCurrentDay(int dayOfMonth) {
if (!mInitialising && dayOfMonth == getCurrentDay()) {
return;
}
mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth);
updateDateControl();
onDateTimeChanged();
}
/**
* Get current hour in 24 hour mode, in the range (0~23)
* @return The current hour in 24 hour mode
*/
public int getCurrentHourOfDay() {
return mDate.get(Calendar.HOUR_OF_DAY);
}
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;
}
}
}
/**
* Set current hour in 24 hour mode, in the range (0~23)
*
* @param hourOfDay
*/
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();
}
/**
* Get currentMinute
*
* @return The Current Minute
*/
public int getCurrentMinute() {
return mDate.get(Calendar.MINUTE);
}
/**
* Set current minute
*/
public void setCurrentMinute(int minute) {
if (!mInitialising && minute == getCurrentMinute()) {
return;
}
mMinuteSpinner.setValue(minute);
mDate.set(Calendar.MINUTE, minute);
onDateTimeChanged();
}
/**
* @return true if this is in 24 hour view else false.
*/
public boolean is24HourView () {
return mIs24HourView;
}
/**
* Set whether in 24 hour or AM/PM mode.
*
* @param is24HourView True for 24 hour mode. False for AM/PM mode.
*/
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());
}
}
}
// 重写父类的setEnabled方法用于设置整个日期时间选择器及其内部

@ -29,24 +29,37 @@ import android.content.DialogInterface.OnClickListener;
import android.text.format.DateFormat;
import android.text.format.DateUtils;
// DateTimePickerDialog类继承自AlertDialog并实现了OnClickListener接口它是一个自定义的对话框用于展示日期和时间选择的界面
// 让用户可以方便地选择具体的日期和时间,并在用户确认选择后通过回调接口通知外部相应的操作。
public class DateTimePickerDialog extends AlertDialog implements OnClickListener {
// 用于存储当前选择的日期时间信息的Calendar对象初始化为获取当前系统时间对应的Calendar实例方便后续进行各种日期时间相关的操作和设置。
private Calendar mDate = Calendar.getInstance();
// 用于标记当前是否处于24小时制视图用于控制时间显示格式以及相关逻辑判断等操作。
private boolean mIs24HourView;
// 定义一个接口类型的成员变量,用于设置当用户设置好日期时间并确认时的回调监听器,外部类可以实现该接口来响应确认操作事件。
private OnDateTimeSetListener mOnDateTimeSetListener;
// 用于实际显示日期和时间选择界面的DateTimePicker自定义组件通过它实现具体的日期时间选择交互功能。
private DateTimePicker mDateTimePicker;
// 定义一个接口,用于外部类实现,当用户在对话框中设置好日期时间并点击确认按钮后,会回调此接口的方法,传递包含该对话框以及选择的日期时间(以毫秒表示)的参数。
public interface OnDateTimeSetListener {
void OnDateTimeSet(AlertDialog dialog, long date);
}
// 构造函数用于创建DateTimePickerDialog实例接收上下文和初始日期以毫秒表示作为参数完成对话框的基本初始化设置工作
// 包括创建DateTimePicker组件、设置其相关监听器、初始化显示的日期时间、设置对话框的按钮以及标题等内容。
public DateTimePickerDialog(Context context, long date) {
super(context);
// 创建一个DateTimePicker实例用于在对话框中展示日期时间选择界面传入当前上下文环境。
mDateTimePicker = new DateTimePicker(context);
// 将DateTimePicker组件设置为对话框的显示内容使其在对话框中展示出来供用户操作。
setView(mDateTimePicker);
// 为DateTimePicker组件设置日期时间变化监听器当用户在DateTimePicker中改变了日期、时间等任何相关值时会触发此监听器内的逻辑。
// 主要作用是根据用户选择的新值更新内部存储的日期时间信息mDate并更新对话框的标题显示以实时反映当前选择的日期时间。
mDateTimePicker.setOnDateTimeChangedListener(new OnDateTimeChangedListener() {
public void onDateTimeChanged(DateTimePicker view, int year, int month,
int dayOfMonth, int hourOfDay, int minute) {
int dayOfMonth, int hourOfDay, int minute) {
mDate.set(Calendar.YEAR, year);
mDate.set(Calendar.MONTH, month);
mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth);
@ -55,34 +68,48 @@ public class DateTimePickerDialog extends AlertDialog implements OnClickListener
updateTitle(mDate.getTimeInMillis());
}
});
// 设置初始显示的日期时间将传入的日期以毫秒表示设置到内部的日期时间对象mDate并将秒数设置为0确保初始时间的准确性。
mDate.setTimeInMillis(date);
mDate.set(Calendar.SECOND, 0);
// 将DateTimePicker组件显示的日期时间设置为当前初始化的日期时间使其在界面上初始展示正确的时间内容。
mDateTimePicker.setCurrentDate(mDate.getTimeInMillis());
// 设置对话框的确认按钮(通常显示为"确定"之类的文本传入对应文本资源的字符串并设置按钮点击事件的监听器为当前类自身因为实现了OnClickListener接口
setButton(context.getString(R.string.datetime_dialog_ok), this);
// 设置对话框的取消按钮(通常显示为"取消"之类的文本传入对应文本资源的字符串并传入null作为点击事件监听器表示默认的取消行为。
setButton2(context.getString(R.string.datetime_dialog_cancel), (OnClickListener)null);
// 根据系统当前设置判断是否为24小时制视图调用DateFormat.is24HourFormat方法获取并设置以决定时间选择器的显示格式。
set24HourView(DateFormat.is24HourFormat(this.getContext()));
// 初始更新一次对话框的标题使其显示当前设置的日期时间信息调用updateTitle方法传入当前日期时间的毫秒数来完成更新。
updateTitle(mDate.getTimeInMillis());
}
// 用于设置当前是否为24小时制视图的方法接收一个布尔值参数更新内部的标记变量mIs24HourView外部可以通过调用此方法来动态切换时间显示格式。
public void set24HourView(boolean is24HourView) {
mIs24HourView = is24HourView;
}
// 用于设置日期时间确认回调监听器的方法接收一个实现了OnDateTimeSetListener接口的对象作为参数将其赋值给内部的监听器成员变量
// 外部可以通过调用此方法来注册自己的监听器,以便在用户确认选择日期时间后执行相应的逻辑。
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;
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));
}
// 实现OnClickListener接口的方法用于处理对话框按钮点击事件当用户点击确认按钮通过传入的参数判断
// 如果设置了日期时间确认回调监听器mOnDateTimeSetListener不为null则调用该监听器的方法传递当前对话框实例和当前选择的日期时间以毫秒表示参数
// 通知外部用户已完成日期时间设置并确认的操作。
public void onClick(DialogInterface arg0, int arg1) {
if (mOnDateTimeSetListener != null) {
if (mOnDateTimeSetListener!= null) {
mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis());
}
}

@ -27,17 +27,31 @@ import android.widget.PopupMenu.OnMenuItemClickListener;
import net.micode.notes.R;
// DropdownMenu类用于创建一个下拉菜单的功能组件它将一个按钮与一个PopupMenu关联起来点击按钮时弹出菜单
// 并且可以设置菜单选项的点击监听器、查找菜单项以及设置按钮显示的标题等操作。
public class DropdownMenu {
// 用于显示下拉菜单的触发按钮,用户点击此按钮会弹出对应的下拉菜单。
private Button mButton;
// 实际的PopupMenu对象用于承载和展示下拉菜单的内容包含多个菜单项供用户选择操作。
private PopupMenu mPopupMenu;
// 对应PopupMenu中的菜单对象通过它可以进行如查找菜单项等与菜单内容相关的操作。
private Menu mMenu;
// 构造函数用于初始化DropdownMenu实例接收上下文、触发按钮以及菜单资源ID作为参数完成下拉菜单的基本构建工作
// 包括设置按钮背景、创建PopupMenu、加载菜单资源以及为按钮设置点击监听器以弹出菜单等操作。
public DropdownMenu(Context context, Button button, int menuId) {
// 将传入的按钮对象赋值给成员变量,后续通过此按钮来触发下拉菜单的显示。
mButton = button;
// 为按钮设置背景资源使用指定的资源IDR.drawable.dropdown_icon来设置按钮的外观样式使其显示为一个具有下拉箭头等特征的图标样式通常
mButton.setBackgroundResource(R.drawable.dropdown_icon);
// 创建一个PopupMenu实例传入上下文和触发按钮作为参数使得PopupMenu与对应的按钮关联起来并且会根据按钮的位置等情况来显示弹出菜单。
mPopupMenu = new PopupMenu(context, mButton);
// 获取PopupMenu中的菜单对象赋值给成员变量方便后续对菜单内容进行操作比如查找菜单项等。
mMenu = mPopupMenu.getMenu();
// 通过菜单填充器MenuInflater将指定的菜单资源由menuId指定加载到PopupMenu的菜单对象中从而显示出具体的菜单项内容。
mPopupMenu.getMenuInflater().inflate(menuId, mMenu);
// 为按钮设置点击监听器当按钮被点击时触发内部的逻辑即显示与之关联的PopupMenu实现点击按钮弹出下拉菜单的功能。
mButton.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
mPopupMenu.show();
@ -45,16 +59,22 @@ public class DropdownMenu {
});
}
// 用于设置下拉菜单中菜单项点击监听器的方法接收一个实现了OnMenuItemClickListener接口的对象作为参数
// 将其设置给PopupMenu使得当用户点击下拉菜单中的任意菜单项时能够触发相应的逻辑处理外部可以通过此方法来响应菜单项的点击操作。
public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) {
if (mPopupMenu != null) {
if (mPopupMenu!= null) {
mPopupMenu.setOnMenuItemClickListener(listener);
}
}
// 用于在菜单中查找指定ID的菜单项的方法接收一个菜单项的ID作为参数通过调用菜单对象mMenu的findItem方法来查找对应的菜单项
// 返回查找到的MenuItem对象方便外部对特定菜单项进行进一步的操作比如获取菜单项属性、修改菜单项状态等。
public MenuItem findItem(int id) {
return mMenu.findItem(id);
}
// 用于设置触发下拉菜单的按钮显示标题的方法,接收一个字符序列(通常是字符串)作为参数,将其设置为按钮显示的文本内容,
// 这样可以根据需要动态改变按钮上显示的提示信息,例如显示当前选中的菜单项相关内容等。
public void setTitle(CharSequence title) {
mButton.setText(title);
}

@ -29,52 +29,74 @@ import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
// FoldersListAdapter类继承自CursorAdapter它主要用于将数据库查询得到的游标数据Cursor适配显示到特定的视图列表中
// 在这里是用于展示文件夹相关信息的列表,比如文件夹名称等内容,并且提供了获取文件夹名称等相关的辅助方法。
public class FoldersListAdapter extends CursorAdapter {
// 定义一个字符串数组用于指定从数据库查询时要获取的列名这里包含笔记的ID和摘要信息可能用于表示文件夹相关的标识或其他关联内容两列。
public static final String [] PROJECTION = {
NoteColumns.ID,
NoteColumns.SNIPPET
NoteColumns.ID,
NoteColumns.SNIPPET
};
// 定义常量表示查询结果中笔记ID所在列的索引值为0方便后续从游标中获取对应数据这里命名为ID_COLUMN。
public static final int ID_COLUMN = 0;
// 定义常量表示查询结果中摘要信息在这里用于表示文件夹名称相关内容所在列的索引值为1方便后续从游标中获取对应数据命名为NAME_COLUMN。
public static final int NAME_COLUMN = 1;
// 构造函数接收上下文和游标Cursor作为参数调用父类的构造函数来完成基本的初始化工作
// 这里暂时没有额外的自定义初始化逻辑(除了调用父类构造函数),只是保留了构造函数的标准写法,注释中提示了待完善的部分(可能后续会添加更多初始化相关操作)。
public FoldersListAdapter(Context context, Cursor c) {
super(context, c);
// TODO Auto-generated constructor stub
}
// 重写CursorAdapter的newView方法该方法用于创建一个新的视图View对象用于显示列表中的每一项数据
// 在这里返回一个自定义的FolderListItem视图意味着每一项列表数据将会使用这个自定义视图来展示相关信息。
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return new FolderListItem(context);
}
// 重写CursorAdapter的bindView方法该方法用于将游标Cursor中的数据绑定到已创建的视图View也就是填充具体的数据到视图中进行显示
// 在这里根据游标中的数据情况判断如果是根文件夹通过ID判断则显示特定的字符串可能是代表根文件夹的固定文本否则显示游标中对应列的字符串内容文件夹名称
// 然后调用自定义视图FolderListItem的bind方法将名称设置到对应的TextView上进行显示。
@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
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);
}
}
// 提供的一个公共方法,用于获取指定位置的文件夹名称,接收上下文和位置索引作为参数,
// 通过调用getItem方法获取对应位置的游标对象然后根据游标中数据判断是根文件夹还是普通文件夹返回相应的名称字符串。
public String getFolderName(Context context, int position) {
Cursor cursor = (Cursor) getItem(position);
return (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context
return (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER)? context
.getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN);
}
// 定义了一个内部私有类FolderListItem它继承自LinearLayout用于作为列表中每一项数据展示的自定义视图布局
// 包含了一个TextView用于显示文件夹名称并且提供了一个bind方法用于将名称设置到这个TextView上进行展示。
private class FolderListItem extends LinearLayout {
// 用于显示文件夹名称的TextView组件通过它将文件夹相关的名称信息展示给用户。
private TextView mName;
// 构造函数接收上下文作为参数调用父类LinearLayout的构造函数完成基本初始化
// 然后通过LayoutInflater将指定的布局文件R.layout.folder_list_item加载到这个视图中
// 最后获取布局中的TextView组件用于后续设置名称显示。
public FolderListItem(Context context) {
super(context);
inflate(context, R.layout.folder_list_item, this);
mName = (TextView) findViewById(R.id.tv_folder_name);
}
// 用于将传入的文件夹名称设置到内部的TextViewmName上进行显示的方法外部通过调用此方法来更新视图中显示的文件夹名称内容。
public void bind(String name) {
mName.setText(name);
}
}
}
}

@ -72,18 +72,19 @@ 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;
public ImageView ivAlertIcon;
public TextView tvAlertDate;
public ImageView ibSetBgColor;
}
// 背景颜色选择按钮与对应颜色资源ID的映射关系以按钮ID为键颜色资源ID为值方便根据按钮点击来确定选择的背景颜色。
private static final Map<Integer, Integer> sBgSelectorBtnsMap = new HashMap<Integer, Integer>();
static {
sBgSelectorBtnsMap.put(R.id.iv_bg_yellow, ResourceParser.YELLOW);
@ -93,6 +94,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
sBgSelectorBtnsMap.put(R.id.iv_bg_white, ResourceParser.WHITE);
}
// 背景颜色选择按钮与对应选中状态显示按钮ID的映射关系用于在选择背景颜色后显示对应的选中标识按钮。
private static final Map<Integer, Integer> sBgSelectorSelectionMap = new HashMap<Integer, Integer>();
static {
sBgSelectorSelectionMap.put(ResourceParser.YELLOW, R.id.iv_bg_yellow_select);
@ -102,6 +104,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
sBgSelectorSelectionMap.put(ResourceParser.WHITE, R.id.iv_bg_white_select);
}
// 字体大小选择按钮与对应字体大小资源ID的映射关系用于根据按钮点击来确定选择的字体大小。
private static final Map<Integer, Integer> sFontSizeBtnsMap = new HashMap<Integer, Integer>();
static {
sFontSizeBtnsMap.put(R.id.ll_font_large, ResourceParser.TEXT_LARGE);
@ -110,6 +113,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
sFontSizeBtnsMap.put(R.id.ll_font_super, ResourceParser.TEXT_SUPER);
}
// 字体大小资源ID与对应选中状态显示按钮ID的映射关系用于在选择字体大小后显示对应的选中标识按钮。
private static final Map<Integer, Integer> sFontSelectorSelectionMap = new HashMap<Integer, Integer>();
static {
sFontSelectorSelectionMap.put(ResourceParser.TEXT_LARGE, R.id.iv_large_select);
@ -118,43 +122,62 @@ public class NoteEditActivity extends Activity implements OnClickListener,
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;
// 用于编辑笔记内容的EditText组件用户在此输入和编辑笔记的具体文本内容。
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;
// 表示复选框选中状态的字符用于在笔记内容以特定格式表示复选框选中情况时使用这里是一个Unicode字符表示对勾。
public static final String TAG_CHECKED = String.valueOf('\u221A');
// 表示复选框未选中状态的字符用于在笔记内容以特定格式表示复选框未选中情况时使用这里是一个Unicode字符表示方块。
public static final String TAG_UNCHECKED = String.valueOf('\u25A1');
// 用于存储笔记内容以列表形式编辑时的多个EditText子项的线性布局容器方便处理列表模式下的笔记内容展示和编辑。
private LinearLayout mEditTextList;
// 用户查询的字符串内容,可能用于搜索等相关功能,比如在笔记中查找特定内容,用于后续的文本高亮等处理。
private String mUserQuery;
// 用于匹配用户查询内容的正则表达式模式对象,根据用户查询字符串编译生成,用于在笔记文本中查找匹配的内容进行相应处理,如高亮显示。
private Pattern mPattern;
// Activity被创建时调用的方法进行一些初始化操作如设置布局、根据Intent初始化Activity状态等如果初始化失败则结束该Activity。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 设置该Activity对应的布局文件用于展示笔记编辑相关的界面内容。
this.setContentView(R.layout.note_edit);
if (savedInstanceState == null && !initActivityState(getIntent())) {
if (savedInstanceState == null &&!initActivityState(getIntent())) {
finish();
return;
}
@ -162,13 +185,14 @@ public class NoteEditActivity extends Activity implements OnClickListener,
}
/**
* Current activity may be killed when the memory is low. Once it is killed, for another time
* user load this activity, we should restore the former state
* ActivityActivity
* Intent.EXTRA_UIDActivity
* Activity
*/
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
if (savedInstanceState != null && savedInstanceState.containsKey(Intent.EXTRA_UID)) {
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)) {
@ -179,10 +203,12 @@ public class NoteEditActivity extends Activity implements OnClickListener,
}
}
// 根据传入的Intent初始化Activity的状态根据不同的Intent动作ACTION_VIEW、ACTION_INSERT_OR_EDIT等来处理不同的情况
// 例如加载已存在的笔记、创建新笔记、处理通话记录相关笔记等若操作过程中出现加载笔记失败等问题则结束该Activity成功则返回true。
private boolean initActivityState(Intent intent) {
/**
* If the user specified the {@link Intent#ACTION_VIEW} but not provided with id,
* then jump to the NotesListActivity
* {@link Intent#ACTION_VIEW}ID
* NotesListActivity
*/
mWorkingNote = null;
if (TextUtils.equals(Intent.ACTION_VIEW, intent.getAction())) {
@ -190,7 +216,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
mUserQuery = "";
/**
* Starting from the searched result
* ID
*/
if (intent.hasExtra(SearchManager.EXTRA_DATA_KEY)) {
noteId = Long.parseLong(intent.getStringExtra(SearchManager.EXTRA_DATA_KEY));
@ -211,11 +237,12 @@ public class NoteEditActivity extends Activity implements OnClickListener,
return false;
}
}
// 设置软键盘的显示模式,初始时隐藏软键盘,并根据内容调整布局大小。
getWindow().setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN
| WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
} else if(TextUtils.equals(Intent.ACTION_INSERT_OR_EDIT, intent.getAction())) {
// New note
} else if (TextUtils.equals(Intent.ACTION_INSERT_OR_EDIT, intent.getAction())) {
// 新建笔记的情况获取相关的额外数据如文件夹ID、小部件ID、小部件类型、背景资源ID等用于创建新的空白笔记对象。
long folderId = intent.getLongExtra(Notes.INTENT_EXTRA_FOLDER_ID, 0);
int widgetId = intent.getIntExtra(Notes.INTENT_EXTRA_WIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);
@ -224,10 +251,10 @@ public class NoteEditActivity extends Activity implements OnClickListener,
int bgResId = intent.getIntExtra(Notes.INTENT_EXTRA_BACKGROUND_ID,
ResourceParser.getDefaultBgId(this));
// Parse call-record note
// 解析通话记录笔记相关的情况,获取电话号码和通话日期,根据是否存在有效数据来决定是加载已存在的通话笔记还是创建新的空白通话笔记并转换为通话笔记格式。
String phoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER);
long callDate = intent.getLongExtra(Notes.INTENT_EXTRA_CALL_DATE, 0);
if (callDate != 0 && phoneNumber != null) {
if (callDate!= 0 && phoneNumber!= null) {
if (TextUtils.isEmpty(phoneNumber)) {
Log.w(TAG, "The call record number is null");
}
@ -250,6 +277,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
bgResId);
}
// 设置软键盘的显示模式,初始时显示软键盘,并根据内容调整布局大小。
getWindow().setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
| WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
@ -258,16 +286,20 @@ public class NoteEditActivity extends Activity implements OnClickListener,
finish();
return false;
}
// 设置笔记相关设置状态变化的监听器为当前Activity自身以便在笔记设置发生改变时能响应处理。
mWorkingNote.setOnSettingStatusChangedListener(this);
return true;
}
// Activity重新恢复到前台可见时调用的方法在此主要用于初始化笔记编辑界面的显示内容如设置字体样式、根据笔记类型展示内容、设置头部和编辑区域背景等。
@Override
protected void onResume() {
super.onResume();
initNoteScreen();
}
// 初始化笔记编辑界面的显示内容,根据当前笔记的相关属性和设置,设置笔记编辑文本的字体样式、展示模式(列表模式或普通模式)、
// 显示或隐藏提醒相关的头部信息、设置头部和编辑区域的背景等,确保界面正确展示当前笔记的状态。
private void initNoteScreen() {
mNoteEditor.setTextAppearance(this, TextAppearanceResources
.getTexAppearanceResource(mFontSizeId));
@ -289,12 +321,12 @@ public class NoteEditActivity extends Activity implements OnClickListener,
| DateUtils.FORMAT_SHOW_YEAR));
/**
* TODO: Add the menu for setting alert. Currently disable it because the DateTimePicker
* is not ready
* TODO: DateTimePicker
*/
showAlertHeader();
}
// 根据笔记是否设置了提醒以及提醒时间与当前时间的关系,显示或隐藏提醒相关的头部信息,如提醒日期文本和提醒图标等。
private void showAlertHeader() {
if (mWorkingNote.hasClockAlert()) {
long time = System.currentTimeMillis();
@ -308,566 +340,5 @@ public class NoteEditActivity extends Activity implements OnClickListener,
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);
/**
* For new note without note id, we should firstly save it to
* generate a id. If the editing note is not worth saving, there
* is no id which is equivalent to create new note
*/
if (!mWorkingNote.existInDatabase()) {
saveNote();
}
outState.putLong(Intent.EXTRA_UID, mWorkingNote.getNoteId());
Log.d(TAG, "Save working note id: " + mWorkingNote.getNoteId() + " onSaveInstanceState");
}
@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);
}
mNoteHeaderHolder.ivAlert
private boolean inRangeOfView(View view, MotionEvent ev) {
int []location = new int[2];
view.getLocationOnScreen(location);
int x = location[0];
int y = location[1];
if (ev.getX() < x
|| ev.getX() > (x + view.getWidth())
|| ev.getY() < y
|| ev.getY() > (y + view.getHeight())) {
return false;
}
return true;
}
private void initResources() {
mHeadViewPanel = findViewById(R.id.note_title);
mNoteHeaderHolder = new HeadViewHolder();
mNoteHeaderHolder.tvModified = (TextView) findViewById(R.id.tv_modified_date);
mNoteHeaderHolder.ivAlertIcon = (ImageView) findViewById(R.id.iv_alert_icon);
mNoteHeaderHolder.tvAlertDate = (TextView) findViewById(R.id.tv_alert_date);
mNoteHeaderHolder.ibSetBgColor = (ImageView) findViewById(R.id.btn_set_bg_color);
mNoteHeaderHolder.ibSetBgColor.setOnClickListener(this);
mNoteEditor = (EditText) findViewById(R.id.note_edit_view);
mNoteEditorPanel = findViewById(R.id.sv_note_edit);
mNoteBgColorSelector = findViewById(R.id.note_bg_color_selector);
for (int id : sBgSelectorBtnsMap.keySet()) {
ImageView iv = (ImageView) findViewById(id);
iv.setOnClickListener(this);
}
mFontSizeSelector = findViewById(R.id.font_size_selector);
for (int id : sFontSizeBtnsMap.keySet()) {
View view = findViewById(id);
view.setOnClickListener(this);
};
mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
mFontSizeId = mSharedPrefs.getInt(PREFERENCE_FONT_SIZE, ResourceParser.BG_DEFAULT_FONT_SIZE);
/**
* HACKME: Fix bug of store the resource id in shared preference.
* The id may larger than the length of resources, in this case,
* return the {@link ResourceParser#BG_DEFAULT_FONT_SIZE}
*/
if(mFontSizeId >= TextAppearanceResources.getResourcesSize()) {
mFontSizeId = ResourceParser.BG_DEFAULT_FONT_SIZE;
}
mEditTextList = (LinearLayout) findViewById(R.id.note_edit_list);
}
@Override
protected void onPause() {
super.onPause();
if(saveNote()) {
Log.d(TAG, "Note data was saved with length:" + mWorkingNote.getContent().length());
}
clearSettingState();
}
private void updateWidget() {
Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
if (mWorkingNote.getWidgetType() == Notes.TYPE_WIDGET_2X) {
intent.setClass(this, NoteWidgetProvider_2x.class);
} else if (mWorkingNote.getWidgetType() == Notes.TYPE_WIDGET_4X) {
intent.setClass(this, NoteWidgetProvider_4x.class);
} else {
Log.e(TAG, "Unspported widget type");
return;
}
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] {
mWorkingNote.getWidgetId()
});
sendBroadcast(intent);
setResult(RESULT_OK, intent);
}
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<Long> ids = new HashSet<Long>();
long id = mWorkingNote.getNoteId();
if (id != Notes.ID_ROOT_FOLDER) {
ids.add(id);
} else {
Log.d(TAG, "Wrong note id, should not happen");
}
if (!isSyncMode()) {
if (!DataUtils.batchDeleteNotes(getContentResolver(), ids)) {
Log.e(TAG, "Delete Note error");
}
} else {
if (!DataUtils.batchMoveToFolder(getContentResolver(), ids, Notes.ID_TRASH_FOLER)) {
Log.e(TAG, "Move notes to trash folder error, should not happens");
}
}
}
mWorkingNote.markDeleted(true);
}
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();
}
}

@ -37,15 +37,25 @@ import net.micode.notes.R;
import java.util.HashMap;
import java.util.Map;
// NoteEditText类继承自EditText是一个自定义的文本编辑框在基础的文本编辑功能上添加了一些特定的交互逻辑
// 比如处理按键事件(回车键、删除键)、焦点变化事件以及创建上下文菜单(处理链接点击等情况),并通过接口与外部进行交互,通知相关的文本变化情况等。
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:" ;
// 定义表示电话号码链接的协议头字符串,用于识别文本中电话号码类型的链接,格式为"tel:"。
private static final String SCHEME_TEL = "tel:";
// 定义表示网页链接的协议头字符串,用于识别文本中网页类型的链接,格式为"http:"。
private static final String SCHEME_HTTP = "http:";
// 定义表示邮件链接的协议头字符串,用于识别文本中邮件类型的链接,格式为"mailto:"。
private static final String SCHEME_EMAIL = "mailto:";
// 用于建立链接协议头与对应提示字符串资源ID的映射关系的集合方便根据链接类型获取相应的显示文本告知用户链接类型。
private static final Map<String, Integer> sSchemaActionResMap = new HashMap<String, Integer>();
static {
sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel);
@ -53,6 +63,8 @@ public class NoteEditText extends EditText {
sSchemaActionResMap.put(SCHEME_EMAIL, R.string.note_link_email);
}
// 定义一个接口,用于外部类实现,该接口提供了几个方法,当文本编辑框内发生特定文本操作事件(如删除、回车键按下、文本内容变化等)时,
// 会回调相应的方法,以便外部可以根据这些事件做出相应的处理,实现与外部的交互逻辑。
/**
* Call by the {@link NoteEditActivity} to delete or add edit text
*/
@ -75,35 +87,45 @@ public class NoteEditText extends EditText {
void onTextChange(int index, boolean hasText);
}
// 用于存储实现了OnTextViewChangeListener接口的对象实例通过设置此监听器使得外部可以接收该文本编辑框内发生相关文本操作事件的通知进行相应处理。
private OnTextViewChangeListener mOnTextViewChangeListener;
// 构造函数接收上下文作为参数调用父类EditText的构造函数进行初始化同时设置当前编辑框的索引初始值为0。
public NoteEditText(Context context) {
super(context, null);
mIndex = 0;
}
// 用于设置当前文本编辑框在父容器中的索引位置的方法,外部可通过调用此方法来更新索引值,方便后续基于索引的相关操作逻辑。
public void setIndex(int index) {
mIndex = index;
}
// 用于设置文本操作事件监听器OnTextViewChangeListener的方法外部类实现该接口后通过调用此方法将实现的监听器对象传递进来
// 使得该文本编辑框能在相应事件发生时通知外部进行处理。
public void setOnTextViewChangeListener(OnTextViewChangeListener listener) {
mOnTextViewChangeListener = listener;
}
// 构造函数接收上下文和属性集AttributeSet作为参数调用父类EditText的构造函数并指定默认的编辑框样式android.R.attr.editTextStyle进行初始化。
public NoteEditText(Context context, AttributeSet attrs) {
super(context, attrs, android.R.attr.editTextStyle);
}
// 构造函数接收上下文、属性集AttributeSet和默认样式defStyle作为参数调用父类EditText的构造函数进行初始化
// 这里暂时没有额外的自定义初始化逻辑(除了调用父类构造函数),只是保留了构造函数的标准写法,注释中提示了待完善的部分(可能后续会添加更多初始化相关操作)。
public NoteEditText(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// TODO Auto-generated constructor stub
}
// 重写父类的onTouchEvent方法用于处理触摸事件主要在触摸按下ACTION_DOWN根据触摸位置来设置文本的选择位置
// 使得用户点击文本区域时能准确地定位到相应位置进行后续操作,如复制、粘贴等文本选择相关操作,其他触摸动作则按照父类默认逻辑处理。
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 获取触摸点的原始X坐标并减去文本编辑框的左内边距得到相对文本内容区域的X坐标再加上滚动的X偏移量处理滚动情况
int x = (int) event.getX();
int y = (int) event.getY();
x -= getTotalPaddingLeft();
@ -111,6 +133,8 @@ public class NoteEditText extends EditText {
x += getScrollX();
y += getScrollY();
// 根据触摸点的垂直位置获取对应的文本行数,然后根据该行数和水平位置获取对应的文本偏移量(字符位置),
// 最后将文本选择位置设置为该偏移量对应的位置,实现触摸定位文本选择的功能。
Layout layout = getLayout();
int line = layout.getLineForVertical(y);
int off = layout.getOffsetForHorizontal(line, x);
@ -121,11 +145,14 @@ public class NoteEditText extends EditText {
return super.onTouchEvent(event);
}
// 重写父类的onKeyDown方法用于处理按键按下事件在这里主要是记录删除键KEYCODE_DEL按下时文本的起始选择位置
// 对于回车键KEYCODE_ENTER按下的情况如果设置了文本操作事件监听器OnTextViewChangeListener则直接返回false
// 可能是让外部来进一步处理回车键按下的逻辑,其他按键则按照父类默认逻辑处理。
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_ENTER:
if (mOnTextViewChangeListener != null) {
if (mOnTextViewChangeListener!= null) {
return false;
}
break;
@ -138,12 +165,17 @@ public class NoteEditText extends EditText {
return super.onKeyDown(keyCode, event);
}
// 重写父类的onKeyUp方法用于处理按键抬起事件在这里重点处理删除键KEYCODE_DEL和回车键KEYCODE_ENTER抬起时的逻辑
// 如果设置了文本操作事件监听器OnTextViewChangeListener对于删除键抬起情况当起始选择位置为0且当前编辑框不是第一个索引不为0
// 调用监听器的onEditTextDelete方法通知外部删除当前编辑框内容对于回车键抬起情况获取当前选择位置后的文本内容
// 然后将文本编辑框内容更新为选择位置前的部分并调用监听器的onEditTextEnter方法通知外部在当前编辑框后添加新的编辑框及相应文本内容
// 其他按键则按照父类默认逻辑处理。
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
switch(keyCode) {
switch (keyCode) {
case KeyEvent.KEYCODE_DEL:
if (mOnTextViewChangeListener != null) {
if (0 == mSelectionStartBeforeDelete && mIndex != 0) {
if (mOnTextViewChangeListener!= null) {
if (0 == mSelectionStartBeforeDelete && mIndex!= 0) {
mOnTextViewChangeListener.onEditTextDelete(mIndex, getText().toString());
return true;
}
@ -152,7 +184,7 @@ public class NoteEditText extends EditText {
}
break;
case KeyEvent.KEYCODE_ENTER:
if (mOnTextViewChangeListener != null) {
if (mOnTextViewChangeListener!= null) {
int selectionStart = getSelectionStart();
String text = getText().subSequence(selectionStart, length()).toString();
setText(getText().subSequence(0, selectionStart));
@ -167,9 +199,12 @@ public class NoteEditText extends EditText {
return super.onKeyUp(keyCode, event);
}
// 重写父类的onFocusChanged方法用于处理焦点变化事件当焦点改变时如果设置了文本操作事件监听器OnTextViewChangeListener
// 根据当前是否获取焦点以及文本内容是否为空调用监听器的onTextChange方法通知外部相应的文本显示相关选项如是否隐藏、显示某些操作按钮等的变化情况
// 然后再按照父类默认逻辑处理焦点变化的其他相关操作。
@Override
protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
if (mOnTextViewChangeListener != null) {
if (mOnTextViewChangeListener!= null) {
if (!focused && TextUtils.isEmpty(getText())) {
mOnTextViewChangeListener.onTextChange(mIndex, false);
} else {
@ -179,6 +214,10 @@ public class NoteEditText extends EditText {
super.onFocusChanged(focused, direction, previouslyFocusedRect);
}
// 重写父类的onCreateContextMenu方法用于创建上下文菜单当文本被长按选中时弹出的菜单内容相关逻辑在此处理。
// 如果文本内容是Spanned类型包含富文本等情况如包含链接获取选中的文本范围查找其中的URLSpan表示链接的一种形式
// 如果只找到一个链接,根据链接的协议头(如"tel:"、"http:"、"mailto:"等获取对应的提示字符串资源ID若找不到对应协议头则使用默认的资源ID
// 然后添加一个菜单项到上下文菜单中点击该菜单项会触发相应的链接跳转逻辑通过调用URLSpan的onClick方法最后再按照父类默认逻辑处理菜单创建的其他相关操作。
@Override
protected void onCreateContextMenu(ContextMenu menu) {
if (getText() instanceof Spanned) {
@ -191,8 +230,8 @@ public class NoteEditText extends EditText {
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) {
for (String schema : sSchemaActionResMap.keySet()) {
if (urls[0].getURL().indexOf(schema) >= 0) {
defaultResId = sSchemaActionResMap.get(schema);
break;
}
@ -214,4 +253,4 @@ public class NoteEditText extends EditText {
}
super.onCreateContextMenu(menu);
}
}
}

@ -26,76 +26,128 @@ import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.tool.DataUtils;
// NoteItemData类主要用于封装从数据库游标Cursor中获取的笔记相关数据并提供一系列方法方便外部获取这些数据以及判断笔记的一些状态属性
// 例如是否是最后一项、是否是通话记录等情况,它是对笔记数据在业务逻辑层面的一种抽象和封装。
public class NoteItemData {
// 定义一个字符串数组用于指定从数据库查询笔记信息时要获取的列名涵盖了笔记的ID、提醒日期、背景颜色ID、创建日期等多个重要属性字段。
static final String [] PROJECTION = new String [] {
NoteColumns.ID,
NoteColumns.ALERTED_DATE,
NoteColumns.BG_COLOR_ID,
NoteColumns.CREATED_DATE,
NoteColumns.HAS_ATTACHMENT,
NoteColumns.MODIFIED_DATE,
NoteColumns.NOTES_COUNT,
NoteColumns.PARENT_ID,
NoteColumns.SNIPPET,
NoteColumns.TYPE,
NoteColumns.WIDGET_ID,
NoteColumns.WIDGET_TYPE,
NoteColumns.ID,
NoteColumns.ALERTED_DATE,
NoteColumns.BG_COLOR_ID,
NoteColumns.CREATED_DATE,
NoteColumns.HAS_ATTACHMENT,
NoteColumns.MODIFIED_DATE,
NoteColumns.NOTES_COUNT,
NoteColumns.PARENT_ID,
NoteColumns.SNIPPET,
NoteColumns.TYPE,
NoteColumns.WIDGET_ID,
NoteColumns.WIDGET_TYPE,
};
// 定义常量表示查询结果中笔记ID所在列的索引值为0方便后续从游标中准确获取对应数据这里命名为ID_COLUMN。
private static final int ID_COLUMN = 0;
// 定义常量表示查询结果中提醒日期所在列的索引值为1用于后续获取提醒日期数据命名为ALERTED_DATE_COLUMN。
private static final int ALERTED_DATE_COLUMN = 1;
// 定义常量表示查询结果中背景颜色ID所在列的索引值为2便于获取背景颜色相关设置信息命名为BG_COLOR_ID_COLUMN。
private static final int BG_COLOR_ID_COLUMN = 2;
// 定义常量表示查询结果中创建日期所在列的索引值为3用于获取笔记创建时间数据命名为CREATED_DATE_COLUMN。
private static final int CREATED_DATE_COLUMN = 3;
// 定义常量表示查询结果中是否有附件所在列的索引值为4通过判断该列的值来确定笔记是否包含附件命名为HAS_ATTACHMENT_COLUMN。
private static final int HAS_ATTACHMENT_COLUMN = 4;
// 定义常量表示查询结果中修改日期所在列的索引值为5用于获取笔记最后修改时间信息命名为MODIFIED_DATE_COLUMN。
private static final int MODIFIED_DATE_COLUMN = 5;
// 定义常量表示查询结果中笔记数量所在列的索引值为6可能用于某些与笔记数量相关的业务逻辑判断命名为NOTES_COUNT_COLUMN。
private static final int NOTES_COUNT_COLUMN = 6;
// 定义常量表示查询结果中父文件夹ID所在列的索引值为7可通过该值确定笔记所属的父文件夹命名为PARENT_ID_COLUMN。
private static final int PARENT_ID_COLUMN = 7;
// 定义常量表示查询结果中摘要片段信息所在列的索引值为8通常用于展示笔记的简短内容描述命名为SNIPPET_COLUMN。
private static final int SNIPPET_COLUMN = 8;
// 定义常量表示查询结果中笔记类型所在列的索引值为9用于判断笔记是普通笔记、文件夹还是其他类型命名为TYPE_COLUMN。
private static final int TYPE_COLUMN = 9;
// 定义常量表示查询结果中小部件ID所在列的索引值为10可能涉及与桌面小部件相关的业务逻辑命名为WIDGET_ID_COLUMN。
private static final int WIDGET_ID_COLUMN = 10;
// 定义常量表示查询结果中小部件类型所在列的索引值为11同样用于小部件相关的类型判断等操作命名为WIDGET_TYPE_COLUMN。
private static final int WIDGET_TYPE_COLUMN = 11;
// 笔记的唯一标识符存储从游标中获取的笔记ID数据用于区分不同的笔记记录。
private long mId;
// 笔记设置的提醒日期,以时间戳的形式存储,通过游标获取相应列的数据,可用于判断笔记是否有提醒以及提醒时间相关的业务逻辑。
private long mAlertDate;
// 笔记的背景颜色ID用于确定笔记在展示时的背景颜色设置从游标对应列获取该值便于后续界面显示相关的操作。
private int mBgColorId;
// 笔记的创建日期,以时间戳形式保存,通过游标获取此数据后可用于展示笔记创建时间等功能需求。
private long mCreatedDate;
// 用于标记笔记是否包含附件根据游标中对应列的值大于0表示有附件进行设置方便在业务逻辑中判断是否需要处理附件相关操作。
private boolean mHasAttachment;
// 笔记的最后修改日期,同样以时间戳形式存储,可用于展示笔记的更新情况以及相关业务逻辑判断,比如判断笔记是否被修改等。
private long mModifiedDate;
// 可能表示与笔记相关的数量信息(具体含义取决于业务场景),从游标对应列获取该整数值,用于相应的业务逻辑处理。
private int mNotesCount;
// 笔记所属的父文件夹ID通过游标获取该值可以确定笔记在文件夹层级结构中的位置关系方便进行分类、查找等操作。
private long mParentId;
// 笔记的摘要(片段)内容,从游标获取后存储,通常是笔记内容的简短描述,用于在列表等场景展示给用户一个大致的内容提示,
// 并且在这里会去除一些特定的标记字符(如复选框相关的标记)进行规范化处理。
private String mSnippet;
// 笔记的类型,通过游标获取对应列的整数值,用于区分是普通笔记、文件夹还是其他特定类型的记录,以便执行不同的业务逻辑。
private int mType;
// 与笔记关联的小部件ID可能用于桌面小部件相关的业务逻辑比如根据小部件ID来更新对应的小部件显示内容等操作。
private int mWidgetId;
// 笔记关联的小部件类型,同样用于小部件相关的业务逻辑判断和处理,例如不同类型小部件展示方式可能不同等情况。
private int mWidgetType;
// 用于存储通话记录相关的联系人姓名,如果笔记属于通话记录文件夹,则尝试获取对应的联系人姓名,若获取失败则使用电话号码作为替代。
private String mName;
// 用于存储通话记录相关的电话号码,当笔记属于通话记录文件夹时,通过工具方法获取对应的电话号码信息,用于通话记录相关的业务逻辑处理。
private String mPhoneNumber;
// 标记当前笔记是否是列表中的最后一项,通过游标的相关方法判断并设置,方便在列表展示等场景中进行相应的界面处理或业务逻辑判断。
private boolean mIsLastItem;
// 标记当前笔记是否是列表中的第一项,同样依据游标状态进行判断和设置,可用于列表相关的展示逻辑以及操作逻辑区分。
private boolean mIsFirstItem;
// 标记当前笔记所在的列表是否只包含这一项通过判断游标获取的总记录数是否为1来确定用于一些特殊业务场景下的逻辑判断。
private boolean mIsOnlyOneItem;
// 标记当前笔记是否是某个文件夹下唯一的一条笔记(后续跟着文件夹的情况),通过特定的游标移动和类型判断逻辑来确定,用于相关业务逻辑处理。
private boolean mIsOneNoteFollowingFolder;
// 标记当前笔记是否是某个文件夹后跟着多条笔记中的一条(即所在文件夹下有多条笔记的情况),同样基于游标操作和类型判断逻辑来设置,用于对应业务场景的处理。
private boolean mIsMultiNotesFollowingFolder;
// 构造函数接收上下文和数据库游标作为参数用于从游标中获取各项笔记数据并进行初始化同时调用checkPostion方法来判断笔记在列表中的相关位置状态属性。
public NoteItemData(Context context, Cursor cursor) {
// 从游标中获取笔记的ID并赋值给对应的成员变量通过使用预定义的列索引常量ID_COLUMN来确保准确获取相应数据。
mId = cursor.getLong(ID_COLUMN);
// 从游标获取笔记的提醒日期并存储使用ALERTED_DATE_COLUMN索引确保获取正确列的数据。
mAlertDate = cursor.getLong(ALERTED_DATE_COLUMN);
// 获取笔记的背景颜色ID依据BG_COLOR_ID_COLUMN索引从游标中提取对应数据并赋值。
mBgColorId = cursor.getInt(BG_COLOR_ID_COLUMN);
// 获取笔记的创建日期按照CREATED_DATE_COLUMN索引从游标获取相应时间戳数据进行存储。
mCreatedDate = cursor.getLong(CREATED_DATE_COLUMN);
mHasAttachment = (cursor.getInt(HAS_ATTACHMENT_COLUMN) > 0) ? true : false;
// 根据游标中对应列的值判断笔记是否有附件大于0则设置为true否则为false使用HAS_ATTACHMENT_COLUMN索引来定位数据列。
mHasAttachment = (cursor.getInt(HAS_ATTACHMENT_COLUMN) > 0)? true : false;
// 获取笔记的修改日期通过MODIFIED_DATE_COLUMN索引从游标获取相应时间戳并赋值给成员变量。
mModifiedDate = cursor.getLong(MODIFIED_DATE_COLUMN);
// 获取笔记相关的数量信息具体含义取决于业务场景利用NOTES_COUNT_COLUMN索引从游标中获取整数值存储起来。
mNotesCount = cursor.getInt(NOTES_COUNT_COLUMN);
// 获取笔记所属的父文件夹ID借助PARENT_ID_COLUMN索引从游标获取对应数据赋值给成员变量用于后续确定笔记的层级关系等操作。
mParentId = cursor.getLong(PARENT_ID_COLUMN);
// 获取笔记的摘要片段内容通过SNIPPET_COLUMN索引从游标获取字符串数据并去除特定的复选框相关标记字符进行规范化处理后存储。
mSnippet = cursor.getString(SNIPPET_COLUMN);
mSnippet = mSnippet.replace(NoteEditActivity.TAG_CHECKED, "").replace(
NoteEditActivity.TAG_UNCHECKED, "");
// 获取笔记的类型根据TYPE_COLUMN索引从游标获取整数值来确定笔记是何种类型如普通笔记、文件夹等用于后续不同类型的业务逻辑处理。
mType = cursor.getInt(TYPE_COLUMN);
// 获取与笔记关联的小部件ID使用WIDGET_ID_COLUMN索引从游标获取对应整数值存储起来用于小部件相关业务逻辑。
mWidgetId = cursor.getInt(WIDGET_ID_COLUMN);
// 获取笔记关联的小部件类型依靠WIDGET_TYPE_COLUMN索引从游标获取相应整数值便于针对不同小部件类型的处理操作。
mWidgetType = cursor.getInt(WIDGET_TYPE_COLUMN);
// 初始化通话记录相关的电话号码为空字符串,后续根据笔记所属文件夹等情况判断是否需要获取实际电话号码。
mPhoneNumber = "";
if (mParentId == Notes.ID_CALL_RECORD_FOLDER) {
// 如果笔记所属的父文件夹是通话记录文件夹通过工具方法DataUtils.getCallNumberByNoteId尝试从内容解析器获取对应的电话号码信息。
mPhoneNumber = DataUtils.getCallNumberByNoteId(context.getContentResolver(), mId);
if (!TextUtils.isEmpty(mPhoneNumber)) {
// 如果获取到了电话号码尝试通过联系人工具类Contact根据电话号码获取对应的联系人姓名若获取失败则使用电话号码作为姓名显示。
mName = Contact.getContact(context, mPhoneNumber);
if (mName == null) {
mName = mPhoneNumber;
@ -109,14 +161,19 @@ public class NoteItemData {
checkPostion(cursor);
}
// 用于判断当前笔记在列表中的位置相关状态属性的私有方法,通过游标操作来判断是否是最后一项、第一项、是否唯一一项以及是否是某个文件夹后的笔记情况等,
// 并设置相应的成员变量标记,方便外部通过对应的访问方法获取这些状态信息进行业务逻辑处理。
private void checkPostion(Cursor cursor) {
mIsLastItem = cursor.isLast() ? true : false;
mIsFirstItem = cursor.isFirst() ? true : false;
// 判断游标是否指向最后一条记录若是则将mIsLastItem设置为true表示当前笔记是列表中的最后一项否则为false。
mIsLastItem = cursor.isLast()? true : false;
// 判断游标是否指向第一条记录若为是则将mIsFirstItem设置为true表明当前笔记是列表中的第一项反之则为false。
mIsFirstItem = cursor.isFirst()? true : false;
// 通过判断游标获取的记录总数是否为1来确定当前笔记所在的列表是否只有这一项若是则将mIsOnlyOneItem设置为true。
mIsOnlyOneItem = (cursor.getCount() == 1);
mIsMultiNotesFollowingFolder = false;
mIsOneNoteFollowingFolder = false;
if (mType == Notes.TYPE_NOTE && !mIsFirstItem) {
if (mType == Notes.TYPE_NOTE &&!mIsFirstItem) {
int position = cursor.getPosition();
if (cursor.moveToPrevious()) {
if (cursor.getInt(TYPE_COLUMN) == Notes.TYPE_FOLDER
@ -134,91 +191,70 @@ public class NoteItemData {
}
}
// 用于判断当前笔记是否是某个文件夹下唯一的一条笔记(后续跟着文件夹的情况)的方法,返回对应的成员变量值,外部可通过调用此方法获取该状态信息进行相应逻辑处理。
public boolean isOneFollowingFolder() {
return mIsOneNoteFollowingFolder;
}
// 用于判断当前笔记是否是某个文件夹后跟着多条笔记中的一条(即所在文件夹下有多条笔记的情况)的方法,返回相应的成员变量值,方便外部根据此状态进行业务逻辑操作。
public boolean isMultiFollowingFolder() {
return mIsMultiNotesFollowingFolder;
}
// 用于判断当前笔记是否是列表中的最后一项的方法,返回对应的成员变量值,可在列表展示等场景中根据此结果进行界面处理或其他相关逻辑判断。
public boolean isLast() {
return mIsLastItem;
}
// 用于获取通话记录相关的联系人姓名的方法,如果笔记属于通话记录文件夹,则返回获取到的联系人姓名(若获取失败则返回电话号码或者空字符串),方便外部展示相关信息。
public String getCallName() {
return mName;
}
// 用于判断当前笔记是否是列表中的第一项的方法,返回对应的成员变量值,在列表相关的业务逻辑中可依据此结果进行不同的处理操作。
public boolean isFirst() {
return mIsFirstItem;
}
// 用于判断当前笔记所在的列表是否只包含这一项的方法,返回对应的成员变量值,可用于特定业务场景下的逻辑判断和处理。
public boolean isSingle() {
return mIsOnlyOneItem;
}
// 用于获取笔记的唯一标识符ID的方法返回存储的笔记ID值外部可通过调用此方法获取该笔记的ID进行如查找、关联等相关操作。
public long getId() {
return mId;
}
// 用于获取笔记设置的提醒日期的方法,返回存储的提醒日期时间戳数据,可用于判断笔记是否有提醒以及相关提醒时间相关的业务逻辑处理。
public long getAlertDate() {
return mAlertDate;
}
// 用于获取笔记的创建日期的方法,返回存储的创建日期时间戳,便于在展示笔记信息等场景中显示创建时间相关内容。
public long getCreatedDate() {
return mCreatedDate;
}
// 用于判断笔记是否包含附件的方法,返回对应的成员变量值,外部可依据此结果决定是否展示附件相关操作入口等业务逻辑处理。
public boolean hasAttachment() {
return mHasAttachment;
}
// 用于获取笔记的最后修改日期的方法,返回存储的修改日期时间戳,可用于展示笔记更新情况以及相关业务逻辑判断,比如判断笔记是否被修改等操作。
public long getModifiedDate() {
return mModifiedDate;
}
// 用于获取笔记的背景颜色ID的方法返回存储的背景颜色ID值方便在界面展示等场景中根据该ID设置笔记的背景颜色相关操作。
public int getBgColorId() {
return mBgColorId;
}
// 用于获取笔记所属的父文件夹ID的方法返回存储的父文件夹ID值通过此方法可确定笔记在文件夹层级结构中的位置关系方便进行分类、查找等操作。
public long getParentId() {
return mParentId;
}
public int getNotesCount() {
return mNotesCount;
}
public long getFolderId () {
return mParentId;
}
public int getType() {
return mType;
}
public int getWidgetType() {
return mWidgetType;
}
public int getWidgetId() {
return mWidgetId;
}
public String getSnippet() {
return mSnippet;
}
public boolean hasAlert() {
return (mAlertDate > 0);
}
public boolean isCallRecord() {
return (mParentId == Notes.ID_CALL_RECORD_FOLDER && !TextUtils.isEmpty(mPhoneNumber));
}
public static int getNoteType(Cursor cursor) {
return cursor.getInt(TYPE_COLUMN);
}
}
// 用于获取与笔记相关的数量信息(具体含义取决于业务场景)的方法,返回存储的整数值,用于相应的业务逻辑处理。
public

@ -32,153 +32,29 @@ import java.util.Iterator;
public class NotesListAdapter extends CursorAdapter {
// 用于日志输出的标签
private static final String TAG = "NotesListAdapter";
// 上下文对象,用于获取资源等操作
private Context mContext;
// 用于记录已选中项的索引及对应选中状态的哈希映射,键为列表项索引,值为是否选中的布尔值
private HashMap<Integer, Boolean> mSelectedIndex;
// 笔记的数量统计
private int mNotesCount;
// 表示是否处于选择模式的标志
private boolean mChoiceMode;
// 内部静态类用于封装与应用小部件相关的属性小部件ID和类型
public static class AppWidgetAttribute {
public int widgetId;
public int widgetType;
};
public NotesListAdapter(Context context) {
// 调用父类构造方法传入上下文并将游标初始化为null
super(context, null);
// 初始化选中索引的哈希映射
mSelectedIndex = new HashMap<Integer, Boolean>();
// 保存传入的上下文对象
mContext = context;
// 初始化笔记数量为0
mNotesCount = 0;
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return new NotesListItem(context);
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
if (view instanceof NotesListItem) {
NoteItemData itemData = new NoteItemData(context, cursor);
((NotesListItem) view).bind(context, itemData, mChoiceMode,
isSelectedItem(cursor.getPosition()));
}
}
public void setCheckedItem(final int position, final boolean checked) {
mSelectedIndex.put(position, checked);
notifyDataSetChanged();
}
public boolean isInChoiceMode() {
return mChoiceMode;
}
public void setChoiceMode(boolean mode) {
mSelectedIndex.clear();
mChoiceMode = mode;
}
public void selectAll(boolean checked) {
Cursor cursor = getCursor();
for (int i = 0; i < getCount(); i++) {
if (cursor.moveToPosition(i)) {
if (NoteItemData.getNoteType(cursor) == Notes.TYPE_NOTE) {
setCheckedItem(i, checked);
}
}
}
}
public HashSet<Long> getSelectedItemIds() {
HashSet<Long> itemSet = new HashSet<Long>();
for (Integer position : mSelectedIndex.keySet()) {
if (mSelectedIndex.get(position) == true) {
Long id = getItemId(position);
if (id == Notes.ID_ROOT_FOLDER) {
Log.d(TAG, "Wrong item id, should not happen");
} else {
itemSet.add(id);
}
}
}
return itemSet;
}
public HashSet<AppWidgetAttribute> getSelectedWidget() {
HashSet<AppWidgetAttribute> itemSet = new HashSet<AppWidgetAttribute>();
for (Integer position : mSelectedIndex.keySet()) {
if (mSelectedIndex.get(position) == true) {
Cursor c = (Cursor) getItem(position);
if (c != null) {
AppWidgetAttribute widget = new AppWidgetAttribute();
NoteItemData item = new NoteItemData(mContext, c);
widget.widgetId = item.getWidgetId();
widget.widgetType = item.getWidgetType();
itemSet.add(widget);
/**
* Don't close cursor here, only the adapter could close it
*/
} else {
Log.e(TAG, "Invalid cursor");
return null;
}
}
}
return itemSet;
}
public int getSelectedCount() {
Collection<Boolean> values = mSelectedIndex.values();
if (null == values) {
return 0;
}
Iterator<Boolean> iter = values.iterator();
int count = 0;
while (iter.hasNext()) {
if (true == iter.next()) {
count++;
}
}
return count;
}
public boolean isAllSelected() {
int checkedCount = getSelectedCount();
return (checkedCount != 0 && checkedCount == mNotesCount);
}
public boolean isSelectedItem(final int position) {
if (null == mSelectedIndex.get(position)) {
return false;
}
return mSelectedIndex.get(position);
}
@Override
protected void onContentChanged() {
super.onContentChanged();
calcNotesCount();
}
@Override
public void changeCursor(Cursor cursor) {
super.changeCursor(cursor);
calcNotesCount();
}
private void calcNotesCount() {
mNotesCount = 0;
for (int i = 0; i < getCount(); i++) {
Cursor c = (Cursor) getItem(i);
if (c != null) {
if (NoteItemData.getNoteType(c) == Notes.TYPE_NOTE) {
mNotesCount++;
}
} else {
Log.e(TAG, "Invalid cursor");
return;
}
}
}
}

@ -31,33 +31,42 @@ import net.micode.notes.tool.ResourceParser.NoteItemBgResources;
public class NotesListItem extends LinearLayout {
// 用于显示提醒相关图标的 ImageView可能表示笔记有提醒等情况
private ImageView mAlert;
// 用于显示笔记或文件夹标题的 TextView
private TextView mTitle;
// 用于显示时间相关信息(如修改时间等)的 TextView
private TextView mTime;
// 用于显示通话记录相关名称的 TextView可能在特定的通话记录相关场景下使用
private TextView mCallName;
// 封装了笔记或文件夹相关数据的对象,用于在列表项中展示具体内容
private NoteItemData mItemData;
// 复选框,用于在选择模式下标记该项是否被选中
private CheckBox mCheckBox;
public NotesListItem(Context context) {
super(context);
// 通过布局资源文件来填充当前的LinearLayout即NotesListItem自身使其具备相应的子视图布局结构
inflate(context, R.layout.note_item, this);
// 通过ID查找并初始化对应的子视图组件
mAlert = (ImageView) findViewById(R.id.iv_alert_icon);
mTitle = (TextView) findViewById(R.id.tv_title);
mTime = (TextView) findViewById(R.id.tv_time);
mCallName = (TextView) findViewById(R.id.tv_name);
mCheckBox = (CheckBox) findViewById(android.R.id.checkbox);
}
public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked) {
if (choiceMode && data.getType() == Notes.TYPE_NOTE) {
// 如果处于选择模式且当前数据对应的是笔记类型,则显示复选框,并根据传入的选中状态设置复选框的勾选状态
mCheckBox.setVisibility(View.VISIBLE);
mCheckBox.setChecked(checked);
} else {
// 否则隐藏复选框,比如在非选择模式或者是文件夹类型等情况时不需要显示复选框
mCheckBox.setVisibility(View.GONE);
}
mItemData = data;
if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) {
// 如果当前数据对应的是通话记录文件夹
mCallName.setVisibility(View.GONE);
mAlert.setVisibility(View.VISIBLE);
mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem);
@ -65,6 +74,7 @@ public class NotesListItem extends LinearLayout {
+ context.getString(R.string.format_folder_files_count, data.getNotesCount()));
mAlert.setImageResource(R.drawable.call_record);
} else if (data.getParentId() == Notes.ID_CALL_RECORD_FOLDER) {
// 如果当前数据的父文件夹是通话记录文件夹,通常表示属于通话记录相关的具体内容
mCallName.setVisibility(View.VISIBLE);
mCallName.setText(data.getCallName());
mTitle.setTextAppearance(context,R.style.TextAppearanceSecondaryItem);
@ -80,11 +90,13 @@ public class NotesListItem extends LinearLayout {
mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem);
if (data.getType() == Notes.TYPE_FOLDER) {
// 如果当前数据对应的是普通文件夹类型,设置标题文本包含文件夹名称及其中文件数量信息,并隐藏提醒图标
mTitle.setText(data.getSnippet()
+ context.getString(R.string.format_folder_files_count,
data.getNotesCount()));
data.getNotesCount()));
mAlert.setVisibility(View.GONE);
} else {
// 如果是普通笔记类型,设置标题为格式化后的笔记摘要内容,并根据是否有提醒来显示或隐藏提醒图标
mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet()));
if (data.hasAlert()) {
mAlert.setImageResource(R.drawable.clock);
@ -94,11 +106,12 @@ public class NotesListItem extends LinearLayout {
}
}
}
// 设置时间文本,显示相对时间(可能是相对于当前时间的修改时间等)
mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate()));
// 根据数据设置列表项的背景样式,区分笔记和文件夹以及笔记的不同状态等情况
setBackground(data);
}
private void setBackground(NoteItemData data) {
int id = data.getBgColorId();
if (data.getType() == Notes.TYPE_NOTE) {
@ -115,8 +128,6 @@ public class NotesListItem extends LinearLayout {
setBackgroundResource(NoteItemBgResources.getFolderBgRes());
}
}
public NoteItemData getItemData() {
return mItemData;
}
}
}

@ -49,42 +49,51 @@ import net.micode.notes.gtask.remote.GTaskSyncService;
public class NotesPreferenceActivity extends PreferenceActivity {
// 存储偏好设置的名称
public static final String PREFERENCE_NAME = "notes_preferences";
// 偏好设置中同步账号名称对应的键
public static final String PREFERENCE_SYNC_ACCOUNT_NAME = "pref_key_account_name";
// 偏好设置中上次同步时间对应的键
public static final String PREFERENCE_LAST_SYNC_TIME = "pref_last_sync_time";
// 偏好设置中设置背景颜色相关键(从变量名推测,可能用于控制背景颜色随机出现相关功能)
public static final String PREFERENCE_SET_BG_COLOR_KEY = "pref_key_bg_random_appear";
// 同步账号相关偏好设置分类的键
private static final String PREFERENCE_SYNC_ACCOUNT_KEY = "pref_sync_account_key";
// 权限相关过滤键(可能用于账户相关权限过滤,具体需结合更多上下文判断)
private static final String AUTHORITIES_FILTER_KEY = "authorities";
// 用于展示同步账号相关偏好设置的分类
private PreferenceCategory mAccountCategory;
// 广播接收器,用于接收同步服务相关广播
private GTaskReceiver mReceiver;
// 记录原始的账号列表(用于对比账号变化等情况)
private Account[] mOriAccounts;
// 标记是否添加了新账号
private boolean mHasAddedAccount;
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
/* using the app icon for navigation */
// 使用应用图标进行导航启用返回上级页面功能ActionBar上显示返回箭头
getActionBar().setDisplayHomeAsUpEnabled(true);
// 从指定的XML资源文件加载偏好设置界面布局
addPreferencesFromResource(R.xml.preferences);
// 获取同步账号相关的偏好设置分类
mAccountCategory = (PreferenceCategory) findPreference(PREFERENCE_SYNC_ACCOUNT_KEY);
// 创建广播接收器实例
mReceiver = new GTaskReceiver();
IntentFilter filter = new IntentFilter();
// 注册接收同步服务广播的过滤器,监听同步服务广播的特定动作
filter.addAction(GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME);
// 注册广播接收器,用于接收同步服务相关广播
registerReceiver(mReceiver, filter);
mOriAccounts = null;
// 加载设置界面头部视图
View header = LayoutInflater.from(this).inflate(R.layout.settings_header, null);
// 将头部视图添加到ListView用于显示偏好设置的列表视图并设置相关参数
getListView().addHeaderView(header, null, true);
}
@ -92,19 +101,23 @@ public class NotesPreferenceActivity extends PreferenceActivity {
protected void onResume() {
super.onResume();
// need to set sync account automatically if user has added a new
// account
// 如果用户添加了新账号,需要自动设置同步账号
if (mHasAddedAccount) {
// 获取当前的谷歌账号列表
Account[] accounts = getGoogleAccounts();
if (mOriAccounts != null && accounts.length > mOriAccounts.length) {
// 判断原始账号列表不为空且当前账号数量比原始账号数量多(意味着可能添加了新账号)
if (mOriAccounts!= null && accounts.length > mOriAccounts.length) {
// 遍历新账号列表
for (Account accountNew : accounts) {
boolean found = false;
// 遍历原始账号列表,对比新账号是否已经存在
for (Account accountOld : mOriAccounts) {
if (TextUtils.equals(accountOld.name, accountNew.name)) {
found = true;
break;
}
}
// 如果新账号在原始账号中不存在,设置该新账号为同步账号,并跳出循环
if (!found) {
setSyncAccount(accountNew.name);
break;
@ -113,120 +126,164 @@ public class NotesPreferenceActivity extends PreferenceActivity {
}
}
// 刷新界面UI更新相关显示内容比如账号信息、同步按钮状态等
refreshUI();
}
@Override
protected void onDestroy() {
if (mReceiver != null) {
// 如果广播接收器不为空,注销广播接收器,避免内存泄漏等问题
if (mReceiver!= null) {
unregisterReceiver(mReceiver);
}
super.onDestroy();
}
/**
*
*/
private void loadAccountPreference() {
// 移除账号分类下的所有已有偏好设置项
mAccountCategory.removeAll();
// 创建一个新的偏好设置项(用于展示账号相关操作入口)
Preference accountPref = new Preference(this);
// 获取当前设置的同步账号名称(如果有的话)
final String defaultAccount = getSyncAccountName(this);
// 设置账号偏好设置项的标题
accountPref.setTitle(getString(R.string.preferences_account_title));
// 设置账号偏好设置项的摘要内容
accountPref.setSummary(getString(R.string.preferences_account_summary));
// 设置账号偏好设置项的点击监听器,处理点击事件逻辑
accountPref.setOnPreferenceClickListener(new OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
// 如果同步服务当前没有在同步中
if (!GTaskSyncService.isSyncing()) {
// 如果还没有设置过同步账号(账号名称为空)
if (TextUtils.isEmpty(defaultAccount)) {
// the first time to set account
// 首次设置账号,弹出选择账号的对话框
showSelectAccountAlertDialog();
} else {
// if the account has already been set, we need to promp
// user about the risk
// 如果已经设置过账号,弹出确认修改账号的对话框(提示相关风险等)
showChangeAccountConfirmAlertDialog();
}
} else {
// 如果同步服务正在同步,显示提示信息告知用户不能修改账号
Toast.makeText(NotesPreferenceActivity.this,
R.string.preferences_toast_cannot_change_account, Toast.LENGTH_SHORT)
R.string.preferences_toast_cannot_change_account, Toast.LENGTH_SHORT)
.show();
}
return true;
}
});
// 将账号偏好设置项添加到账号分类下
mAccountCategory.addPreference(accountPref);
}
/**
*
*/
private void loadSyncButton() {
// 获取同步按钮实例
Button syncButton = (Button) findViewById(R.id.preference_sync_button);
// 获取显示上次同步时间的文本视图实例
TextView lastSyncTimeView = (TextView) findViewById(R.id.prefenerece_sync_status_textview);
// set button state
// 根据同步服务是否正在同步来设置同步按钮的文本和点击事件逻辑
if (GTaskSyncService.isSyncing()) {
// 如果正在同步,按钮文本设置为取消同步
syncButton.setText(getString(R.string.preferences_button_sync_cancel));
// 设置按钮点击事件,点击时取消同步操作
syncButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
GTaskSyncService.cancelSync(NotesPreferenceActivity.this);
}
});
} else {
// 如果没有同步,按钮文本设置为立即同步
syncButton.setText(getString(R.string.preferences_button_sync_immediately));
// 设置按钮点击事件,点击时开始同步操作
syncButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
GTaskSyncService.startSync(NotesPreferenceActivity.this);
}
});
}
// 按钮是否可用取决于是否设置了同步账号(账号名称不为空时可用)
syncButton.setEnabled(!TextUtils.isEmpty(getSyncAccountName(this)));
// set last sync time
// 根据同步服务状态设置上次同步时间文本视图的显示内容和可见性
if (GTaskSyncService.isSyncing()) {
// 如果正在同步,显示同步进度相关信息
lastSyncTimeView.setText(GTaskSyncService.getProgressString());
lastSyncTimeView.setVisibility(View.VISIBLE);
} else {
// 如果没有同步,获取上次同步时间
long lastSyncTime = getLastSyncTime(this);
if (lastSyncTime != 0) {
if (lastSyncTime!= 0) {
// 如果上次同步时间不为0格式化时间并设置显示文本
lastSyncTimeView.setText(getString(R.string.preferences_last_sync_time,
DateFormat.format(getString(R.string.preferences_last_sync_time_format),
lastSyncTime)));
lastSyncTimeView.setVisibility(View.VISIBLE);
} else {
// 如果上次同步时间为0隐藏该文本视图
lastSyncTimeView.setVisibility(View.GONE);
}
}
}
/**
* UI
*/
private void refreshUI() {
loadAccountPreference();
loadSyncButton();
}
/**
*
*/
private void showSelectAccountAlertDialog() {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
// 加载对话框标题视图布局
View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null);
TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title);
// 设置对话框标题文本
titleTextView.setText(getString(R.string.preferences_dialog_select_account_title));
TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle);
// 设置对话框副标题文本(提示相关信息)
subtitleTextView.setText(getString(R.string.preferences_dialog_select_account_tips));
// 设置对话框的自定义标题视图
dialogBuilder.setCustomTitle(titleView);
// 设置对话框的确定按钮这里先设置为null可能后续根据需求再处理
dialogBuilder.setPositiveButton(null, null);
// 获取当前的谷歌账号列表
Account[] accounts = getGoogleAccounts();
// 获取当前设置的同步账号名称(如果有的话)
String defAccount = getSyncAccountName(this);
mOriAccounts = accounts;
mHasAddedAccount = false;
if (accounts.length > 0) {
// 创建用于显示账号名称的字符序列数组(长度与账号列表长度一致)
CharSequence[] items = new CharSequence[accounts.length];
final CharSequence[] itemMapping = items;
int checkedItem = -1;
int index = 0;
// 遍历账号列表,填充显示账号名称的数组,并记录当前已设置账号在数组中的位置(用于默认选中)
for (Account account : accounts) {
if (TextUtils.equals(account.name, defAccount)) {
checkedItem = index;
}
items[index++] = account.name;
}
// 设置对话框的单选列表项,用户选择账号后执行相应逻辑(设置选中账号为同步账号等)
dialogBuilder.setSingleChoiceItems(items, checkedItem,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
@ -237,16 +294,18 @@ public class NotesPreferenceActivity extends PreferenceActivity {
});
}
// 加载添加账号相关视图布局(可能用于提示用户添加账号等操作)
View addAccountView = LayoutInflater.from(this).inflate(R.layout.add_account_text, null);
dialogBuilder.setView(addAccountView);
final AlertDialog dialog = dialogBuilder.show();
// 设置添加账号视图的点击事件,点击时跳转到添加账号设置页面,并标记添加了新账号等操作
addAccountView.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
mHasAddedAccount = true;
Intent intent = new Intent("android.settings.ADD_ACCOUNT_SETTINGS");
intent.putExtra(AUTHORITIES_FILTER_KEY, new String[] {
"gmail-ls"
"gmail-ls"
});
startActivityForResult(intent, -1);
dialog.dismiss();
@ -254,22 +313,30 @@ public class NotesPreferenceActivity extends PreferenceActivity {
});
}
/**
*
*/
private void showChangeAccountConfirmAlertDialog() {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
// 加载对话框标题视图布局
View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null);
TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title);
// 设置对话框标题文本,包含当前已设置的同步账号名称
titleTextView.setText(getString(R.string.preferences_dialog_change_account_title,
getSyncAccountName(this)));
TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle);
// 设置对话框副标题文本(提示修改账号相关的警告信息)
subtitleTextView.setText(getString(R.string.preferences_dialog_change_account_warn_msg));
dialogBuilder.setCustomTitle(titleView);
// 创建菜单项字符序列数组,包含修改账号、删除账号、取消等选项
CharSequence[] menuItemArray = new CharSequence[] {
getString(R.string.preferences_menu_change_account),
getString(R.string.preferences_menu_remove_account),
getString(R.string.preferences_menu_cancel)
};
// 设置对话框的菜单项点击事件逻辑,根据用户选择执行相应操作
dialogBuilder.setItems(menuItemArray, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
if (which == 0) {
@ -283,26 +350,40 @@ public class NotesPreferenceActivity extends PreferenceActivity {
dialogBuilder.show();
}
/**
*
*
* @return
*/
private Account[] getGoogleAccounts() {
AccountManager accountManager = AccountManager.get(this);
return accountManager.getAccountsByType("com.google");
}
/**
*
*
* @param account
*/
private void setSyncAccount(String account) {
// 如果传入的账号与当前设置的账号不一致
if (!getSyncAccountName(this).equals(account)) {
// 获取偏好设置实例
SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
if (account != null) {
if (account!= null) {
// 如果账号不为空,将账号名称保存到偏好设置中
editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, account);
} else {
// 如果账号为空,清除已保存的账号名称
editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, "");
}
editor.commit();
// clean up last sync time
// 清理上次同步时间设置为0
setLastSyncTime(this, 0);
// clean up local gtask related info
// 清理本地与GTask相关的信息在新线程中执行数据库更新操作
new Thread(new Runnable() {
public void run() {
ContentValues values = new ContentValues();
@ -312,12 +393,16 @@ public class NotesPreferenceActivity extends PreferenceActivity {
}
}).start();
// 显示设置账号成功的提示信息
Toast.makeText(NotesPreferenceActivity.this,
getString(R.string.preferences_toast_success_set_accout, account),
Toast.LENGTH_SHORT).show();
}
}
/**
* GTask
*/
private void removeSyncAccount() {
SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
@ -329,7 +414,7 @@ public class NotesPreferenceActivity extends PreferenceActivity {
}
editor.commit();
// clean up local gtask related info
// 清理本地与GTask相关的信息在新线程中执行数据库更新操作
new Thread(new Runnable() {
public void run() {
ContentValues values = new ContentValues();
@ -340,49 +425,18 @@ public class NotesPreferenceActivity extends PreferenceActivity {
}).start();
}
/**
*
*
* @param context
* @return
*/
public static String getSyncAccountName(Context context) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
return settings.getString(PREFERENCE_SYNC_ACCOUNT_NAME, "");
}
public static void setLastSyncTime(Context context, long time) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
editor.putLong(PREFERENCE_LAST_SYNC_TIME, time);
editor.commit();
}
public static long getLastSyncTime(Context context) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
return settings.getLong(PREFERENCE_LAST_SYNC_TIME, 0);
}
private class GTaskReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
refreshUI();
if (intent.getBooleanExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_IS_SYNCING, false)) {
TextView syncStatus = (TextView) findViewById(R.id.prefenerece_sync_status_textview);
syncStatus.setText(intent
.getStringExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_PROGRESS_MSG));
}
}
}
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
Intent intent = new Intent(this, NotesListActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
return true;
default:
return false;
}
}
}
/**
*
*

@ -32,24 +32,37 @@ import net.micode.notes.tool.ResourceParser;
import net.micode.notes.ui.NoteEditActivity;
import net.micode.notes.ui.NotesListActivity;
// NoteWidgetProvider抽象类继承自AppWidgetProvider用于处理与笔记相关的桌面小部件逻辑
public abstract class NoteWidgetProvider extends AppWidgetProvider {
// 定义一个字符串数组,用于指定从数据库查询时要获取的列名
// 包含笔记的ID、背景颜色ID以及摘要信息等列
public static final String [] PROJECTION = new String [] {
NoteColumns.ID,
NoteColumns.BG_COLOR_ID,
NoteColumns.SNIPPET
NoteColumns.ID,
NoteColumns.BG_COLOR_ID,
NoteColumns.SNIPPET
};
public static final int COLUMN_ID = 0;
public static final int COLUMN_BG_COLOR_ID = 1;
public static final int COLUMN_SNIPPET = 2;
// 定义常量表示PROJECTION数组中对应列的索引方便后续从查询结果如Cursor中获取具体数据
public static final int COLUMN_ID = 0;
public static final int COLUMN_BG_COLOR_ID = 1;
public static final int COLUMN_SNIPPET = 2;
// 定义一个私有静态常量,用于日志记录时标识当前类
private static final String TAG = "NoteWidgetProvider";
// 重写AppWidgetProvider的onDeleted方法当桌面小部件被删除时触发该方法
@Override
public void onDeleted(Context context, int[] appWidgetIds) {
// 创建一个ContentValues对象用于存储要更新的数据
ContentValues values = new ContentValues();
// 将NoteColumns.WIDGET_ID对应的字段值设为AppWidgetManager.INVALID_APPWIDGET_ID标记小部件已无效
values.put(NoteColumns.WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
// 遍历被删除小部件的ID数组
for (int i = 0; i < appWidgetIds.length; i++) {
// 通过ContentResolver根据小部件ID条件去更新数据库中相关记录
// 将对应的小部件ID标记为无效Notes.CONTENT_NOTE_URI表示数据源
context.getContentResolver().update(Notes.CONTENT_NOTE_URI,
values,
NoteColumns.WIDGET_ID + "=?",
@ -57,7 +70,12 @@ public abstract class NoteWidgetProvider extends AppWidgetProvider {
}
}
// 私有方法用于根据给定的小部件IDwidgetId从内容提供器查询相关的笔记小部件信息
private Cursor getNoteWidgetInfo(Context context, int widgetId) {
// 通过ContentResolver进行查询操作
// 查询的数据源是Notes.CONTENT_NOTE_URI查询的列由PROJECTION指定
// 筛选条件要求NoteColumns.WIDGET_ID等于传入的小部件ID且NoteColumns.PARENT_ID不等于特定的垃圾文件夹IDNotes.ID_TRASH_FOLER
// 最后返回查询得到的Cursor对象如果没有符合条件的数据则返回null
return context.getContentResolver().query(Notes.CONTENT_NOTE_URI,
PROJECTION,
NoteColumns.WIDGET_ID + "=? AND " + NoteColumns.PARENT_ID + "<>?",
@ -65,68 +83,95 @@ public abstract class NoteWidgetProvider extends AppWidgetProvider {
null);
}
// 对外可调用的更新小部件的方法它内部调用另一个带有更多参数的update方法并传入false作为隐私模式privacyMode的参数值
protected void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
update(context, appWidgetManager, appWidgetIds, false);
}
// 具体执行小部件更新逻辑的方法,用于更新桌面小部件的显示内容等信息
private void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds,
boolean privacyMode) {
boolean privacyMode) {
// 遍历传入的小部件ID数组
for (int i = 0; i < appWidgetIds.length; i++) {
if (appWidgetIds[i] != AppWidgetManager.INVALID_APPWIDGET_ID) {
// 只处理有效的小部件ID不等于AppWidgetManager.INVALID_APPWIDGET_ID的情况
if (appWidgetIds[i]!= AppWidgetManager.INVALID_APPWIDGET_ID) {
// 获取默认的背景ID具体获取逻辑由ResourceParser.getDefaultBgId方法实现
int bgId = ResourceParser.getDefaultBgId(context);
// 初始化摘要信息为空字符串
String snippet = "";
// 创建一个用于启动NoteEditActivity的Intent设置相关标志位和额外数据
Intent intent = new Intent(context, NoteEditActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
intent.putExtra(Notes.INTENT_EXTRA_WIDGET_ID, appWidgetIds[i]);
intent.putExtra(Notes.INTENT_EXTRA_WIDGET_TYPE, getWidgetType());
// 获取对应小部件的笔记信息Cursor
Cursor c = getNoteWidgetInfo(context, appWidgetIds[i]);
if (c != null && c.moveToFirst()) {
if (c!= null && c.moveToFirst()) {
// 如果查询结果中存在多条符合条件的数据理论上小部件ID应该唯一对应一条记录则记录错误日志并关闭Cursor直接返回
if (c.getCount() > 1) {
Log.e(TAG, "Multiple message with same widget id:" + appWidgetIds[i]);
c.close();
return;
}
// 从Cursor中获取摘要信息
snippet = c.getString(COLUMN_SNIPPET);
// 从Cursor中获取背景颜色ID
bgId = c.getInt(COLUMN_BG_COLOR_ID);
// 向Intent中添加额外数据用户ID等信息
intent.putExtra(Intent.EXTRA_UID, c.getLong(COLUMN_ID));
// 设置Intent的动作表示是查看操作
intent.setAction(Intent.ACTION_VIEW);
} else {
// 如果没有查询到对应小部件的笔记信息,设置默认的摘要信息
snippet = context.getResources().getString(R.string.widget_havenot_content);
// 设置Intent的动作表示是插入/编辑操作
intent.setAction(Intent.ACTION_INSERT_OR_EDIT);
}
if (c != null) {
// 如果Cursor不为空关闭它以释放资源
if (c!= null) {
c.close();
}
// 创建一个RemoteViews对象用于构建小部件的界面显示传入当前应用的包名和通过抽象方法获取的布局资源ID
RemoteViews rv = new RemoteViews(context.getPackageName(), getLayoutId());
// 根据抽象方法获取的背景资源ID来设置小部件的背景图片资源
rv.setImageViewResource(R.id.widget_bg_image, getBgResourceId(bgId));
// 向Intent中添加额外的背景ID数据
intent.putExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, bgId);
/**
* Generate the pending intent to start host for the widget
*/
// 用于生成点击小部件时启动对应Activity的PendingIntent初始化为null
PendingIntent pendingIntent = null;
if (privacyMode) {
// 如果处于隐私模式,设置小部件文本显示特定的隐私提示信息
rv.setTextViewText(R.id.widget_text,
context.getString(R.string.widget_under_visit_mode));
// 创建一个用于启动NotesListActivity的PendingIntent用于隐私模式下的点击响应
pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], new Intent(
context, NotesListActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);
} else {
// 正常情况下设置小部件文本显示摘要信息snippet
rv.setTextViewText(R.id.widget_text, snippet);
// 创建一个用于启动NoteEditActivity由前面intent定义的PendingIntent用于正常模式下的点击响应
pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], intent,
PendingIntent.FLAG_UPDATE_CURRENT);
}
// 为小部件文本设置点击PendingIntent实现点击交互功能
rv.setOnClickPendingIntent(R.id.widget_text, pendingIntent);
// 通过AppWidgetManager更新小部件的实际显示内容
appWidgetManager.updateAppWidget(appWidgetIds[i], rv);
}
}
}
// 抽象方法要求子类实现该方法根据传入的背景ID获取对应的背景资源ID用于设置小部件的背景显示资源
protected abstract int getBgResourceId(int bgId);
// 抽象方法子类需实现此方法来返回小部件对应的布局资源ID用于构建小部件的界面布局
protected abstract int getLayoutId();
// 抽象方法,需要子类实现,用于返回小部件的类型相关信息,具体的类型定义由业务逻辑决定,在小部件相关处理中可能根据类型做不同操作
protected abstract int getWidgetType();
}
}

@ -24,22 +24,34 @@ import net.micode.notes.data.Notes;
import net.micode.notes.tool.ResourceParser;
// NoteWidgetProvider_2x类继承自NoteWidgetProvider抽象类用于实现特定尺寸可能是2倍尺寸相关从类名推测的笔记桌面小部件相关逻辑
public class NoteWidgetProvider_2x extends NoteWidgetProvider {
// 重写onUpdate方法当桌面小部件需要更新时会触发该方法
// 这里调用了父类NoteWidgetProvider的update方法来执行具体的更新操作
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
super.update(context, appWidgetManager, appWidgetIds);
}
// 实现父类NoteWidgetProvider中定义的抽象方法getLayoutId
// 返回用于该特定小部件2x尺寸相关的布局资源ID对应布局资源为R.layout.widget_2x
@Override
protected int getLayoutId() {
return R.layout.widget_2x;
}
// 实现父类NoteWidgetProvider中定义的抽象方法getBgResourceId
// 根据传入的背景IDbgId通过ResourceParser.WidgetBgResources.getWidget2xBgResource方法来获取对应的2x尺寸小部件的背景资源ID
// 用于设置该特定小部件的背景显示资源
@Override
protected int getBgResourceId(int bgId) {
return ResourceParser.WidgetBgResources.getWidget2xBgResource(bgId);
}
// 实现父类NoteWidgetProvider中定义的抽象方法getWidgetType
// 返回该小部件的类型标识此处返回值为Notes.TYPE_WIDGET_2X表示这是2x尺寸类型的小部件
// 在整个小部件相关的处理逻辑中,可能会根据这个类型做不同的操作(例如不同类型小部件显示不同样式、功能等)
@Override
protected int getWidgetType() {
return Notes.TYPE_WIDGET_2X;

@ -24,23 +24,34 @@ import net.micode.notes.data.Notes;
import net.micode.notes.tool.ResourceParser;
// NoteWidgetProvider_4x类继承自NoteWidgetProvider抽象类它主要负责实现特定的推测为4倍尺寸相关从类名判断笔记桌面小部件相关的具体逻辑。
public class NoteWidgetProvider_4x extends NoteWidgetProvider {
// 重写onUpdate方法该方法会在桌面小部件需要进行更新操作时被触发。
// 在这里它调用了父类NoteWidgetProvider的update方法来执行实际的小部件更新逻辑利用了父类中已经定义好的通用更新流程。
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
super.update(context, appWidgetManager, appWidgetIds);
}
// 重写父类NoteWidgetProvider中定义的抽象方法getLayoutId用于返回此特定小部件4x尺寸相关对应的布局资源ID。
// 通过返回R.layout.widget_4x指定了该小部件所使用的布局文件这个布局文件应该是按照4x尺寸的设计需求进行定制的。
// 注意:这里方法的修饰符应该是@Override原代码中缺少该注解可能会导致代码逻辑与预期不符若后续在父类中修改了抽象方法的签名等情况编译器无法准确检查此处是否正确重写。
protected int getLayoutId() {
return R.layout.widget_4x;
}
// 重写父类NoteWidgetProvider中定义的抽象方法getBgResourceId根据传入的背景IDbgId来获取对应的背景资源ID。
// 具体是通过调用ResourceParser.WidgetBgResources.getWidget4xBgResource方法来获取适用于4x尺寸小部件的背景资源以用于设置该小部件的背景显示效果。
@Override
protected int getBgResourceId(int bgId) {
return ResourceParser.WidgetBgResources.getWidget4xBgResource(bgId);
}
// 重写父类NoteWidgetProvider中定义的抽象方法getWidgetType用于返回该小部件的类型标识。
// 这里返回Notes.TYPE_WIDGET_4X表示这是一个4x尺寸类型的小部件在整个小部件相关的处理流程中可能会依据这个类型标识进行不同的业务处理比如展现不同的样式、启用不同的功能等。
@Override
protected int getWidgetType() {
return Notes.TYPE_WIDGET_4X;
}
}
}
Loading…
Cancel
Save