Maike 1 year ago
parent daec56f211
commit fab1fa9d83

@ -0,0 +1,139 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// AlarmAlertActivity 类是一个 Activity它用于处理笔记的提醒警报。
public class AlarmAlertActivity extends Activity implements OnClickListener, OnDismissListener {
private long mNoteId; // 用于存储笔记 ID 的变量。
private String mSnippet; // 用于存储笔记摘要的变量。
private static final int SNIPPET_PREW_MAX_LEN = 60; // 设置摘要的最大长度。
MediaPlayer mPlayer; // 用于播放音频的 MediaPlayer 对象。
// onCreate 方法在 Activity 被创建时调用。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE); // 隐藏标题栏。
// 设置窗口属性。
final Window win = getWindow();
win.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
if (!isScreenOn()) {
win.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
| WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
| WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
| WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR);
}
// 获取意图中的数据。
Intent intent = getIntent();
// 尝试获取笔记 ID 和摘要。
try {
mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1));
mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId);
mSnippet = mSnippet.length() > SNIPPET_PREW_MAX_LEN ? mSnippet.substring(0,
SNIPPET_PREW_MAX_LEN) + getResources().getString(R.string.notelist_string_info)
: mSnippet;
} catch (IllegalArgumentException e) {
e.printStackTrace();
return; // 如果出错,结束活动。
}
mPlayer = new MediaPlayer(); // 创建新的 MediaPlayer 对象。
if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) {
showActionDialog(); // 显示操作对话框。
playAlarmSound(); // 播放警报声。
} else {
finish(); // 如果笔记不存在,结束活动。
}
}
// isScreenOn 方法检查屏幕是否开启。
private boolean isScreenOn() {
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
return pm.isScreenOn();
}
// playAlarmSound 方法播放警报声。
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) {
mPlayer.setAudioStreamType(silentModeStreams);
} else {
mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM);
}
try {
mPlayer.setDataSource(this, url);
mPlayer.prepare();
mPlayer.setLooping(true);
mPlayer.start();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
} catch (IllegalStateException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
// showActionDialog 方法显示一个操作对话框。
private void showActionDialog() {
AlertDialog.Builder dialog = new AlertDialog.Builder(this);
dialog.setTitle(R.string.app_name);
dialog.setMessage(mSnippet);
dialog.setPositiveButton(R.string.notealert_ok, this); // 设置确定按钮。
if (isScreenOn()) {
dialog.setNegativeButton(R.string.notealert_enter, this); // 设置返回按钮。
}
dialog.show().setOnDismissListener(this); // 显示对话框并设置关闭监听器。
}
// onClick 方法是 OnClickListener 接口的实现,用于处理确定按钮的点击事件。
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case DialogInterface.BUTTON_NEGATIVE:
Intent intent = new Intent(this, NoteEditActivity.class);
intent.setAction(Intent.ACTION_VIEW);
intent.putExtra(Intent.EXTRA_UID, mNoteId);
startActivity(intent); // 启动 NoteEditActivity。
break;
default:
break;
}
}
// onDismiss 方法是 OnDismissListener 接口的实现,用于处理对话框关闭的事件。
public void onDismiss(DialogInterface dialog) {
stopAlarmSound(); // 停止警报声。
finish(); // 结束活动。
}
// stopAlarmSound 方法停止警报声的播放。
private void stopAlarmSound() {
if (mPlayer != null) {
mPlayer.stop();
mPlayer.release();
mPlayer = null; // 释放 MediaPlayer 对象。
}
}
}

@ -0,0 +1,64 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// 定义一个广播接收器,用于初始化闹钟
public class AlarmInitReceiver extends BroadcastReceiver {
// 静态字段,定义投影的列
private static final String [] PROJECTION = new String [] {
NoteColumns.ID,
NoteColumns.ALERTED_DATE
};
// 定义投影列的索引
private static final int COLUMN_ID = 0;
private static final int COLUMN_ALERTED_DATE = 1;
@Override
public void onReceive(Context context, Intent intent) {
// 获取当前时间的时间戳
long currentDate = System.currentTimeMillis();
// 获取内容解析器的实例
Cursor c = context.getContentResolver().query(Notes.CONTENT_NOTE_URI,
PROJECTION,
NoteColumns.ALERTED_DATE + ">? AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE,
new String[] { String.valueOf(currentDate) },
null);
if (c != null) {
if (c.moveToFirst()) {
// 如果查询结果有数据,循环处理
do {
// 获取闹钟提醒的时间戳
long alertDate = c.getLong(COLUMN_ALERTED_DATE);
// 创建一个意图指向AlarmReceiver类
Intent sender = new Intent(context, AlarmReceiver.class);
// 设置意图的数据为笔记的Uri并通过ID来唯一标识
sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, c.getLong(COLUMN_ID)));
// 创建一个挂起意图,用于在指定时间发送广播
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, 0);
// 获取AlarmManager的实例
AlarmManager alermManager = (AlarmManager) context
.getSystemService(Context.ALARM_SERVICE);
// 设置闹钟,在指定的时间戳前触发,并使用挂起意图发送广播
alermManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent);
} while (c.moveToNext());
}
// 关闭游标
c.close();
}
}
}

@ -0,0 +1,43 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
/**
*
*
*/
public class AlarmReceiver extends BroadcastReceiver {
/**
*
* @param context
* @param intent Intent
*/
@Override
public void onReceive(Context context, Intent intent) {
// 设置目标活动的类这里指向AlarmAlertActivity。
intent.setClass(context, AlarmAlertActivity.class);
// 添加FLAG_ACTIVITY_NEW_TASK标志以确保在新的任务栈中启动活动。
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
// 启动目标活动。
context.startActivity(intent);
}
}

@ -0,0 +1,519 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import java.text.DateFormatSymbols;
import java.util.Calendar;
import net.micode.notes.R;
import android.content.Context;
import android.text.format.DateFormat;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.NumberPicker;
//日期选择器的值变化监听器
private NumberPicker.OnValueChangeListener mOnDateChangedListener = new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
// 当日期选择器的值发生变化时,增加或减少日期
mDate.add(Calendar.DAY_OF_YEAR, newVal - oldVal);
// 更新日期控件以反映新的日期
updateDateControl();
// 通知监听器日期时间已改变
onDateTimeChanged();
}
};
//小时选择器的值变化监听器
private NumberPicker.OnValueChangeListener mOnHourChangedListener = new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
// 检查是否需要更改日期
boolean isDateChanged = false;
Calendar cal = Calendar.getInstance();
// 根据是否为24小时视图处理小时变化
if (!mIs24HourView) {
if (!mIsAm && oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) {
// 如果从11:59变成00:00增加一天
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) {
// 如果从00:00变成11:59减少一天
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, -1);
isDateChanged = true;
}
// 如果小时从11:59变成00:00或从00:00变成11:59切换上午/下午
if (oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY ||
oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) {
mIsAm = !mIsAm;
updateAmPmControl();
}
} else {
// 如果是24小时视图处理小时变化
if (oldVal == HOURS_IN_ALL_DAY - 1 && newVal == 0) {
// 如果从23:59变成00:00增加一天
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, 1);
isDateChanged = true;
} else if (oldVal == 0 && newVal == HOURS_IN_ALL_DAY - 1) {
// 如果从00:00变成23:59减少一天
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, -1);
isDateChanged = true;
}
}
// 设置新的小时值,考虑上午/下午
int newHour = mHourSpinner.getValue() % HOURS_IN_HALF_DAY + (mIsAm ? 0 : HOURS_IN_HALF_DAY);
mDate.set(Calendar.HOUR_OF_DAY, newHour);
// 通知监听器日期时间已改变
onDateTimeChanged();
// 如果日期发生变化,更新年、月、日控件
if (isDateChanged) {
setCurrentYear(cal.get(Calendar.YEAR));
setCurrentMonth(cal.get(Calendar.MONTH));
setCurrentDay(cal.get(Calendar.DAY_OF_MONTH));
}
}
};
//分钟选择器的值变化监听器
private NumberPicker.OnValueChangeListener mOnMinuteChangedListener = new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
// 检查是否需要更改小时
int minValue = mMinuteSpinner.getMinValue();
int maxValue = mMinuteSpinner.getMaxValue();
int offset = 0;
if (oldVal == maxValue && newVal == minValue) {
// 如果从59变为00增加一个小时
offset += 1;
} else if (oldVal == minValue && newVal == maxValue) {
// 如果从00变为59减少一个小时
offset -= 1;
}
// 如果需要,更改小时值
if (offset != 0) {
mDate.add(Calendar.HOUR_OF_DAY, offset);
mHourSpinner.setValue(getCurrentHour());
updateDateControl();
int newHour = getCurrentHourOfDay();
// 切换上午/下午
if (newHour >= HOURS_IN_HALF_DAY) {
mIsAm = false;
updateAmPmControl();
} else {
mIsAm = true;
updateAmPmControl();
}
}
// 设置新的分钟值
mDate.set(Calendar.MINUTE, newVal);
// 通知监听器日期时间已改变
onDateTimeChanged();
}
};
//AM/PM选择器的值变化监听器
private NumberPicker.OnValueChangeListener mOnAmPmChangedListener = new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
// 切换上午/下午
mIsAm = !mIsAm;
if (mIsAm) {
// 如果现在是下午,减去半天的小时数
mDate.add(Calendar.HOUR_OF_DAY, -HOURS_IN_HALF_DAY);
} else {
// 如果现在是上午,加上半天的小时数
mDate.add(Calendar.HOUR_OF_DAY, HOURS_IN_HALF_DAY);
}
updateAmPmControl();
// 通知监听器日期时间已改变
onDateTimeChanged();
}
};
public interface OnDateTimeChangedListener {
/**
*
* @param view DateTimePicker
* @param year
* @param month 0-11
* @param dayOfMonth
* @param hourOfDay 0-23
* @param minute 0-59
*/
void onDateTimeChanged(DateTimePicker view, int year, int month,
int dayOfMonth, int hourOfDay, int minute);
}
public DateTimePicker(Context context) {
this(context, System.currentTimeMillis());
}
public DateTimePicker(Context context, long date) {
this(context, date, DateFormat.is24HourFormat(context));
}
public DateTimePicker(Context context, long date, boolean is24HourView) {
super(context);
// 初始化日期时间对象
mDate = Calendar.getInstance();
// 标记是否正在初始化
mInitialising = true;
// 初始化上午/下午标志
mIsAm = getCurrentHourOfDay() >= HOURS_IN_HALF_DAY;
// 加载布局文件
inflate(context, R.layout.datetime_picker, this);
// 初始化日期选择器
mDateSpinner = (NumberPicker) findViewById(R.id.date);
mDateSpinner.setMinValue(DATE_SPINNER_MIN_VAL);
mDateSpinner.setMaxValue(DATE_SPINNER_MAX_VAL);
mDateSpinner.setOnValueChangedListener(mOnDateChangedListener);
// 初始化小时选择器
mHourSpinner = (NumberPicker) findViewById(R.id.hour);
mHourSpinner.setOnValueChangedListener(mOnHourChangedListener);
// 初始化分钟选择器
mMinuteSpinner = (NumberPicker) findViewById(R.id.minute);
mMinuteSpinner.setMinValue(MINUT_SPINNER_MIN_VAL);
mMinuteSpinner.setMaxValue(MINUT_SPINNER_MAX_VAL);
mMinuteSpinner.setOnLongPressUpdateInterval(100);
mMinuteSpinner.setOnValueChangedListener(mOnMinuteChangedListener);
// 初始化AM/PM选择器
String[] stringsForAmPm = new DateFormatSymbols().getAmPmStrings();
mAmPmSpinner = (NumberPicker) findViewById(R.id.amPm);
mAmPmSpinner.setMinValue(AMPM_SPINNER_MIN_VAL);
mAmPmSpinner.setMaxValue(AMPM_SPINNER_MAX_VAL);
mAmPmSpinner.setDisplayedValues(stringsForAmPm);
mAmPmSpinner.setOnValueChangedListener(mOnAmPmChangedListener);
// 更新控件到初始状态
updateDateControl();
updateHourControl();
updateAmPmControl();
// 设置是否为24小时视图
set24HourView(is24HourView);
// 设置当前日期
setCurrentDate(date);
// 设置是否启用
setEnabled(isEnabled());
// 设置内容描述
mInitialising = false;
}
// 设置DateTimePicker是否启用
@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;
}
// 获取DateTimePicker是否启用
@Override
public boolean isEnabled() {
return mIsEnabled;
}
/**
*
* @return
*/
/**
*
* @return
*/
public long getCurrentDateInTimeMillis() {
return mDate.getTimeInMillis();
}
/**
*
*
* @param date
*/
public void setCurrentDate(long date) {
// 创建一个Calendar实例并设置其时间戳
Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(date);
// 调用更详细的setCurrentDate方法来设置日期
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));
}
/**
*
*
* @param year
* @param month 0-11
* @param dayOfMonth
* @param hourOfDay 0-23
* @param minute 0-59
*/
public void setCurrentDate(int year, int month,
int dayOfMonth, int hourOfDay, int minute) {
// 设置年份
setCurrentYear(year);
// 设置月份
setCurrentMonth(month);
// 设置日期
setCurrentDay(dayOfMonth);
// 设置小时
setCurrentHour(hourOfDay);
// 设置分钟
setCurrentMinute(minute);
}
/**
*
* @return
*/
public int getCurrentYear() {
return mDate.get(Calendar.YEAR);
}
/**
*
*
* @param year
*/
public void setCurrentYear(int year) {
// 如果在初始化过程中,不执行任何操作
if (mInitialising && year == getCurrentYear()) {
return;
}
// 设置年份
mDate.set(Calendar.YEAR, year);
// 更新日期控件以反映新的年份
updateDateControl();
// 通知监听器日期时间已改变
onDateTimeChanged();
}
/**
*
* @return 0-11
*/
public int getCurrentMonth() {
return mDate.get(Calendar.MONTH);
}
/**
*
*
* @param month 0-11
*/
public void setCurrentMonth(int month) {
if (!mInitialising && month == getCurrentMonth()) {
return;
}
mDate.set(Calendar.MONTH, month);
updateDateControl();
onDateTimeChanged();
}
/**
*
* @return
*/
public int getCurrentDay() {
return mDate.get(Calendar.DAY_OF_MONTH);
}
/**
*
*
* @param dayOfMonth
*/
public void setCurrentDay(int dayOfMonth) {
if (!mInitialising && dayOfMonth == getCurrentDay()) {
return;
}
mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth);
updateDateControl();
onDateTimeChanged();
}
/**
* 24
* @return 240-23
*/
public int getCurrentHourOfDay() {
return mDate.get(Calendar.HOUR_OF_DAY);
}
/**
*
* 24
* @return 0-110-23
*/
private int getCurrentHour() {
if (mIs24HourView) {
return getCurrentHourOfDay();
} else {
int hour = getCurrentHourOfDay();
if (hour > HOURS_IN_HALF_DAY) {
return hour - HOURS_IN_HALF_DAY;
} else {
return hour == 0 ? HOURS_IN_HALF_DAY : hour;
}
}
}
/**
* 24
*
* @param hourOfDay 240-23
*/
/**
*
* @return 0-59
*/
public int getCurrentMinute() {
return mDate.get(Calendar.MINUTE);
}
/**
*
*
*/
public void setCurrentMinute(int minute) {
if (!mInitialising && minute == getCurrentMinute()) {
return;
}
mMinuteSpinner.setValue(minute);
mDate.set(Calendar.MINUTE, minute);
onDateTimeChanged();
}
/**
* 24
* @return 24truefalse
*/
public boolean is24HourView() {
return mIs24HourView;
}
/**
* 24AM/PM
*
* @param is24HourView true24falseAM/PM
*/
public void set24HourView(boolean is24HourView) {
if (mIs24HourView == is24HourView) {
return;
}
mIs24HourView = is24HourView;
// 根据24小时视图设置AM/PM选择器的可见性
mAmPmSpinner.setVisibility(is24HourView ? View.GONE : View.VISIBLE);
// 更新小时选择器以适应新的视图
updateHourControl();
// 设置当前小时以适应新的视图
setCurrentHour(getCurrentHourOfDay());
// 更新AM/PM控制
updateAmPmControl();
}
/**
*
*/
private void updateDateControl() {
// 创建一个Calendar实例并将时间设置为当前日期减去一周的一半
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();
}
/**
* AM/PM
*/
private void updateAmPmControl() {
// 如果为24小时视图则隐藏AM/PM选择器
if (mIs24HourView) {
mAmPmSpinner.setVisibility(View.GONE);
} else {
// 设置AM/PM选择器的值
int index = mIsAm ? Calendar.AM : Calendar.PM;
mAmPmSpinner.setValue(index);
// 显示AM/PM选择器
mAmPmSpinner.setVisibility(View.VISIBLE);
}
}
/**
*
*/
private void updateHourControl() {
if (mIs24HourView) {
// 如果为24小时视图设置小时选择器的最小和最大值
mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW);
mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW);
} else {
// 如果为AM/PM视图设置小时选择器的最小和最大值
mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW);
mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW);
}
}
/**
*
* "设置"
* @param callback null
*/
public void setOnDateTimeChangedListener(OnDateTimeChangedListener callback) {
mOnDateTimeChangedListener = callback;
}
/**
*
*/
private void onDateTimeChanged() {
if (mOnDateTimeChangedListener != null) {
// 通知监听器日期时间已改变
mOnDateTimeChangedListener.onDateTimeChanged(this, getCurrentYear(),
getCurrentMonth(), getCurrentDay(), getCurrentHourOfDay(), getCurrentMinute());
}
}

@ -0,0 +1,135 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import java.util.Calendar;
import net.micode.notes.R;
import net.micode.notes.ui.DateTimePicker;
import net.micode.notes.ui.DateTimePicker.OnDateTimeChangedListener;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.text.format.DateFormat;
import android.text.format.DateUtils;
/**
* AlertDialog
*/
public class DateTimePickerDialog extends AlertDialog implements OnClickListener {
/**
* Calendar
*/
private Calendar mDate = Calendar.getInstance();
/**
* 使 24
*/
private boolean mIs24HourView;
/**
*
*/
private OnDateTimeSetListener mOnDateTimeSetListener;
/**
* DateTimePicker
*/
private DateTimePicker mDateTimePicker;
/**
*
*/
public interface OnDateTimeSetListener {
/**
*
* @param dialog
* @param date
*/
void OnDateTimeSet(AlertDialog dialog, long date);
}
/**
* long
* @param context
* @param date
*/
public DateTimePickerDialog(Context context, long date) {
super(context);
mDateTimePicker = new DateTimePicker(context);
setView(mDateTimePicker);
setButton(context.getString(R.string.datetime_dialog_ok), this);
setButton2(context.getString(R.string.datetime_dialog_cancel), null);
set24HourView(DateFormat.is24HourFormat(context));
mDateTimePicker.setOnDateTimeChangedListener(new OnDateTimeChangedListener() {
@Override
public void onDateTimeChanged(DateTimePicker view, int year, int month,
int dayOfMonth, int hourOfDay, int minute) {
mDate.set(Calendar.YEAR, year);
mDate.set(Calendar.MONTH, month);
mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth);
mDate.set(Calendar.HOUR_OF_DAY, hourOfDay);
mDate.set(Calendar.MINUTE, minute);
updateTitle(mDate.getTimeInMillis());
}
});
mDate.setTimeInMillis(date);
mDate.set(Calendar.SECOND, 0);
mDateTimePicker.setCurrentDate(mDate.getTimeInMillis());
updateTitle(mDate.getTimeInMillis());
}
/**
* 使 24
* @param is24HourView true使 24 false使 12
*/
public void set24HourView(boolean is24HourView) {
mIs24HourView = is24HourView;
}
/**
*
* @param callBack
*/
public void setOnDateTimeSetListener(OnDateTimeSetListener callBack) {
mOnDateTimeSetListener = callBack;
}
/**
*
* @param date
*/
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_12HOUR;
setTitle(DateUtils.formatDateTime(getContext(), date, flag));
}
/**
*
* @param dialogInterface
* @param whichButton
*/
public void onClick(DialogInterface dialogInterface, int whichButton) {
if (mOnDateTimeSetListener != null) {
mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis());
}
}
}

@ -0,0 +1,117 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.content.Context;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.PopupMenu;
import android.widget.PopupMenu.OnMenuItemClickListener;
import net.micode.notes.R;
/**
* DropdownMenu
*/
public class DropdownMenu {
/**
*
*/
private Button mButton;
/**
* PopupMenu
*/
private PopupMenu mPopupMenu;
/**
*
*/
private Menu mMenu;
/**
* DropdownMenu
* @param context
* @param button
* @param menuId ID
*/
public DropdownMenu(Context context, Button button, int menuId) {
mButton = button;
setButtonIcon();
mPopupMenu = new PopupMenu(context, mButton);
mMenu = mPopupMenu.getMenu();
inflateMenu(menuId);
setButtonClickListener();
}
/**
*
*/
private void setButtonIcon() {
mButton.setBackgroundResource(R.drawable.dropdown_icon);
}
/**
* PopupMenu inflate menuId
* @param menuId ID
*/
private void inflateMenu(int menuId) {
mPopupMenu.getMenuInflater().inflate(menuId, mMenu);
}
/**
*
*/
private void setButtonClickListener() {
mButton.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
mPopupMenu.show();
}
});
}
/**
*
* @param listener
*/
public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) {
if (mPopupMenu != null) {
mPopupMenu.setOnMenuItemClickListener(listener);
}
}
/**
* ID
* @param id ID
* @return null
*/
public MenuItem findItem(int id) {
return mMenu.findItem(id);
}
/**
*
* @param title
*/
public void setTitle(CharSequence title) {
mButton.setText(title);
}
}

@ -0,0 +1,146 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.content.Context;
import android.database.Cursor;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CursorAdapter;
import android.widget.LinearLayout;
import android.widget.TextView;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
package net.micode.notes.ui;
import android.content.Context;
import android.database.Cursor;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CursorAdapter;
import android.widget.LinearLayout;
import android.widget.TextView;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
/**
* FoldersListAdapter CursorAdapter
*/
public class FoldersListAdapter extends CursorAdapter {
/**
*
*/
public static final String [] PROJECTION = {
NoteColumns.ID,
NoteColumns.SNIPPET
};
/**
* ID
*/
public static final int ID_COLUMN = 0;
/**
*
*/
public static final int NAME_COLUMN = 1;
/**
* FoldersListAdapter
* @param context
* @param c Cursor
*/
public FoldersListAdapter(Context context, Cursor c) {
super(context, c);
// TODO: 自动生成的构造函数存根
}
/**
* View
* @param context
* @param cursor Cursor
* @param parent ViewGroup View
* @return View
*/
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return new FolderListItem(context);
}
/**
* Cursor View
* @param view View
* @param context
* @param cursor Cursor
*/
@Override
public void bindView(View view, Context context, Cursor cursor) {
if (view instanceof FolderListItem) {
String folderName = (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context
.getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN);
((FolderListItem) view).bind(folderName);
}
}
/**
*
* @param context
* @param position
* @return
*/
public String getFolderName(Context context, int position) {
Cursor cursor = (Cursor) getItem(position);
return (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context
.getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN);
}
/**
* FolderListItem LinearLayout
*/
private class FolderListItem extends LinearLayout {
/**
* TextView
*/
private TextView mName;
/**
* FolderListItem
* @param context
*/
public FolderListItem(Context context) {
super(context);
inflate(context, R.layout.folder_list_item, this);
mName = (TextView) findViewById(R.id.tv_folder_name);
}
/**
* TextView
* @param name
*/
public void bind(String name) {
mName.setText(name);
}
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,199 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
// 导入所需的 Android SDK 类和包
public class NoteEditText extends EditText {
// 类成员变量
private static final String TAG = "NoteEditText";
private int mIndex; // 当前笔记的索引
private int mSelectionStartBeforeDelete; // 删除前的文本选择开始位置
// 定义几种常见的 URL Scheme
private static final String SCHEME_TEL = "tel:" ;
private static final String SCHEME_HTTP = "http:" ;
private static final String SCHEME_EMAIL = "mailto:" ;
// 定义一个映射表,用于将 URL Scheme 映射到对应的操作资源 ID
private static final Map<String, Integer> sSchemaActionResMap = new HashMap<String, Integer>();
static {
sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel);
sSchemaActionResMap.put(SCHEME_HTTP, R.string.note_link_web);
sSchemaActionResMap.put(SCHEME_EMAIL, R.string.note_link_email);
}
// 定义一个接口,用于处理文本视图的变化事件
public interface OnTextViewChangeListener {
void onEditTextDelete(int index, String text);
void onEditTextEnter(int index, String text);
void onTextChange(int index, boolean hasText);
}
// 成员变量,用于回调文本变化事件
private OnTextViewChangeListener mOnTextViewChangeListener;
// 构造函数,用于创建 NoteEditText 实例
public NoteEditText(Context context) {
super(context, null);
mIndex = 0;
}
// 设置当前笔记的索引
public void setIndex(int index) {
mIndex = index;
}
// 设置文本变化事件的监听器
public void setOnTextViewChangeListener(OnTextViewChangeListener listener) {
mOnTextViewChangeListener = listener;
}
// 其他构造函数,用于从 XML 布局文件中创建 NoteEditText 实例
public NoteEditText(Context context, AttributeSet attrs) {
super(context, attrs, android.R.attr.editTextStyle);
}
public NoteEditText(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// 空的构造函数体,可能用于后续扩展
}
// 重写 onTouchEvent 方法,用于处理触摸事件
@Override
public boolean onTouchEvent(MotionEvent event) {
// 当用户触摸屏幕时,获取触摸位置,并更新文本的选择位置
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 计算触摸位置,并设置文本选择
// ...
break;
}
// 调用父类的 onTouchEvent 方法
return super.onTouchEvent(event);
}
// 重写 onKeyDown 方法,用于处理按键事件
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
// 处理回车键和删除键的事件
switch (keyCode) {
case KeyEvent.KEYCODE_ENTER:
// 如果设置了文本变化监听器,调用其 onEditTextEnter 方法
// ...
return false;
case KeyEvent.KEYCODE_DEL:
// 记录删除前的文本选择开始位置
mSelectionStartBeforeDelete = getSelectionStart();
break;
default:
// 默认行为
break;
}
// 调用父类的 onKeyDown 方法
return super.onKeyDown(keyCode, event);
}
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
switch(keyCode) {
case KeyEvent.KEYCODE_DEL:
if (mOnTextViewChangeListener != null) {
if (0 == mSelectionStartBeforeDelete && mIndex != 0) {
mOnTextViewChangeListener.onEditTextDelete(mIndex, getText().toString());
return true;
}
} else {
Log.d(TAG, "OnTextViewChangeListener was not seted");
}
break;
case KeyEvent.KEYCODE_ENTER:
if (mOnTextViewChangeListener != null) {
int selectionStart = getSelectionStart();
String text = getText().subSequence(selectionStart, length()).toString();
setText(getText().subSequence(0, selectionStart));
mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, text);
} else {
Log.d(TAG, "OnTextViewChangeListener was not seted");
}
break;
default:
break;
}
return super.onKeyUp(keyCode, event);
}
@Override
protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
// 当控件获得或失去焦点时调用
if (mOnTextViewChangeListener != null) {
// 如果设置了文本变化监听器
if (!focused && TextUtils.isEmpty(getText())) {
// 如果控件失去焦点并且文本为空,则通知监听器没有文本
mOnTextViewChangeListener.onTextChange(mIndex, false);
} else {
// 否则,通知监听器文本已更改
mOnTextViewChangeListener.onTextChange(mIndex, true);
}
}
// 调用父类的 onFocusChanged 方法
super.onFocusChanged(focused, direction, previouslyFocusedRect);
}
@Override
protected void onCreateContextMenu(ContextMenu menu) {
// 当需要创建上下文菜单时调用
if (getText() instanceof Spanned) {
// 如果文本是 Spanned 类型(可以包含样式,如 URLSpan
int selStart = getSelectionStart();
int selEnd = getSelectionEnd();
int min = Math.min(selStart, selEnd);
int max = Math.max(selStart, selEnd);
// 获取当前选中文本中的 URLSpan 对象
final URLSpan[] urls = ((Spanned) getText()).getSpans(min, max, URLSpan.class);
if (urls.length == 1) {
// 如果选中的文本只包含一个 URLSpan
int defaultResId = 0;
for(String schema: sSchemaActionResMap.keySet()) {
// 检查 URL 是否匹配预定义的 Scheme
if(urls[0].getURL().indexOf(schema) >= 0) {
defaultResId = sSchemaActionResMap.get(schema);
break;
}
}
// 如果没有匹配的 Scheme使用默认的资源 ID
if (defaultResId == 0) {
defaultResId = R.string.note_link_other;
}
// 添加一个菜单项,并设置点击事件
menu.add(0, 0, 0, defaultResId).setOnMenuItemClickListener(
new OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
// 当点击菜单项时,执行 URLSpan 的 onClick 方法(可能会打开一个网页或打电话等)
urls[0].onClick(NoteEditText.this);
return true;
}
});
}
}
// 调用父类的 onCreateContextMenu 方法
super.onCreateContextMenu(menu);
}

@ -0,0 +1,326 @@
package net.micode.notes.ui;
import android.content.Context;
import android.database.Cursor;
import android.text.TextUtils;
import net.micode.notes.data.Contact;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.tool.DataUtils;
/**
*
*/
public class NoteItemData {
// 定义要投影的字段
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,
};
// 定义每个字段在Cursor中的索引
private static final int ID_COLUMN = 0;
private static final int ALERTED_DATE_COLUMN = 1;
private static final int BG_COLOR_ID_COLUMN = 2;
private static final int CREATED_DATE_COLUMN = 3;
private static final int HAS_ATTACHMENT_COLUMN = 4;
private static final int MODIFIED_DATE_COLUMN = 5;
private static final int NOTES_COUNT_COLUMN = 6;
private static final int PARENT_ID_COLUMN = 7;
private static final int SNIPPET_COLUMN = 8;
private static final int TYPE_COLUMN = 9;
private static final int WIDGET_ID_COLUMN = 10;
private static final int WIDGET_TYPE_COLUMN = 11;
// 成员变量,对应数据库中的字段
private long mId; // 笔记的ID
private long mAlertDate; // 提醒日期
private int mBgColorId; // 背景颜色ID
private long mCreatedDate; // 创建日期
private boolean mHasAttachment; // 是否有附件
private long mModifiedDate; // 修改日期
private int mNotesCount; // 笔记数量
private long mParentId; // 父ID
private String mSnippet; // 笔记摘要
private int mType; // 笔记类型
private int mWidgetId; // 小部件ID
private int mWidgetType; // 小部件类型
private String mName; // 联系人名称
private String mPhoneNumber; // 联系电话号码
// 用于表示某些特定的位置信息
private boolean mIsLastItem; // 是否是最后一项
private boolean mIsFirstItem; // 是否是第一项
private boolean mIsOnlyOneItem; // 是否只有一项
private boolean mIsOneNoteFollowingFolder; // 是否是一篇笔记跟随一个文件夹
private boolean mIsMultiNotesFollowingFolder; // 是否有多篇笔记跟随一个文件夹
/**
* 使ContextCursorNoteItemData
* @param context Context
* @param cursor Cursor
*/
public NoteItemData(Context context, Cursor cursor) {
// 从Cursor中获取数据
mId = cursor.getLong(ID_COLUMN);
mAlertDate = cursor.getLong(ALERTED_DATE_COLUMN);
mBgColorId = cursor.getInt(BG_COLOR_ID_COLUMN);
mCreatedDate = cursor.getLong(CREATED_DATE_COLUMN);
mHasAttachment = (cursor.getInt(HAS_ATTACHMENT_COLUMN) > 0) ? true : false;
mModifiedDate = cursor.getLong(MODIFIED_DATE_COLUMN);
mNotesCount = cursor.getInt(NOTES_COUNT_COLUMN);
mParentId = cursor.getLong(PARENT_ID_COLUMN);
mSnippet = cursor.getString(SNIPPET_COLUMN);
// 清理摘要中的标签
mSnippet = mSnippet.replace(NoteEditActivity.TAG_CHECKED, "").replace(
NoteEditActivity.TAG_UNCHECKED, "");
mType = cursor.getInt(TYPE_COLUMN);
mWidgetId = cursor.getInt(WIDGET_ID_COLUMN);
mWidgetType = cursor.getInt(WIDGET_TYPE_COLUMN);
// 初始化联系人和电话号码
mPhoneNumber = "";
if (mParentId == Notes.ID_CALL_RECORD_FOLDER) {
// 获取联系人的电话号码
mPhoneNumber = DataUtils.getCallNumberByNoteId(context.getContentResolver(), mId);
// 如果电话号码不为空,尝试获取联系人的名称
if (!TextUtils.isEmpty(mPhoneNumber)) {
mName = Contact.getContact(context, mPhoneNumber);
// 如果获取名称失败,使用电话号码作为名称
if (mName == null) {
mName = mPhoneNumber;
}
}
}
// 如果名称不为空,则使用它,否则使用空字符串
if (mName == null) {
mName = "";
}
// 检查位置信息
checkPostion(cursor);
}
/**
* Cursor
* @param cursor Cursor
*/
private void checkPostion(Cursor cursor) {
mIsLastItem = cursor.isLast() ? true : false;
mIsFirstItem = cursor.isFirst() ? true : false;
mIsOnlyOneItem = (cursor.getCount() == 1);
mIsMultiNotesFollowingFolder = false;
mIsOneNoteFollowingFolder = false;
// 如果是笔记类型,并且不是第一项,则检查前一项的类型
if (mType == Notes.TYPE_NOTE && !mIsFirstItem) {
int position = cursor.getPosition();
// 如果可以移动到前一项,则检查前一项的类型
if (cursor.moveToPrevious()) {
// 如果前一项是文件夹或者系统类型,则根据情况设置标志位
if (cursor.getInt(TYPE_COLUMN) == Notes.TYPE_FOLDER
|| cursor.getInt(TYPE_COLUMN) == Notes.TYPE_SYSTEM) {
// 如果文件夹或者系统类型的项后还有其他项,则设置为多笔记跟随文件夹
if (cursor.getCount() > (position + 1)) {
mIsMultiNotesFollowingFolder = true;
} else {
// 否则设置为单笔记跟随文件夹
mIsOneNoteFollowingFolder = true;
}
}
// 如果不能移动回原来的位置,则抛出异常
if (!cursor.moveToNext()) {
throw new IllegalStateException("cursor move to previous but can't move back");
}
}
}
}
}
/**
*
* @return truefalse
*/
public boolean isOneFollowingFolder() {
return mIsOneNoteFollowingFolder;
}
/**
*
* @return truefalse
*/
public boolean isMultiFollowingFolder() {
return mIsMultiNotesFollowingFolder;
}
/**
*
* @return truefalse
*/
public boolean isLast() {
return mIsLastItem;
}
/**
*
* @return
*/
public String getCallName() {
return mName;
}
/**
*
* @return truefalse
*/
public boolean isFirst() {
return mIsFirstItem;
}
/**
*
* @return truefalse
*/
public boolean isSingle() {
return mIsOnlyOneItem;
}
/**
* ID
* @return ID
*/
public long getId() {
return mId;
}
/**
*
* @return
*/
public long getAlertDate() {
return mAlertDate;
}
/**
*
* @return
*/
public long getCreatedDate() {
return mCreatedDate;
}
/**
*
* @return truefalse
*/
public boolean hasAttachment() {
return mHasAttachment;
}
/**
*
* @return
*/
public long getModifiedDate() {
return mModifiedDate;
}
/**
* ID
* @return ID
*/
public int getBgColorId() {
return mBgColorId;
}
/**
* IDID
* @return ID
*/
public long getParentId() {
return mParentId;
}
/**
*
* @return
*/
public int getNotesCount() {
return mNotesCount;
}
/**
* IDIDgetParentId
* @return ID
*/
public long getFolderId() {
return mParentId;
}
/**
*
* @return
*/
public int getType() {
return mType;
}
/**
*
* @return
*/
public int getWidgetType() {
return mWidgetType;
}
/**
* ID
* @return ID
*/
public int getWidgetId() {
return mWidgetId;
}
/**
*
* @return
*/
public String getSnippet() {
return mSnippet;
}
/**
*
* @return truefalse
*/
public boolean hasAlert() {
return (mAlertDate > 0);
}
/**
*
* @return truefalse
*/
public boolean isCallRecord() {
return (mParentId == Notes.ID_CALL_RECORD_FOLDER && !TextUtils.isEmpty(mPhoneNumber));
}
/**
* Cursor
* @param cursor Cursor
* @return
*/
public static int getNoteType(Cursor cursor) {
return cursor.getInt(TYPE_COLUMN);
}

@ -0,0 +1,996 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.appwidget.AppWidgetManager;
import android.content.AsyncQueryHandler;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.os.AsyncTask;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.Log;
import android.view.ActionMode;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.Display;
import android.view.HapticFeedbackConstants;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MenuItem.OnMenuItemClickListener;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnCreateContextMenuListener;
import android.view.View.OnTouchListener;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.AdapterView.OnItemLongClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.PopupMenu;
import android.widget.TextView;
import android.widget.Toast;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.gtask.remote.GTaskSyncService;
import net.micode.notes.model.WorkingNote;
import net.micode.notes.tool.BackupUtils;
import net.micode.notes.tool.DataUtils;
import net.micode.notes.tool.ResourceParser;
import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute;
import net.micode.notes.widget.NoteWidgetProvider_2x;
import net.micode.notes.widget.NoteWidgetProvider_4x;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashSet;
public class NotesListActivity extends Activity implements OnClickListener, OnItemLongClickListener {
// 定义了一些常量,例如查询标记、菜单标识等。
private static final int FOLDER_NOTE_LIST_QUERY_TOKEN = 0;
private static final int FOLDER_LIST_QUERY_TOKEN = 1;
private static final int MENU_FOLDER_DELETE = 0;
private static final int MENU_FOLDER_VIEW = 1;
private static final int MENU_FOLDER_CHANGE_NAME = 2;
// 定义了一些配置选项的键值对,例如是否显示添加笔记的引导。
private static final String PREFERENCE_ADD_INTRODUCTION = "net.micode.notes.introduction";
// 定义了一个枚举类型,表示列表编辑状态,包括笔记列表、子文件夹、通话记录文件夹。
private enum ListEditState {
NOTE_LIST, SUB_FOLDER, CALL_RECORD_FOLDER
};
// 初始化背景查询处理类,用于异步查询数据库。
private BackgroundQueryHandler mBackgroundQueryHandler;
// 初始化 NotesListAdapter它是用来管理列表视图中的项目适配器。
private NotesListAdapter mNotesListAdapter;
// 初始化 ListView它是显示笔记列表的控件。
private ListView mNotesListView;
// 初始化添加新笔记的按钮。
private Button mAddNewNote;
// 一些标志位和变量,用于处理列表的触摸和滚动事件。
private boolean mDispatch;
private int mOriginY;
private int mDispatchY;
// 初始化标题栏的 TextView。
private TextView mTitleBar;
// 初始化当前文件夹的ID。
private long mCurrentFolderId;
// 初始化内容解析器,用于与数据库交互。
private ContentResolver mContentResolver;
// 初始化模式回调接口,用于处理多选模式下的交互。
private ModeCallback mModeCallBack;
// 定义了 log 标签,用于调试。
private static final String TAG = "NotesListActivity";
// 定义了一些常量,例如笔记列表滚动速度等。
public static final int NOTES_LISTVIEW_SCROLL_RATE = 30;
// 初始化焦点笔记数据项,用于管理当前选中的笔记。
private NoteItemData mFocusNoteDataItem;
// 定义了一些选择器字符串,用于数据库查询。
private static final String NORMAL_SELECTION = NoteColumns.PARENT_ID + "=?";
private static final String ROOT_FOLDER_SELECTION = "(" + NoteColumns.TYPE + "<>"
+ Notes.TYPE_SYSTEM + " AND " + NoteColumns.PARENT_ID + "=?)" + " OR ("
+ NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER + " AND "
+ NoteColumns.NOTES_COUNT + ">0)";
// 定义了一些请求码,用于区分不同的操作。
private final static int REQUEST_CODE_OPEN_NODE = 102;
private final static int REQUEST_CODE_NEW_NODE = 103;
// onCreate 方法是 Activity 生命周期中的一个回调方法,用于在 Activity 创建时进行初始化。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.note_list); // 设置界面的布局文件。
initResources(); // 初始化资源,如布局中的控件。
// 插入应用信息的引导,当用户第一次使用应用时显示。
setAppInfoFromRawRes();
}
// onActivityResult 方法是 Activity 生命周期中的一个回调方法,用于处理活动的结果。
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == RESULT_OK
&& (requestCode == REQUEST_CODE_OPEN_NODE || requestCode == REQUEST_CODE_NEW_NODE)) {
// 如果 activity 结果是成功的,并且请求码是打开节点或新建节点,则刷新笔记列表适配器。
mNotesListAdapter.changeCursor(null);
} else {
// 否则,调用父类的 onActivityResult 方法处理其他情况。
super.onActivityResult(requestCode, resultCode, data);
}
}
// setAppInfoFromRawRes 方法用于从资源文件中读取应用信息,通常用于首次启动应用时显示引导信息。
private void setAppInfoFromRawRes() {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
// 检查是否已经显示过添加笔记的引导。
if (!sp.getBoolean(PREFERENCE_ADD_INTRODUCTION, false)) {
StringBuilder sb = new StringBuilder();
InputStream in = null;
try {
// 尝试打开资源文件夹中的引导信息文件。
in = getResources().openRawResource(R.raw.introduction);
if (in != null) {
// 如果文件打开成功,则读取内容。
InputStreamReader isr = new InputStreamReader(in);
BufferedReader br = new BufferedReader(isr);
char [] buf = new char[1024];
int len = 0;
while ((len = br.read(buf)) > 0) {
sb.append(buf, 0, len);
}
} else {
// 如果文件打开失败,记录错误。
Log.e(TAG, "Read introduction file error");
return;
}
} catch (IOException e) {
// 如果读取文件过程中发生异常,记录异常。
e.printStackTrace();
return;
} finally {
// 确保流关闭,防止资源泄露。
if(in != null) {
try {
in.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
// 使用读取的内容创建一个空白笔记。
WorkingNote note = WorkingNote.createEmptyNote(this, Notes.ID_ROOT_FOLDER,
AppWidgetManager.INVALID_APPWIDGET_ID, Notes.TYPE_WIDGET_INVALIDE,
ResourceParser.RED);
// 设置笔记的内容为读取的文本。
note.setWorkingText(sb.toString());
// 尝试保存笔记,如果成功,则标记为已显示引导信息。
if (note.saveNote()) {
sp.edit().putBoolean(PREFERENCE_ADD_INTRODUCTION, true).commit();
} else {
// 如果保存笔记失败,记录错误。
Log.e(TAG, "Save introduction note error");
return;
}
}
}
// onStart 方法是 Activity 生命周期中的一个回调方法,在 Activity 启动时调用。
@Override
protected void onStart() {
super.onStart(); // 调用父类的 onStart 方法。
startAsyncNotesListQuery(); // 启动异步的笔记列表查询。
}
// initResources 方法用于初始化应用的资源,如 ContentResolver、查询处理器、当前文件夹 ID 等。
private void initResources() {
mContentResolver = this.getContentResolver(); // 获取 ContentResolver。
mBackgroundQueryHandler = new BackgroundQueryHandler(this.getContentResolver()); // 创建一个后台查询处理器。
mCurrentFolderId = Notes.ID_ROOT_FOLDER; // 设置当前文件夹 ID 为根文件夹。
mNotesListView = (ListView) findViewById(R.id.notes_list); // 获取笔记列表视图。
mNotesListView.addFooterView(LayoutInflater.from(this).inflate(R.layout.note_list_footer, null), null, false); // 向列表视图添加一个脚部视图。
mNotesListView.setOnItemClickListener(new OnListItemClickListener()); // 设置列表项点击监听器。
mNotesListView.setOnItemLongClickListener(this); // 设置列表项长按监听器。
mNotesListAdapter = new NotesListAdapter(this); // 创建一个 NotesListAdapter。
mNotesListView.setAdapter(mNotesListAdapter); // 为列表视图设置适配器。
mAddNewNote = (Button) findViewById(R.id.btn_new_note); // 获取添加新笔记的按钮。
mAddNewNote.setOnClickListener(this); // 设置按钮点击监听器。
mAddNewNote.setOnTouchListener(new NewNoteOnTouchListener()); // 设置按钮触摸监听器。
mDispatch = false; // 初始化一些标志位和变量。
mDispatchY = 0;
mOriginY = 0;
mTitleBar = (TextView) findViewById(R.id.tv_title_bar); // 获取标题栏的 TextView。
mState = ListEditState.NOTE_LIST; // 设置列表编辑状态为笔记列表。
mModeCallBack = new ModeCallback(); // 创建一个 ModeCallback 实例。
}
// ModeCallback 类实现了 ListView.MultiChoiceModeListener 和 OnMenuItemClickListener 接口,用于处理列表的多选模式和菜单项点击事件。
private class ModeCallback implements ListView.MultiChoiceModeListener, OnMenuItemClickListener {
private DropdownMenu mDropDownMenu; // 定义一个 DropdownMenu 对象。
private ActionMode mActionMode; // 定义一个 ActionMode 对象。
private MenuItem mMoveMenu; // 定义一个菜单项,用于移动操作。
// onCreateActionMode 方法在 ActionMode 创建时调用,用于初始化 ActionMode。
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
getMenuInflater().inflate(R.menu.note_list_options, menu); // 加载菜单资源。
menu.findItem(R.id.delete).setOnMenuItemClickListener(this); // 设置删除菜单项的点击监听器。
mMoveMenu = menu.findItem(R.id.move); // 获取移动菜单项。
if (mFocusNoteDataItem.getParentId() == Notes.ID_CALL_RECORD_FOLDER
|| DataUtils.getUserFolderCount(mContentResolver) == 0) {
mMoveMenu.setVisible(false); // 如果焦点项是通话记录文件夹或没有用户文件夹,则隐藏移动菜单项。
} else {
mMoveMenu.setVisible(true); // 否则,显示移动菜单项。
mMoveMenu.setOnMenuItemClickListener(this); // 设置移动菜单项的点击监听器。
}
mActionMode = mode; // 保存 ActionMode 对象。
mNotesListAdapter.setChoiceMode(true); // 设置适配器为多选模式。
mNotesListView.setLongClickable(false); // 设置列表项不可长按。
mAddNewNote.setVisibility(View.GONE); // 隐藏添加新笔记的按钮。
View customView = LayoutInflater.from(NotesListActivity.this).inflate(
R.layout.note_list_dropdown_menu, null); // 加载自定义视图。
mode.setCustomView(customView); // 设置自定义视图到 ActionMode。
mDropDownMenu = new DropdownMenu(NotesListActivity.this,
(Button) customView.findViewById(R.id.selection_menu), R.menu.note_list_dropdown); // 创建一个 DropdownMenu。
mDropDownMenu.setOnDropdownMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { // 设置下拉菜单项点击监听器。
public boolean onMenuItemClick(MenuItem item) {
mNotesListAdapter.selectAll(!mNotesListAdapter.isAllSelected()); // 选择或取消所有列表项。
updateMenu(); // 更新菜单状态。
return true;
}
});
return true;
}
// updateMenu 方法用于更新下拉菜单的标题,显示当前选中项目的数量。
private void updateMenu() {
int selectedCount = mNotesListAdapter.getSelectedCount(); // 获取选中项目的数量。
// 更新下拉菜单
String format = getResources().getString(R.string.menu_select_title, selectedCount);
mDropDownMenu.setTitle(format); // 设置下拉菜单的标题。
MenuItem item = mDropDownMenu.findItem(R.id.action_select_all); // 获取全选菜单项。
if (item != null) { // 检查菜单项是否存在。
if (mNotesListAdapter.isAllSelected()) { // 检查是否已经全选。
item.setChecked(true); // 如果全选,则设置菜单项为选中状态。
item.setTitle(R.string.menu_deselect_all); // 设置菜单项标题为取消全选。
} else {
item.setChecked(false); // 否则,设置菜单项为未选中状态。
item.setTitle(R.string.menu_select_all); // 设置菜单项标题为全选。
}
}
}
// onPrepareActionMode 方法在 ActionMode 准备时调用,可以用来准备菜单等。
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
// onActionItemClicked 方法在用户点击菜单项时调用,可以在这里处理菜单项的点击事件。
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
return false;
}
// onDestroyActionMode 方法在 ActionMode 销毁时调用,可以在这里清理状态。
public void onDestroyActionMode(ActionMode mode) {
mNotesListAdapter.setChoiceMode(false); // 关闭多选模式。
mNotesListView.setLongClickable(true); // 恢复列表项的长按可点击性。
mAddNewNote.setVisibility(View.VISIBLE); // 显示添加新笔记的按钮。
}
// finishActionMode 方法用于结束当前的多选模式。
public void finishActionMode() {
mActionMode.finish(); // 结束 ActionMode。
}
// onItemCheckedStateChanged 方法在用户选中或取消选中列表项时调用,可以在这里更新状态。
public void onItemCheckedStateChanged(ActionMode mode, int position, long id,
boolean checked) {
mNotesListAdapter.setCheckedItem(position, checked); // 更新适配器中的选中状态。
updateMenu(); // 更新菜单状态。
}
// onMenuItemClick 方法用于处理菜单项的点击事件,可以在这里添加点击事件的逻辑。
public boolean onMenuItemClick(MenuItem item) {
if (mNotesListAdapter.getSelectedCount() == 0) { // 如果没有选中任何项目。
Toast.makeText(NotesListActivity.this, getString(R.string.menu_select_none),
Toast.LENGTH_SHORT).show(); // 显示提示信息。
return true; // 返回 true 表示处理了该事件。
}
switch (item.getItemId()) { // 处理菜单项的点击事件。
case R.id.delete: // 如果点击了删除菜单项。
AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.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_notes,
mNotesListAdapter.getSelectedCount()));
builder.setPositiveButton(android.R.string.ok,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog,
int which) {
batchDelete(); // 调用批量删除方法。
}
});
builder.setNegativeButton(android.R.string.cancel, null);
builder.show(); // 显示确认删除对话框。
break;
case R.id.move: // 如果点击了移动菜单项。
startQueryDestinationFolders(); // 启动查询目的地文件夹的方法。
break;
default:
return false; // 默认返回 false表示不处理该菜单项的点击事件。
}
return true; // 返回 true表示处理了该事件。
}
// NewNoteOnTouchListener 类实现了 OnTouchListener 接口,用于处理添加新笔记按钮的触摸事件。
private class NewNoteOnTouchListener implements OnTouchListener {
// onTouch 方法是触摸事件监听器的主要方法,用于处理触摸事件。
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
Display display = getWindowManager().getDefaultDisplay(); // 获取默认显示器。
int screenHeight = display.getHeight(); // 获取屏幕高度。
int newNoteViewHeight = mAddNewNote.getHeight(); // 获取添加新笔记按钮的高度。
int start = screenHeight - newNoteViewHeight; // 计算按钮底部的位置。
int eventY = start + (int) event.getY(); // 计算触摸事件相对于按钮底部的位置。
// 如果当前状态是子文件夹,则需要减去标题栏的高度。
if (mState == ListEditState.SUB_FOLDER) {
eventY -= mTitleBar.getHeight();
start -= mTitleBar.getHeight();
}
/**
* HACKME:
*
*
*/
if (event.getY() < (event.getX() * (-0.12) + 94)) {
View view = mNotesListView.getChildAt(mNotesListView.getChildCount() - 1
- mNotesListView.getFooterViewsCount()); // 获取列表视图中最后一个可见子视图。
if (view != null && view.getBottom() > start
&& (view.getTop() < (start + 94))) {
mOriginY = (int) event.getY(); // 记录原始触摸事件的Y坐标。
mDispatchY = eventY; // 设置转发触摸事件的Y坐标。
event.setLocation(event.getX(), mDispatchY); // 设置新的触摸事件位置。
mDispatch = true; // 设置事件转发标志位。
return mNotesListView.dispatchTouchEvent(event); // 转发触摸事件到列表视图。
}
}
break;
}
case MotionEvent.ACTION_MOVE: {
if (mDispatch) {
mDispatchY += (int) event.getY() - mOriginY; // 更新转发触摸事件的Y坐标。
event.setLocation(event.getX(), mDispatchY); // 设置新的触摸事件位置。
return mNotesListView.dispatchTouchEvent(event); // 转发触摸事件到列表视图。
}
break;
}
default: {
if (mDispatch) {
event.setLocation(event.getX(), mDispatchY); // 设置触摸事件的新位置。
mDispatch = false; // 重置事件转发标志位。
return mNotesListView.dispatchTouchEvent(event); // 转发触摸事件到列表视图。
}
break;
}
}
return false;
}
}
// startAsyncNotesListQuery 方法用于启动异步查询笔记列表的操作。
private void startAsyncNotesListQuery() {
String selection = (mCurrentFolderId == Notes.ID_ROOT_FOLDER) ? ROOT_FOLDER_SELECTION
: NORMAL_SELECTION; // 根据当前文件夹 ID 构建查询选择器。
mBackgroundQueryHandler.startQuery(FOLDER_NOTE_LIST_QUERY_TOKEN, null,
Notes.CONTENT_NOTE_URI, NoteItemData.PROJECTION, selection, new String[] {
String.valueOf(mCurrentFolderId)
}, NoteColumns.TYPE + " DESC," + NoteColumns.MODIFIED_DATE + " DESC");
}
// BackgroundQueryHandler 类是一个扩展自 AsyncQueryHandler 的类,用于异步处理查询操作。
private final class BackgroundQueryHandler extends AsyncQueryHandler {
public BackgroundQueryHandler(ContentResolver contentResolver) {
super(contentResolver); // 调用父类的构造方法。
}
// onQueryComplete 方法在查询完成时调用,用于处理查询结果。
@Override
protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
switch (token) {
case FOLDER_NOTE_LIST_QUERY_TOKEN:
mNotesListAdapter.changeCursor(cursor); // 刷新笔记列表适配器。
break;
case FOLDER_LIST_QUERY_TOKEN:
if (cursor != null && cursor.getCount() > 0) {
showFolderListMenu(cursor); // 显示文件夹列表菜单。
} else {
Log.e(TAG, "Query folder failed"); // 记录查询文件夹失败的日志。
}
break;
default:
return;
}
}
}
// showFolderListMenu 方法用于显示一个对话框,让用户选择要将选中的笔记移动到的文件夹。
private void showFolderListMenu(Cursor cursor) {
AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this);
builder.setTitle(R.string.menu_title_select_folder);
final FoldersListAdapter adapter = new FoldersListAdapter(this, cursor);
builder.setAdapter(adapter, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
DataUtils.batchMoveToFolder(mContentResolver,
mNotesListAdapter.getSelectedItemIds(), adapter.getItemId(which));
Toast.makeText(
NotesListActivity.this,
getString(R.string.format_move_notes_to_folder,
mNotesListAdapter.getSelectedCount(),
adapter.getFolderName(NotesListActivity.this, which)),
Toast.LENGTH_SHORT).show();
mModeCallBack.finishActionMode(); // 结束多选模式。
}
});
builder.show();
}
// createNewNote 方法用于创建一个新的笔记。
private void createNewNote() {
Intent intent = new Intent(this, NoteEditActivity.class);
intent.setAction(Intent.ACTION_INSERT_OR_EDIT);
intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mCurrentFolderId);
this.startActivityForResult(intent, REQUEST_CODE_NEW_NODE);
}
// batchDelete 方法用于批量删除选中的笔记。
private void batchDelete() {
new AsyncTask<Void, Void, HashSet<AppWidgetAttribute>>() {
protected HashSet<AppWidgetAttribute> doInBackground(Void... unused) {
HashSet<AppWidgetAttribute> widgets = mNotesListAdapter.getSelectedWidget();
if (!isSyncMode()) {
// 如果没有同步,直接删除笔记
if (DataUtils.batchDeleteNotes(mContentResolver, mNotesListAdapter
.getSelectedItemIds())) {
} else {
Log.e(TAG, "Delete notes error, should not happens"); // 记录删除笔记失败的日志。
}
} else {
// 如果处于同步模式,将删除的笔记移动到回收站文件夹
if (!DataUtils.batchMoveToFolder(mContentResolver, mNotesListAdapter
.getSelectedItemIds(), Notes.ID_TRASH_FOLER)) {
Log.e(TAG, "Move notes to trash folder error, should not happens"); // 记录移动笔记到回收站失败的日志。
}
}
return widgets;
}
}.execute();
}
@Override
// onPostExecute 方法在异步任务完成后调用,用于处理批量删除笔记后的更新小部件操作。
protected void onPostExecute(HashSet<AppWidgetAttribute> widgets) {
if (widgets != null) {
for (AppWidgetAttribute widget : widgets) {
if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID
&& widget.widgetType != Notes.TYPE_WIDGET_INVALIDE) {
updateWidget(widget.widgetId, widget.widgetType); // 更新小部件的数据。
}
}
}
mModeCallBack.finishActionMode(); // 结束多选模式。
}
// deleteFolder 方法用于删除指定的文件夹。
private void deleteFolder(long folderId) {
if (folderId == Notes.ID_ROOT_FOLDER) {
Log.e(TAG, "Wrong folder id, should not happen " + folderId); // 如果尝试删除根文件夹,记录错误日志。
return;
}
HashSet<Long> ids = new HashSet<Long>();
ids.add(folderId); // 将文件夹 ID 添加到 ID 集合中。
HashSet<AppWidgetAttribute> widgets = DataUtils.getFolderNoteWidget(mContentResolver,
folderId); // 获取文件夹相关的小部件属性。
if (!isSyncMode()) {
// 如果没有同步,直接删除文件夹
DataUtils.batchDeleteNotes(mContentResolver, ids);
} else {
// 如果在同步模式下,将删除的文件夹移动到回收站文件夹
DataUtils.batchMoveToFolder(mContentResolver, ids, Notes.ID_TRASH_FOLER);
}
if (widgets != null) {
for (AppWidgetAttribute widget : widgets) {
if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID
&& widget.widgetType != Notes.TYPE_WIDGET_INVALIDE) {
updateWidget(widget.widgetId, widget.widgetType); // 更新小部件的数据。
}
}
}
}
// openNode 方法用于打开一个笔记。
private void openNode(NoteItemData data) {
Intent intent = new Intent(this, NoteEditActivity.class);
intent.setAction(Intent.ACTION_VIEW);
intent.putExtra(Intent.EXTRA_UID, data.getId()); // 传递笔记的 ID 作为 Intent 的一部分。
this.startActivityForResult(intent, REQUEST_CODE_OPEN_NODE); // 启动活动并返回结果。
}
// openFolder 方法用于打开一个文件夹。
private void openFolder(NoteItemData data) {
mCurrentFolderId = data.getId(); // 更新当前文件夹 ID。
startAsyncNotesListQuery(); // 启动异步查询以获取文件夹中的笔记列表。
if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) {
mState = ListEditState.CALL_RECORD_FOLDER; // 设置状态为通话记录文件夹。
mAddNewNote.setVisibility(View.GONE); // 隐藏添加新笔记按钮。
} else {
mState = ListEditState.SUB_FOLDER; // 设置状态为子文件夹。
}
if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) {
mTitleBar.setText(R.string.call_record_folder_name); // 设置标题栏文本为通话记录文件夹名称。
} else {
mTitleBar.setText(data.getSnippet()); // 设置标题栏文本为文件夹的片段。
}
mTitleBar.setVisibility(View.VISIBLE); // 显示标题栏。
}
// onClick 方法是 View.OnClickListener 接口的方法,用于处理按钮点击事件。
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_new_note:
createNewNote(); // 当点击添加新笔记按钮时,创建新笔记。
break;
default:
break; // 处理其他按钮点击事件。
}
}
// showSoftInput 方法用于显示软键盘。
private void showSoftInput() {
InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
if (inputMethodManager != null) {
inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
}
}
// hideSoftInput 方法用于隐藏软键盘。
private void hideSoftInput(View view) {
InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
if (inputMethodManager != null) {
inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
}
// showCreateOrModifyFolderDialog 方法用于显示创建或修改文件夹的对话框。
private void showCreateOrModifyFolderDialog(final boolean create) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
View view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_text, null);
final EditText etName = (EditText) view.findViewById(R.id.et_foler_name);
showSoftInput(); // 显示软键盘。
if (!create) {
if (mFocusNoteDataItem != null) {
etName.setText(mFocusNoteDataItem.getSnippet()); // 如果不是创建文件夹,设置输入框为当前焦点项的片段。
builder.setTitle(getString(R.string.menu_folder_change_name));
} else {
Log.e(TAG, "The long click data item is null"); // 记录错误日志。
return;
}
} else {
etName.setText(""); // 如果是创建文件夹,清空输入框。
builder.setTitle(this.getString(R.string.menu_create_folder));
}
builder.setPositiveButton(android.R.string.ok, null);
builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
hideSoftInput(etName); // 当用户点击取消按钮时,隐藏软键盘。
}
});
final Dialog dialog = builder.setView(view).show(); // 显示对话框。
Button positive = (Button)dialog.findViewById(android.R.id.button1);
positive.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
hideSoftInput(etName); // 当用户点击确定按钮时,隐藏软键盘。
String name = etName.getText().toString();
if (DataUtils.checkVisibleFolderName(mContentResolver, name)) {
Toast.makeText(NotesListActivity.this, getString(R.string.folder_exist, name),
Toast.LENGTH_LONG).show(); // 如果文件夹名称已存在,显示提示信息。
etName.setSelection(0, etName.length()); // 选择输入框中的文本。
return;
}
if (!create) {
if (!TextUtils.isEmpty(name)) {
ContentValues values = new ContentValues();
values.put(NoteColumns.SNIPPET, name);
values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER);
values.put(NoteColumns.LOCAL_MODIFIED, 1);
mContentResolver.update(Notes.CONTENT_NOTE_URI, values, NoteColumns.ID
+ "=?", new String[] {
String.valueOf(mFocusNoteDataItem.getId())
});
}
} else if (!TextUtils.isEmpty(name)) {
ContentValues values = new ContentValues();
values.put(NoteColumns.SNIPPET, name);
values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER);
mContentResolver.insert(Notes.CONTENT_NOTE_URI, values); // 插入新的文件夹。
}
dialog.dismiss(); // 关闭对话框。
}
});
if (TextUtils.isEmpty(etName.getText())) {
positive.setEnabled(false); // 如果输入框为空,禁用确定按钮。
}
/**
*
*/
etName.addTextChangedListener(new TextWatcher() {
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
// TODO Auto-generated method stub
}
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (TextUtils.isEmpty(etName.getText())) {
positive.setEnabled(false); // 如果输入框为空,禁用确定按钮。
} else {
positive.setEnabled(true); // 否则,启用确定按钮。
}
}
public void afterTextChanged(Editable s) {
// TODO Auto-generated method stub
}
});
}
// onBackPressed 方法是 Activity 生命周期中的一个回调方法,当用户按下返回按钮时调用。
@Override
public void onBackPressed() {
switch (mState) { // 根据当前状态处理返回按钮。
case SUB_FOLDER: // 如果当前状态是子文件夹。
mCurrentFolderId = Notes.ID_ROOT_FOLDER; // 更新当前文件夹 ID 为根文件夹。
mState = ListEditState.NOTE_LIST; // 更新状态为笔记列表。
startAsyncNotesListQuery(); // 启动异步查询以获取笔记列表。
mTitleBar.setVisibility(View.GONE); // 隐藏标题栏。
break;
case CALL_RECORD_FOLDER: // 如果当前状态是通话记录文件夹。
mCurrentFolderId = Notes.ID_ROOT_FOLDER; // 更新当前文件夹 ID 为根文件夹。
mState = ListEditState.NOTE_LIST; // 更新状态为笔记列表。
mAddNewNote.setVisibility(View.VISIBLE); // 显示添加新笔记按钮。
mTitleBar.setVisibility(View.GONE); // 隐藏标题栏。
startAsyncNotesListQuery(); // 启动异步查询以获取笔记列表。
break;
case NOTE_LIST: // 如果当前状态是笔记列表。
super.onBackPressed(); // 调用父类的 onBackPressed 方法。
break;
default:
break;
}
}
// updateWidget 方法用于更新小部件的数据。
private void updateWidget(int appWidgetId, int appWidgetType) {
Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
if (appWidgetType == Notes.TYPE_WIDGET_2X) {
intent.setClass(this, NoteWidgetProvider_2x.class); // 设置意图的类为 2x 类型的小部件提供者。
} else if (appWidgetType == Notes.TYPE_WIDGET_4X) {
intent.setClass(this, NoteWidgetProvider_4x.class); // 设置意图的类为 4x 类型的小部件提供者。
} else {
Log.e(TAG, "Unspported widget type"); // 记录不支持的类型日志。
return;
}
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] {
appWidgetId
});
sendBroadcast(intent); // 发送广播以更新小部件。
setResult(RESULT_OK, intent); // 设置结果为成功。
}
// OnCreateContextMenuListener 接口的实现,用于为文件夹上下文菜单创建项。
private final OnCreateContextMenuListener mFolderOnCreateContextMenuListener = new OnCreateContextMenuListener() {
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
if (mFocusNoteDataItem != null) { // 如果当前焦点项不为空。
menu.setHeaderTitle(mFocusNoteDataItem.getSnippet()); // 设置上下文菜单的标题为焦点项的片段。
menu.add(0, MENU_FOLDER_VIEW, 0, R.string.menu_folder_view); // 添加查看文件夹菜单项。
menu.add(0, MENU_FOLDER_DELETE, 0, R.string.menu_folder_delete); // 添加删除文件夹菜单项。
menu.add(0, MENU_FOLDER_CHANGE_NAME, 0, R.string.menu_folder_change_name); // 添加修改文件夹名称菜单项。
}
}
};
// onContextMenuClosed 方法在上下文菜单关闭时调用,用于清理状态。
@Override
public void onContextMenuClosed(Menu menu) {
if (mNotesListView != null) { // 如果列表视图不为空。
mNotesListView.setOnCreateContextMenuListener(null); // 清除列表视图的上下文菜单监听器。
}
super.onContextMenuClosed(menu); // 调用父类的 onContextMenuClosed 方法。
}
// onContextItemSelected 方法在用户选择上下文菜单项时调用,用于处理文件夹上下文菜单项的点击事件。
@Override
public boolean onContextItemSelected(MenuItem item) {
if (mFocusNoteDataItem == null) { // 如果当前焦点项为空。
Log.e(TAG, "The long click data item is null"); // 记录错误日志。
return false; // 返回 false表示不处理该事件。
}
switch (item.getItemId()) { // 处理上下文菜单项的点击事件。
case MENU_FOLDER_VIEW:
openFolder(mFocusNoteDataItem); // 打开文件夹。
break;
case MENU_FOLDER_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_folder));
builder.setPositiveButton(android.R.string.ok,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
deleteFolder(mFocusNoteDataItem.getId()); // 删除文件夹。
}
});
builder.setNegativeButton(android.R.string.cancel, null);
builder.show(); // 显示对话框。
break;
case MENU_FOLDER_CHANGE_NAME:
showCreateOrModifyFolderDialog(false); // 显示修改文件夹名称的对话框。
break;
default:
break;
}
return true; // 返回 true表示处理了该事件。
}
// onPrepareOptionsMenu 方法用于准备选项菜单,根据当前状态填充不同的菜单项。
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
menu.clear(); // 清空当前菜单项。
if (mState == ListEditState.NOTE_LIST) {
getMenuInflater().inflate(R.menu.note_list, menu); // 加载笔记列表的菜单项。
// 设置同步或取消同步的选项
menu.findItem(R.id.menu_sync).setTitle(
GTaskSyncService.isSyncing() ? R.string.menu_sync_cancel : R.string.menu_sync);
} else if (mState == ListEditState.SUB_FOLDER) {
getMenuInflater().inflate(R.menu.sub_folder, menu); // 加载子文件夹的菜单项。
} else if (mState == ListEditState.CALL_RECORD_FOLDER) {
getMenuInflater().inflate(R.menu.call_record_folder, menu); // 加载通话记录文件夹的菜单项。
} else {
Log.e(TAG, "Wrong state:" + mState); // 如果状态错误,记录日志。
}
return true; // 返回 true表示处理了该事件。
}
// onOptionsItemSelected 方法用于处理选项菜单项的点击事件。
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_new_folder:
showCreateOrModifyFolderDialog(true); // 显示创建文件夹的对话框。
break;
case R.id.menu_export_text:
exportNoteToText(); // 导出笔记到文本文件。
break;
case R.id.menu_sync:
if (isSyncMode()) {
if (TextUtils.equals(item.getTitle(), getString(R.string.menu_sync))) {
GTaskSyncService.startSync(this); // 启动同步服务。
} else {
GTaskSyncService.cancelSync(this); // 取消同步服务。
}
} else {
startPreferenceActivity(); // 启动首选项活动。
}
break;
case R.id.menu_setting:
startPreferenceActivity(); // 启动首选项活动。
break;
case R.id.menu_new_note:
createNewNote(); // 创建新笔记。
break;
case R.id.menu_search:
onSearchRequested(); // 请求搜索。
break;
default:
break;
}
return true; // 返回 true表示处理了该事件。
}
// onSearchRequested 方法是 Activity 生命周期中的一个回调方法,当用户请求搜索时调用。
@Override
public boolean onSearchRequested() {
startSearch(null, false, null /* appData */, false); // 启动搜索活动。
return true; // 返回 true表示处理了该事件。
}
// exportNoteToText 方法用于导出笔记到文本文件。
private void exportNoteToText() {
final BackupUtils backup = BackupUtils.getInstance(NotesListActivity.this);
new AsyncTask<Void, Void, Integer>() {
@Override
protected Integer doInBackground(Void... unused) {
return backup.exportToText(); // 异步导出笔记到文本文件。
}
@Override
protected void onPostExecute(Integer result) {
if (result == BackupUtils.STATE_SD_CARD_UNMOUONTED) {
AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this);
builder.setTitle(getString(R.string.failed_sdcard_export));
builder.setMessage(getString(R.string.error_sdcard_unmounted));
builder.setPositiveButton(android.R.string.ok, null);
builder.show();
} else if (result == BackupUtils.STATE_SUCCESS) {
AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this);
builder.setTitle(getString(R.string.success_sdcard_export));
builder.setMessage(getString(
R.string.format_exported_file_location, backup.getExportedTextFileName(), backup
.getExportedTextFileDir()));
builder.setPositiveButton(android.R.string.ok, null);
builder.show();
} else if (result == BackupUtils.STATE_SYSTEM_ERROR) {
AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this);
builder.setTitle(getString(R.string.failed_sdcard_export));
builder.setMessage(getString(R.string.error_sdcard_export));
builder.setPositiveButton(android.R.string.ok, null);
builder.show();
}
}
}.execute();
}
// isSyncMode 方法用于检查是否处于同步模式。
private boolean isSyncMode() {
return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0;
}
// startPreferenceActivity 方法用于启动首选项活动。
private void startPreferenceActivity() {
Activity from = getParent() != null ? getParent() : this;
Intent intent = new Intent(from, NotesPreferenceActivity.class);
from.startActivityIfNeeded(intent, -1);
}
// OnListItemClickListener 类实现了 OnItemClickListener 接口,用于处理列表项的点击事件。
private class OnListItemClickListener implements OnItemClickListener {
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
if (view instanceof NotesListItem) {
NoteItemData item = ((NotesListItem) view).getItemData();
if (mNotesListAdapter.isInChoiceMode()) {
if (item.getType() == Notes.TYPE_NOTE) {
position = position - mNotesListView.getHeaderViewsCount();
mModeCallBack.onItemCheckedStateChanged(null, position, id,
!mNotesListAdapter.isSelectedItem(position));
}
return;
}
switch (mState) {
case NOTE_LIST:
if (item.getType() == Notes.TYPE_FOLDER
|| item.getType() == Notes.TYPE_SYSTEM) {
openFolder(item);
} else if (item.getType() == Notes.TYPE_NOTE) {
openNode(item);
} else {
Log.e(TAG, "Wrong note type in NOTE_LIST");
}
break;
case SUB_FOLDER:
case CALL_RECORD_FOLDER:
if (item.getType() == Notes.TYPE_NOTE) {
openNode(item);
} else {
Log.e(TAG, "Wrong note type in SUB_FOLDER");
}
break;
default:
break;
}
}
}
}
// startQueryDestinationFolders 方法用于启动查询目的地文件夹的异步查询。
private void startQueryDestinationFolders() {
String selection = NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>? AND " + NoteColumns.ID + "<>?";
selection = (mState == ListEditState.NOTE_LIST) ? selection:
"(" + selection + ") OR (" + NoteColumns.ID + "=" + Notes.ID_ROOT_FOLDER + ")";
mBackgroundQueryHandler.startQuery(FOLDER_LIST_QUERY_TOKEN,
null,
Notes.CONTENT_NOTE_URI,
FoldersListAdapter.PROJECTION,
selection,
new String[] {
String.valueOf(Notes.TYPE_FOLDER),
String.valueOf(Notes.ID_TRASH_FOLER),
String.valueOf(mCurrentFolderId)
},
NoteColumns.MODIFIED_DATE + " DESC");
}
// onItemLongClick 方法用于处理列表项的长按点击事件。
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
if (view instanceof NotesListItem) {
mFocusNoteDataItem = ((NotesListItem) view).getItemData();
if (mFocusNoteDataItem.getType() == Notes.TYPE_NOTE && !mNotesListAdapter.isInChoiceMode()) {
if (mNotesListView.startActionMode(mModeCallBack) != null) {
mModeCallBack.onItemCheckedStateChanged(null, position, id, true);
mNotesListView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
} else {
Log.e(TAG, "startActionMode fails");
}
} else if (mFocusNoteDataItem.getType() == Notes.TYPE_FOLDER) {
mNotesListView.setOnCreateContextMenuListener(mFolderOnCreateContextMenuListener);
}
}
return false;
}

@ -0,0 +1,174 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.content.Context;
import android.database.Cursor;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CursorAdapter;
import net.micode.notes.data.Notes;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
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; // 选择模式标志
// AppWidgetAttribute 内部类,用于存储应用 Widget 的属性
public static class AppWidgetAttribute {
public int widgetId; // Widget ID
public int widgetType; // Widget 类型
};
// 构造函数
public NotesListAdapter(Context context) {
super(context, null); // 调用父类的构造函数
mSelectedIndex = new HashMap<Integer, Boolean>();
mContext = context;
mNotesCount = 0;
}
// 实现 CursorAdapter 的新视图创建方法
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return new NotesListItem(context); // 返回自定义的列表项视图
}
// 实现 CursorAdapter 的绑定视图方法
@Override
public void bindView(View view, Context context, Cursor cursor) {
if (view instanceof NotesListItem) {
NoteItemData itemData = new NoteItemData(context, cursor); // 创建 NoteItemData 实例
((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); // 设置选中状态
}
}
}
}
// 获取所有选中项的 ID
public HashSet<Long> getSelectedItemIds() {
HashSet<Long> itemSet = new HashSet<Long>();
// 遍历 mSelectedIndex 并添加选中项的 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 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;
}
}
}
}

@ -0,0 +1,116 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
private ImageView mAlert; // 用于显示提醒图标的 ImageView
private TextView mTitle; // 显示笔记标题的 TextView
private TextView mTime; // 显示笔记修改时间的 TextView
private TextView mCallName; // 显示通话记录名称的 TextView
private NoteItemData mItemData; // 当前条目关联的数据对象
private CheckBox mCheckBox; // 如果处于选择模式,用于选择当前条目的 CheckBox
public class NotesListItem extends LinearLayout {
private ImageView mAlert;
private TextView mTitle;
private TextView mTime;
private TextView mCallName;
private NoteItemData mItemData;
private CheckBox mCheckBox;
public NotesListItem(Context context) {
super(context);
inflate(context, R.layout.note_item, this);
mAlert = (ImageView) findViewById(R.id.iv_alert_icon);
mTitle = (TextView) findViewById(R.id.tv_title);
mTime = (TextView) findViewById(R.id.tv_time);
mCallName = (TextView) findViewById(R.id.tv_name);
mCheckBox = (CheckBox) findViewById(android.R.id.checkbox);
}
public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked) {
if (choiceMode && data.getType() == Notes.TYPE_NOTE) {
mCheckBox.setVisibility(View.VISIBLE);
mCheckBox.setChecked(checked);
} else {
mCheckBox.setVisibility(View.GONE);
}
mItemData = data;
if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) {
mCallName.setVisibility(View.GONE);
mAlert.setVisibility(View.VISIBLE);
mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem);
mTitle.setText(context.getString(R.string.call_record_folder_name)
+ context.getString(R.string.format_folder_files_count, data.getNotesCount()));
mAlert.setImageResource(R.drawable.call_record);
} else if (data.getParentId() == Notes.ID_CALL_RECORD_FOLDER) {
mCallName.setVisibility(View.VISIBLE);
mCallName.setText(data.getCallName());
mTitle.setTextAppearance(context,R.style.TextAppearanceSecondaryItem);
mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet()));
if (data.hasAlert()) {
mAlert.setImageResource(R.drawable.clock);
mAlert.setVisibility(View.VISIBLE);
} else {
mAlert.setVisibility(View.GONE);
}
} else {
mCallName.setVisibility(View.GONE);
mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem);
if (data.getType() == Notes.TYPE_FOLDER) {
mTitle.setText(data.getSnippet()
+ context.getString(R.string.format_folder_files_count,
data.getNotesCount()));
mAlert.setVisibility(View.GONE);
} else {
mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet()));
if (data.hasAlert()) {
mAlert.setImageResource(R.drawable.clock);
mAlert.setVisibility(View.VISIBLE);
} else {
mAlert.setVisibility(View.GONE);
}
}
}
mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate()));
setBackground(data);
}
private void setBackground(NoteItemData data) {
int id = data.getBgColorId();
if (data.getType() == Notes.TYPE_NOTE) {
if (data.isSingle() || data.isOneFollowingFolder()) {
setBackgroundResource(NoteItemBgResources.getNoteBgSingleRes(id));
} else if (data.isLast()) {
setBackgroundResource(NoteItemBgResources.getNoteBgLastRes(id));
} else if (data.isFirst() || data.isMultiFollowingFolder()) {
setBackgroundResource(NoteItemBgResources.getNoteBgFirstRes(id));
} else {
setBackgroundResource(NoteItemBgResources.getNoteBgNormalRes(id));
}
} else {
setBackgroundResource(NoteItemBgResources.getFolderBgRes());
}
}
public NoteItemData getItemData() {
return mItemData;
}
}

@ -0,0 +1,388 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.ActionBar;
import android.app.AlertDialog;
import android.content.BroadcastReceiver;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.Preference;
import android.preference.Preference.OnPreferenceClickListener;
import android.preference.PreferenceActivity;
import android.preference.PreferenceCategory;
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.gtask.remote.GTaskSyncService;
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 */
getActionBar().setDisplayHomeAsUpEnabled(true);
addPreferencesFromResource(R.xml.preferences);
mAccountCategory = (PreferenceCategory) findPreference(PREFERENCE_SYNC_ACCOUNT_KEY);
mReceiver = new GTaskReceiver();
IntentFilter filter = new IntentFilter();
filter.addAction(GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME);
registerReceiver(mReceiver, filter);
mOriAccounts = null;
View header = LayoutInflater.from(this).inflate(R.layout.settings_header, null);
getListView().addHeaderView(header, null, true);
}
@Override
protected void onResume() {
super.onResume();
// 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) {
for (Account accountNew : accounts) {
boolean found = false;
for (Account accountOld : mOriAccounts) {
if (TextUtils.equals(accountOld.name, accountNew.name)) {
found = true;
break;
}
}
if (!found) {
setSyncAccount(accountNew.name);
break;
}
}
}
}
refreshUI();
}
@Override
protected void onDestroy() {
if (mReceiver != null) {
unregisterReceiver(mReceiver);
}
super.onDestroy();
}
private void loadAccountPreference() {
mAccountCategory.removeAll();
Preference accountPref = new Preference(this);
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)
.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) {
lastSyncTimeView.setText(getString(R.string.preferences_last_sync_time,
DateFormat.format(getString(R.string.preferences_last_sync_time_format),
lastSyncTime)));
lastSyncTimeView.setVisibility(View.VISIBLE);
} else {
lastSyncTimeView.setVisibility(View.GONE);
}
}
}
private void refreshUI() {
loadAccountPreference();
loadSyncButton();
}
private void showSelectAccountAlertDialog() {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null);
TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title);
titleTextView.setText(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);
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) {
setSyncAccount(itemMapping[which].toString());
dialog.dismiss();
refreshUI();
}
});
}
View addAccountView = LayoutInflater.from(this).inflate(R.layout.add_account_text, null);
dialogBuilder.setView(addAccountView);
final AlertDialog dialog = dialogBuilder.show();
addAccountView.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
mHasAddedAccount = true;
Intent intent = new Intent("android.settings.ADD_ACCOUNT_SETTINGS");
intent.putExtra(AUTHORITIES_FILTER_KEY, new String[] {
"gmail-ls"
});
startActivityForResult(intent, -1);
dialog.dismiss();
}
});
}
private void showChangeAccountConfirmAlertDialog() {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null);
TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title);
titleTextView.setText(getString(R.string.preferences_dialog_change_account_title,
getSyncAccountName(this)));
TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle);
subtitleTextView.setText(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) {
showSelectAccountAlertDialog();
} else if (which == 1) {
removeSyncAccount();
refreshUI();
}
}
});
dialogBuilder.show();
}
private Account[] getGoogleAccounts() {
AccountManager accountManager = AccountManager.get(this);
return accountManager.getAccountsByType("com.google");
}
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) {
editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, account);
} else {
editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, "");
}
editor.commit();
// clean up last sync time
setLastSyncTime(this, 0);
// clean up local gtask related info
new Thread(new Runnable() {
public void run() {
ContentValues values = new ContentValues();
values.put(NoteColumns.GTASK_ID, "");
values.put(NoteColumns.SYNC_ID, 0);
getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null);
}
}).start();
Toast.makeText(NotesPreferenceActivity.this,
getString(R.string.preferences_toast_success_set_accout, account),
Toast.LENGTH_SHORT).show();
}
}
private void removeSyncAccount() {
SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
if (settings.contains(PREFERENCE_SYNC_ACCOUNT_NAME)) {
editor.remove(PREFERENCE_SYNC_ACCOUNT_NAME);
}
if (settings.contains(PREFERENCE_LAST_SYNC_TIME)) {
editor.remove(PREFERENCE_LAST_SYNC_TIME);
}
editor.commit();
// clean up local gtask related info
new Thread(new Runnable() {
public void run() {
ContentValues values = new ContentValues();
values.put(NoteColumns.GTASK_ID, "");
values.put(NoteColumns.SYNC_ID, 0);
getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null);
}
}).start();
}
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;
}
}
}

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
Loading…
Cancel
Save