新增功能:习惯打卡 #28

Merged
p6vxzahlf merged 1 commits from wangyijia_branch into master 1 month ago

@ -35,7 +35,7 @@ import net.micode.notes.data.Notes.NoteColumns;
public class NotesDatabaseHelper extends SQLiteOpenHelper {
private static final String DB_NAME = "note.db";
private static final int DB_VERSION = 4;
private static final int DB_VERSION = 5;
public interface TABLE {
public static final String NOTE = "note";
@ -68,7 +68,9 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
NoteColumns.LOCAL_MODIFIED + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.ORIGIN_PARENT_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''," +
NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0" +
NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.IS_HABIT + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.HABIT_CONFIG + " TEXT NOT NULL DEFAULT ''" +
")";
/**datasql
@ -404,6 +406,11 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
oldVersion++;
}
if (oldVersion == 4) {
upgradeToV5(db);
oldVersion++;
}
if (reCreateTriggers) {
reCreateNoteTableTriggers(db);
reCreateDataTableTriggers(db);
@ -444,4 +451,12 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.VERSION
+ " INTEGER NOT NULL DEFAULT 0");
}
//更新到版本5增加习惯打卡支持字段
private void upgradeToV5(SQLiteDatabase db) {
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.IS_HABIT
+ " INTEGER NOT NULL DEFAULT 0");
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.HABIT_CONFIG
+ " TEXT NOT NULL DEFAULT ''");
}
}

@ -18,11 +18,16 @@ package net.micode.notes.ui;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.PendingIntent;
import android.app.AlarmManager;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.DialogInterface.OnDismissListener;
import android.content.Intent;
import android.database.Cursor;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.RingtoneManager;
@ -30,17 +35,29 @@ import android.net.Uri;
import android.os.Bundle;
import android.os.PowerManager;
import android.provider.Settings;
import android.util.Log;
import android.view.Window;
import android.view.WindowManager;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.tool.DataUtils;
import java.io.IOException;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Calendar;
//继承Activity //承诺实现clickdismiss接口不继承功能
public class AlarmAlertActivity extends Activity implements OnClickListener, OnDismissListener {
private static final String TAG = "AlarmAlertActivity";
private long mNoteId;
private String mSnippet;
private static final int SNIPPET_PREW_MAX_LEN = 60;
@ -83,7 +100,12 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD
mPlayer = new MediaPlayer();
//如果不在回收站就显示文字并发出声音
if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) {
showActionDialog();
int isHabit = intent.getIntExtra("habit_alarm", 0);
if (isHabit == 1) {
showHabitDialog(intent);
} else {
showActionDialog();
}
playAlarmSound();
} else {
finish();
@ -134,11 +156,190 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD
dialog.setMessage(mSnippet);
dialog.setPositiveButton(R.string.notealert_ok, this); //setPositive/Negative/NeutralButton()设置:确定,取消,中立按钮;
if (isScreenOn()) {
dialog.setNegativeButton(R.string.notealert_enter, this); //取消
dialog.setNegativeButton(R.string.notealert_enter, this);
}
dialog.show().setOnDismissListener(this); //设置关闭监听
dialog.show().setOnDismissListener(this);
}
//选择negative时执行跳转到笔记 那可能positive仅关闭弹窗
// Habit specific dialog with actions: complete, snooze, skip, abandon
private void showHabitDialog(Intent intent) {
View v = getLayoutInflater().inflate(R.layout.habit_alert_dialog, null);
TextView tvTitle = (TextView) v.findViewById(R.id.habit_alert_title);
TextView tvSnippet = (TextView) v.findViewById(R.id.habit_alert_snippet);
final Button btnComplete = (Button) v.findViewById(R.id.habit_btn_complete);
final Button btnSnooze10 = (Button) v.findViewById(R.id.habit_btn_snooze10);
final Button btnSnooze30 = (Button) v.findViewById(R.id.habit_btn_snooze30);
final Button btnSkip = (Button) v.findViewById(R.id.habit_btn_skip);
final Button btnAbandon = (Button) v.findViewById(R.id.habit_btn_abandon);
tvTitle.setText(getString(R.string.app_name));
tvSnippet.setText(mSnippet);
final AlertDialog d = new AlertDialog.Builder(this)
.setView(v)
.create();
btnComplete.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
recordHabitHistory(mNoteId, "completed", "");
Toast.makeText(AlarmAlertActivity.this, R.string.habit_record_complete, Toast.LENGTH_SHORT).show();
d.dismiss();
}
});
btnSnooze10.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
scheduleSnooze(mNoteId, 10);
Toast.makeText(AlarmAlertActivity.this, R.string.habit_snoozed, Toast.LENGTH_SHORT).show();
d.dismiss();
}
});
btnSnooze30.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
scheduleSnooze(mNoteId, 30);
Toast.makeText(AlarmAlertActivity.this, R.string.habit_snoozed, Toast.LENGTH_SHORT).show();
d.dismiss();
}
});
btnSkip.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showSkipReasonDialog();
// 不要立即关闭对话框,让用户选择原因
}
});
btnAbandon.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
abandonHabit(mNoteId);
Toast.makeText(AlarmAlertActivity.this, R.string.habit_abandoned, Toast.LENGTH_SHORT).show();
d.dismiss();
}
});
d.setOnDismissListener(this);
d.show();
}
private void showSkipReasonDialog() {
final String[] reasons = new String[] { getString(R.string.skip_reason_busy),
getString(R.string.skip_reason_sick), getString(R.string.skip_reason_other) };
AlertDialog.Builder b = new AlertDialog.Builder(this);
b.setTitle(R.string.habit_skip_title);
b.setItems(reasons, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String reason = reasons[which];
recordHabitHistory(mNoteId, "skipped", reason);
Toast.makeText(AlarmAlertActivity.this, R.string.habit_record_skipped, Toast.LENGTH_SHORT).show();
// 选择原因后关闭主对话框
dialog.dismiss();
finish();
}
});
b.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
b.show();
}
private void scheduleSnooze(long noteId, int minutes) {
try {
Intent intent = new Intent(this, AlarmReceiver.class);
intent.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId));
intent.putExtra("habit_alarm", 1);
int req = (int) (noteId ^ 0x100000) + minutes; // unique-ish
PendingIntent pi = PendingIntent.getBroadcast(this, req, intent, 0);
AlarmManager am = (AlarmManager) getSystemService(ALARM_SERVICE);
long trigger = System.currentTimeMillis() + minutes * 60 * 1000L;
am.set(AlarmManager.RTC_WAKEUP, trigger, pi);
} catch (Exception e) {
Log.e(TAG, "Schedule snooze error", e);
}
}
private void abandonHabit(long noteId) {
try {
ContentValues values = new ContentValues();
values.put(Notes.NoteColumns.IS_HABIT, 0);
values.put(Notes.NoteColumns.HABIT_CONFIG, "");
getContentResolver().update(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), values, null, null);
// cancel repeating alarm
Intent intent = new Intent(this, AlarmReceiver.class);
intent.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId));
intent.putExtra("habit_alarm", 1);
PendingIntent pi = PendingIntent.getBroadcast(this, 0, intent, 0);
AlarmManager am = (AlarmManager) getSystemService(ALARM_SERVICE);
am.cancel(pi);
} catch (Exception e) {
Log.e(TAG, "Abandon habit error", e);
}
}
// Record history into habit_config.history (append object {date,status,reason})
private void recordHabitHistory(long noteId, String status, String reason) {
try {
Uri uri = ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId);
Cursor c = getContentResolver().query(uri, new String[]{Notes.NoteColumns.HABIT_CONFIG}, null, null, null);
String cfg = "";
if (c != null) {
if (c.moveToFirst()) cfg = c.getString(0);
c.close();
}
JSONObject jo = cfg != null && cfg.length() > 0 ? new JSONObject(cfg) : new JSONObject();
JSONArray history = jo.has("history") ? jo.getJSONArray("history") : new JSONArray();
// 获取当前时间戳
long recordTime = System.currentTimeMillis();
// 创建新记录
JSONObject newEntry = new JSONObject();
newEntry.put("date", recordTime);
newEntry.put("status", status);
newEntry.put("reason", reason == null ? "" : reason);
// 检查是否已经存在该日期的记录,如果有则替换
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault());
String recordKey = sdf.format(new java.util.Date(recordTime));
boolean foundRecord = false;
for (int i = 0; i < history.length(); i++) {
JSONObject entry = history.getJSONObject(i);
long entryDate = entry.optLong("date", 0);
String entryKey = sdf.format(new java.util.Date(entryDate));
if (recordKey.equals(entryKey)) {
// 替换该日期的记录
history.put(i, newEntry);
foundRecord = true;
break;
}
}
// 如果没有该日期的记录,添加新记录
if (!foundRecord) {
history.put(newEntry);
}
jo.put("history", history);
ContentValues values = new ContentValues();
values.put(Notes.NoteColumns.HABIT_CONFIG, jo.toString());
getContentResolver().update(uri, values, null, null);
// 通知数据变化,以便日历视图刷新
getContentResolver().notifyChange(uri, null);
} catch (JSONException e) {
Log.e(TAG, "Record habit history json error", e);
}
}
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case DialogInterface.BUTTON_NEGATIVE:

@ -23,10 +23,14 @@ import android.app.PendingIntent;
import android.app.SearchManager;
import android.appwidget.AppWidgetManager;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Bundle;
import android.preference.PreferenceManager;
@ -53,10 +57,12 @@ import android.view.WindowManager;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.AdapterView;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.TimePicker;
import android.widget.Toast;
import net.micode.notes.R;
@ -78,22 +84,236 @@ import java.util.HashSet;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.widget.Button;
import android.widget.CompoundButton;
import android.widget.Spinner;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import java.util.Calendar;
import java.util.Date;
import android.app.PendingIntent;
import android.app.AlarmManager;
import android.widget.GridLayout;
import android.widget.ProgressBar;
import android.widget.LinearLayout;
import java.text.SimpleDateFormat;
import java.util.Locale;
import java.util.TimeZone;
import java.util.Map;
import java.util.HashMap;
public class NoteEditActivity extends Activity implements OnClickListener,
NoteSettingChangedListener, OnTextViewChangeListener,OnSelectionChangeListener {
private class HeadViewHolder {//头视图持有者类???
public TextView tvModified;//文本修改
public ImageView ivAlertIcon;//图像提醒按钮?
NoteSettingChangedListener, OnTextViewChangeListener, OnSelectionChangeListener {
private class HeadViewHolder {
public TextView tvModified;
public ImageView ivAlertIcon;
public TextView tvAlertDate;//文本提醒日期?
public ImageView ibSetBgColor;//设置背景颜色图像视图
public TextView tvCharNum;//新增字符数显示控件
}
private static final Map<Integer, Integer> sBgSelectorBtnsMap = new HashMap<Integer, Integer>();//背景颜色选择按钮映射
// 显示习惯配置对话框(简单表单),将结果保存为 JSON 到 habit_config
private void showHabitConfigDialog() {
LayoutInflater inflater = LayoutInflater.from(this);
View view = inflater.inflate(R.layout.habit_config_dialog, null);
final Spinner spinnerPeriod = (Spinner) view.findViewById(R.id.spinner_period);
final TimePicker tpRemindTime = (TimePicker) view.findViewById(R.id.tp_remind_time);
final Spinner spinnerTargetType = (Spinner) view.findViewById(R.id.spinner_target_type);
final EditText etTargetValue = (EditText) view.findViewById(R.id.et_target_value);
final LinearLayout llWeeklyTimes = (LinearLayout) view.findViewById(R.id.ll_weekly_times);
final EditText etWeeklyTimes = (EditText) view.findViewById(R.id.et_weekly_times);
// 设置 TimePicker 为 24 小时制
tpRemindTime.setIs24HourView(true);
// setup spinners
ArrayAdapter<CharSequence> periodAdapter = ArrayAdapter.createFromResource(this,
R.array.habit_period_options, android.R.layout.simple_spinner_item);
periodAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerPeriod.setAdapter(periodAdapter);
ArrayAdapter<CharSequence> targetAdapter = ArrayAdapter.createFromResource(this,
R.array.habit_target_type_options, android.R.layout.simple_spinner_item);
targetAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerTargetType.setAdapter(targetAdapter);
// 当选择周期变化时,显示/隐藏每周次数设置
spinnerPeriod.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
String period = spinnerPeriod.getSelectedItem().toString();
if (period.equals("每周 X 次")) {
llWeeklyTimes.setVisibility(View.VISIBLE);
} else {
llWeeklyTimes.setVisibility(View.GONE);
}
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
// prefill if existing
try {
String cfg = mWorkingNote.getHabitConfig();
if (cfg != null && cfg.length() > 0) {
JSONObject jo = new JSONObject(cfg);
String period = jo.optString("period", "每日");
spinnerPeriod.setSelection(periodAdapter.getPosition(period));
// 设置提醒时间
String remindTime = jo.optString("remind_time", "08:00");
if (!remindTime.isEmpty()) {
String[] parts = remindTime.split(":");
int hour = Integer.parseInt(parts[0]);
int minute = Integer.parseInt(parts[1]);
tpRemindTime.setHour(hour);
tpRemindTime.setMinute(minute);
}
// 设置每周次数
if (period.equals("每周 X 次")) {
llWeeklyTimes.setVisibility(View.VISIBLE);
etWeeklyTimes.setText(String.valueOf(jo.optInt("weekly_times", 3)));
}
spinnerTargetType.setSelection(targetAdapter.getPosition(jo.optString("target_type", "连续天数")));
etTargetValue.setText(String.valueOf(jo.optInt("target_value", 0)));
}
} catch (Exception e) {
// ignore
}
AlertDialog.Builder b = new AlertDialog.Builder(this);
b.setTitle(R.string.habit_config_title);
b.setView(view);
b.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String period = spinnerPeriod.getSelectedItem().toString();
// 获取提醒时间
int hour = tpRemindTime.getHour();
int minute = tpRemindTime.getMinute();
String remind = String.format("%02d:%02d", hour, minute);
String targetType = spinnerTargetType.getSelectedItem().toString();
int targetValue = 0;
try {
targetValue = Integer.parseInt(etTargetValue.getText().toString());
} catch (Exception e) {
targetValue = 0;
}
// 获取每周次数
int weeklyTimes = 3;
if (period.equals("每周 X 次")) {
try {
weeklyTimes = Integer.parseInt(etWeeklyTimes.getText().toString());
} catch (Exception e) {
weeklyTimes = 3;
}
}
JSONObject jo = new JSONObject();
try {
jo.put("period", period);
jo.put("remind_time", remind);
jo.put("target_type", targetType);
jo.put("target_value", targetValue);
jo.put("weekly_times", weeklyTimes);
} catch (JSONException e) {
// ignore
}
if (mWorkingNote != null) {
mWorkingNote.setHabit(true, jo.toString());
// schedule alarm according to remind_time
scheduleHabitAlarm(mWorkingNote);
// refresh the menu to update the habit settings button
invalidateOptionsMenu();
}
}
});
b.setNegativeButton(android.R.string.cancel, null);
b.show();
}
// Schedule or cancel habit alarm according to current WorkingNote.habit_config
private void scheduleHabitAlarm(WorkingNote note) {
if (note == null || !note.isHabit()) {
// cancel any existing alarm
cancelHabitAlarm(note);
return;
}
String cfg = note.getHabitConfig();
if (cfg == null || cfg.length() == 0) {
return;
}
try {
JSONObject jo = new JSONObject(cfg);
String remind = jo.optString("remind_time", "");
if (remind == null || remind.length() == 0) {
// no remind time provided
return;
}
String[] parts = remind.split(":");
int hour = Integer.parseInt(parts[0]);
int minute = Integer.parseInt(parts[1]);
// ensure note saved and has id
if (!note.existInDatabase()) {
note.saveNote();
}
if (!note.existInDatabase()) return;
long noteId = note.getNoteId();
Intent intent = new Intent(this, AlarmReceiver.class);
intent.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId));
intent.putExtra("habit_alarm", 1);
PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0);
AlarmManager alarmManager = ((AlarmManager) getSystemService(ALARM_SERVICE));
Calendar c = Calendar.getInstance();
c.set(Calendar.HOUR_OF_DAY, hour);
c.set(Calendar.MINUTE, minute);
c.set(Calendar.SECOND, 0);
long trigger = c.getTimeInMillis();
long now = System.currentTimeMillis();
if (trigger <= now) {
// schedule for next day
trigger += AlarmManager.INTERVAL_DAY;
}
alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, trigger,
AlarmManager.INTERVAL_DAY, pendingIntent);
} catch (Exception e) {
Log.e(TAG, "Schedule habit alarm error", e);
}
}
private void cancelHabitAlarm(WorkingNote note) {
try {
if (note == null || !note.existInDatabase()) return;
long noteId = note.getNoteId();
Intent intent = new Intent(this, AlarmReceiver.class);
intent.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId));
intent.putExtra("habit_alarm", 1);
PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0);
AlarmManager alarmManager = ((AlarmManager) getSystemService(ALARM_SERVICE));
alarmManager.cancel(pendingIntent);
} catch (Exception e) {
Log.e(TAG, "Cancel habit alarm error", e);
}
}
private static final Map<Integer, Integer> sBgSelectorBtnsMap = new HashMap<Integer, Integer>();
static {
sBgSelectorBtnsMap.put(R.id.iv_bg_yellow, ResourceParser.YELLOW);
sBgSelectorBtnsMap.put(R.id.iv_bg_red, ResourceParser.RED);
@ -143,6 +363,20 @@ public class NoteEditActivity extends Activity implements OnClickListener,
private WorkingNote mWorkingNote;
// habit calendar UI
private LinearLayout mHabitPanel;
private Button mBtnPrevMonth;
private Button mBtnNextMonth;
private TextView mTvHabitMonth;
private GridLayout mHabitCalendarGrid;
private TextView mTvHabitTotal;
private ProgressBar mProgressHabitGoal;
// calendar state
private Calendar mRenderCalendar = Calendar.getInstance();
// habit ui
private SharedPreferences mSharedPrefs;
private int mFontSizeId;
@ -330,9 +564,341 @@ public class NoteEditActivity extends Activity implements OnClickListener,
* is not ready
*/
showAlertHeader();
// render habit calendar if this note is a habit
if (mWorkingNote.isHabit()) {
mHabitPanel.setVisibility(View.VISIBLE);
// ensure calendar shows current month
mRenderCalendar = Calendar.getInstance();
renderHabitPanel();
} else {
mHabitPanel.setVisibility(View.GONE);
}
}
private void showAlertHeader() {//显示笔记的 闹钟提醒 信息
// Render calendar and statistics from habit_config.history
private void renderHabitPanel() {
if (mWorkingNote == null || !mWorkingNote.isHabit()) return;
Map<String, String> dayStatus = new HashMap<String, String>(); // yyyy-MM-dd -> status
int totalCompleted = 0;
// 优先使用 WorkingNote 中的 habit_config避免日历点击改状态后 DB 缓存导致视图不刷新
String cfg = (mWorkingNote != null) ? mWorkingNote.getHabitConfig() : null;
if (cfg == null || cfg.length() == 0) {
if (mWorkingNote != null && mWorkingNote.existInDatabase()) {
try {
Uri uri = ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mWorkingNote.getNoteId());
Cursor c = getContentResolver().query(uri, new String[]{Notes.NoteColumns.HABIT_CONFIG}, null, null, null);
if (c != null) {
if (c.moveToFirst()) {
String fromDb = c.getString(0);
if (fromDb != null && fromDb.length() > 0) cfg = fromDb;
}
c.close();
}
} catch (Exception e) {
Log.e(TAG, "Failed to read latest habit config", e);
}
}
}
if (cfg == null) cfg = "";
try {
if (cfg != null && cfg.length() > 0) {
JSONObject jo = new JSONObject(cfg);
if (jo.has("history")) {
JSONArray history = jo.getJSONArray("history");
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
for (int i = 0; i < history.length(); i++) {
JSONObject e = history.getJSONObject(i);
long date = e.optLong("date", 0);
String status = e.optString("status", "");
if (date > 0) {
Calendar c = Calendar.getInstance();
c.setTimeInMillis(date);
String key = sdf.format(c.getTime());
dayStatus.put(key, status);
if ("completed".equals(status)) totalCompleted++;
}
}
}
}
} catch (Exception e) {
// ignore parse errors
Log.e(TAG, "Failed to parse habit config", e);
}
// prepare month display
Calendar cal = (Calendar) mRenderCalendar.clone();
cal.set(Calendar.DAY_OF_MONTH, 1);
int month = cal.get(Calendar.MONTH);
int year = cal.get(Calendar.YEAR);
SimpleDateFormat monthFmt = new SimpleDateFormat("yyyy年MM月", Locale.getDefault());
mTvHabitMonth.setText(monthFmt.format(cal.getTime()));
// clear grid
mHabitCalendarGrid.removeAllViews();
// add weekday headers
String[] w = new String[] {"日","一","二","三","四","五","六"};
for (int i = 0; i < 7; i++) {
TextView tv = new TextView(this);
tv.setText(w[i]);
tv.setGravity(android.view.Gravity.CENTER);
tv.setPadding(6,6,6,6);
mHabitCalendarGrid.addView(tv);
}
int firstWeekday = cal.get(Calendar.DAY_OF_WEEK) - 1; // 0..6
int daysInMonth = cal.getActualMaximum(Calendar.DAY_OF_MONTH);
// fill blanks
for (int i = 0; i < firstWeekday; i++) {
TextView tv = new TextView(this);
mHabitCalendarGrid.addView(tv);
}
SimpleDateFormat sdfKey = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
for (int d = 1; d <= daysInMonth; d++) {
cal.set(Calendar.DAY_OF_MONTH, d);
String key = sdfKey.format(cal.getTime());
TextView cell = new TextView(this);
cell.setText(String.valueOf(d));
cell.setGravity(android.view.Gravity.CENTER);
cell.setPadding(12,12,12,12);
String status = dayStatus.get(key);
String icon = "";
boolean isToday = isSameDay(cal, Calendar.getInstance());
if (status != null) {
if ("completed".equals(status)) {
cell.setBackgroundResource(isToday ? R.drawable.habit_day_today_bg : R.drawable.habit_day_completed_bg);
icon = "✅ ";
} else if ("skipped".equals(status)) {
cell.setBackgroundResource(isToday ? R.drawable.habit_day_today_bg : R.drawable.habit_day_skipped_bg);
icon = " ";
} else {
cell.setBackgroundResource(isToday ? R.drawable.habit_day_today_bg : R.drawable.habit_day_pending_bg);
icon = "🔄 ";
}
} else {
// future or empty
if (isToday) {
cell.setBackgroundResource(R.drawable.habit_day_today_bg);
icon = "🔄 ";
}
}
// 设置文本为图标+日期
cell.setText(icon + String.valueOf(d));
final String selKey = key;
final Calendar clickedCal = (Calendar) cal.clone();
final String cellStatus = status;
cell.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 检查点击的日期是否是过去的日期或今天
Calendar today = Calendar.getInstance();
today.set(Calendar.HOUR_OF_DAY, 0);
today.set(Calendar.MINUTE, 0);
today.set(Calendar.SECOND, 0);
today.set(Calendar.MILLISECOND, 0);
if (clickedCal.before(today) || isSameDay(clickedCal, today)) {
// 过去的日期或今天,显示三个选项
AlertDialog.Builder builder = new AlertDialog.Builder(NoteEditActivity.this);
builder.setTitle("设置打卡状态");
builder.setItems(new CharSequence[]{"设为✅ 已完成", "设为➖ 跳过", "设为🔄 待打卡"}, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String status = "";
String reason = "";
switch (which) {
case 0:
// 设为已完成
status = "completed";
reason = clickedCal.before(today) ? "补打卡" : "";
break;
case 1:
// 设为跳过
status = "skipped";
reason = "手动设置跳过";
break;
case 2:
// 设为待打卡
status = "pending";
reason = "手动设置待打卡";
break;
}
// 记录点击日期的打卡状态传入点击的Calendar对象
recordHabitHistory(mWorkingNote.getNoteId(), status, reason, clickedCal);
renderHabitPanel();
}
});
builder.show();
} else {
// 未来日期,不允许打卡
Toast.makeText(NoteEditActivity.this, "未来日期暂不允许设置状态", Toast.LENGTH_SHORT).show();
}
}
});
mHabitCalendarGrid.addView(cell);
}
int goal = 0;
try { if (cfg != null && cfg.length() > 0) { JSONObject j = new JSONObject(cfg); goal = j.optInt("target_value", 0); } } catch (Exception e) {}
mTvHabitTotal.setText("总计" + totalCompleted + "天");
if (goal > 0) {
int prog = Math.min(100, (int) ((totalCompleted * 100L) / goal));
mProgressHabitGoal.setProgress(prog);
} else {
mProgressHabitGoal.setProgress(0);
}
}
private boolean isSameDay(Calendar a, Calendar b) {
return a.get(Calendar.YEAR) == b.get(Calendar.YEAR) && a.get(Calendar.DAY_OF_YEAR) == b.get(Calendar.DAY_OF_YEAR);
}
// 计算完成的天数
private int calculateCompletedDays(JSONArray history) {
int count = 0;
try {
Log.d(TAG, "Calculating completed days from history with length: " + history.length());
for (int i = 0; i < history.length(); i++) {
JSONObject entry = history.getJSONObject(i);
String status = entry.optString("status", "");
long date = entry.optLong("date", 0);
Log.d(TAG, "History entry " + i + ": date=" + date + ", status=" + status);
if ("completed".equals(status)) {
count++;
}
}
Log.d(TAG, "Calculated completed days: " + count);
} catch (JSONException e) {
Log.e(TAG, "Calculate completed days error", e);
}
return count;
}
// 检查并显示目标达成弹窗
private void checkAndShowGoalAchievement(JSONObject config, int completedBefore, int completedAfter) {
try {
int goal = config.optInt("target_value", 0);
Log.d(TAG, "Goal achievement check - completedBefore: " + completedBefore + ", completedAfter: " + completedAfter + ", goal: " + goal);
if (goal > 0) {
// 检查是否刚刚达成目标(更新后达到或超过目标,更新前未达到)
boolean shouldShowDialog = completedAfter >= goal && completedBefore < goal;
Log.d(TAG, "Should show celebration dialog: " + shouldShowDialog);
if (shouldShowDialog) {
// 显示喝彩弹窗
showCelebrationDialog();
}
}
} catch (Exception e) {
Log.e(TAG, "Check goal achievement error", e);
}
}
// 显示喝彩弹窗
private void showCelebrationDialog() {
// 确保Activity处于可见状态避免Window handle错误
if (isFinishing() || isDestroyed()) {
Log.d(TAG, "Activity is not visible, skipping celebration dialog");
return;
}
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("🎉 恭喜你!");
builder.setMessage("你已经达成了习惯目标!继续保持,加油!");
builder.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
builder.setCancelable(true);
AlertDialog dialog = builder.create();
dialog.show();
}
// Record history into habit_config.history (same logic as AlarmAlertActivity)
private void recordHabitHistory(long noteId, String status, String reason, Calendar recordCal) {
try {
Uri uri = ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId);
Cursor c = getContentResolver().query(uri, new String[]{Notes.NoteColumns.HABIT_CONFIG}, null, null, null);
String cfg = "";
if (c != null) {
if (c.moveToFirst()) cfg = c.getString(0);
c.close();
}
JSONObject jo = cfg != null && cfg.length() > 0 ? new JSONObject(cfg) : new JSONObject();
JSONArray history = jo.has("history") ? jo.getJSONArray("history") : new JSONArray();
// 计算更新前的完成天数
int completedBefore = calculateCompletedDays(history);
// 获取点击的日期的时间戳
long recordTime = recordCal.getTimeInMillis();
// 创建新记录
JSONObject newEntry = new JSONObject();
newEntry.put("date", recordTime);
newEntry.put("status", status);
newEntry.put("reason", reason == null ? "" : reason);
// 检查是否已经存在该日期的记录,如果有则替换
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
String recordKey = sdf.format(new Date(recordTime));
boolean foundRecord = false;
for (int i = 0; i < history.length(); i++) {
JSONObject entry = history.getJSONObject(i);
long entryDate = entry.optLong("date", 0);
String entryKey = sdf.format(new Date(entryDate));
if (recordKey.equals(entryKey)) {
// 替换该日期的记录
history.put(i, newEntry);
foundRecord = true;
break;
}
}
// 如果没有该日期的记录,添加新记录
if (!foundRecord) {
history.put(newEntry);
}
// 计算更新后的完成天数
int completedAfter = calculateCompletedDays(history);
jo.put("history", history);
String updatedConfig = jo.toString();
// 更新数据库
ContentValues values = new ContentValues();
values.put(Notes.NoteColumns.HABIT_CONFIG, updatedConfig);
getContentResolver().update(uri, values, null, null);
getContentResolver().notifyChange(uri, null);
// 同时更新本地 WorkingNote 缓存,确保 renderHabitPanel 使用最新数据、日历视图立即刷新
if (mWorkingNote != null) {
mWorkingNote.setHabit(true, updatedConfig);
}
// 检查是否达成目标
checkAndShowGoalAchievement(jo, completedBefore, completedAfter);
} catch (JSONException e) {
Log.e(TAG, "Record habit history json error", e);
}
}
// 兼容原有调用的重载方法
private void recordHabitHistory(long noteId, String status, String reason) {
recordHabitHistory(noteId, status, reason, Calendar.getInstance());
}
private void showAlertHeader() {
if (mWorkingNote.hasClockAlert()) {
long time = System.currentTimeMillis();
if (time > mWorkingNote.getAlertDate()) {
@ -453,6 +1019,29 @@ public class NoteEditActivity extends Activity implements OnClickListener,
mFontSizeId = ResourceParser.BG_DEFAULT_FONT_SIZE;
}
mEditTextList = (LinearLayout) findViewById(R.id.note_edit_list);
// habit UI binds
mHabitPanel = (LinearLayout) findViewById(R.id.habit_panel);
mBtnPrevMonth = (Button) findViewById(R.id.btn_prev_month);
mBtnNextMonth = (Button) findViewById(R.id.btn_next_month);
mTvHabitMonth = (TextView) findViewById(R.id.tv_habit_month);
mHabitCalendarGrid = (GridLayout) findViewById(R.id.habit_calendar_grid);
mTvHabitTotal = (TextView) findViewById(R.id.tv_habit_total);
mProgressHabitGoal = (ProgressBar) findViewById(R.id.progress_habit_goal);
mBtnPrevMonth.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mRenderCalendar.add(Calendar.MONTH, -1);
renderHabitPanel();
}
});
mBtnNextMonth.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mRenderCalendar.add(Calendar.MONTH, 1);
renderHabitPanel();
}
});
}
@Override
@ -586,6 +1175,11 @@ public class NoteEditActivity extends Activity implements OnClickListener,
} else {
menu.findItem(R.id.menu_delete_remind).setVisible(false);
}
// 习惯相关菜单处理
boolean isHabit = mWorkingNote.isHabit();
menu.findItem(R.id.menu_set_habit).setVisible(!isHabit);
menu.findItem(R.id.menu_habit_settings).setVisible(isHabit);
menu.findItem(R.id.menu_stop_habit).setVisible(isHabit);
return true;
}
@ -634,6 +1228,38 @@ public class NoteEditActivity extends Activity implements OnClickListener,
case R.id.menu_delete_remind://删除提醒
mWorkingNote.setAlertDate(0, false);
break;
case R.id.menu_set_habit:
// 设置为习惯便签并显示设置对话框
mWorkingNote.setHabit(true, "");
showHabitConfigDialog();
break;
case R.id.menu_habit_settings:
// 显示习惯设置对话框
showHabitConfigDialog();
break;
case R.id.menu_stop_habit:
// 停止习惯,转换为普通便签
AlertDialog.Builder stopHabitBuilder = new AlertDialog.Builder(this);
stopHabitBuilder.setTitle(R.string.habit_config_title);
stopHabitBuilder.setMessage("确定要停止这个习惯吗?");
stopHabitBuilder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// 将习惯便签转换为普通便签
mWorkingNote.setHabit(false, "");
// 取消习惯提醒
cancelHabitAlarm(mWorkingNote);
// 隐藏习惯面板
mHabitPanel.setVisibility(View.GONE);
// 刷新菜单
invalidateOptionsMenu();
// 刷新UI
renderHabitPanel();
}
});
stopHabitBuilder.setNegativeButton(android.R.string.cancel, null);
stopHabitBuilder.show();
break;
default:
break;
}

@ -110,8 +110,13 @@ public class NoteItemData {
mType = cursor.getInt(TYPE_COLUMN);
mWidgetId = cursor.getInt(WIDGET_ID_COLUMN);
mWidgetType = cursor.getInt(WIDGET_TYPE_COLUMN);
// habit flag, may not exist on older DBs
try {
mIsHabit = cursor.getInt(IS_HABIT_COLUMN) > 0;
} catch (Exception e) {
mIsHabit = false;
}
// 处理通话记录相关属性
mPhoneNumber = "";
if (mParentId == Notes.ID_CALL_RECORD_FOLDER) {
// 获取通话记录的电话号码
@ -326,6 +331,11 @@ public class NoteItemData {
return (mAlertDate > 0);
}
public boolean isHabit() {
return mIsHabit;
}
/**
*
* @return truefalse

Loading…
Cancel
Save