小米便签——ui包详细解读

目录

ui:用户界面类

1 AlarmAlertActivity

2 AlarmInitReceiver

3 AlarmReceiver

4 DateTimePicker

5 DateTimePickerDialog

6 DropdownMenu

7 FoldersListAdapter

8 NoteEditActivity

9 NoteltemData

10 NotesListActivity

11 NoteEditText

12 NotesListAdapter

13 NotesListltem

14 NotesPreferenceActivity


ui:用户界面类

AlarmAlertActivity:闹钟提醒界面
AlarmmlnitReceiver:闹钟提醒启动消息接收器
AlarmReceiver:闹钟提醒接收器
DataTimePicker:设置提醒时间的部件
DataTimePickerDialog:设置提醒时间的对话框界面
DropdownMenu:下拉菜单界面
FoldersListAdapter:文件夹列表链接器(链接数据库)
NoteEditAcfvity:便签编辑活动
NoteEditText:便签的文本编辑界面
NoteltemData:便签项数据
NoteUstActivity:主界面,实现处理文件列表的活动

NoteUstAdapter:便签列表链接器(链接数据库)
NoteUstltem:便签列表项
NotePreferenceActivity:便签同步的设置界面


1 AlarmAlertActivity

package net.micode.notes.ui; // 定义了该类的包路径

import android.app.Activity; // 导入Activity类,用于创建Android活动
import android.app.AlertDialog; // 导入AlertDialog类,用于创建对话框
import android.content.Context; // 导入Context类,用于获取应用程序环境
import android.content.DialogInterface; // 导入DialogInterface类,用于处理对话框事件
import android.content.DialogInterface.OnClickListener; // 导入OnClickListener接口,用于处理对话框按钮点击事件
import android.content.DialogInterface.OnDismissListener; // 导入OnDismissListener接口,用于处理对话框关闭事件
import android.content.Intent; // 导入Intent类,用于启动活动和传递数据
import android.media.AudioManager; // 导入AudioManager类,用于管理音频设置
import android.media.MediaPlayer; // 导入MediaPlayer类,用于播放音频
import android.media.RingtoneManager; // 导入RingtoneManager类,用于获取铃声
import android.net.Uri; // 导入Uri类,用于表示资源路径
import android.os.Bundle; // 导入Bundle类,用于保存活动状态
import android.os.PowerManager; // 导入PowerManager类,用于管理电源设置
import android.provider.Settings; // 导入Settings类,用于获取系统设置
import android.view.Window; // 导入Window类,用于管理窗口
import android.view.WindowManager; // 导入WindowManager类,用于管理窗口标志

import net.micode.notes.R; // 导入应用内部定义的资源类
import net.micode.notes.data.Notes; // 导入应用内部定义的笔记数据类
import net.micode.notes.tool.DataUtils; // 导入应用内部定义的数据工具类

import java.io.IOException; // 导入IOException类,用于处理输入输出异常

public class AlarmAlertActivity extends Activity implements OnClickListener, OnDismissListener { // 定义AlarmAlertActivity类,继承自Activity,并实现OnClickListener和OnDismissListener接口
    private long mNoteId; // 定义笔记的ID
    private String mSnippet; // 定义笔记内容的简短预览
    private static final int SNIPPET_PREW_MAX_LEN = 60; // 定义预览文本的最大长度
    private MediaPlayer mPlayer; // 定义用于播放提醒声音的MediaPlayer对象

    @Override
    protected void onCreate(Bundle savedInstanceState) { // 重写onCreate方法,在活动创建时调用
        super.onCreate(savedInstanceState); // 调用父类的onCreate方法
        requestWindowFeature(Window.FEATURE_NO_TITLE); // 请求无标题的窗口

        final Window win = getWindow(); // 获取当前窗口
        win.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); // 设置窗口在锁屏时显示
        if (!isScreenOn()) { // 检查屏幕是否处于打开状态
            win.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON // 保持屏幕唤醒
                    | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON // 打开屏幕
                    | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON // 允许在屏幕打开时锁定
                    | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR); // 布局插入装饰
        }

        Intent intent = getIntent(); // 获取启动该活动的意图
        try {
            mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1)); // 从意图中获取笔记ID
            mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId); // 获取笔记内容的简短预览
            mSnippet = mSnippet.length() > SNIPPET_PREW_MAX_LEN ? mSnippet.substring(0, // 如果预览文本长度超过最大长度,则截取
                    SNIPPET_PREW_MAX_LEN) + getResources().getString(R.string.notelist_string_info)
                    : mSnippet; // 否则使用原始预览文本
        } catch (IllegalArgumentException e) { // 捕获非法参数异常
            e.printStackTrace(); // 打印异常信息
            return; // 返回
        }

        mPlayer = new MediaPlayer(); // 创建MediaPlayer对象
        if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) { // 检查笔记在数据库中是否可见
            showActionDialog(); // 显示动作对话框
            playAlarmSound(); // 播放提醒声音
        } else {
            finish(); // 结束活动
        }
    }

    private boolean isScreenOn() { // 定义检查屏幕是否处于打开状态的方法
        PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); // 获取电源管理服务
        return pm.isScreenOn(); // 返回屏幕是否处于打开状态
    }

    private void playAlarmSound() { // 定义播放提醒声音的方法
        Uri url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM); // 获取默认的闹钟铃声URI

        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(); // 打印异常信息
        }
    }

    private void showActionDialog() { // 定义显示动作对话框的方法
        AlertDialog.Builder dialog = new AlertDialog.Builder(this); // 创建对话框构建器
        dialog.setTitle(R.string.app_name); // 设置对话框标题
        dialog.setMessage(mSnippet); // 设置对话框消息
        dialog.setPositiveButton(R.string.notealert_ok, this); // 设置正面按钮
        if (isScreenOn()) { // 检查屏幕是否处于打开状态
            dialog.setNegativeButton(R.string.notealert_enter, this); // 设置负面按钮
        }
        dialog.show().setOnDismissListener(this); // 显示对话框并设置关闭监听器
    }

    public void onClick(DialogInterface dialog, int which) { // 实现OnClickListener接口的onClick方法,处理对话框按钮点击事件
        switch (which) { // 根据按钮类型进行处理
            case DialogInterface.BUTTON_NEGATIVE: // 如果是负面按钮
                Intent intent = new Intent(this, NoteEditActivity.class); // 创建启动笔记编辑活动的意图
                intent.setAction(Intent.ACTION_VIEW); // 设置动作
                intent.putExtra(Intent.EXTRA_UID, mNoteId); // 传递笔记ID
                startActivity(intent); // 启动活动
                break; // 结束处理
            default: // 默认情况
                break; // 结束处理
        }
    }

    public void onDismiss(DialogInterface dialog) { // 实现OnDismissListener接口的onDismiss方法,处理对话框关闭事件
        stopAlarmSound(); // 停止播放提醒声音
        finish(); // 结束活动
    }

    private void stopAlarmSound() { // 定义停止播放提醒声音的方法
        if (mPlayer != null) { // 检查MediaPlayer对象是否为空
            mPlayer.stop(); // 停止播放
            mPlayer.release(); // 释放资源
            mPlayer = null; // 将MediaPlayer对象设置为空
        }
    }
}

2 AlarmInitReceiver

/*
 * 该类是广播接收器,用于在应用启动时初始化提醒设置。
 * 当系统启动时,它会检查数据库中所有设置了提醒的笔记,并为每个笔记设置相应的提醒。
 */
package net.micode.notes.ui; // 定义该类的包路径

import android.app.AlarmManager; // 导入AlarmManager类,用于设置提醒
import android.app.PendingIntent; // 导入PendingIntent类,用于延迟执行意图
import android.content.BroadcastReceiver; // 导入BroadcastReceiver类,用于接收广播
import android.content.ContentUris; // 导入ContentUris类,用于处理URI
import android.content.Context; // 导入Context类,用于获取应用环境
import android.content.Intent; // 导入Intent类,用于启动活动和传递数据
import android.database.Cursor; // 导入Cursor类,用于查询数据库

import net.micode.notes.data.Notes; // 导入应用内部定义的笔记数据类
import net.micode.notes.data.Notes.NoteColumns; // 导入笔记数据类的列定义

public class AlarmInitReceiver extends BroadcastReceiver { // 定义AlarmInitReceiver类,继承自BroadcastReceiver

    // 查询笔记时需要的列
    private static final String[] PROJECTION = new String[]{
            NoteColumns.ID, // 笔记ID
            NoteColumns.ALERTED_DATE // 提醒日期
    };

    // 列的索引
    private static final int COLUMN_ID = 0; // ID列的索引
    private static final int COLUMN_ALERTED_DATE = 1; // 提醒日期列的索引

    /**
     * 当接收到广播时执行的操作。主要用于设置所有已记录的提醒时间。
     *
     * @param context 上下文,提供访问应用全局功能的入口。
     * @param intent  携带了触发该接收器的广播信息。
     */
    @Override
    public void onReceive(Context context, Intent intent) { // 重写onReceive方法,当接收到广播时执行
        // 获取当前日期和时间
        long currentDate = System.currentTimeMillis(); // 获取当前时间戳
        // 查询数据库中所有需要提醒的笔记
        Cursor c = context.getContentResolver().query(Notes.CONTENT_NOTE_URI, // 查询笔记内容的URI
                PROJECTION, // 查询的列
                NoteColumns.ALERTED_DATE + ">? AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE, // 查询条件
                new String[]{String.valueOf(currentDate)}, // 查询参数
                null); // 排序方式

        if (c != null) { // 检查Cursor是否为空
            // 遍历查询结果,为每个需要提醒的笔记设置提醒
            if (c.moveToFirst()) { // 移动到第一行
                do {
                    // 获取提醒日期
                    long alertDate = c.getLong(COLUMN_ALERTED_DATE); // 获取提醒日期
                    // 创建Intent,用于在提醒时间触发AlarmReceiver
                    Intent sender = new Intent(context, AlarmReceiver.class); // 创建启动AlarmReceiver的意图
                    sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, c.getLong(COLUMN_ID))); // 设置意图的数据URI
                    // 创建PendingIntent,它是一个延迟的意图,可以在特定时间由系统触发
                    PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, 0); // 创建PendingIntent
                    // 获取AlarmManager服务,用于设置提醒
                    AlarmManager alermManager = (AlarmManager) context
                            .getSystemService(Context.ALARM_SERVICE); // 获取AlarmManager服务
                    // 设置提醒
                    alermManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent); // 设置提醒时间
                } while (c.moveToNext()); // 移动到下一行
            }
            // 关闭Cursor,释放资源
            c.close(); // 关闭Cursor
        }
    }
}

3 AlarmReceiver

/*
 * AlarmReceiver类 - 用于处理闹钟广播接收
 * 当接收到闹钟相关的广播时,该类会启动一个指定的Activity
 *
 * extends BroadcastReceiver: 继承自Android的BroadcastReceiver类
 */
package net.micode.notes.ui; // 定义该类的包路径

import android.content.BroadcastReceiver; // 导入BroadcastReceiver类,用于接收广播
import android.content.Context; // 导入Context类,用于获取应用环境
import android.content.Intent; // 导入Intent类,用于启动活动和传递数据

public class AlarmReceiver extends BroadcastReceiver { // 定义AlarmReceiver类,继承自BroadcastReceiver
    /*
     * onReceive方法 - 系统调用的接收广播的方法
     * 当接收到广播时,该方法会被调用,然后启动AlarmAlertActivity
     *
     * @param context 上下文对象,提供了调用环境的信息
     * @param intent  包含广播的内容
     */
    @Override
    public void onReceive(Context context, Intent intent) { // 重写onReceive方法,当接收到广播时执行
        // 设置Intent的类,以便启动AlarmAlertActivity
        intent.setClass(context, AlarmAlertActivity.class); // 设置Intent的目标Activity为AlarmAlertActivity
        // 添加标志,表示在一个新的任务中启动Activity
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // 添加标志,确保Activity在一个新的任务中启动
        // 根据设置的Intent启动Activity
        context.startActivity(intent); // 启动AlarmAlertActivity
    }
}

4 DateTimePicker



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;

public class DateTimePicker extends FrameLayout {

    // 默认启用状态
    private static final boolean DEFAULT_ENABLE_STATE = true;

    // 半天的小时数
    private static final int HOURS_IN_HALF_DAY = 12;
    // 一整天的小时数
    private static final int HOURS_IN_ALL_DAY = 24;
    // 一周的天数
    private static final int DAYS_IN_ALL_WEEK = 7;
    // 日期选择器的最小值
    private static final int DATE_SPINNER_MIN_VAL = 0;
    // 日期选择器的最大值
    private static final int DATE_SPINNER_MAX_VAL = DAYS_IN_ALL_WEEK - 1;
    // 24小时制小时选择器的最小值
    private static final int HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW = 0;
    // 24小时制小时选择器的最大值
    private static final int HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW = 23;
    // 12小时制小时选择器的最小值
    private static final int HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW = 1;
    // 12小时制小时选择器的最大值
    private static final int HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW = 12;
    // 分钟选择器的最小值
    private static final int MINUT_SPINNER_MIN_VAL = 0;
    // 分钟选择器的最大值
    private static final int MINUT_SPINNER_MAX_VAL = 59;
    // 上下午选择器的最小值
    private static final int AMPM_SPINNER_MIN_VAL = 0;
    // 上下午选择器的最大值
    private static final int AMPM_SPINNER_MAX_VAL = 1;

    // 日期选择器
    private final NumberPicker mDateSpinner;
    // 小时选择器
    private final NumberPicker mHourSpinner;
    // 分钟选择器
    private final NumberPicker mMinuteSpinner;
    // 上下午选择器
    private final NumberPicker mAmPmSpinner;
    // 当前日期
    private Calendar mDate;

    // 用于显示日期的字符串数组
    private String[] mDateDisplayValues = new String[DAYS_IN_ALL_WEEK];

    // 当前是否为上午
    private boolean mIsAm;

    // 当前是否为24小时制视图
    private boolean mIs24HourView;

    // 控件是否启用
    private boolean mIsEnabled = DEFAULT_ENABLE_STATE;

    // 是否正在初始化
    private boolean mInitialising;

    // 日期时间改变监听器
    private OnDateTimeChangedListener mOnDateTimeChangedListener;

    // 日期选择器的值改变监听器
    private NumberPicker.OnValueChangeListener mOnDateChangedListener = new NumberPicker.OnValueChangeListener() {
        @Override
        public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
            // 根据新旧值的差异更新日期
            mDate.add(Calendar.DAY_OF_YEAR, newVal - oldVal);
            updateDateControl();
            onDateTimeChanged();
        }
    };

    // 小时选择器的值改变监听器
    private NumberPicker.OnValueChangeListener mOnHourChangedListener = new NumberPicker.OnValueChangeListener() {
        @Override
        public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
            // 根据小时的变化更新日期和上下午状态
            boolean isDateChanged = false;
            Calendar cal = Calendar.getInstance();
            if (!mIs24HourView) {
                // 处理12小时制下的日期变化和上下午切换
                if (!mIsAm && oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) {
                    cal.setTimeInMillis(mDate.getTimeInMillis());
                    cal.add(Calendar.DAY_OF_YEAR, 1);
                    isDateChanged = true;
                } else if (mIsAm && oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) {
                    cal.setTimeInMillis(mDate.getTimeInMillis());
                    cal.add(Calendar.DAY_OF_YEAR, -1);
                    isDateChanged = true;
                }
                if (oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY ||
                        oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) {
                    mIsAm = !mIsAm;
                    updateAmPmControl();
                }
            } else {
                // 处理24小时制下的日期变化
                if (oldVal == HOURS_IN_ALL_DAY - 1 && newVal == 0) {
                    cal.setTimeInMillis(mDate.getTimeInMillis());
                    cal.add(Calendar.DAY_OF_YEAR, 1);
                    isDateChanged = true;
                } else if (oldVal == 0 && newVal == HOURS_IN_ALL_DAY - 1) {
                    cal.setTimeInMillis(mDate.getTimeInMillis());
                    cal.add(Calendar.DAY_OF_YEAR, -1);
                    isDateChanged = true;
                }
            }
            // 更新小时并触发日期时间改变事件
            int newHour = mHourSpinner.getValue() % HOURS_IN_HALF_DAY + (mIsAm ? 0 : HOURS_IN_HALF_DAY);
            mDate.set(Calendar.HOUR_OF_DAY, newHour);
            onDateTimeChanged();
            // 如果日期有变化,则更新年月日
            if (isDateChanged) {
                setCurrentYear(cal.get(Calendar.YEAR));
                setCurrentMonth(cal.get(Calendar.MONTH));
                setCurrentDay(cal.get(Calendar.DAY_OF_MONTH));
            }
        }
    };


    // 分别为分钟和AM/PM选择器监听器设置匿名内部类,实现数值变化时的处理逻辑。
    private NumberPicker.OnValueChangeListener mOnMinuteChangedListener = new NumberPicker.OnValueChangeListener() {
        @Override
        public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
            // 计算小时的偏移量,当从最大值变为最小值或从最小值变为最大值时调整
            int minValue = mMinuteSpinner.getMinValue();
            int maxValue = mMinuteSpinner.getMaxValue();
            int offset = 0;
            if (oldVal == maxValue && newVal == minValue) {
                offset += 1;
            } else if (oldVal == minValue && newVal == maxValue) {
                offset -= 1;
            }
            // 根据偏移量更新日期和小时选择器,并检查是否需要切换AM/PM
            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();
        }
    };

    private NumberPicker.OnValueChangeListener mOnAmPmChangedListener = new NumberPicker.OnValueChangeListener() {
        @Override
        public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
            // 切换AM/PM状态,并更新日期和AM/PM选择器
            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 {
        void onDateTimeChanged(DateTimePicker view, int year, int month,
                               int dayOfMonth, int hourOfDay, int minute);
    }

    // 构造函数:初始化日期时间选择器
    public DateTimePicker(Context context) {
        this(context, System.currentTimeMillis());
    }

    // 构造函数:指定初始日期时间
    public DateTimePicker(Context context, long date) {
        this(context, date, DateFormat.is24HourFormat(context));
    }

    // 构造函数:指定是否使用24小时制视图
    public DateTimePicker(Context context, long date, boolean is24HourView) {
        super(context);
        mDate = Calendar.getInstance();
        mInitialising = true;
        mIsAm = getCurrentHourOfDay() >= HOURS_IN_HALF_DAY;
        inflate(context, R.layout.datetime_picker, this);

        // 初始化日期、小时、分钟和AM/PM选择器,并设置相应的监听器
        mDateSpinner = (NumberPicker) findViewById(R.id.date);
        mDateSpinner.setMinValue(DATE_SPINNER_MIN_VAL);
        mDateSpinner.setMaxValue(DATE_SPINNER_MAX_VAL);
        mDateSpinner.setOnValueChangedListener(mOnDateChangedListener);

        mHourSpinner = (NumberPicker) findViewById(R.id.hour);
        mHourSpinner.setOnValueChangedListener(mOnHourChangedListener);
        mMinuteSpinner = (NumberPicker) findViewById(R.id.minute);
        mMinuteSpinner.setMinValue(MINUT_SPINNER_MIN_VAL);
        mMinuteSpinner.setMaxValue(MINUT_SPINNER_MAX_VAL);
        mMinuteSpinner.setOnLongPressUpdateInterval(100);
        mMinuteSpinner.setOnValueChangedListener(mOnMinuteChangedListener);

        String[] stringsForAmPm = new DateFormatSymbols().getAmPmStrings();
        mAmPmSpinner = (NumberPicker) findViewById(R.id.amPm);
        mAmPmSpinner.setMinValue(AMPM_SPINNER_MIN_VAL);
        mAmPmSpinner.setMaxValue(AMPM_SPINNER_MAX_VAL);
        mAmPmSpinner.setDisplayedValues(stringsForAmPm);
        mAmPmSpinner.setOnValueChangedListener(mOnAmPmChangedListener);

        // 更新控件至初始状态
        updateDateControl();
        updateHourControl();
        updateAmPmControl();

        set24HourView(is24HourView);

        // 设置为当前时间
        setCurrentDate(date);

        setEnabled(isEnabled());

        // 设置内容描述
        mInitialising = false;
    }


    /**
     * 设置控件的启用状态。
     * 如果当前状态与传入状态相同,则不进行任何操作。
     * 启用或禁用日期和时间选择器,并更新内部启用状态。
     *
     * @param enabled 控件是否启用
     */
    @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;
    }

    /**
     * 获取控件的启用状态。
     *
     * @return 控件是否启用
     */
    @Override
    public boolean isEnabled() {
        return mIsEnabled;
    }

    /**
     * 获取当前日期的时间戳(毫秒)。
     *
     * @return 当前日期的毫秒时间戳
     */
    public long getCurrentDateInTimeMillis() {
        return mDate.getTimeInMillis();
    }

    /**
     * 设置当前日期。
     * 根据传入的毫秒时间戳更新日期选择器的值。
     *
     * @param date The current date in millis
     */
    public void setCurrentDate(long date) {
        Calendar cal = Calendar.getInstance();
        cal.setTimeInMillis(date);
        // 通过日历实例的详细字段设置当前日期和时间
        setCurrentDate(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH),
                cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE));
    }

    /**
     * 设置当前日期和时间。
     * 分别设置年、月、日、时和分。
     *
     * @param year       当前年份
     * @param month      当前月份
     * @param dayOfMonth 当前日
     * @param hourOfDay  当前小时
     * @param minute     当前分钟
     */
    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开始)
     */
    public int getCurrentMonth() {
        return mDate.get(Calendar.MONTH);
    }

    /**
     * 设置当前月份
     *
     * @param month 当前的月份(从0开始)
     */
    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小时制),范围为(0~23)
     *
     * @return 当前的小时(24小时制)
     */
    public int getCurrentHourOfDay() {
        return mDate.get(Calendar.HOUR_OF_DAY);
    }

    /**
     * 获取当前小时,根据是否为24小时制返回不同的值。
     * 如果是24小时制,与{@link #getCurrentHourOfDay()}返回相同结果;
     * 否则,将小时转换为12小时制,并考虑上午/下午。
     *
     * @return 当前的小时(根据视图模式可能是12小时制)
     */
    private int getCurrentHour() {
        if (mIs24HourView) {
            return getCurrentHourOfDay();
        } else {
            int hour = getCurrentHourOfDay();
            // 转换为12小时制
            if (hour > HOURS_IN_HALF_DAY) {
                return hour - HOURS_IN_HALF_DAY;
            } else {
                return hour == 0 ? HOURS_IN_HALF_DAY : hour;
            }
        }
    }


    /**
     * 设置当前小时(24小时制),范围为(0~23)
     *
     * @param hourOfDay 当前小时数
     */
    public void setCurrentHour(int hourOfDay) {
        // 如果在初始化中或者小时未改变,则直接返回
        if (!mInitialising && hourOfDay == getCurrentHourOfDay()) {
            return;
        }
        mDate.set(Calendar.HOUR_OF_DAY, hourOfDay);
        // 如果不是24小时视图,则调整小时数并更新AM/PM控制
        if (!mIs24HourView) {
            if (hourOfDay >= HOURS_IN_HALF_DAY) {
                mIsAm = false;
                if (hourOfDay > HOURS_IN_HALF_DAY) {
                    hourOfDay -= HOURS_IN_HALF_DAY;
                }
            } else {
                mIsAm = true;
                if (hourOfDay == 0) {
                    hourOfDay = HOURS_IN_HALF_DAY;
                }
            }
            updateAmPmControl();
        }
        mHourSpinner.setValue(hourOfDay);
        onDateTimeChanged();
    }

    /**
     * 获取当前分钟数
     *
     * @return 当前分钟数
     */
    public int getCurrentMinute() {
        return mDate.get(Calendar.MINUTE);
    }

    /**
     * 设置当前分钟数
     *
     * @param minute 当前分钟数值
     */
    public void setCurrentMinute(int minute) {
        // 如果在初始化中或者分钟数未改变,则直接返回
        if (!mInitialising && minute == getCurrentMinute()) {
            return;
        }
        mMinuteSpinner.setValue(minute);
        mDate.set(Calendar.MINUTE, minute);
        onDateTimeChanged();
    }

    /**
     * 获取当前是否为24小时视图
     *
     * @return 如果是24小时视图返回true,否则返回false
     */
    public boolean is24HourView() {
        return mIs24HourView;
    }

    /**
     * 设置当前视图为24小时制还是AM/PM制
     *
     * @param is24HourView 如果为true表示24小时制,false表示AM/PM制
     */
    public void set24HourView(boolean is24HourView) {
        // 如果视图模式未改变,则直接返回
        if (mIs24HourView == is24HourView) {
            return;
        }
        mIs24HourView = is24HourView;
        mAmPmSpinner.setVisibility(is24HourView ? View.GONE : View.VISIBLE);
        int hour = getCurrentHourOfDay();
        updateHourControl();
        setCurrentHour(hour);
        updateAmPmControl();
    }

    /**
     * 更新日期控制组件,显示正确的日期选项
     */
    private void updateDateControl() {
        Calendar cal = Calendar.getInstance();
        cal.setTimeInMillis(mDate.getTimeInMillis());
        cal.add(Calendar.DAY_OF_YEAR, -DAYS_IN_ALL_WEEK / 2 - 1);
        mDateSpinner.setDisplayedValues(null);
        // 循环设置一周内每一天的显示文本
        for (int i = 0; i < DAYS_IN_ALL_WEEK; ++i) {
            cal.add(Calendar.DAY_OF_YEAR, 1);
            mDateDisplayValues[i] = (String) DateFormat.format("MM.dd EEEE", cal);
        }
        mDateSpinner.setDisplayedValues(mDateDisplayValues);
        mDateSpinner.setValue(DAYS_IN_ALL_WEEK / 2);
        mDateSpinner.invalidate();
    }

    /**
     * 根据当前是否为24小时视图来更新AM/PM控制组件的显示和值
     */
    private void updateAmPmControl() {
        if (mIs24HourView) {
            mAmPmSpinner.setVisibility(View.GONE);
        } else {
            int index = mIsAm ? Calendar.AM : Calendar.PM;
            mAmPmSpinner.setValue(index);
            mAmPmSpinner.setVisibility(View.VISIBLE);
        }
    }

    /**
     * 根据当前是否为24小时视图来更新小时控制组件的最小值和最大值
     */
    private void updateHourControl() {
        if (mIs24HourView) {
            mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW);
            mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW);
        } else {
            mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW);
            mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW);
        }
    }

    /**
     * 设置点击“设置”按钮时的回调接口
     *
     * @param callback 回调接口实例,如果为null则不执行任何操作
     */
    public void setOnDateTimeChangedListener(OnDateTimeChangedListener callback) {
        mOnDateTimeChangedListener = callback;
    }

    /**
     * 当日期时间被改变时调用此方法,如果设置了日期时间改变监听器,则触发监听器的回调方法
     */
    private void onDateTimeChanged() {
        if (mOnDateTimeChangedListener != null) {
            mOnDateTimeChangedListener.onDateTimeChanged(this, getCurrentYear(),
                    getCurrentMonth(), getCurrentDay(), getCurrentHourOfDay(), getCurrentMinute());
        }
    }
}

5 DateTimePickerDialog

  1. 包声明package net.micode.notes.ui; 声明了该类所在的包。
  2. 导入语句:导入了一系列Java和Android相关的类,包括日历、对话框、上下文、日期时间格式化等。
  3. 类声明public class DateTimePickerDialog extends AlertDialog implements OnClickListener { 声明了一个名为 DateTimePickerDialog 的公共类,继承自 AlertDialog 并实现 OnClickListener 接口。
  4. 成员变量
    • private Calendar mDate = Calendar.getInstance(); 声明了一个私有成员变量 mDate,用于存储当前选择的日期和时间。
    • private boolean mIs24HourView; 声明了一个私有成员变量 mIs24HourView,用于指示日期时间选择器是否使用24小时制。
    • private OnDateTimeSetListener mOnDateTimeSetListener; 声明了一个私有成员变量 mOnDateTimeSetListener,用于存储日期时间设置监听器。
    • private DateTimePicker mDateTimePicker; 声明了一个私有成员变量 mDateTimePicker,用于存储日期时间选择器视图。
  5. 接口声明public interface OnDateTimeSetListener { ... } 声明了一个公共接口 OnDateTimeSetListener,用于处理日期时间被设置的事件。
  6. 构造函数
    • public DateTimePickerDialog(Context context, long date) { ... } 声明了类的构造函数,接受上下文和初始日期时间值作为参数。
    • super(context); 调用父类构造函数。
    • mDateTimePicker = new DateTimePicker(context); 创建 DateTimePicker 实例。
    • setView(mDateTimePicker); 设置对话框视图为 DateTimePicker
    • mDateTimePicker.setOnDateTimeChangedListener(new OnDateTimeChangedListener() { ... }); 设置日期时间改变的监听器。
    • mDate.setTimeInMillis(date); 设置初始日期时间。
    • mDate.set(Calendar.SECOND, 0); 设置秒数为0。
    • mDateTimePicker.setCurrentDate(mDate.getTimeInMillis()); 设置 DateTimePicker 的当前日期时间。
    • setButton(context.getString(R.string.datetime_dialog_ok), this); 设置对话框的确认按钮。
    • setButton2(context.getString(R.string.datetime_dialog_cancel), (OnClickListener) null); 设置对话框的取消按钮。
    • set24HourView(DateFormat.is24HourFormat(this.getContext())); 根据系统设置决定是否使用24小时制。
    • updateTitle(mDate.getTimeInMillis()); 更新标题以显示当前选择的日期和时间。
  7. 方法
    • public void set24HourView(boolean is24HourView) { ... } 设置日期时间选择器是否使用24小时制。
    • public void setOnDateTimeSetListener(OnDateTimeSetListener callBack) { ... } 设置日期时间被设置时的监听器。
    • private void updateTitle(long date) { ... } 更新对话框标题以显示当前选择的日期和时间。
    • public void onClick(DialogInterface arg0, int arg1) { ... } 点击按钮时的处理逻辑,如果设置了日期时间设置监听器,则调用其 OnDateTimeSet 方法。
/*
 * DateTimePickerDialog类提供了一个日期和时间选择器对话框。
 * 用户可以选择一个日期和时间,然后通过监听器回调返回选择的值。
 */
package net.micode.notes.ui; // 定义包名

import java.util.Calendar; // 导入Calendar类,用于处理日期和时间

import net.micode.notes.R; // 导入资源类,用于访问资源
import net.micode.notes.ui.DateTimePicker; // 导入DateTimePicker类,用于日期时间选择器视图
import net.micode.notes.ui.DateTimePicker.OnDateTimeChangedListener; // 导入日期时间改变监听器接口

import android.app.AlertDialog; // 导入AlertDialog类,用于创建对话框
import android.content.Context; // 导入Context类,用于获取上下文
import android.content.DialogInterface; // 导入DialogInterface类,用于处理对话框事件
import android.content.DialogInterface.OnClickListener; // 导入OnClickListener接口,用于处理按钮点击事件
import android.text.format.DateFormat; // 导入DateFormat类,用于日期时间格式化
import android.text.format.DateUtils; // 导入DateUtils类,用于日期时间格式化

public class DateTimePickerDialog extends AlertDialog implements OnClickListener { // 定义DateTimePickerDialog类,继承自AlertDialog并实现OnClickListener接口

    // 当前选择的日期和时间
    private Calendar mDate = Calendar.getInstance();
    // 用于指示日期时间选择器是否使用24小时制
    private boolean mIs24HourView;
    // 日期时间设置监听器,用于处理日期时间选择后的回调
    private OnDateTimeSetListener mOnDateTimeSetListener;
    // 日期时间选择器视图
    private DateTimePicker mDateTimePicker;

    /**
     * 日期时间设置监听器接口。
     * 实现此接口的类需要提供OnDateTimeSet方法来处理日期时间被设置的事件。
     */
    public interface OnDateTimeSetListener {
        void OnDateTimeSet(AlertDialog dialog, long date);
    }

    /**
     * 构造函数初始化日期时间选择器对话框。
     *
     * @param context 上下文对象,通常是指Activity。
     * @param date    初始显示的日期时间值。
     */
    public DateTimePickerDialog(Context context, long date) {
        super(context); // 调用父类构造函数
        mDateTimePicker = new DateTimePicker(context); // 创建DateTimePicker实例
        setView(mDateTimePicker); // 设置对话框视图为DateTimePicker
        // 设置日期时间改变的监听器
        mDateTimePicker.setOnDateTimeChangedListener(new OnDateTimeChangedListener() {
            public void onDateTimeChanged(DateTimePicker view, int year, int month,
                                          int dayOfMonth, int hourOfDay, int minute) {
                // 更新内部日期时间值
                mDate.set(Calendar.YEAR, year);
                mDate.set(Calendar.MONTH, month);
                mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth);
                mDate.set(Calendar.HOUR_OF_DAY, hourOfDay);
                mDate.set(Calendar.MINUTE, minute);
                // 更新对话框的标题显示
                updateTitle(mDate.getTimeInMillis());
            }
        });
        mDate.setTimeInMillis(date); // 设置初始日期时间
        mDate.set(Calendar.SECOND, 0); // 设置秒数为0
        mDateTimePicker.setCurrentDate(mDate.getTimeInMillis()); // 设置DateTimePicker的当前日期时间
        // 设置对话框的确认和取消按钮
        setButton(context.getString(R.string.datetime_dialog_ok), this);
        setButton2(context.getString(R.string.datetime_dialog_cancel), (OnClickListener) null);
        // 根据系统设置决定是否使用24小时制
        set24HourView(DateFormat.is24HourFormat(this.getContext()));
        // 更新标题以显示当前选择的日期和时间
        updateTitle(mDate.getTimeInMillis());
    }

    /**
     * 设置日期时间选择器是否使用24小时制。
     *
     * @param is24HourView 是否使用24小时制。
     */
    public void set24HourView(boolean is24HourView) {
        mIs24HourView = is24HourView;
    }

    /**
     * 设置日期时间被设置时的监听器。
     *
     * @param callBack 日期时间设置监听器对象。
     */
    public void setOnDateTimeSetListener(OnDateTimeSetListener callBack) {
        mOnDateTimeSetListener = callBack;
    }

    /**
     * 更新对话框标题以显示当前选择的日期和时间。
     *
     * @param date 当前选择的日期时间值。
     */
    private void updateTitle(long date) {
        // 根据是否使用24小时制来格式化日期时间显示
        int flag =
                DateUtils.FORMAT_SHOW_YEAR |
                        DateUtils.FORMAT_SHOW_DATE |
                        DateUtils.FORMAT_SHOW_TIME;
        flag |= mIs24HourView ? DateUtils.FORMAT_24HOUR : DateUtils.FORMAT_24HOUR;
        setTitle(DateUtils.formatDateTime(this.getContext(), date, flag));
    }

    /**
     * 点击按钮时的处理逻辑。
     * 如果设置了日期时间设置监听器,则调用其OnDateTimeSet方法,传入当前选择的日期时间值。
     *
     * @param arg0 对话框对象。
     * @param arg1 按钮标识。
     */
    public void onClick(DialogInterface arg0, int arg1) {
        if (mOnDateTimeSetListener != null) {
            mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis());
        }
    }

}

6 DropdownMenu

  1. 包声明package net.micode.notes.ui; 声明了该类所在的包。
  2. 导入语句:导入了一系列Android相关的类,包括上下文、菜单、视图、按钮、弹出菜单等。
  3. 类声明public class DropdownMenu { 声明了一个名为 DropdownMenu 的公共类。
  4. 成员变量
    • private Button mButton; 声明了一个私有成员变量 mButton,用于存储按钮实例。
    • private PopupMenu mPopupMenu; 声明了一个私有成员变量 mPopupMenu,用于存储弹出菜单实例。
    • private Menu mMenu; 声明了一个私有成员变量 mMenu,用于存储菜单项集合。
  5. 构造函数
    • public DropdownMenu(Context context, Button button, int menuId) { 声明了类的构造函数,接受上下文、按钮和菜单资源ID作为参数。
    • mButton = button; 初始化按钮。
    • mButton.setBackgroundResource(R.drawable.dropdown_icon); 设置按钮背景为下拉图标。
    • mPopupMenu = new PopupMenu(context, mButton); 创建 PopupMenu 实例,锚点为按钮。
    • mMenu = mPopupMenu.getMenu(); 获取菜单项的集合。
    • mPopupMenu.getMenuInflater().inflate(menuId, mMenu); 加载菜单项。
    • mButton.setOnClickListener(new OnClickListener() { ... }); 设置按钮点击事件,点击后显示下拉菜单。
  6. 方法
    • public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) { ... } 设置下拉菜单项的点击事件监听器。
    • public MenuItem findItem(int id) { ... } 根据ID查找菜单项。
    • public void setTitle(CharSequence title) { ... } 设置按钮的标题。
/*
 * DropdownMenu类用于创建和管理一个下拉菜单。
 * 该类封装了一个Button和一个PopupMenu,通过点击Button来显示下拉菜单。
 */
package net.micode.notes.ui; // 定义包名

import android.content.Context; // 导入Context类,用于获取上下文
import android.view.Menu; // 导入Menu类,用于管理菜单项
import android.view.MenuItem; // 导入MenuItem类,用于表示菜单项
import android.view.View; // 导入View类,用于处理视图
import android.view.View.OnClickListener; // 导入OnClickListener接口,用于处理点击事件
import android.widget.Button; // 导入Button类,用于创建按钮
import android.widget.PopupMenu; // 导入PopupMenu类,用于创建弹出菜单
import android.widget.PopupMenu.OnMenuItemClickListener; // 导入OnMenuItemClickListener接口,用于处理菜单项点击事件

import net.micode.notes.R; // 导入资源类,用于访问资源

public class DropdownMenu { // 定义DropdownMenu类
    private Button mButton; // 弹出下拉菜单的按钮
    private PopupMenu mPopupMenu; // 弹出的下拉菜单
    private Menu mMenu; // 下拉菜单的项目集合

    /**
     * DropdownMenu的构造函数。
     *
     * @param context 上下文对象,通常是指Activity。
     * @param button  用于触发下拉菜单显示的按钮。
     * @param menuId  菜单资源ID,用于加载下拉菜单的项目。
     */
    public DropdownMenu(Context context, Button button, int menuId) {
        mButton = button; // 初始化按钮
        mButton.setBackgroundResource(R.drawable.dropdown_icon); // 设置按钮背景为下拉图标
        mPopupMenu = new PopupMenu(context, mButton); // 创建PopupMenu实例,锚点为按钮
        mMenu = mPopupMenu.getMenu(); // 获取菜单项的集合
        mPopupMenu.getMenuInflater().inflate(menuId, mMenu); // 加载菜单项
        // 设置按钮点击事件,点击后显示下拉菜单
        mButton.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                mPopupMenu.show(); // 显示下拉菜单
            }
        });
    }

    /**
     * 设置下拉菜单项的点击事件监听器。
     *
     * @param listener PopupMenu的OnMenuItemClickListener,用于监听菜单项的点击事件。
     */
    public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) {
        if (mPopupMenu != null) {
            mPopupMenu.setOnMenuItemClickListener(listener); // 设置菜单项点击监听器
        }
    }

    /**
     * 根据ID查找菜单项。
     *
     * @param id 菜单项的ID。
     * @return 返回找到的MenuItem对象,如果未找到则返回null。
     */
    public MenuItem findItem(int id) {
        return mMenu.findItem(id); // 根据ID查找菜单项
    }

    /**
     * 设置按钮的标题。
     *
     * @param title 按钮要显示的标题。
     */
    public void setTitle(CharSequence title) {
        mButton.setText(title); // 设置按钮显示的文本
    }
}

7 FoldersListAdapter

  1. 包声明package net.micode.notes.ui; 声明了该类所在的包。
  2. 导入语句:导入了一系列Java和Android相关的类,包括上下文、游标、视图、适配器、布局、文本显示等。
  3. 类声明public class FoldersListAdapter extends CursorAdapter { 声明了一个名为 FoldersListAdapter 的公共类,继承自 CursorAdapter
  4. 常量声明
    • public static final String[] PROJECTION = { ... }; 声明了一个公共静态最终的字符串数组 PROJECTION,用于查询时使用的列名。
    • public static final int ID_COLUMN = 0; 声明了一个公共静态最终的整数 ID_COLUMN,表示ID列的索引。
    • public static final int NAME_COLUMN = 1; 声明了一个公共静态最终的整数 NAME_COLUMN,表示名称列的索引。
  5. 构造函数
    • public FoldersListAdapter(Context context, Cursor c) { ... } 声明了类的构造函数,接受上下文和数据源游标作为参数,并调用父类构造函数。
  6. 方法
    • @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { ... } 重写了 newView 方法,用于创建新的列表项视图,返回 FolderListItem 实例。
    • @Override public void bindView(View view, Context context, Cursor cursor) { ... } 重写了 bindView 方法,用于绑定数据到已有的视图上,根据文件夹ID设置文件夹名称。
    • public String getFolderName(Context context, int position) { ... } 声明了一个公共方法 getFolderName,用于根据位置获取文件夹名称。
  7. 内部类
    • private class FolderListItem extends LinearLayout { ... } 声明了一个私有内部类 FolderListItem,继承自 LinearLayout,用于表示文件夹列表中的一个项的视图。
    • public FolderListItem(Context context) { ... } 声明了 FolderListItem 的构造函数,加载布局文件并初始化文件夹名称的文本视图。
    • public void bind(String name) { ... } 声明了一个公共方法 bind,用于绑定文件夹名称到视图上。
/*
 * FoldersListAdapter 类
 * 用于管理和适配文件夹列表的适配器,继承自 CursorAdapter。
 */

package net.micode.notes.ui; // 定义包名

// 导入相关类
import android.content.Context; // 导入Context类,用于获取上下文
import android.database.Cursor; // 导入Cursor类,用于数据源游标
import android.view.View; // 导入View类,用于视图
import android.view.ViewGroup; // 导入ViewGroup类,用于视图的父容器
import android.widget.CursorAdapter; // 导入CursorAdapter类,用于适配器
import android.widget.LinearLayout; // 导入LinearLayout类,用于布局
import android.widget.TextView; // 导入TextView类,用于文本显示

import net.micode.notes.R; // 导入资源类,用于访问资源
import net.micode.notes.data.Notes; // 导入Notes类,用于数据操作
import net.micode.notes.data.Notes.NoteColumns; // 导入NoteColumns类,用于数据列名

public class FoldersListAdapter extends CursorAdapter { // 定义FoldersListAdapter类,继承自CursorAdapter
    // 查询时使用的列名数组
    public static final String[] PROJECTION = {
            NoteColumns.ID, // 文件夹ID列
            NoteColumns.SNIPPET // 文件夹名称列
    };

    // 列名数组中的索引常量
    public static final int ID_COLUMN = 0; // ID列索引
    public static final int NAME_COLUMN = 1; // 名称列索引

    /*
     * 构造函数
     * @param context 上下文对象,通常指Activity或Application对象。
     * @param c 数据源游标Cursor。
     */
    public FoldersListAdapter(Context context, Cursor c) {
        super(context, c); // 调用父类构造函数
    }

    /*
     * 创建新的列表项View
     * @param context 上下文对象。
     * @param cursor 当前数据项的游标。
     * @param parent 视图的父容器。
     * @return 返回新创建的列表项View。
     */
    @Override
    public View newView(Context context, Cursor cursor, ViewGroup parent) {
        return new FolderListItem(context); // 返回新的FolderListItem实例
    }

    /*
     * 绑定数据到已有的View上
     * @param view 要绑定数据的视图。
     * @param context 上下文对象。
     * @param cursor 当前数据项的游标。
     */
    @Override
    public void bindView(View view, Context context, Cursor cursor) {
        if (view instanceof FolderListItem) { // 判断视图是否为FolderListItem类型
            // 根据文件夹ID判断是根文件夹还是普通文件夹,并设置文件夹名称
            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 内部类
     * 用于表示文件夹列表中的一个项的视图类。
     */
    private class FolderListItem extends LinearLayout { // 定义FolderListItem内部类,继承自LinearLayout
        private TextView mName; // 文件夹名称的文本视图

        /*
         * 构造函数
         * @param context 上下文对象。
         */
        public FolderListItem(Context context) {
            super(context); // 调用父类构造函数
            // 加载布局文件,并将自己作为根视图
            inflate(context, R.layout.folder_list_item, this);
            mName = (TextView) findViewById(R.id.tv_folder_name); // 获取文件夹名称的视图
        }

        /*
         * 绑定数据到视图上
         * @param name 要显示的文件夹名称。
         */
        public void bind(String name) {
            mName.setText(name); // 设置文件夹名称
        }
    }

}

8 NoteEditActivity

package net.micode.notes.ui;

import android.app.Activity;
import android.app.AlarmManager;
import android.app.AlertDialog;
import android.app.PendingIntent;
import android.app.SearchManager;
import android.appwidget.AppWidgetManager;
import android.content.ContentUris;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Paint;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.text.style.BackgroundColorSpan;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.WindowManager;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.EditText;
import android.widget.ImageView;
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.data.Notes.TextNote;
import net.micode.notes.model.WorkingNote;
import net.micode.notes.model.WorkingNote.NoteSettingChangedListener;
import net.micode.notes.tool.DataUtils;
import net.micode.notes.tool.ResourceParser;
import net.micode.notes.tool.ResourceParser.TextAppearanceResources;
import net.micode.notes.ui.DateTimePickerDialog.OnDateTimeSetListener;
import net.micode.notes.ui.NoteEditText.OnTextViewChangeListener;
import net.micode.notes.widget.NoteWidgetProvider_2x;
import net.micode.notes.widget.NoteWidgetProvider_4x;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


public class NoteEditActivity extends Activity implements OnClickListener,
        NoteSettingChangedListener, OnTextViewChangeListener {
    /**
     * 头部视图的ViewHolder类,用于存储头部视图中的UI组件引用。
     */
    private class HeadViewHolder {
        public TextView tvModified; // 显示修改日期的文本视图
        public ImageView ivAlertIcon; // 提示图标图像视图
        public TextView tvAlertDate; // 显示提醒日期的文本视图
        public ImageView ibSetBgColor; // 设置背景颜色的图像视图
    }

    /**
     * 背景选择按钮与其对应选中状态图标的映射。
     */
    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);
        sBgSelectorBtnsMap.put(R.id.iv_bg_blue, ResourceParser.BLUE);
        sBgSelectorBtnsMap.put(R.id.iv_bg_green, ResourceParser.GREEN);
        sBgSelectorBtnsMap.put(R.id.iv_bg_white, ResourceParser.WHITE);
    }

    /**
     * 背景选择按钮选中状态与其对应图标的映射。
     */
    private static final Map<Integer, Integer> sBgSelectorSelectionMap = new HashMap<Integer, Integer>();

    static {
        // 初始化背景选择按钮选中状态映射
        sBgSelectorSelectionMap.put(ResourceParser.YELLOW, R.id.iv_bg_yellow_select);
        sBgSelectorSelectionMap.put(ResourceParser.RED, R.id.iv_bg_red_select);
        sBgSelectorSelectionMap.put(ResourceParser.BLUE, R.id.iv_bg_blue_select);
        sBgSelectorSelectionMap.put(ResourceParser.GREEN, R.id.iv_bg_green_select);
        sBgSelectorSelectionMap.put(ResourceParser.WHITE, R.id.iv_bg_white_select);
    }

    /**
     * 字号选择按钮与其对应字体大小的映射。
     */
    private static final Map<Integer, Integer> sFontSizeBtnsMap = new HashMap<Integer, Integer>();

    static {
        // 初始化字号选择按钮映射
        sFontSizeBtnsMap.put(R.id.ll_font_large, ResourceParser.TEXT_LARGE);
        sFontSizeBtnsMap.put(R.id.ll_font_small, ResourceParser.TEXT_SMALL);
        sFontSizeBtnsMap.put(R.id.ll_font_normal, ResourceParser.TEXT_MEDIUM);
        sFontSizeBtnsMap.put(R.id.ll_font_super, ResourceParser.TEXT_SUPER);
    }

    /**
     * 字号选择按钮选中状态与其对应图标的映射。
     */
    private static final Map<Integer, Integer> sFontSelectorSelectionMap = new HashMap<Integer, Integer>();

    static {
        // 初始化字号选择按钮选中状态映射
        sFontSelectorSelectionMap.put(ResourceParser.TEXT_LARGE, R.id.iv_large_select);
        sFontSelectorSelectionMap.put(ResourceParser.TEXT_SMALL, R.id.iv_small_select);
        sFontSelectorSelectionMap.put(ResourceParser.TEXT_MEDIUM, R.id.iv_medium_select);
        sFontSelectorSelectionMap.put(ResourceParser.TEXT_SUPER, R.id.iv_super_select);
    }

    private static final String TAG = "NoteEditActivity"; // 日志标签

    private HeadViewHolder mNoteHeaderHolder; // 头部视图的ViewHolder

    private View mHeadViewPanel; // 头部视图面板

    private View mNoteBgColorSelector; // 笔记背景颜色选择器

    private View mFontSizeSelector; // 字号选择器

    private EditText mNoteEditor; // 笔记编辑器

    private View mNoteEditorPanel; // 笔记编辑器面板

    private WorkingNote mWorkingNote; // 当前正在编辑的笔记

    private SharedPreferences mSharedPrefs; // 共享偏好设置
    private int mFontSizeId; // 当前选中的字体大小资源ID

    private static final String PREFERENCE_FONT_SIZE = "pref_font_size"; // 字体大小偏好设置键

    private static final int SHORTCUT_ICON_TITLE_MAX_LEN = 10; // 快捷图标标题的最大长度

    public static final String TAG_CHECKED = String.valueOf('\u221A'); // 标记为已检查的字符串
    public static final String TAG_UNCHECKED = String.valueOf('\u25A1'); // 标记为未检查的字符串

    private LinearLayout mEditTextList; // 编辑文本列表

    private String mUserQuery; // 用户查询字符串
    private Pattern mPattern; // 正则表达式模式


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.setContentView(R.layout.note_edit); // 设置活动的视图布局

        // 检查实例状态是否被保存,如果未保存且初始化活动状态失败,则结束该活动
        if (savedInstanceState == null && !initActivityState(getIntent())) {
            finish();
            return;
        }
        initResources(); // 初始化资源
    }

    /**
     * 当活动被系统销毁后,为了恢复之前的状态,此方法会被调用。
     * 主要用于处理活动重新创建时的数据恢复。
     *
     * @param savedInstanceState 包含之前保存状态的Bundle,如果活动之前保存了状态,这里会传入非空的Bundle。
     */
    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
        // 检查是否有保存的状态并且包含必要的UID信息,用于恢复活动状态
        if (savedInstanceState != null && savedInstanceState.containsKey(Intent.EXTRA_UID)) {
            Intent intent = new Intent(Intent.ACTION_VIEW);
            intent.putExtra(Intent.EXTRA_UID, savedInstanceState.getLong(Intent.EXTRA_UID));
            // 使用intent尝试恢复活动状态,如果失败则结束该活动
            if (!initActivityState(intent)) {
                finish();
                return;
            }
            Log.d(TAG, "Restoring from killed activity"); // 日志记录,表示活动状态正在从被杀死的状态恢复
        }
    }


    /**
     * 初始化活动状态,根据传入的Intent确定是查看笔记、新建笔记还是编辑笔记。
     *
     * @param intent 传入的Intent,包含了动作类型和相关数据。
     * @return boolean 返回true表示成功初始化活动状态,false表示初始化失败。
     */
    private boolean initActivityState(Intent intent) {
        // 如果用户指定的是查看笔记的动作但未提供笔记ID,则跳转到笔记列表活动
        mWorkingNote = null;
        if (TextUtils.equals(Intent.ACTION_VIEW, intent.getAction())) {
            long noteId = intent.getLongExtra(Intent.EXTRA_UID, 0);
            mUserQuery = "";

            // 从搜索结果开始
            if (intent.hasExtra(SearchManager.EXTRA_DATA_KEY)) {
                noteId = Long.parseLong(intent.getStringExtra(SearchManager.EXTRA_DATA_KEY));
                mUserQuery = intent.getStringExtra(SearchManager.USER_QUERY);
            }

            // 检查指定的笔记在数据库中是否可见
            if (!DataUtils.visibleInNoteDatabase(getContentResolver(), noteId, Notes.TYPE_NOTE)) {
                Intent jump = new Intent(this, NotesListActivity.class);
                startActivity(jump);
                showToast(R.string.error_note_not_exist);
                finish();
                return false;
            } else {
                // 加载指定ID的笔记
                mWorkingNote = WorkingNote.load(this, noteId);
                if (mWorkingNote == null) {
                    Log.e(TAG, "load note failed with note id" + noteId);
                    finish();
                    return false;
                }
            }
            // 隐藏软键盘
            getWindow().setSoftInputMode(
                    WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN
                            | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
        } else if (TextUtils.equals(Intent.ACTION_INSERT_OR_EDIT, intent.getAction())) {
            // 处理新建或编辑笔记的情况
            long folderId = intent.getLongExtra(Notes.INTENT_EXTRA_FOLDER_ID, 0);
            int widgetId = intent.getIntExtra(Notes.INTENT_EXTRA_WIDGET_ID,
                    AppWidgetManager.INVALID_APPWIDGET_ID);
            int widgetType = intent.getIntExtra(Notes.INTENT_EXTRA_WIDGET_TYPE,
                    Notes.TYPE_WIDGET_INVALIDE);
            int bgResId = intent.getIntExtra(Notes.INTENT_EXTRA_BACKGROUND_ID,
                    ResourceParser.getDefaultBgId(this));

            // 解析通话记录笔记
            String phoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER);
            long callDate = intent.getLongExtra(Notes.INTENT_EXTRA_CALL_DATE, 0);
            if (callDate != 0 && phoneNumber != null) {
                // 根据电话号码和通话日期尝试获取已有笔记ID
                if (TextUtils.isEmpty(phoneNumber)) {
                    Log.w(TAG, "The call record number is null");
                }
                long noteId = 0;
                if ((noteId = DataUtils.getNoteIdByPhoneNumberAndCallDate(getContentResolver(),
                        phoneNumber, callDate)) > 0) {
                    // 加载该笔记
                    mWorkingNote = WorkingNote.load(this, noteId);
                    if (mWorkingNote == null) {
                        Log.e(TAG, "load call note failed with note id" + noteId);
                        finish();
                        return false;
                    }
                } else {
                    // 创建新的通话记录笔记
                    mWorkingNote = WorkingNote.createEmptyNote(this, folderId, widgetId,
                            widgetType, bgResId);
                    mWorkingNote.convertToCallNote(phoneNumber, callDate);
                }
            } else {
                // 创建普通空笔记
                mWorkingNote = WorkingNote.createEmptyNote(this, folderId, widgetId, widgetType,
                        bgResId);
            }

            // 显示软键盘
            getWindow().setSoftInputMode(
                    WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
                            | WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
        } else {
            // 不支持的Intent动作
            Log.e(TAG, "Intent not specified action, should not support");
            finish();
            return false;
        }
        // 设置笔记状态改变的监听器
        mWorkingNote.setOnSettingStatusChangedListener(this);
        return true;
    }


    /**
     * 当Activity恢复到前台时调用。
     * 主要负责初始化笔记界面。
     */
    @Override
    protected void onResume() {
        super.onResume();
        initNoteScreen();
    }

    /**
     * 初始化笔记界面的函数。
     * 该函数设置笔记编辑器的外观,根据笔记类型切换到相应的模式,设置背景和头部信息,
     * 以及处理提醒头部的显示。
     */
    private void initNoteScreen() {
        // 设置编辑器的文本外观
        mNoteEditor.setTextAppearance(this, TextAppearanceResources
                .getTexAppearanceResource(mFontSizeId));
        // 根据当前笔记的类型,切换到列表模式或高亮查询结果模式
        if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) {
            switchToListMode(mWorkingNote.getContent());
        } else {
            mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery));
            mNoteEditor.setSelection(mNoteEditor.getText().length());
        }
        // 隐藏所有背景选择器
        for (Integer id : sBgSelectorSelectionMap.keySet()) {
            findViewById(sBgSelectorSelectionMap.get(id)).setVisibility(View.GONE);
        }
        // 设置标题和编辑区域的背景
        mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId());
        mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId());

        // 设置修改时间
        mNoteHeaderHolder.tvModified.setText(DateUtils.formatDateTime(this,
                mWorkingNote.getModifiedDate(), DateUtils.FORMAT_SHOW_DATE
                        | DateUtils.FORMAT_NUMERIC_DATE | DateUtils.FORMAT_SHOW_TIME
                        | DateUtils.FORMAT_SHOW_YEAR));

        // 显示提醒头部信息(当前禁用,因DateTimePicker未准备好)
        showAlertHeader();
    }

    /**
     * 显示提醒头部信息的方法。
     * 如果当前笔记设置了提醒,该方法将根据提醒时间显示相对的时间或者过期信息。
     */
    private void showAlertHeader() {
        // 处理提醒显示
        if (mWorkingNote.hasClockAlert()) {
            long time = System.currentTimeMillis();
            // 如果提醒时间已过,显示提醒已过期的信息
            if (time > mWorkingNote.getAlertDate()) {
                mNoteHeaderHolder.tvAlertDate.setText(R.string.note_alert_expired);
            } else {
                // 否则,显示相对时间
                mNoteHeaderHolder.tvAlertDate.setText(DateUtils.getRelativeTimeSpanString(
                        mWorkingNote.getAlertDate(), time, DateUtils.MINUTE_IN_MILLIS));
            }
            // 设置提醒视图可见
            mNoteHeaderHolder.tvAlertDate.setVisibility(View.VISIBLE);
            mNoteHeaderHolder.ivAlertIcon.setVisibility(View.VISIBLE);
        } else {
            // 如果没有设置提醒,隐藏提醒视图
            mNoteHeaderHolder.tvAlertDate.setVisibility(View.GONE);
            mNoteHeaderHolder.ivAlertIcon.setVisibility(View.GONE);
        }
    }

    /**
     * 当Activity接收到新的Intent时调用。
     * 用于根据新的Intent重新初始化Activity状态。
     *
     * @param intent 新的Intent
     */
    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        initActivityState(intent);
    }

    /**
     * 保存Activity状态时调用。
     * 用于保存当前编辑的笔记,如果该笔记还未保存到数据库,则首先保存它。
     * 并且保存当前笔记的ID到Bundle中。
     *
     * @param outState 用于保存Activity状态的Bundle
     */
    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        // 如果当前编辑的笔记不存在于数据库中,即还未保存,先保存它
        if (!mWorkingNote.existInDatabase()) {
            saveNote();
        }
        // 保存笔记ID
        outState.putLong(Intent.EXTRA_UID, mWorkingNote.getNoteId());
        Log.d(TAG, "Save working note id: " + mWorkingNote.getNoteId() + " onSaveInstanceState");
    }


    /**
     * 分发触摸事件。如果触摸事件不在指定视图范围内,则隐藏该视图。
     *
     * @param ev 触摸事件
     * @return 如果事件被消费则返回true,否则返回false
     */
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        // 如果背景颜色选择器可见且不在触摸范围内,则隐藏它并消费事件
        if (mNoteBgColorSelector.getVisibility() == View.VISIBLE
                && !inRangeOfView(mNoteBgColorSelector, ev)) {
            mNoteBgColorSelector.setVisibility(View.GONE);
            return true;
        }

        // 如果字体大小选择器可见且不在触摸范围内,则隐藏它并消费事件
        if (mFontSizeSelector.getVisibility() == View.VISIBLE
                && !inRangeOfView(mFontSizeSelector, ev)) {
            mFontSizeSelector.setVisibility(View.GONE);
            return true;
        }
        // 如果上述条件都不满足,则将事件传递给父类处理
        return super.dispatchTouchEvent(ev);
    }

    /**
     * 判断触摸点是否在指定视图范围内。
     *
     * @param view 视图
     * @param ev   触摸事件
     * @return 如果触摸点在视图范围内则返回true,否则返回false
     */
    private boolean inRangeOfView(View view, MotionEvent ev) {
        int[] location = new int[2];
        view.getLocationOnScreen(location);
        int x = location[0];
        int y = location[1];
        // 判断触摸点是否在视图外
        if (ev.getX() < x
                || ev.getX() > (x + view.getWidth())
                || ev.getY() < y
                || ev.getY() > (y + view.getHeight())) {
            return false;
        }
        return true;
    }

    /**
     * 初始化资源,包括视图和偏好设置。
     */
    private void initResources() {
        // 初始化头部视图和相关组件
        mHeadViewPanel = findViewById(R.id.note_title);
        mNoteHeaderHolder = new HeadViewHolder();
        mNoteHeaderHolder.tvModified = (TextView) findViewById(R.id.tv_modified_date);
        mNoteHeaderHolder.ivAlertIcon = (ImageView) findViewById(R.id.iv_alert_icon);
        mNoteHeaderHolder.tvAlertDate = (TextView) findViewById(R.id.tv_alert_date);
        mNoteHeaderHolder.ibSetBgColor = (ImageView) findViewById(R.id.btn_set_bg_color);
        mNoteHeaderHolder.ibSetBgColor.setOnClickListener(this);
        // 初始化编辑器和相关组件
        mNoteEditor = (EditText) findViewById(R.id.note_edit_view);
        mNoteEditorPanel = findViewById(R.id.sv_note_edit);
        mNoteBgColorSelector = findViewById(R.id.note_bg_color_selector);
        // 设置背景选择器按钮点击监听器
        for (int id : sBgSelectorBtnsMap.keySet()) {
            ImageView iv = (ImageView) findViewById(id);
            iv.setOnClickListener(this);
        }

        mFontSizeSelector = findViewById(R.id.font_size_selector);
        // 设置字体大小选择器按钮点击监听器
        for (int id : sFontSizeBtnsMap.keySet()) {
            View view = findViewById(id);
            view.setOnClickListener(this);
        }
        ;
        // 从偏好设置中读取字体大小
        mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
        mFontSizeId = mSharedPrefs.getInt(PREFERENCE_FONT_SIZE, ResourceParser.BG_DEFAULT_FONT_SIZE);
        // 修复存储在偏好设置中的字体大小资源ID的错误
        if (mFontSizeId >= TextAppearanceResources.getResourcesSize()) {
            mFontSizeId = ResourceParser.BG_DEFAULT_FONT_SIZE;
        }
        // 初始化编辑列表
        mEditTextList = (LinearLayout) findViewById(R.id.note_edit_list);
    }

    /**
     * 暂停时保存笔记数据并清除设置状态。
     */
    @Override
    protected void onPause() {
        super.onPause();
        // 保存笔记数据
        if (saveNote()) {
            Log.d(TAG, "Note data was saved with length:" + mWorkingNote.getContent().length());
        }
        // 清除设置状态
        clearSettingState();
    }

    /**
     * 更新小部件显示。
     */
    private void updateWidget() {
        Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
        // 根据小部件类型设置对应的类
        if (mWorkingNote.getWidgetType() == Notes.TYPE_WIDGET_2X) {
            intent.setClass(this, NoteWidgetProvider_2x.class);
        } else if (mWorkingNote.getWidgetType() == Notes.TYPE_WIDGET_4X) {
            intent.setClass(this, NoteWidgetProvider_4x.class);
        } else {
            Log.e(TAG, "Unspported widget type");
            return;
        }

        // 设置小部件ID
        intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[]{
                mWorkingNote.getWidgetId()
        });

        // 发送广播更新小部件
        sendBroadcast(intent);
        // 设置结果为OK
        setResult(RESULT_OK, intent);
    }


    /**
     * 点击事件的处理函数。
     *
     * @param v 被点击的视图对象。
     */
    public void onClick(View v) {
        int id = v.getId();
        // 如果点击的是设置背景颜色的按钮
        if (id == R.id.btn_set_bg_color) {
            mNoteBgColorSelector.setVisibility(View.VISIBLE);
            // 根据当前笔记的背景颜色,设置对应的色板按钮可见
            findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility(
                    View.VISIBLE);
        } else if (sBgSelectorBtnsMap.containsKey(id)) {
            // 如果点击的是背景色板上的颜色
            findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility(
                    View.GONE); // 隐藏当前选择的背景颜色
            mWorkingNote.setBgColorId(sBgSelectorBtnsMap.get(id)); // 更新笔记的背景颜色
            mNoteBgColorSelector.setVisibility(View.GONE); // 隐藏背景颜色选择器
        } else if (sFontSizeBtnsMap.containsKey(id)) {
            // 如果点击的是字体大小按钮
            findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.GONE); // 隐藏当前选择的字体大小
            mFontSizeId = sFontSizeBtnsMap.get(id); // 更新选择的字体大小
            mSharedPrefs.edit().putInt(PREFERENCE_FONT_SIZE, mFontSizeId).commit(); // 保存选择的字体大小到SharedPreferences
            findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.VISIBLE); // 设置新选择的字体大小按钮为可见
            // 根据当前笔记是否为清单模式,进行相应的文本更新
            if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) {
                getWorkingText();
                switchToListMode(mWorkingNote.getContent());
            } else {
                mNoteEditor.setTextAppearance(this,
                        TextAppearanceResources.getTexAppearanceResource(mFontSizeId));
            }
            mFontSizeSelector.setVisibility(View.GONE); // 隐藏字体大小选择器
        }
    }

    /**
     * 按下返回键时的处理函数。
     */
    @Override
    public void onBackPressed() {
        // 尝试清除设置状态,如果成功,则不执行保存笔记操作
        if (clearSettingState()) {
            return;
        }
        // 保存笔记并执行父类的onBackPressed()方法(结束当前Activity)
        saveNote();
        super.onBackPressed();
    }

    /**
     * 尝试清除设置状态(背景颜色选择或字体大小选择)。
     *
     * @return 如果成功清除设置状态,返回true;否则返回false。
     */
    private boolean clearSettingState() {
        // 如果背景颜色选择器可见,则隐藏它并返回true
        if (mNoteBgColorSelector.getVisibility() == View.VISIBLE) {
            mNoteBgColorSelector.setVisibility(View.GONE);
            return true;
        } else if (mFontSizeSelector.getVisibility() == View.VISIBLE) { // 如果字体大小选择器可见,则隐藏它并返回true
            mFontSizeSelector.setVisibility(View.GONE);
            return true;
        }
        return false; // 没有可见的设置状态需要清除,返回false
    }

    /**
     * 当背景颜色发生变化时的处理函数。
     */
    public void onBackgroundColorChanged() {
        // 根据当前笔记的背景颜色,设置对应的色板按钮可见,并更新编辑器及标题栏的背景颜色
        findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility(
                View.VISIBLE);
        mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId());
        mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId());
    }

    /**
     * 准备选项菜单的函数。
     *
     * @param menu 选项菜单对象。
     * @return 总是返回true。
     */
    @Override
    public boolean onPrepareOptionsMenu(Menu menu) {
        // 如果Activity正在结束,则不进行任何操作
        if (isFinishing()) {
            return true;
        }
        // 尝试清除设置状态
        clearSettingState();
        menu.clear(); // 清除菜单项
        // 根据笔记所属的文件夹,加载不同的菜单资源
        if (mWorkingNote.getFolderId() == Notes.ID_CALL_RECORD_FOLDER) {
            getMenuInflater().inflate(R.menu.call_note_edit, menu);
        } else {
            getMenuInflater().inflate(R.menu.note_edit, menu);
        }
        // 根据当前笔记的清单模式,更新“清单模式”菜单项的标题
        if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) {
            menu.findItem(R.id.menu_list_mode).setTitle(R.string.menu_normal_mode);
        } else {
            menu.findItem(R.id.menu_list_mode).setTitle(R.string.menu_list_mode);
        }
        // 根据笔记是否有提醒,更新“删除提醒”菜单项的可见性
        if (mWorkingNote.hasClockAlert()) {
            menu.findItem(R.id.menu_alert).setVisible(false);
        } else {
            menu.findItem(R.id.menu_delete_remind).setVisible(false);
        }
        return true;
    }


    /**
     * 处理选项菜单项的选择事件。
     *
     * @param item 选中的菜单项
     * @return 总是返回true,表示事件已处理。
     */
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.menu_new_note:
                // 创建新笔记
                createNewNote();
                break;
            case R.id.menu_delete:
                // 显示删除笔记的确认对话框
                AlertDialog.Builder builder = new AlertDialog.Builder(this);
                builder.setTitle(getString(R.string.alert_title_delete));
                builder.setIcon(android.R.drawable.ic_dialog_alert);
                builder.setMessage(getString(R.string.alert_message_delete_note));
                builder.setPositiveButton(android.R.string.ok,
                        new DialogInterface.OnClickListener() {
                            public void onClick(DialogInterface dialog, int which) {
                                // 确认删除当前笔记并结束当前活动
                                deleteCurrentNote();
                                finish();
                            }
                        });
                builder.setNegativeButton(android.R.string.cancel, null);
                builder.show();
                break;
            case R.id.menu_font_size:
                // 显示字体大小选择器
                mFontSizeSelector.setVisibility(View.VISIBLE);
                findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.VISIBLE);
                break;
            case R.id.menu_list_mode:
                // 切换笔记的列表模式
                mWorkingNote.setCheckListMode(mWorkingNote.getCheckListMode() == 0 ?
                        TextNote.MODE_CHECK_LIST : 0);
                break;
            case R.id.menu_share:
                // 获取当前编辑的笔记内容并分享
                getWorkingText();
                sendTo(this, mWorkingNote.getContent());
                break;
            case R.id.menu_send_to_desktop:
                // 将笔记发送到桌面
                sendToDesktop();
                break;
            case R.id.menu_alert:
                // 设置提醒
                setReminder();
                break;
            case R.id.menu_delete_remind:
                // 删除提醒设置
                mWorkingNote.setAlertDate(0, false);
                break;
            default:
                break;
        }
        return true;
    }

    /**
     * 弹出日期时间选择器,用于设置提醒时间。
     */
    private void setReminder() {
        DateTimePickerDialog d = new DateTimePickerDialog(this, System.currentTimeMillis());
        d.setOnDateTimeSetListener(new OnDateTimeSetListener() {
            public void OnDateTimeSet(AlertDialog dialog, long date) {
                // 用户设定时间后,设置提醒
                mWorkingNote.setAlertDate(date, true);
            }
        });
        d.show();
    }

    /**
     * 分享笔记到支持 {@link Intent#ACTION_SEND} 操作和 {@text/plain} 类型的应用。
     *
     * @param context 上下文
     * @param info    要分享的信息
     */
    private void sendTo(Context context, String info) {
        Intent intent = new Intent(Intent.ACTION_SEND);
        intent.putExtra(Intent.EXTRA_TEXT, info);
        intent.setType("text/plain");
        context.startActivity(intent);
    }

    /**
     * 首先保存当前正在编辑的笔记,然后启动一个新的NoteEditActivity用于创建新笔记。
     */
    private void createNewNote() {
        // 首先保存当前笔记
        saveNote();

        // 安全地开始一个新的NoteEditActivity
        finish();
        Intent intent = new Intent(this, NoteEditActivity.class);
        intent.setAction(Intent.ACTION_INSERT_OR_EDIT);
        intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mWorkingNote.getFolderId());
        startActivity(intent);
    }


    /**
     * 删除当前正在编辑的笔记。
     * 如果笔记存在于数据库中,并且当前不是同步模式,将直接删除该笔记;
     * 如果处于同步模式,则将笔记移动到回收站文件夹。
     */
    private void deleteCurrentNote() {
        if (mWorkingNote.existInDatabase()) {
            HashSet<Long> ids = new HashSet<Long>();
            long id = mWorkingNote.getNoteId();
            if (id != Notes.ID_ROOT_FOLDER) {
                ids.add(id);
            } else {
                Log.d(TAG, "Wrong note id, should not happen");
            }
            if (!isSyncMode()) {
                // 非同步模式下直接删除笔记
                if (!DataUtils.batchDeleteNotes(getContentResolver(), ids)) {
                    Log.e(TAG, "Delete Note error");
                }
            } else {
                // 同步模式下将笔记移动到回收站
                if (!DataUtils.batchMoveToFolder(getContentResolver(), ids, Notes.ID_TRASH_FOLER)) {
                    Log.e(TAG, "Move notes to trash folder error, should not happens");
                }
            }
        }
        mWorkingNote.markDeleted(true);
    }

    /**
     * 判断当前是否为同步模式。
     * 同步模式是指在设置中配置了同步账户名。
     *
     * @return 如果配置了同步账户名返回true,否则返回false。
     */
    private boolean isSyncMode() {
        return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0;
    }

    /**
     * 处理时钟提醒变更事件。
     * 首先检查当前笔记是否已保存,未保存则先保存。
     * 如果笔记存在,根据set参数设置或取消提醒。
     * 如果笔记不存在(即无有效ID),记录错误并提示用户输入内容。
     *
     * @param date 提醒的日期时间戳
     * @param set  是否设置提醒
     */
    public void onClockAlertChanged(long date, boolean set) {
        if (!mWorkingNote.existInDatabase()) {
            saveNote();
        }
        if (mWorkingNote.getNoteId() > 0) {
            Intent intent = new Intent(this, AlarmReceiver.class);
            intent.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mWorkingNote.getNoteId()));
            PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0);
            AlarmManager alarmManager = ((AlarmManager) getSystemService(ALARM_SERVICE));
            showAlertHeader();
            if (!set) {
                alarmManager.cancel(pendingIntent);
            } else {
                alarmManager.set(AlarmManager.RTC_WAKEUP, date, pendingIntent);
            }
        } else {
            Log.e(TAG, "Clock alert setting error");
            showToast(R.string.error_note_empty_for_clock);
        }
    }

    /**
     * 更新小部件显示。
     */
    public void onWidgetChanged() {
        updateWidget();
    }

    /**
     * 当删除某个编辑框中的文本时的处理逻辑。
     * 重新设置后续编辑框的索引,并将删除的文本添加到前一个或当前编辑框中。
     *
     * @param index 被删除文本的编辑框索引
     * @param text  被删除的文本内容
     */
    public void onEditTextDelete(int index, String text) {
        int childCount = mEditTextList.getChildCount();
        if (childCount == 1) {
            return;
        }

        for (int i = index + 1; i < childCount; i++) {
            ((NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text))
                    .setIndex(i - 1);
        }

        mEditTextList.removeViewAt(index);
        NoteEditText edit = null;
        if (index == 0) {
            edit = (NoteEditText) mEditTextList.getChildAt(0).findViewById(
                    R.id.et_edit_text);
        } else {
            edit = (NoteEditText) mEditTextList.getChildAt(index - 1).findViewById(
                    R.id.et_edit_text);
        }
        int length = edit.length();
        edit.append(text);
        edit.requestFocus();
        edit.setSelection(length);
    }

    /**
     * 当在编辑框中按下“Enter”键时的处理逻辑。
     * 在列表中添加一个新的编辑框,并重新设置后续编辑框的索引。
     *
     * @param index 当前编辑框的索引
     * @param text  当前编辑框中的文本内容
     */
    public void onEditTextEnter(int index, String text) {
        if (index > mEditTextList.getChildCount()) {
            Log.e(TAG, "Index out of mEditTextList boundrary, should not happen");
        }

        View view = getListItem(text, index);
        mEditTextList.addView(view, index);
        NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text);
        edit.requestFocus();
        edit.setSelection(0);
        for (int i = index + 1; i < mEditTextList.getChildCount(); i++) {
            ((NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text))
                    .setIndex(i);
        }
    }


    /**
     * 切换到列表模式。
     * 将文本分割成多行,并为每行创建一个列表项,展示在编辑文本列表中。
     *
     * @param text 要转换成列表模式的文本,每行代表一个列表项。
     */
    private void switchToListMode(String text) {
        // 清空当前的视图
        mEditTextList.removeAllViews();
        // 使用换行符分割文本,创建列表项
        String[] items = text.split("\n");
        int index = 0;
        for (String item : items) {
            // 忽略空行
            if (!TextUtils.isEmpty(item)) {
                mEditTextList.addView(getListItem(item, index));
                index++;
            }
        }
        // 添加一个空的列表项作为占位符
        mEditTextList.addView(getListItem("", index));
        // 请求焦点以便于编辑
        mEditTextList.getChildAt(index).findViewById(R.id.et_edit_text).requestFocus();

        // 隐藏编辑器,显示列表
        mNoteEditor.setVisibility(View.GONE);
        mEditTextList.setVisibility(View.VISIBLE);
    }

    /**
     * 高亮显示查询结果。
     * 在给定的文本中,根据用户查询字符串高亮显示匹配的部分。
     *
     * @param fullText  完整的文本。
     * @param userQuery 用户的查询字符串。
     * @return 包含高亮显示的文本的Spannable对象。
     */
    private Spannable getHighlightQueryResult(String fullText, String userQuery) {
        SpannableString spannable = new SpannableString(fullText == null ? "" : fullText);
        // 如果有查询字符串,则进行高亮处理
        if (!TextUtils.isEmpty(userQuery)) {
            mPattern = Pattern.compile(userQuery);
            Matcher m = mPattern.matcher(fullText);
            int start = 0;
            while (m.find(start)) {
                // 设置高亮背景
                spannable.setSpan(
                        new BackgroundColorSpan(this.getResources().getColor(
                                R.color.user_query_highlight)), m.start(), m.end(),
                        Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
                start = m.end();
            }
        }
        return spannable;
    }

    /**
     * 创建列表项视图。
     * 为列表模式创建并配置一个包含文本编辑框和复选框的视图。
     *
     * @param item  列表项的文本内容。
     * @param index 列表项的索引。
     * @return 配置好的列表项视图。
     */
    private View getListItem(String item, int index) {
        // 加载列表项布局
        View view = LayoutInflater.from(this).inflate(R.layout.note_edit_list_item, null);
        final NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text);
        // 设置文本样式
        edit.setTextAppearance(this, TextAppearanceResources.getTexAppearanceResource(mFontSizeId));
        CheckBox cb = ((CheckBox) view.findViewById(R.id.cb_edit_item));
        // 复选框的监听器,用于切换文本的划线状态
        cb.setOnCheckedChangeListener(new OnCheckedChangeListener() {
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                if (isChecked) {
                    edit.setPaintFlags(edit.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
                } else {
                    edit.setPaintFlags(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG);
                }
            }
        });

        // 根据文本前缀设置复选框状态和文本内容
        if (item.startsWith(TAG_CHECKED)) {
            cb.setChecked(true);
            edit.setPaintFlags(edit.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
            item = item.substring(TAG_CHECKED.length(), item.length()).trim();
        } else if (item.startsWith(TAG_UNCHECKED)) {
            cb.setChecked(false);
            edit.setPaintFlags(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG);
            item = item.substring(TAG_UNCHECKED.length(), item.length()).trim();
        }

        // 设置文本变化监听和索引
        edit.setOnTextViewChangeListener(this);
        edit.setIndex(index);
        // 设置带有查询结果高亮的文本
        edit.setText(getHighlightQueryResult(item, mUserQuery));
        return view;
    }

    /**
     * 根据文本内容是否为空,切换复选框的可见性。
     *
     * @param index   列表项索引。
     * @param hasText 列表项是否包含文本。
     */
    public void onTextChange(int index, boolean hasText) {
        if (index >= mEditTextList.getChildCount()) {
            Log.e(TAG, "Wrong index, should not happen");
            return;
        }
        // 根据文本内容决定复选框的可见性
        if (hasText) {
            mEditTextList.getChildAt(index).findViewById(R.id.cb_edit_item).setVisibility(View.VISIBLE);
        } else {
            mEditTextList.getChildAt(index).findViewById(R.id.cb_edit_item).setVisibility(View.GONE);
        }
    }

    /**
     * 在切换编辑模式和列表模式时更新UI。
     *
     * @param oldMode 旧的编辑模式。
     * @param newMode 新的编辑模式。
     */
    public void onCheckListModeChanged(int oldMode, int newMode) {
        // 切换到列表模式
        if (newMode == TextNote.MODE_CHECK_LIST) {
            switchToListMode(mNoteEditor.getText().toString());
        } else {
            // 切换回编辑模式
            if (!getWorkingText()) {
                mWorkingNote.setWorkingText(mWorkingNote.getContent().replace(TAG_UNCHECKED + " ",
                        ""));
            }
            mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery));
            mEditTextList.setVisibility(View.GONE);
            mNoteEditor.setVisibility(View.VISIBLE);
        }
    }

    /**
     * 根据当前列表项的选中状态,构建并返回工作文本。
     *
     * @return 是否有已选中的列表项。
     */
    private boolean getWorkingText() {
        boolean hasChecked = false;
        if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) {
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < mEditTextList.getChildCount(); i++) {
                View view = mEditTextList.getChildAt(i);
                NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text);
                // 构建带有选中状态前缀的文本
                if (!TextUtils.isEmpty(edit.getText())) {
                    if (((CheckBox) view.findViewById(R.id.cb_edit_item)).isChecked()) {
                        sb.append(TAG_CHECKED).append(" ").append(edit.getText()).append("\n");
                        hasChecked = true;
                    } else {
                        sb.append(TAG_UNCHECKED).append(" ").append(edit.getText()).append("\n");
                    }
                }
            }
            mWorkingNote.setWorkingText(sb.toString());
        } else {
            mWorkingNote.setWorkingText(mNoteEditor.getText().toString());
        }
        return hasChecked;
    }

    /**
     * 保存笔记。
     * 更新笔记内容并保存。
     *
     * @return 是否成功保存笔记。
     */
    private boolean saveNote() {
        getWorkingText();
        boolean saved = mWorkingNote.saveNote();
        if (saved) {
            // 设置结果为成功,以便外部调用者知道保存操作的状态
            setResult(RESULT_OK);
        }
        return saved;
    }


    /**
     * 将当前编辑的笔记发送到桌面。首先会检查当前编辑的笔记是否已存在于数据库中,
     * 如果不存在,则先保存。如果存在,会创建一个快捷方式放在桌面。
     */
    private void sendToDesktop() {
        // 检查当前编辑的笔记是否存在于数据库,若不存在则先保存
        if (!mWorkingNote.existInDatabase()) {
            saveNote();
        }

        // 如果笔记存在于数据库(有noteId),则创建快捷方式
        if (mWorkingNote.getNoteId() > 0) {
            Intent sender = new Intent();
            Intent shortcutIntent = new Intent(this, NoteEditActivity.class);
            shortcutIntent.setAction(Intent.ACTION_VIEW);
            shortcutIntent.putExtra(Intent.EXTRA_UID, mWorkingNote.getNoteId());
            sender.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
            sender.putExtra(Intent.EXTRA_SHORTCUT_NAME,
                    makeShortcutIconTitle(mWorkingNote.getContent()));
            sender.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
                    Intent.ShortcutIconResource.fromContext(this, R.drawable.icon_app));
            sender.putExtra("duplicate", true);
            sender.setAction("com.android.launcher.action.INSTALL_SHORTCUT");
            showToast(R.string.info_note_enter_desktop);
            sendBroadcast(sender);
        } else {
            // 如果用户未输入任何内容,无法创建快捷方式,提醒用户输入内容
            Log.e(TAG, "Send to desktop error");
            showToast(R.string.error_note_empty_for_send_to_desktop);
        }
    }

    /**
     * 根据笔记内容生成快捷方式的标题。移除内容中的已选和未选标签,并确保标题长度不超过上限。
     *
     * @param content 符合快捷方式图标标题要求的笔记内容
     * @return 标题字符串
     */
    private String makeShortcutIconTitle(String content) {
        content = content.replace(TAG_CHECKED, "");
        content = content.replace(TAG_UNCHECKED, "");
        return content.length() > SHORTCUT_ICON_TITLE_MAX_LEN ? content.substring(0,
                SHORTCUT_ICON_TITLE_MAX_LEN) : content;
    }

    /**
     * 显示一个Toast消息。
     *
     * @param resId 资源ID,指向要显示的字符串
     */
    private void showToast(int resId) {
        showToast(resId, Toast.LENGTH_SHORT);
    }

    /**
     * 显示一个指定时长的Toast消息。
     *
     * @param resId    资源ID,指向要显示的字符串
     * @param duration 显示时长,可以是Toast.LENGTH_SHORT或Toast.LENGTH_LONG
     */
    private void showToast(int resId, int duration) {
        Toast.makeText(this, resId, duration).show();
    }

}

9 NoteltemData

  1. 包声明package net.micode.notes.ui; 声明了该类所在的包。
  2. 导入语句:导入了一系列Java和Android相关的类,包括上下文、游标、字符串操作、联系人数据操作、笔记数据操作、数据工具操作等。
  3. 类声明public class NoteItemData { 声明了一个名为 NoteItemData 的公共类。
  4. 常量声明
    • static final String[] PROJECTION = new String[]{ ... }; 声明了一个静态最终的字符串数组 PROJECTION,用于查询时使用的列名。
    • private static final int ID_COLUMN = 0; 声明了一个私有静态最终的整数 ID_COLUMN,表示ID列的索引。
    • private static final int ALERTED_DATE_COLUMN = 1; 声明了一个私有静态最终的整数 ALERTED_DATE_COLUMN,表示提醒日期列的索引。
    • 其他列索引常量的声明类似。
  5. 成员变量
    • private long mId; 声明了一个私有长整型变量 mId,用于存储笔记ID。
    • private long mAlertDate; 声明了一个私有长整型变量 mAlertDate,用于存储提醒日期。
    • 其他成员变量的声明类似。
  6. 构造函数
    • public NoteItemData(Context context, Cursor cursor) { ... } 声明了类的构造函数,接受上下文和数据源游标作为参数,并从游标中提取各项数据赋值给成员变量。
  7. 方法
    • private void checkPostion(Cursor cursor) { ... } 声明了一个私有方法 checkPostion,用于根据当前游标位置更新笔记在列表中的位置状态。
    • public boolean isOneFollowingFolder() { ... } 声明了一个公共方法 isOneFollowingFolder,用于返回一个笔记跟随文件夹状态。
    • public boolean isMultiFollowingFolder() { ... } 声明了一个公共方法 isMultiFollowingFolder,用于返回多个笔记跟随文件夹状态。
    • 其他获取属性的方法类似。
  8. 静态方法
    • public static int getNoteType(Cursor cursor) { ... } 声明了一个公共静态方法 getNoteType,用于从游标中获取笔记的类型。
package net.micode.notes.ui; // 定义包名

import android.content.Context; // 导入Context类,用于获取上下文
import android.database.Cursor; // 导入Cursor类,用于数据源游标
import android.text.TextUtils; // 导入TextUtils类,用于字符串操作

import net.micode.notes.data.Contact; // 导入Contact类,用于联系人数据操作
import net.micode.notes.data.Notes; // 导入Notes类,用于笔记数据操作
import net.micode.notes.data.Notes.NoteColumns; // 导入NoteColumns类,用于笔记数据列名
import net.micode.notes.tool.DataUtils; // 导入DataUtils类,用于数据工具操作

/**
 * 代表一个笔记项的数据类,用于存储和管理笔记的各种信息。
 */
public class NoteItemData { // 定义NoteItemData类
    // 定义查询时要投影的列
    static final String[] PROJECTION = new String[]{
            NoteColumns.ID, // 笔记ID
            NoteColumns.ALERTED_DATE, // 提醒日期
            NoteColumns.BG_COLOR_ID, // 背景颜色ID
            NoteColumns.CREATED_DATE, // 创建日期
            NoteColumns.HAS_ATTACHMENT, // 是否有附件
            NoteColumns.MODIFIED_DATE, // 修改日期
            NoteColumns.NOTES_COUNT, // 笔记数量
            NoteColumns.PARENT_ID, // 父ID
            NoteColumns.SNIPPET, // 摘要
            NoteColumns.TYPE, // 类型
            NoteColumns.WIDGET_ID, // 小部件ID
            NoteColumns.WIDGET_TYPE, // 小部件类型
    };

    // 各列数据的索引
    private static final int ID_COLUMN = 0; // ID列索引
    private static final int ALERTED_DATE_COLUMN = 1; // 提醒日期列索引
    private static final int BG_COLOR_ID_COLUMN = 2; // 背景颜色ID列索引
    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; // 父ID列索引
    private static final int SNIPPET_COLUMN = 8; // 摘要列索引
    private static final int TYPE_COLUMN = 9; // 类型列索引
    private static final int WIDGET_ID_COLUMN = 10; // 小部件ID列索引
    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; // 是否为多个笔记跟随文件夹

    /**
     * 根据Cursor数据构造一个NoteItemData对象。
     *
     * @param context 上下文对象,用于访问应用全局功能。
     * @param cursor  包含笔记数据的Cursor对象。
     */
    public NoteItemData(Context context, Cursor cursor) {
        // 从Cursor中提取各项数据并赋值
        mId = cursor.getLong(ID_COLUMN); // 获取笔记ID
        mAlertDate = cursor.getLong(ALERTED_DATE_COLUMN); // 获取提醒日期
        mBgColorId = cursor.getInt(BG_COLOR_ID_COLUMN); // 获取背景颜色ID
        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); // 获取父ID
        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); // 获取小部件ID
        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位置,更新NoteItemData的状态信息(如是否为列表中的最后一个项目等)。
     *
     * @param cursor 包含笔记数据的Cursor对象。
     */
    private void checkPostion(Cursor cursor) {
        // 更新位置状态信息
        mIsLastItem = cursor.isLast(); // 判断是否为最后一个项目
        mIsFirstItem = cursor.isFirst(); // 判断是否为第一个项目
        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; // 设置一个笔记跟随文件夹状态
                    }
                }
                // 确保Cursor能够回到原来的位置
                if (!cursor.moveToNext()) { // 移动回原来的位置
                    throw new IllegalStateException("cursor move to previous but can't move back"); // 抛出异常
                }
            }
        }
    }

    // 以下为获取NoteItemData各项属性的方法

    public boolean isOneFollowingFolder() {
        return mIsOneNoteFollowingFolder; // 返回一个笔记跟随文件夹状态
    }

    public boolean isMultiFollowingFolder() {
        return mIsMultiNotesFollowingFolder; // 返回多个笔记跟随文件夹状态
    }

    public boolean isLast() {
        return mIsLastItem; // 返回是否为最后一个项目
    }

    public String getCallName() {
        return mName; // 返回联系人名称
    }

    public boolean isFirst() {
        return mIsFirstItem; // 返回是否为第一个项目
    }

    public boolean isSingle() {
        return mIsOnlyOneItem; // 返回是否为唯一一个项目
    }

    public long getId() {
        return mId; // 返回笔记ID
    }

    public long getAlertDate() {
        return mAlertDate; // 返回提醒日期
    }

    public long getCreatedDate() {
        return mCreatedDate; // 返回创建日期
    }

    public boolean hasAttachment() {
        return mHasAttachment; // 返回是否有附件
    }

    public long getModifiedDate() {
        return mModifiedDate; // 返回修改日期
    }

    public int getBgColorId() {
        return mBgColorId; // 返回背景颜色ID
    }

    public long getParentId() {
        return mParentId; // 返回父ID
    }

    public int getNotesCount() {
        return mNotesCount; // 返回笔记数量
    }

    public long getFolderId() {
        return mParentId; // 返回文件夹ID
    }

    public int getType() {
        return mType; // 返回类型
    }

    public int getWidgetType() {
        return mWidgetType; // 返回小部件类型
    }

    public int getWidgetId() {
        return mWidgetId; // 返回小部件ID
    }

    public String getSnippet() {
        return mSnippet; // 返回摘要
    }

    public boolean hasAlert() {
        return (mAlertDate > 0); // 返回是否有提醒
    }

    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); // 返回笔记类型
    }
}

10 NotesListActivity

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 ListEditState mState;

    // 后台查询处理器
    private BackgroundQueryHandler mBackgroundQueryHandler;

    // 笔记列表的适配器
    private NotesListAdapter mNotesListAdapter;

    // 笔记列表视图
    private ListView mNotesListView;

    // 添加新笔记的按钮
    private Button mAddNewNote;

    // 是否分发事件的标志
    private boolean mDispatch;

    // 触摸点的原始Y坐标
    private int mOriginY;

    // 分发事件时的Y坐标
    private int mDispatchY;

    // 标题栏文本视图
    private TextView mTitleBar;

    // 当前文件夹的ID
    private long mCurrentFolderId;

    // 内容解析器
    private ContentResolver mContentResolver;

    // 模式回调接口
    private ModeCallback mModeCallBack;

    // 日志标签
    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;

    /**
     * 在活动创建时调用,用于初始化资源和设置应用信息。
     *
     * @param savedInstanceState 如果活动之前被销毁,这参数包含之前的状态。如果活动没被销毁之前,这参数是null。
     */
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.note_list);
        initResources();

        // 用户首次使用时插入介绍信息
        setAppInfoFromRawRes();
    }

    /**
     * 处理从其他活动返回的结果。
     *
     * @param requestCode 启动其他活动时传入的请求代码。
     * @param resultCode  其他活动返回的结果代码。
     * @param data        其他活动返回的数据。
     */
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        // 如果返回结果为OK且请求代码为打开节点或新建节点,则刷新列表
        if (resultCode == RESULT_OK
                && (requestCode == REQUEST_CODE_OPEN_NODE || requestCode == REQUEST_CODE_NEW_NODE)) {
            mNotesListAdapter.changeCursor(null);
        } else {
            super.onActivityResult(requestCode, resultCode, data);
        }
    }


    /**
     * 从原始资源中设置应用信息。此方法会读取R.raw.introduction中的内容,
     * 并且只有当之前未添加介绍信息时,才将读取到的内容保存为一个工作笔记。
     */
    private void setAppInfoFromRawRes() {
        // 获取SharedPreferences实例
        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
        // 检查是否已经添加了介绍信息
        if (!sp.getBoolean(PREFERENCE_ADD_INTRODUCTION, false)) {
            StringBuilder sb = new StringBuilder();
            InputStream in = null;
            try {
                // 从资源中打开introduction文件
                in = getResources().openRawResource(R.raw.introduction);
                if (in != null) {
                    // 读取文件内容到StringBuilder
                    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 {
                // 确保InputStream被关闭
                if (in != null) {
                    try {
                        in.close();
                    } catch (IOException e) {
                        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;
            }
        }
    }

    /**
     * Activity启动时调用,开始异步查询笔记列表。
     */
    @Override
    protected void onStart() {
        super.onStart();
        startAsyncNotesListQuery();
    }

    /**
     * 初始化资源,包括ListView、适配器和其他UI组件。
     */
    private void initResources() {
        // 获取ContentResolver实例
        mContentResolver = this.getContentResolver();
        // 创建后台查询处理器
        mBackgroundQueryHandler = new BackgroundQueryHandler(this.getContentResolver());
        mCurrentFolderId = Notes.ID_ROOT_FOLDER;
        // 初始化ListView和相关监听器
        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);
        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);
        mState = ListEditState.NOTE_LIST;
        mModeCallBack = new ModeCallback();
    }


    /**
     * 用于处理列表的多选择模式和菜单点击事件的回调类。
     */
    private class ModeCallback implements ListView.MultiChoiceModeListener, OnMenuItemClickListener {
        private DropdownMenu mDropDownMenu; // 下拉菜单
        private ActionMode mActionMode; // 动作模式
        private MenuItem mMoveMenu; // 移动菜单项

        /**
         * 创建动作模式时的回调方法。
         *
         * @param mode 动作模式实例。
         * @param menu 菜单实例。
         * @return 如果成功创建动作模式,返回true;否则返回false。
         */
        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;
            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);
            mDropDownMenu = new DropdownMenu(NotesListActivity.this,
                    (Button) customView.findViewById(R.id.selection_menu),
                    R.menu.note_list_dropdown);
            // 设置下拉菜单项点击监听器
            mDropDownMenu.setOnDropdownMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
                public boolean onMenuItemClick(MenuItem item) {
                    mNotesListAdapter.selectAll(!mNotesListAdapter.isAllSelected());
                    updateMenu();
                    return true;
                }

            });
            return true;
        }

        /**
         * 更新动作模式下的菜单项。
         */
        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);
                }
            }
        }

        /**
         * 准备动作模式时的回调方法。
         *
         * @param mode 动作模式实例。
         * @param menu 菜单实例。
         * @return 返回false,表示未进行任何操作。
         */
        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
            // TODO Auto-generated method stub
            return false;
        }

        /**
         * 点击动作模式中的菜单项时的回调方法。
         *
         * @param mode 动作模式实例。
         * @param item 被点击的菜单项。
         * @return 返回false,表示未进行任何操作。
         */
        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
            // TODO Auto-generated method stub
            return false;
        }

        /**
         * 销毁动作模式时的回调方法。
         *
         * @param mode 动作模式实例。
         */
        public void onDestroyActionMode(ActionMode mode) {
            // 还原列表选择模式和设置
            mNotesListAdapter.setChoiceMode(false);
            mNotesListView.setLongClickable(true);
            mAddNewNote.setVisibility(View.VISIBLE);
        }

        public void finishActionMode() {
            mActionMode.finish();
        }

        /**
         * 处理列表项选择状态变化的回调方法。
         *
         * @param mode     动作模式实例。
         * @param position 列表中被改变选择状态的项的位置。
         * @param id       项的ID。
         * @param checked  项的新选择状态。
         */
        public void onItemCheckedStateChanged(ActionMode mode, int position, long id,
                                              boolean checked) {
            // 更新列表项的选择状态并更新菜单
            mNotesListAdapter.setCheckedItem(position, checked);
            updateMenu();
        }

        /**
         * 处理菜单项点击事件的回调方法。
         *
         * @param item 被点击的菜单项。
         * @return 如果已处理点击事件,返回true;否则返回false。
         */
        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;
            }

            // 根据菜单项ID执行相应操作
            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;
            }
            return true;
        }
    }


    /**
     * 为“新建笔记”按钮添加触摸监听器的内部类,实现点击和拖动事件的处理。
     */
    private class NewNoteOnTouchListener implements OnTouchListener {

        /**
         * 处理触摸事件。
         *
         * @param v     触摸的视图。
         * @param event 触摸事件。
         * @return 如果事件被处理则返回true,否则返回false。
         */
        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();
                    }
                    // 当点击到“新建笔记”按钮透明部分时,将事件分发给背后的列表视图
                    // 这里使用了一种硬编码的方式处理透明部分的点击,依赖于当前的背景公式
                    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();
                            mDispatchY = eventY;
                            event.setLocation(event.getX(), mDispatchY);
                            mDispatch = true;
                            return mNotesListView.dispatchTouchEvent(event);
                        }
                    }
                    break;
                }
                case MotionEvent.ACTION_MOVE: {
                    // 如果正在分发触摸事件,则更新事件的位置并继续分发
                    if (mDispatch) {
                        mDispatchY += (int) event.getY() - mOriginY;
                        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;
                }
            }
            // 如果事件未被分发,则返回false
            return false;
        }

    }

    ;


    /**
     * 异步查询笔记列表。
     * 根据当前文件夹ID选择不同的查询条件,启动一个后台查询处理该查询。
     */
    private void startAsyncNotesListQuery() {
        // 根据当前文件夹ID选择查询条件
        String selection = (mCurrentFolderId == Notes.ID_ROOT_FOLDER) ? ROOT_FOLDER_SELECTION
                : NORMAL_SELECTION;
        // 启动查询,排序方式为类型降序,修改日期降序
        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");
    }

    /**
     * 处理后台查询的类。
     * 继承自AsyncQueryHandler,用于处理异步查询完成后的操作。
     */
    private final class BackgroundQueryHandler extends AsyncQueryHandler {
        public BackgroundQueryHandler(ContentResolver contentResolver) {
            super(contentResolver);
        }

        /**
         * 查询完成时的处理逻辑。
         * 根据查询标记的不同,执行不同的操作,如更新笔记列表或显示文件夹列表。
         *
         * @param token  查询标记,用于区分不同的查询。
         * @param cookie 查询时传入的附加对象。
         * @param cursor 查询结果的游标。
         */
        @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;
            }
        }
    }

    /**
     * 显示文件夹列表的菜单。
     * 使用查询结果构建一个对话框,让用户选择一个文件夹。
     *
     * @param cursor 查询结果的游标,包含文件夹信息。
     */
    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() {

            /**
             * 用户选择文件夹时的处理逻辑。
             * 将选中的笔记移动到用户选择的文件夹中,并给出反馈。
             *
             * @param dialog 对话框实例。
             * @param which  用户选择的项的索引。
             */
            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();
    }

    /**
     * 创建新的笔记。
     * 启动一个活动用于编辑新笔记或编辑现有笔记。
     */
    private void createNewNote() {
        // 构建意图并指定动作为插入或编辑,以及初始文件夹ID
        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);
    }


    /**
     * 批量删除笔记的函数。根据当前是否处于同步模式,采取不同的删除策略:如果不处于同步模式,则直接删除笔记;如果处于同步模式,则将笔记移动到回收站文件夹。
     * 执行删除操作后,会更新相应的widgets。
     */
    private void batchDelete() {
        new AsyncTask<Void, Void, HashSet<AppWidgetAttribute>>() {
            // 在后台执行任务,获取选中的widgets并执行删除操作
            protected HashSet<AppWidgetAttribute> doInBackground(Void... unused) {
                // 获取当前选中的widgets
                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;
            }

            // 删除操作完成后,在UI线程执行后续操作
            @Override
            protected void onPostExecute(HashSet<AppWidgetAttribute> widgets) {
                // 遍历所有受影响的widgets,对有效的widgets进行更新
                if (widgets != null) {
                    for (AppWidgetAttribute widget : widgets) {
                        if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID
                                && widget.widgetType != Notes.TYPE_WIDGET_INVALIDE) {
                            // 更新有效的widget
                            updateWidget(widget.widgetId, widget.widgetType);
                        }
                    }
                }
                // 结束动作模式
                mModeCallBack.finishActionMode();
            }
        }.execute();
    }


    /**
     * 删除指定的文件夹。
     * 如果是在同步模式下,文件夹会被移动到回收站,否则直接删除。
     * 同时,也会更新与该文件夹相关的所有小部件。
     *
     * @param folderId 要删除的文件夹ID。
     */
    private void deleteFolder(long folderId) {
        // 根据ID判断是否为根文件夹,根文件夹不能被删除
        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);

        // 获取与文件夹相关联的小部件信息
        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);
                }
            }
        }
    }

    /**
     * 打开指定的笔记节点进行编辑。
     *
     * @param data 包含要打开的笔记节点信息的对象。
     */
    private void openNode(NoteItemData data) {
        // 构造Intent并设置动作和额外数据,然后启动Activity
        Intent intent = new Intent(this, NoteEditActivity.class);
        intent.setAction(Intent.ACTION_VIEW);
        intent.putExtra(Intent.EXTRA_UID, data.getId());
        this.startActivityForResult(intent, REQUEST_CODE_OPEN_NODE);
    }

    /**
     * 打开指定的文件夹,并加载其笔记列表。
     * 根据文件夹ID的不同,更新UI状态,包括标题和新增笔记按钮的可见性。
     *
     * @param data 包含要打开的文件夹信息的对象。
     */
    private void openFolder(NoteItemData data) {
        // 设置当前文件夹ID并启动异步查询
        mCurrentFolderId = data.getId();
        startAsyncNotesListQuery();

        // 根据文件夹ID更新UI状态
        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);
    }

    /**
     * 点击事件的处理方法。
     * 目前仅处理新建笔记按钮的点击事件。
     *
     * @param v 被点击的视图对象。
     */
    public void onClick(View v) {
        // 根据视图ID执行相应的操作
        switch (v.getId()) {
            case R.id.btn_new_note:
                createNewNote();
                break;
            default:
                break;
        }
    }


    /**
     * 显示软键盘。
     */
    private void showSoftInput() {
        InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
        if (inputMethodManager != null) {
            inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
        }
    }

    /**
     * 隐藏软键盘。
     *
     * @param view 触发隐藏软键盘的视图。
     */
    private void hideSoftInput(View view) {
        InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
        inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
    }

    /**
     * 显示创建或修改文件夹的对话框。
     *
     * @param create 如果为true,则为创建文件夹;如果为false,则为修改文件夹。
     */
    private void showCreateOrModifyFolderDialog(final boolean create) {
        final 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"); // 日志记录,长按的数据项为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(); // 显示对话框
        final 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) {
                // 空实现
            }

            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) {
                // 空实现
            }
        });
    }


    /**
     * 当用户按下返回键时调用的方法,根据当前状态执行不同的操作。
     * 在子文件夹状态下,返回根文件夹并显示笔记列表;
     * 在通话记录文件夹状态下,也返回根文件夹但显示添加新笔记按钮;
     * 在笔记列表状态下,执行父类的onBackPressed方法,通常是退出或返回上一级。
     */
    @Override
    public void onBackPressed() {
        switch (mState) {
            case SUB_FOLDER:
                // 从子文件夹状态返回到根文件夹的笔记列表状态
                mCurrentFolderId = Notes.ID_ROOT_FOLDER;
                mState = ListEditState.NOTE_LIST;
                startAsyncNotesListQuery();
                mTitleBar.setVisibility(View.GONE);
                break;
            case CALL_RECORD_FOLDER:
                // 从通话记录文件夹状态返回到根文件夹的笔记列表状态,并显示添加新笔记按钮
                mCurrentFolderId = Notes.ID_ROOT_FOLDER;
                mState = ListEditState.NOTE_LIST;
                mAddNewNote.setVisibility(View.VISIBLE);
                mTitleBar.setVisibility(View.GONE);
                startAsyncNotesListQuery();
                break;
            case NOTE_LIST:
                // 在笔记列表状态下,执行父类的返回操作
                super.onBackPressed();
                break;
            default:
                // 对于其他状态,不执行任何操作
                break;
        }
    }

    /**
     * 更新小部件显示。
     * 根据传入的小部件类型,设置对应的Provider并发送更新广播。
     *
     * @param appWidgetId   小部件ID
     * @param appWidgetType 小部件类型
     */
    private void updateWidget(int appWidgetId, int appWidgetType) {
        Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
        // 根据小部件类型设置Provider
        if (appWidgetType == Notes.TYPE_WIDGET_2X) {
            intent.setClass(this, NoteWidgetProvider_2x.class);
        } else if (appWidgetType == Notes.TYPE_WIDGET_4X) {
            intent.setClass(this, NoteWidgetProvider_4x.class);
        } else {
            Log.e(TAG, "Unspported widget type");
            return;
        }

        intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[]{
                appWidgetId
        });

        sendBroadcast(intent);
        setResult(RESULT_OK, intent);
    }

    /**
     * 文件夹列表的上下文菜单创建监听器。
     * 在焦点笔记项不为空时,添加查看、删除和重命名菜单项。
     */
    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);
            }
        }
    };

    /**
     * 上下文菜单关闭时的回调方法。
     * 在列表视图中取消上下文菜单的监听器。
     *
     * @param menu 被关闭的菜单对象
     */
    @Override
    public void onContextMenuClosed(Menu menu) {
        if (mNotesListView != null) {
            mNotesListView.setOnCreateContextMenuListener(null);
        }
        super.onContextMenuClosed(menu);
    }


    /**
     * 当上下文菜单中的项目被选择时调用。
     *
     * @param item 被选择的菜单项。
     * @return 如果事件已成功处理,则返回true;否则如果事件未处理,则返回false。
     */
    @Override
    public boolean onContextItemSelected(MenuItem item) {
        if (mFocusNoteDataItem == null) {
            Log.e(TAG, "The long click data item is null");
            return 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;
    }

    /**
     * 准备选项菜单。
     *
     * @param menu 菜单对象。
     * @return 总是返回true。
     */
    @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;
    }

    /**
     * 处理选项菜单项的选择。
     *
     * @param item 被选择的菜单项。
     * @return 如果事件已成功处理,则返回true;否则如果事件未处理,则返回false。
     */
    @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;
    }

    /**
     * 处理搜索请求。
     *
     * @return 总是返回true。
     */
    @Override
    public boolean onSearchRequested() {
        startSearch(null, false, null /* appData */, false);
        return true;
    }


    /**
     * 将笔记导出为文本文件。
     * 在后台任务中执行导出操作,并根据操作结果展示不同的对话框。
     */
    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) {
                    showExportFailedDialog(NotesListActivity.this.getString(R.string.failed_sdcard_export),
                            NotesListActivity.this.getString(R.string.error_sdcard_unmounted));
                } else if (result == BackupUtils.STATE_SUCCESS) {
                    showExportSuccessDialog(NotesListActivity.this.getString(R.string.success_sdcard_export),
                            backup.getExportedTextFileName(), backup.getExportedTextFileDir());
                } else if (result == BackupUtils.STATE_SYSTEM_ERROR) {
                    showExportFailedDialog(NotesListActivity.this.getString(R.string.failed_sdcard_export),
                            NotesListActivity.this.getString(R.string.error_sdcard_export));
                }
            }

        }.execute();
    }

    /**
     * 检查当前是否为同步模式。
     *
     * @return 如果已配置同步账户名则返回true,否则返回false。
     */
    private boolean isSyncMode() {
        return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0;
    }

    /**
     * 启动设置活动。
     * 用于打开设置界面。
     */
    private void startPreferenceActivity() {
        Activity from = getParent() != null ? getParent() : this;
        Intent intent = new Intent(from, NotesPreferenceActivity.class);
        from.startActivityIfNeeded(intent, -1);
    }

    /**
     * 列表项点击监听器。
     * 处理列表项的点击事件,根据不同的状态和项类型执行相应的操作。
     */
    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;
                }
            }
        }

    }

    /**
     * 启动查询目标文件夹。
     * 根据当前状态查询并显示文件夹列表。
     */
    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");
    }

    /**
     * 长按列表项时的处理。
     * 根据不同的项类型启动选择模式或显示上下文菜单。
     *
     * @return 总是返回false。
     */
    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;
    }

    /**
     * 显示导出失败的对话框。
     *
     * @param title   对话框标题
     * @param message 对话框消息内容
     */
    private void showExportFailedDialog(String title, String message) {
        AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this);
        builder.setTitle(title);
        builder.setMessage(message);
        builder.setPositiveButton(android.R.string.ok, null);
        builder.show();
    }

    /**
     * 显示导出成功的对话框。
     *
     * @param title    对话框标题
     * @param fileName 导出文件的名称
     * @param fileDir  导出文件的目录
     */
    private void showExportSuccessDialog(String title, String fileName, String fileDir) {
        AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this);
        builder.setTitle(title);
        builder.setMessage(NotesListActivity.this.getString(R.string.format_exported_file_location, fileName, fileDir));
        builder.setPositiveButton(android.R.string.ok, null);
        builder.show();
    }

}

11 NoteEditText

/*
 * 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.graphics.Rect;
import android.text.Layout;
import android.text.Selection;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.URLSpan;
import android.util.AttributeSet;
import android.util.Log;
import android.view.ContextMenu;
import android.view.KeyEvent;
import android.view.MenuItem;
import android.view.MenuItem.OnMenuItemClickListener;
import android.view.MotionEvent;
import android.widget.EditText;

import net.micode.notes.R;

import java.util.HashMap;
import java.util.Map;

/**
 * 自定义的可编辑文本视图,支持文本变化监听、删除和新增文本事件。
 */
public class NoteEditText extends EditText {
    private static final String TAG = "NoteEditText";
    private int mIndex; // 当前文本视图的索引
    private int mSelectionStartBeforeDelete; // 删除操作前的选择起始位置

    private static final String SCHEME_TEL = "tel:";
    private static final String SCHEME_HTTP = "http:";
    private static final String SCHEME_EMAIL = "mailto:";

    // URL方案与对应操作资源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;

    /**
     * 构造函数,初始化编辑文本视图。
     *
     * @param context 上下文对象
     */
    public NoteEditText(Context context) {
        super(context, null);
        mIndex = 0;
    }

    /**
     * 设置当前文本视图的索引。
     *
     * @param index 当前文本视图的索引
     */
    public void setIndex(int index) {
        mIndex = index;
    }

    /**
     * 设置文本视图变化监听器。
     *
     * @param listener 文本视图变化监听器
     */
    public void setOnTextViewChangeListener(OnTextViewChangeListener listener) {
        mOnTextViewChangeListener = listener;
    }

    /**
     * 构造函数,初始化编辑文本视图。
     *
     * @param context 上下文对象
     * @param attrs   属性集
     */
    public NoteEditText(Context context, AttributeSet attrs) {
        super(context, attrs, android.R.attr.editTextStyle);
    }

    /**
     * 构造函数,初始化编辑文本视图。
     *
     * @param context  上下文对象
     * @param attrs    属性集
     * @param defStyle 样式
     */
    public NoteEditText(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    /**
     * 处理触摸事件,调整光标位置。
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 计算触摸位置相对于文本的偏移量,并调整光标位置
                int x = (int) event.getX();
                int y = (int) event.getY();
                x -= getTotalPaddingLeft();
                y -= getTotalPaddingTop();
                x += getScrollX();
                y += getScrollY();

                Layout layout = getLayout();
                int line = layout.getLineForVertical(y);
                int off = layout.getOffsetForHorizontal(line, x);
                Selection.setSelection(getText(), off);
                break;
        }

        return super.onTouchEvent(event);
    }

    /**
     * 处理键盘按下事件。
     */
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        switch (keyCode) {
            case KeyEvent.KEYCODE_ENTER:
                // 处理回车键事件,准备新增文本视图
                if (mOnTextViewChangeListener != null) {
                    return false;
                }
                break;
            case KeyEvent.KEYCODE_DEL:
                // 记录删除操作前的选择位置
                mSelectionStartBeforeDelete = getSelectionStart();
                break;
            default:
                break;
        }
        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);
            }
        }
        super.onFocusChanged(focused, direction, previouslyFocusedRect);
    }

    /**
     * 创建上下文菜单,支持点击URL跳转。
     */
    @Override
    protected void onCreateContextMenu(ContextMenu menu) {
        if (getText() instanceof Spanned) {
            int selStart = getSelectionStart();
            int selEnd = getSelectionEnd();

            int min = Math.min(selStart, selEnd);
            int max = Math.max(selStart, selEnd);

            final URLSpan[] urls = ((Spanned) getText()).getSpans(min, max, URLSpan.class);
            if (urls.length == 1) {
                int defaultResId = 0;
                for (String schema : sSchemaActionResMap.keySet()) {
                    if (urls[0].getURL().indexOf(schema) >= 0) {
                        defaultResId = sSchemaActionResMap.get(schema);
                        break;
                    }
                }

                if (defaultResId == 0) {
                    defaultResId = R.string.note_link_other;
                }

                menu.add(0, 0, 0, defaultResId).setOnMenuItemClickListener(
                        new OnMenuItemClickListener() {
                            public boolean onMenuItemClick(MenuItem item) {
                                // 跳转到URL指向的页面
                                urls[0].onClick(NoteEditText.this);
                                return true;
                            }
                        });
            }
        }
        super.onCreateContextMenu(menu);
    }
}

12 NotesListAdapter

  1. 包声明package net.micode.notes.ui; 声明了该类所在的包。
  2. 导入语句:导入了一系列Java和Android相关的类,包括上下文、游标、日志输出、视图操作、游标适配器、集合操作等。
  3. 类声明public class NotesListAdapter extends CursorAdapter { 声明了一个名为 NotesListAdapter 的公共类,继承自 CursorAdapter
  4. 常量声明private static final String TAG = "NotesListAdapter"; 声明了一个私有静态最终的字符串 TAG,用于日志输出。
  5. 成员变量
    • private Context mContext; 声明了一个私有上下文对象 mContext
    • private HashMap<Integer, Boolean> mSelectedIndex; 声明了一个私有哈希映射 mSelectedIndex,用于存储选中项的索引和状态。
    • private int mNotesCount; 声明了一个私有整数 mNotesCount,用于存储笔记总数。
    • private boolean mChoiceMode; 声明了一个私有布尔值 mChoiceMode,用于存储选择模式标志。
  6. 内部类声明public static class AppWidgetAttribute { ... } 声明了一个公共静态内部类 AppWidgetAttribute,用于存储与小部件相关的数据。
  7. 构造函数public NotesListAdapter(Context context) { ... } 声明了类的构造函数,接受上下文对象作为参数,并初始化成员变量。
  8. 方法
    • @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { ... } 重写了 newView 方法,用于创建新的列表项视图。
    • @Override public void bindView(View view, Context context, Cursor cursor) { ... } 重写了 bindView 方法,用于绑定数据到视图。
    • public void setCheckedItem(final int position, final boolean checked) { ... } 声明了一个公共方法 setCheckedItem,用于设置指定位置的项为选中或未选中状态。
    • public boolean isInChoiceMode() { ... } 声明了一个公共方法 isInChoiceMode,用于获取当前是否处于选择模式。
    • public void setChoiceMode(boolean mode) { ... } 声明了一个公共方法 setChoiceMode,用于设置选择模式。
    • public void selectAll(boolean checked) { ... } 声明了一个公共方法 selectAll,用于全选或全不选。
    • public HashSet<Long> getSelectedItemIds() { ... } 声明了一个公共方法 getSelectedItemIds,用于获取所有选中项的ID集合。
    • public HashSet<AppWidgetAttribute> getSelectedWidget() { ... } 声明了一个公共方法 getSelectedWidget,用于获取所有选中小部件的属性集合。
    • public int getSelectedCount() { ... } 声明了一个公共方法 getSelectedCount,用于获取选中项的数量。
    • public boolean isAllSelected() { ... } 声明了一个公共方法 isAllSelected,用于判断是否全部选中。
    • public boolean isSelectedItem(final int position) { ... } 声明了一个公共方法 isSelectedItem,用于检查指定位置的项是否被选中。
    • @Override protected void onContentChanged() { ... } 重写了 onContentChanged 方法,用于当内容改变时调用,更新笔记数量。
    • @Override public void changeCursor(Cursor cursor) { ... } 重写了 changeCursor 方法,用于当游标改变时调用,更新笔记数量。
    • private void calcNotesCount() { ... } 声明了一个私有方法 calcNotesCount,用于计算并更新笔记总数。


package net.micode.notes.ui; // 定义包名

import android.content.Context; // 导入Context类,用于获取上下文
import android.database.Cursor; // 导入Cursor类,用于数据源游标
import android.util.Log; // 导入Log类,用于日志输出
import android.view.View; // 导入View类,用于视图操作
import android.view.ViewGroup; // 导入ViewGroup类,用于视图组操作
import android.widget.CursorAdapter; // 导入CursorAdapter类,用于游标适配器

import net.micode.notes.data.Notes; // 导入Notes类,用于笔记数据操作

import java.util.Collection; // 导入Collection类,用于集合操作
import java.util.HashMap; // 导入HashMap类,用于哈希映射
import java.util.HashSet; // 导入HashSet类,用于哈希集合
import java.util.Iterator; // 导入Iterator类,用于迭代器

/**
 * 用于管理笔记列表的适配器,继承自CursorAdapter。
 */
public class NotesListAdapter extends CursorAdapter { // 定义NotesListAdapter类,继承自CursorAdapter
    private static final String TAG = "NotesListAdapter"; // 定义日志标签
    private Context mContext; // 上下文对象
    // 用于存储选中项的索引和状态
    private HashMap<Integer, Boolean> mSelectedIndex; // 选中项索引和状态的哈希映射
    private int mNotesCount; // 笔记总数
    private boolean mChoiceMode; // 选择模式标志

    /**
     * AppWidget属性容器,用于存储与小部件相关的数据。
     */
    public static class AppWidgetAttribute { // 定义AppWidgetAttribute类
        public int widgetId; // 小部件ID
        public int widgetType; // 小部件类型
    }

    /**
     * 构造函数。
     *
     * @param context 上下文对象
     */
    public NotesListAdapter(Context context) { // 构造函数
        super(context, null); // 调用父类构造函数
        mSelectedIndex = new HashMap<Integer, Boolean>(); // 初始化选中项索引和状态的哈希映射
        mContext = context; // 赋值上下文对象
        mNotesCount = 0; // 初始化笔记总数
    }

    /**
     * 创建新的列表项视图。
     *
     * @param context 上下文对象
     * @param cursor  数据游标
     * @param parent  父视图
     * @return 新的列表项视图
     */
    @Override
    public View newView(Context context, Cursor cursor, ViewGroup parent) { // 重写newView方法
        return new NotesListItem(context); // 返回新的列表项视图
    }

    /**
     * 绑定数据到视图。
     *
     * @param view    列表项视图
     * @param context 上下文对象
     * @param cursor  数据游标
     */
    @Override
    public void bindView(View view, Context context, Cursor cursor) { // 重写bindView方法
        if (view instanceof NotesListItem) { // 判断视图是否为NotesListItem类型
            NoteItemData itemData = new NoteItemData(context, cursor); // 创建NoteItemData对象
            ((NotesListItem) view).bind(context, itemData, mChoiceMode,
                    isSelectedItem(cursor.getPosition())); // 绑定数据到视图
        }
    }

    /**
     * 设置指定位置的项为选中或未选中状态。
     *
     * @param position 项的位置
     * @param checked  选中状态
     */
    public void setCheckedItem(final int position, final boolean checked) { // 设置选中项方法
        mSelectedIndex.put(position, checked); // 更新选中项索引和状态
        notifyDataSetChanged(); // 通知数据集改变
    }

    /**
     * 获取当前是否处于选择模式。
     *
     * @return 选择模式状态
     */
    public boolean isInChoiceMode() { // 获取选择模式方法
        return mChoiceMode; // 返回选择模式状态
    }

    /**
     * 设置选择模式。
     *
     * @param mode 选择模式状态
     */
    public void setChoiceMode(boolean mode) { // 设置选择模式方法
        mSelectedIndex.clear(); // 清空选中项索引和状态
        mChoiceMode = mode; // 更新选择模式状态
    }

    /**
     * 全选或全不选。
     *
     * @param checked 选中状态
     */
    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集合。
     *
     * @return 选中项ID的HashSet
     */
    public HashSet<Long> getSelectedItemIds() { // 获取选中项ID集合方法
        HashSet<Long> itemSet = new HashSet<Long>(); // 创建哈希集合
        for (Integer position : mSelectedIndex.keySet()) { // 遍历选中项索引
            if (mSelectedIndex.get(position) == true) { // 判断是否为选中状态
                Long id = getItemId(position); // 获取项ID
                if (id == Notes.ID_ROOT_FOLDER) { // 判断是否为根文件夹
                    Log.d(TAG, "Wrong item id, should not happen"); // 输出日志
                } else {
                    itemSet.add(id); // 添加到哈希集合
                }
            }
        }

        return itemSet; // 返回哈希集合
    }

    /**
     * 获取所有选中小部件的属性集合。
     *
     * @return 选中小部件属性的HashSet
     */
    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(); // 创建AppWidgetAttribute对象
                    NoteItemData item = new NoteItemData(mContext, c); // 创建NoteItemData对象
                    widget.widgetId = item.getWidgetId(); // 获取小部件ID
                    widget.widgetType = item.getWidgetType(); // 获取小部件类型
                    itemSet.add(widget); // 添加到哈希集合
                } else {
                    Log.e(TAG, "Invalid cursor"); // 输出日志
                    return null; // 返回空
                }
            }
        }
        return itemSet; // 返回哈希集合
    }

    /**
     * 获取选中项的数量。
     *
     * @return 选中项数量
     */
    public int getSelectedCount() { // 获取选中项数量方法
        Collection<Boolean> values = mSelectedIndex.values(); // 获取选中项状态集合
        if (null == values) { // 判断集合是否为空
            return 0; // 返回0
        }
        Iterator<Boolean> iter = values.iterator(); // 获取迭代器
        int count = 0; // 初始化计数器
        while (iter.hasNext()) { // 遍历集合
            if (true == iter.next()) { // 判断是否为选中状态
                count++; // 计数器加1
            }
        }
        return count; // 返回计数器
    }

    /**
     * 判断是否全部选中。
     *
     * @return 全部选中的状态
     */
    public boolean isAllSelected() { // 判断是否全部选中方法
        int checkedCount = getSelectedCount(); // 获取选中项数量
        return (checkedCount != 0 && checkedCount == mNotesCount); // 判断是否全部选中
    }

    /**
     * 检查指定位置的项是否被选中。
     *
     * @param position 项的位置
     * @return 选中状态
     */
    public boolean isSelectedItem(final int position) { // 检查选中项方法
        if (null == mSelectedIndex.get(position)) { // 判断是否为空
            return false; // 返回未选中状态
        }
        return mSelectedIndex.get(position); // 返回选中状态
    }

    /**
     * 当内容改变时调用,更新笔记数量。
     */
    @Override
    protected void onContentChanged() { // 重写onContentChanged方法
        super.onContentChanged(); // 调用父类方法
        calcNotesCount(); // 计算笔记数量
    }

    /**
     * 当游标改变时调用,更新笔记数量。
     *
     * @param cursor 新的游标
     */
    @Override
    public void changeCursor(Cursor cursor) { // 重写changeCursor方法
        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++; // 笔记总数加1
                }
            } else {
                Log.e(TAG, "Invalid cursor"); // 输出日志
                return; // 返回
            }
        }
    }
}

13 NotesListltem

  1. 包声明package net.micode.notes.ui; 声明了该类所在的包。
  2. 导入语句:导入了一系列Java和Android相关的类,包括上下文、日期格式化、视图操作、复选框、图像显示、线性布局、文本显示等。
  3. 类声明public class NotesListItem extends LinearLayout { 声明了一个名为 NotesListItem 的公共类,继承自 LinearLayout
  4. 成员变量
    • private ImageView mAlert; 声明了一个私有 ImageView 对象 mAlert,用于显示提醒图标。
    • private TextView mTitle; 声明了一个私有 TextView 对象 mTitle,用于显示笔记标题。
    • private TextView mTime; 声明了一个私有 TextView 对象 mTime,用于显示修改时间。
    • private TextView mCallName; 声明了一个私有 TextView 对象 mCallName,用于在通话记录笔记中显示通话名称。
    • private NoteItemData mItemData; 声明了一个私有 NoteItemData 对象 mItemData,用于绑定笔记数据。
    • private CheckBox mCheckBox; 声明了一个私有 CheckBox 对象 mCheckBox,用于多选模式。
  5. 构造函数public NotesListItem(Context context) { ... } 声明了类的构造函数,接受上下文对象作为参数,并初始化视图组件。
  6. 方法
    • public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked) { ... } 声明了一个公共方法 bind,用于绑定数据到视图,根据数据设置视图状态。
    • private void setBackground(NoteItemData data) { ... } 声明了一个私有方法 setBackground,用于根据笔记数据设置列表项的背景资源。
    • public NoteItemData getItemData() { ... } 声明了一个公共方法 getItemData,用于获取绑定的笔记数据。

package net.micode.notes.ui; // 定义包名

import android.content.Context; // 导入Context类,用于获取上下文
import android.text.format.DateUtils; // 导入DateUtils类,用于日期格式化
import android.view.View; // 导入View类,用于视图操作
import android.widget.CheckBox; // 导入CheckBox类,用于复选框
import android.widget.ImageView; // 导入ImageView类,用于图像显示
import android.widget.LinearLayout; // 导入LinearLayout类,用于线性布局
import android.widget.TextView; // 导入TextView类,用于文本显示

import net.micode.notes.R; // 导入R类,用于资源引用
import net.micode.notes.data.Notes; // 导入Notes类,用于笔记数据操作
import net.micode.notes.tool.DataUtils; // 导入DataUtils类,用于数据处理
import net.micode.notes.tool.ResourceParser.NoteItemBgResources; // 导入NoteItemBgResources类,用于背景资源解析

/*
 * 该类表示一个笔记列表项,继承自LinearLayout,并包含了显示笔记各种信息的组件。
 * 它用于在UI中展示一个笔记或文件夹的条目。
 */

public class NotesListItem extends LinearLayout { // 定义NotesListItem类,继承自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); // 初始化复选框
    }

    /*
     * 绑定数据到视图,根据数据设置视图状态。
     *
     * @param context 上下文
     * @param data 要绑定的笔记数据
     * @param choiceMode 是否为选择模式
     * @param checked 是否选中
     */
    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(); // 获取背景颜色ID
        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()); // 设置文件夹背景
        }
    }

    /*
     * 获取绑定的笔记数据。
     *
     * @return 绑定的NoteItemData对象
     */
    public NoteItemData getItemData() { // 获取笔记数据方法
        return mItemData; // 返回笔记数据
    }
}

14 NotesPreferenceActivity

/*
 * 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"; // 权限过滤键

    // 类成员变量定义部分:主要用于账户同步和UI更新
    private PreferenceCategory mAccountCategory; // 账户分类偏好项
    private GTaskReceiver mReceiver; // 接收同步任务的广播接收器
    private Account[] mOriAccounts; // 原始账户数组
    private boolean mHasAddedAccount; // 标记是否已添加新账户

    /**
     * 当设置Activity创建时调用。
     * 主要进行界面初始化和设置账户同步。
     *
     * @param icicle 保存Activity状态的Bundle,用于恢复状态。
     */
    @Override
    protected void onCreate(Bundle icicle) {
        super.onCreate(icicle);

        // 设置返回按钮
        getActionBar().setDisplayHomeAsUpEnabled(true);

        // 从XML加载偏好设置
        addPreferencesFromResource(R.xml.preferences);
        mAccountCategory = (PreferenceCategory) findPreference(PREFERENCE_SYNC_ACCOUNT_KEY);
        mReceiver = new GTaskReceiver();
        IntentFilter filter = new IntentFilter();
        filter.addAction(GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME);
        registerReceiver(mReceiver, filter); // 注册广播接收器以监听同步服务

        mOriAccounts = null;
        // 添加设置头部视图
        View header = LayoutInflater.from(this).inflate(R.layout.settings_header, null);
        getListView().addHeaderView(header, null, true);
    }

    /**
     * 当设置Activity恢复到前台时调用。
     * 主要用于检查并自动设置新添加的账户进行同步。
     */
    @Override
    protected void onResume() {
        super.onResume();

        // 自动设置新添加的账户进行同步
        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;
                    }
                }
            }
        }

        // 刷新UI
        refreshUI();
    }


    /**
     * 当Activity即将被销毁时调用,用于注销广播接收器。
     */
    @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)) {
                        // 如果尚未设置账户,则展示选择账户对话框
                        showSelectAccountAlertDialog();
                    } else {
                        // 如果已经设置账户,则展示更改账户确认对话框
                        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); // 获取上次同步时间视图

        // 根据同步状态设置按钮文本和点击事件
        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))); // 只有在设置了同步账户时才使能同步按钮

        // 根据同步状态设置上次同步时间的显示
        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(); // 加载同步按钮
    }

    /**
     * 显示选择账户的对话框。
     * 该对话框列出了已连接的Google账户,并允许用户选择一个账户用于同步。
     * 如果没有账户,对话框将提供添加账户的选项。
     */
    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); // 移除默认的确定按钮

        // 获取当前设备上的Google账户
        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) {
                    // 选择移除账户,执行移除操作并刷新UI
                    removeSyncAccount();
                    refreshUI();
                }
            }
        });
        dialogBuilder.show();
    }

    /**
     * 获取设备上的Google账户列表。
     *
     * @return Account[] 返回设备上所有类型为“com.google”的账户数组。
     */
    private Account[] getGoogleAccounts() {
        AccountManager accountManager = AccountManager.get(this);
        return accountManager.getAccountsByType("com.google");
    }


    /**
     * 设置同步账户信息。
     * 如果当前账户与传入账户不一致,则更新SharedPreferences中的账户信息,并清理本地相关的gtask信息。
     *
     * @param account 需要设置的账户名
     */
    private void setSyncAccount(String account) {
        // 检查当前账户是否与传入账户名一致,不一致则更新账户信息
        if (!getSyncAccountName(this).equals(account)) {
            SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
            SharedPreferences.Editor editor = settings.edit();
            // 如果账户名非空,则保存账户名,否则清除账户名
            if (account != null) {
                editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, account);
            } else {
                editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, "");
            }
            editor.commit();

            // 清理上次同步时间
            setLastSyncTime(this, 0);

            // 清理本地相关的gtask信息
            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();
        }
    }

    /**
     * 移除同步账户信息。
     * 清除SharedPreferences中的账户信息和上次同步时间,并清理本地相关的gtask信息。
     */
    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();

        // 清理本地相关的gtask信息
        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();
    }

    /**
     * 获取当前同步账户名。
     * 从SharedPreferences中获取存储的账户名,默认为空字符串。
     *
     * @param context 上下文
     * @return 同步账户名
     */
    public static String getSyncAccountName(Context context) {
        SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
                Context.MODE_PRIVATE);
        return settings.getString(PREFERENCE_SYNC_ACCOUNT_NAME, "");
    }

    /**
     * 设置上次同步的时间。
     * 将指定的时间保存到SharedPreferences中。
     *
     * @param context 上下文
     * @param time    上次同步的时间戳
     */
    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();
    }

    /**
     * 获取上次同步的时间。
     * 从SharedPreferences中获取上次同步的时间戳,默认为0。
     *
     * @param context 上下文
     * @return 上次同步的时间戳
     */
    public static long getLastSyncTime(Context context) {
        SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
                Context.MODE_PRIVATE);
        return settings.getLong(PREFERENCE_LAST_SYNC_TIME, 0);
    }

    /**
     * 广播接收器类,用于接收gtask同步相关的广播消息,并据此刷新UI。
     */
    private class GTaskReceiver extends BroadcastReceiver {

        @Override
        public void onReceive(Context context, Intent intent) {
            refreshUI();
            // 如果广播消息表明正在同步,则更新UI显示的同步状态信息
            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));
            }

        }
    }

    /**
     * 处理选项菜单项的选择事件。
     *
     * @param item 选中的菜单项
     * @return 如果事件已处理,则返回true;否则返回false。
     */
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case android.R.id.home:
                // 当选择返回按钮时,启动NotesListActivity并清除当前活动栈顶以上的所有活动
                Intent intent = new Intent(this, NotesListActivity.class);
                intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
                startActivity(intent);
                return true;
            default:
                return false;
        }
    }
}

  • 26
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
小米便签widget是一个用于创建桌面小工具的代码。以下是对小米便签widget中的代码注释的解释: 1. 名:com.xiaomi.milink.widget.note.widget 这个代码的根名是com.xiaomi.milink.widget.note.widget,表明这个含了小米便签widget相关的代码。 2. 类名:NoteWidgetProvider 这个类是一个小米便签widget的提供者,负责创建和更新小米便签widget。 3. 类名:NoteWidgetService 这个类是一个小米便签widget的服务类,负责处理小米便签widget的各种操作。 4. 类名:NoteWidgetConfigActivity 这个类是一个小米便签widget的配置界面活动,用于用户配置小米便签widget的相关设置。 5. 类名:NoteWidgetUtils 这个类是一个工具类,含了一些小米便签widget使用的常用方法和功能实现。 6. 类名:NoteWidgetProviderInfo 这个类是一个小米便签widget提供者信息类,用于标识小米便签widget的相关信息。 7. 类名:NoteWidgetManager 这个类是一个小米便签widget管理类,用于管理小米便签widget的创建、更新和删除等操作。 8. 类名:NoteWidgetLayoutHelper 这个类是一个小米便签widget布局辅助类,用于帮助小米便签widget实现自定义的布局。 9. 类名:NoteWidgetDbHelper 这个类是一个小米便签widget数据库辅助类,用于小米便签widget与数据库的交互操作。 10. 类名:NoteWidgetProviderReceiver 这个类是一个小米便签widget提供者接收器,用于接收小米便签widget相关的广播。 以上是对小米便签widget中主要代码文件的类名和功能的简单注释。这些代码文件一起协同工作,实现了小米便签widget的创建、更新、配置和管理等诸多功能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一号言安

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值