|
|
|
|
@ -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;
|
|
|
|
|
}
|
|
|
|
|
|