From 1a0f9d62c02e96fb7d65368d68ae707c614ff025 Mon Sep 17 00:00:00 2001 From: shihaoxuan <1514599782@qq.com> Date: Sun, 29 Dec 2024 20:51:25 +0800 Subject: [PATCH 1/5] explanatory note --- src/widget/NoteWidgetProvider.java | 162 ++++++++++++++++++++++++++ src/widget/NoteWidgetProvider_2x.java | 63 ++++++++++ src/widget/NoteWidgetProvider_4x.java | 63 ++++++++++ 3 files changed, 288 insertions(+) create mode 100644 src/widget/NoteWidgetProvider.java create mode 100644 src/widget/NoteWidgetProvider_2x.java create mode 100644 src/widget/NoteWidgetProvider_4x.java diff --git a/src/widget/NoteWidgetProvider.java b/src/widget/NoteWidgetProvider.java new file mode 100644 index 0000000..a39041a --- /dev/null +++ b/src/widget/NoteWidgetProvider.java @@ -0,0 +1,162 @@ +/* + * 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.widget; +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.util.Log; +import android.widget.RemoteViews; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.tool.ResourceParser; +import net.micode.notes.ui.NoteEditActivity; +import net.micode.notes.ui.NotesListActivity; + +// 继承自AppWidgetProvider,用于管理Note小部件的抽象类 +public abstract class NoteWidgetProvider extends AppWidgetProvider { + // 定义查询数据库时需要返回的列 + public static final String [] PROJECTION = new String [] { + NoteColumns.ID, // 笔记ID + NoteColumns.BG_COLOR_ID, // 笔记背景颜色ID + NoteColumns.SNIPPET // 笔记摘要 + }; + + // 定义PROJECTION数组中各列的索引,方便后续操作 + public static final int COLUMN_ID = 0; + public static final int COLUMN_BG_COLOR_ID = 1; + public static final int COLUMN_SNIPPET = 2; + + // 用于日志输出的标签 + private static final String TAG = "NoteWidgetProvider"; + + // 当小部件被删除时,更新数据库中对应的笔记,将WIDGET_ID设置为无效 + @Override + public void onDeleted(Context context, int[] appWidgetIds) { + // 创建ContentValues对象,用于更新数据库 + ContentValues values = new ContentValues(); + values.put(NoteColumns.WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); // 设置WIDGET_ID为无效值 + // 遍历所有被删除的小部件ID + for (int i = 0; i < appWidgetIds.length; i++) { + // 更新数据库中对应小部件ID的笔记,将WIDGET_ID设置为无效 + context.getContentResolver().update(Notes.CONTENT_NOTE_URI, + values, + NoteColumns.WIDGET_ID + "=?", + new String[] { String.valueOf(appWidgetIds[i])}); + } + } + + // 获取与小部件相关的笔记信息 + private Cursor getNoteWidgetInfo(Context context, int widgetId) { + // 查询数据库,获取与小部件ID相关的笔记信息 + return context.getContentResolver().query(Notes.CONTENT_NOTE_URI, + PROJECTION, + NoteColumns.WIDGET_ID + "=? AND " + NoteColumns.PARENT_ID + "<>?", + new String[] { String.valueOf(widgetId), String.valueOf(Notes.ID_TRASH_FOLER) }, + null); + } + + // 更新小部件的显示内容,该方法被外部调用 + protected void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + update(context, appWidgetManager, appWidgetIds, false); // 默认不开启隐私模式 + } + + // 更新小部件的显示内容,考虑隐私模式 + private void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds, + boolean privacyMode) { + // 遍历所有小部件ID + for (int i = 0; i < appWidgetIds.length; i++) { + // 如果小部件ID有效 + if (appWidgetIds[i] != AppWidgetManager.INVALID_APPWIDGET_ID) { + // 获取默认背景ID + int bgId = ResourceParser.getDefaultBgId(context); + // 初始化笔记摘要为空字符串 + String snippet = ""; + // 创建意图,用于启动NoteEditActivity + Intent intent = new Intent(context, NoteEditActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); // 设置意图标志,确保Activity单实例 + intent.putExtra(Notes.INTENT_EXTRA_WIDGET_ID, appWidgetIds[i]); // 传递小部件ID + intent.putExtra(Notes.INTENT_EXTRA_WIDGET_TYPE, getWidgetType()); // 传递小部件类型 + + // 获取与小部件相关的笔记信息 + Cursor c = getNoteWidgetInfo(context, appWidgetIds[i]); + if (c != null && c.moveToFirst()) { + // 如果查询结果有多条,记录错误日志并返回 + if (c.getCount() > 1) { + Log.e(TAG, "Multiple message with same widget id:" + appWidgetIds[i]); + c.close(); + return; + } + // 获取笔记摘要和背景ID + snippet = c.getString(COLUMN_SNIPPET); + bgId = c.getInt(COLUMN_BG_COLOR_ID); + intent.putExtra(Intent.EXTRA_UID, c.getLong(COLUMN_ID)); + intent.setAction(Intent.ACTION_VIEW); // 设置意图动作为查看 + } else { + // 如果没有查询到笔记信息,设置默认文本和意图动作 + snippet = context.getResources().getString(R.string.widget_havenot_content); + intent.setAction(Intent.ACTION_INSERT_OR_EDIT); + } + + // 关闭游标 + if (c != null) { + c.close(); + } + + // 创建RemoteViews对象,用于更新小部件视图 + RemoteViews rv = new RemoteViews(context.getPackageName(), getLayoutId()); + // 设置背景图片资源 + rv.setImageViewResource(R.id.widget_bg_image, getBgResourceId(bgId)); + intent.putExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, bgId); + + // 生成PendingIntent,用于启动宿主Activity + PendingIntent pendingIntent = null; + if (privacyMode) { + // 隐私模式下,显示默认文本,并启动笔记列表Activity + rv.setTextViewText(R.id.widget_text, + context.getString(R.string.widget_under_visit_mode)); + pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], new Intent( + context, NotesListActivity.class), PendingIntent.FLAG_UPDATE_CURRENT); + } else { + // 非隐私模式下,显示笔记摘要,并启动编辑笔记Activity + rv.setTextViewText(R.id.widget_text, snippet); + pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], intent, + PendingIntent.FLAG_UPDATE_CURRENT); + } + + // 设置点击事件,点击小部件时启动对应的Activity + rv.setOnClickPendingIntent(R.id.widget_text, pendingIntent); + // 更新小部件视图 + appWidgetManager.updateAppWidget(appWidgetIds[i], rv); + } + } + } + + // 获取背景图片资源ID的方法,需要子类实现 + protected abstract int getBgResourceId(int bgId); + + // 获取小部件布局资源ID的方法,需要子类实现 + protected abstract int getLayoutId(); + + // 获取小部件类型的方法,需要子类实现 + protected abstract int getWidgetType(); +} \ No newline at end of file diff --git a/src/widget/NoteWidgetProvider_2x.java b/src/widget/NoteWidgetProvider_2x.java new file mode 100644 index 0000000..db93365 --- /dev/null +++ b/src/widget/NoteWidgetProvider_2x.java @@ -0,0 +1,63 @@ +/* + * 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.widget; + +import android.appwidget.AppWidgetManager; +import android.content.Context; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.tool.ResourceParser; + +// NoteWidgetProvider_2x类继承自NoteWidgetProvider类,专门用于实现2x2网格大小的笔记小部件(Note Widget)。 +// 这个类提供了小部件的更新逻辑,以及如何根据笔记数据渲染小部件的UI。 +public class NoteWidgetProvider_2x extends NoteWidgetProvider { + + // 当AppWidgetManager需要更新小部件时,会调用此方法。 + // 这是AppWidgetProvider生命周期中的一个重要方法,用于响应小部件的更新请求。 + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + // 调用父类(NoteWidgetProvider)的update方法,传入必要的参数,以更新小部件的UI。 + // 这里使用super关键字来访问父类的update方法,确保父类的逻辑被正确执行。 + super.update(context, appWidgetManager, appWidgetIds); + } + + // getLayoutId方法被用来获取小部件的布局资源ID。 + // 每个小部件都有自己的布局文件,这里返回的是2x2网格小部件的布局资源ID。 + @Override + protected int getLayoutId() { + // 返回R.layout.widget_2x,这是2x2网格小部件对应的布局资源ID。 + return R.layout.widget_2x; + } + + // getBgResourceId方法根据传入的背景ID(bgId),返回对应的背景资源ID。 + // 这个方法使用ResourceParser工具类中的WidgetBgResources来获取正确的背景资源ID。 + @Override + protected int getBgResourceId(int bgId) { + // 调用ResourceParser.WidgetBgResources的getWidget2xBgResource方法,传入bgId。 + // 这个方法负责根据bgId返回2x2网格小部件的背景资源ID。 + return ResourceParser.WidgetBgResources.getWidget2xBgResource(bgId); + } + + // getWidgetType方法返回当前小部件的类型。 + // 小部件类型用于区分不同的小部件,例如,不同的网格大小可能会有不同的类型。 + @Override + protected int getWidgetType() { + // 返回Notes.TYPE_WIDGET_2X,这是一个常量,表示当前小部件是2x2网格大小的笔记小部件。 + return Notes.TYPE_WIDGET_2X; + } +} \ No newline at end of file diff --git a/src/widget/NoteWidgetProvider_4x.java b/src/widget/NoteWidgetProvider_4x.java new file mode 100644 index 0000000..d325630 --- /dev/null +++ b/src/widget/NoteWidgetProvider_4x.java @@ -0,0 +1,63 @@ +/* + * 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.widget; + +import android.appwidget.AppWidgetManager; +import android.content.Context; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.tool.ResourceParser; + +// NoteWidgetProvider_4x类继承自NoteWidgetProvider类,专门用于实现4x4网格大小的Note小部件功能。 +public class NoteWidgetProvider_4x extends NoteWidgetProvider { + + // 当AppWidgetManager需要更新小部件时,会调用此方法。 + // 这是AppWidgetProvider生命周期中的一个重要方法,用于响应小部件的更新请求。 + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + // 调用父类(NoteWidgetProvider)的update方法,传入必要的参数,以更新小部件的UI。 + // 这里使用super关键字来访问父类的update方法,确保父类的逻辑被正确执行。 + super.update(context, appWidgetManager, appWidgetIds); + } + + // getLayoutId方法被用来获取小部件的布局资源ID。 + // 每个小部件都有自己的布局文件,这里返回的是4x4网格小部件的布局资源ID。 + // 注意:该方法没有@Override注解,这是一个遗漏,因为方法体与父类中的方法签名相同, + // 应该加上@Override注解以明确表示这是一个重写的方法。 + protected int getLayoutId() { + // 返回R.layout.widget_4x,这是4x4网格小部件对应的布局资源ID。 + return R.layout.widget_4x; + } + + // getBgResourceId方法根据传入的背景ID(bgId),返回对应的背景资源ID。 + // 这个方法使用ResourceParser工具类中的WidgetBgResources来获取正确的背景资源ID。 + @Override + protected int getBgResourceId(int bgId) { + // 调用ResourceParser.WidgetBgResources的getWidget4xBgResource方法,传入bgId。 + // 这个方法负责根据bgId返回4x4网格小部件的背景资源ID。 + return ResourceParser.WidgetBgResources.getWidget4xBgResource(bgId); + } + + // getWidgetType方法返回当前小部件的类型。 + // 小部件类型用于区分不同的小部件,例如,不同的网格大小可能会有不同的类型。 + @Override + protected int getWidgetType() { + // 返回Notes.TYPE_WIDGET_4X,这是一个常量,表示当前小部件是4x4网格大小的笔记小部件。 + return Notes.TYPE_WIDGET_4X; + } +} \ No newline at end of file From 2fa77522fba32cdde8557efede9a3e438005c40d Mon Sep 17 00:00:00 2001 From: shihaoxuan <1514599782@qq.com> Date: Sun, 29 Dec 2024 20:53:54 +0800 Subject: [PATCH 2/5] explanatory note --- src/ui(s)/AlarmAlertActivity.java | 174 ++++++++++ src/ui(s)/AlarmInitReceiver.java | 81 +++++ src/ui(s)/AlarmReceiver.java | 35 ++ src/ui(s)/DateTimePicker.java | 511 ++++++++++++++++++++++++++++ src/ui(s)/DateTimePickerDialog.java | 122 +++++++ src/ui(s)/DropdownMenu.java | 82 +++++ src/ui(s)/FoldersListAdapter.java | 118 +++++++ 7 files changed, 1123 insertions(+) create mode 100644 src/ui(s)/AlarmAlertActivity.java create mode 100644 src/ui(s)/AlarmInitReceiver.java create mode 100644 src/ui(s)/AlarmReceiver.java create mode 100644 src/ui(s)/DateTimePicker.java create mode 100644 src/ui(s)/DateTimePickerDialog.java create mode 100644 src/ui(s)/DropdownMenu.java create mode 100644 src/ui(s)/FoldersListAdapter.java diff --git a/src/ui(s)/AlarmAlertActivity.java b/src/ui(s)/AlarmAlertActivity.java new file mode 100644 index 0000000..dc4b618 --- /dev/null +++ b/src/ui(s)/AlarmAlertActivity.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.DialogInterface.OnDismissListener; +import android.content.Intent; +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.media.RingtoneManager; +import android.net.Uri; +import android.os.Bundle; +import android.os.PowerManager; +import android.provider.Settings; +import android.view.Window; +import android.view.WindowManager; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.tool.DataUtils; + +import java.io.IOException; + +// AlarmAlertActivity类继承自Activity,用于处理闹钟提醒的UI和逻辑。 +public class AlarmAlertActivity extends Activity implements OnClickListener, OnDismissListener { + // 用于存储笔记ID和笔记预览文本 + private long mNoteId; + private String mSnippet; + // 定义预览文本的最大长度 + private static final int SNIPPET_PREW_MAX_LEN = 60; + // 用于播放闹钟声音的MediaPlayer对象 + MediaPlayer mPlayer; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_NO_TITLE); // 请求无标题栏的窗口特性 + + final Window win = getWindow(); // 获取窗口对象 + win.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); // 设置窗口在锁屏时也能显示 + + // 如果屏幕不是开启状态,则添加额外的窗口参数以点亮屏幕 + if (!isScreenOn()) { + win.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON + | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON + | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON + | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR); + } + + // 获取启动该Activity的Intent对象 + Intent intent = getIntent(); + + try { + // 从Intent中解析笔记ID + mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1)); + // 根据笔记ID获取笔记预览文本 + mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId); + // 如果预览文本超过最大长度,则截断并添加省略号 + mSnippet = mSnippet.length() > SNIPPET_PREW_MAX_LEN ? mSnippet.substring(0, + SNIPPET_PREW_MAX_LEN) + getResources().getString(R.string.notelist_string_info) + : mSnippet; + } catch (IllegalArgumentException e) { + e.printStackTrace(); + return; + } + + // 初始化MediaPlayer对象 + mPlayer = new 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 + Uri url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM); + + // 获取受影响的静音模式流类型 + int silentModeStreams = Settings.System.getInt(getContentResolver(), + Settings.System.MODE_RINGER_STREAMS_AFFECTED, 0); + + // 设置MediaPlayer的音频流类型 + if ((silentModeStreams & (1 << AudioManager.STREAM_ALARM)) != 0) { + mPlayer.setAudioStreamType(silentModeStreams); + } else { + mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM); + } + try { + // 设置MediaPlayer的数据源为闹钟铃声,并准备播放 + 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); // 显示对话框并设置对话框消失监听器 + } + + // DialogInterface.OnClickListener接口的实现,处理对话框按钮点击事件 + public void onClick(DialogInterface dialog, int which) { + switch (which) { + case DialogInterface.BUTTON_NEGATIVE: // 如果点击编辑按钮 + Intent intent = new Intent(this, NoteEditActivity.class); // 创建Intent,跳转到编辑笔记界面 + intent.setAction(Intent.ACTION_VIEW); + intent.putExtra(Intent.EXTRA_UID, mNoteId); // 传递笔记ID + startActivity(intent); // 启动编辑笔记界面 + break; + default: + break; + } + } + + // DialogInterface.OnDismissListener接口的实现,处理对话框消失事件 + public void onDismiss(DialogInterface dialog) { + stopAlarmSound(); // 停止闹钟声音 + finish(); // 结束当前Activity + } + + // 停止闹钟声音 + private void stopAlarmSound() { + if (mPlayer != null) { + mPlayer.stop(); // 停止播放 + mPlayer.release(); // 释放资源 + mPlayer = null; // 置空MediaPlayer对象 + } + } +} \ No newline at end of file diff --git a/src/ui(s)/AlarmInitReceiver.java b/src/ui(s)/AlarmInitReceiver.java new file mode 100644 index 0000000..6f99ea7 --- /dev/null +++ b/src/ui(s)/AlarmInitReceiver.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.ContentUris; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; + +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; + + +// AlarmInitReceiver类继承自BroadcastReceiver,用于初始化闹钟提醒。 +public class AlarmInitReceiver extends BroadcastReceiver { + + // 定义查询数据库时需要返回的列 + private static final String [] PROJECTION = new String [] { + NoteColumns.ID, // 笔记ID + NoteColumns.ALERTED_DATE // 笔记的已提醒日期 + }; + + // 定义PROJECTION数组中各列的索引 + private static final int COLUMN_ID = 0; // 笔记ID索引 + private static final int COLUMN_ALERTED_DATE = 1; // 已提醒日期索引 + + // onReceive方法在接收到广播时被调用,这里用于设置闹钟提醒。 + @Override + public void onReceive(Context context, Intent intent) { + // 获取当前日期时间 + long currentDate = System.currentTimeMillis(); + // 查询数据库,获取所有未提醒且类型为笔记的笔记 + Cursor c = context.getContentResolver().query(Notes.CONTENT_NOTE_URI, + PROJECTION, + NoteColumns.ALERTED_DATE + ">? AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE, + new String[] { String.valueOf(currentDate) }, + null); + + // 如果查询结果不为空 + if (c != null) { + // 如果查询结果至少有一条记录 + if (c.moveToFirst()) { + // 遍历查询结果 + do { + // 获取笔记的提醒日期 + long alertDate = c.getLong(COLUMN_ALERTED_DATE); + // 创建一个Intent,用于触发闹钟提醒 + Intent sender = new Intent(context, AlarmReceiver.class); + // 为Intent设置数据,标识具体的笔记 + sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, c.getLong(COLUMN_ID))); + // 创建PendingIntent,用于AlarmManager设置闹钟 + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, 0); + // 获取AlarmManager实例 + AlarmManager alermManager = (AlarmManager) context + .getSystemService(Context.ALARM_SERVICE); + // 设置闹钟,使用RTC_WAKEUP模式在指定时间唤醒设备 + alermManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent); + } while (c.moveToNext()); + } + // 关闭游标 + c.close(); + } + } +} \ No newline at end of file diff --git a/src/ui(s)/AlarmReceiver.java b/src/ui(s)/AlarmReceiver.java new file mode 100644 index 0000000..6356bd5 --- /dev/null +++ b/src/ui(s)/AlarmReceiver.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +// AlarmReceiver类继承自BroadcastReceiver,用于处理闹钟提醒的广播事件。 +public class AlarmReceiver extends BroadcastReceiver { + // onReceive方法在接收到广播时被调用,这里用于启动闹钟提醒的Activity。 + @Override + public void onReceive(Context context, Intent intent) { + // 设置intent的目标类为AlarmAlertActivity,即闹钟提醒的Activity。 + intent.setClass(context, AlarmAlertActivity.class); + // 为intent添加标志,表示需要创建一个新的任务栈。 + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + // 使用context启动AlarmAlertActivity,展示闹钟提醒界面。 + context.startActivity(intent); + } +} \ No newline at end of file diff --git a/src/ui(s)/DateTimePicker.java b/src/ui(s)/DateTimePicker.java new file mode 100644 index 0000000..3744de1 --- /dev/null +++ b/src/ui(s)/DateTimePicker.java @@ -0,0 +1,511 @@ +/** + * 文件名称:DateTimePicker.java + * 版权声明:版权所有 (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * 许可证声明:根据Apache License, Version 2.0(“许可证”)获得许可; + * 除非遵守许可证,否则不得使用此文件。 + * 您可以在以下网址获得许可证的副本: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 除非适用法律要求或书面同意,否则按“现状”分发的软件是在没有任何明示或暗示的 + * 保证和条件的情况下提供的,包括但不限于关于所有权、不侵犯第三方权利、适销性或 + * 适用于特定用途的任何保证和条件。有关权限和限制的具体语言,请参见许可证。 + */ + +package net.micode.notes.ui; // 定义包名 + +import java.text.DateFormatSymbols; // 用于日期时间格式化的符号 +import java.util.Calendar; // 用于日期时间操作的日历类 + +import net.micode.notes.R; // 导入资源文件R,用于访问资源 + +import android.content.Context; // Android上下文环境 +import android.text.format.DateFormat; // 用于日期时间格式化 +import android.view.View; // 用于视图操作 +import android.widget.FrameLayout; // 布局容器FrameLayout +import android.widget.NumberPicker; // 自定义视图,用于选择数字 + +/** + * DateTimePicker 类是一个自定义视图组件,用于在Android应用中选择日期和时间。 + */ +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; + // NumberPicker组件的值范围常量 + private static final int DATE_SPINNER_MIN_VAL = 0; + private static final int DATE_SPINNER_MAX_VAL = DAYS_IN_ALL_WEEK - 1; + private static final int HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW = 0; + private static final int HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW = 23; + private static final int HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW = 1; + 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; // 用于选择日期的NumberPicker + private final NumberPicker mHourSpinner; // 用于选择小时的NumberPicker + private final NumberPicker mMinuteSpinner; // 用于选择分钟的NumberPicker + private final NumberPicker mAmPmSpinner; // 用于选择上午/下午的NumberPicker + private Calendar mDate; // 用于管理日期和时间的Calendar实例 + + // 日期显示值数组 + 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; + + // NumberPicker值变化监听器 + private NumberPicker.OnValueChangeListener mOnDateChangedListener = new NumberPicker.OnValueChangeListener() { + @Override + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + // 当日期值变化时,更新日历并通知监听器 + mDate.add(Calendar.DAY_OF_YEAR, newVal - oldVal); + updateDateControl(); + onDateTimeChanged(); + } + }; + + // 小时变化监听器 + private NumberPicker.OnValueChangeListener mOnHourChangedListener = new NumberPicker.OnValueChangeListener() { + @Override + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + // 当小时值变化时,更新日历并通知监听器 + boolean isDateChanged = false; + Calendar cal = Calendar.getInstance(); + 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)); + } + } + }; + + // 分钟变化监听器 + 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; + } + 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) { + // 当上午/下午变化时,更新日历并通知监听器 + 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); + } + + // DateTimePicker的构造函数 + public DateTimePicker(Context context) { + this(context, System.currentTimeMillis()); + } + + public DateTimePicker(Context context, long date) { + this(context, date, DateFormat.is24HourFormat(context)); + } + + public DateTimePicker(Context context, long date, boolean is24HourView) { + super(context); + mDate = Calendar.getInstance(); + mInitialising = true; + mIsAm = getCurrentHourOfDay() >= HOURS_IN_HALF_DAY; + inflate(context, R.layout.datetime_picker, this); + + mDateSpinner = (NumberPicker) findViewById(R.id.date); + mDateSpinner.setMinValue(DATE_SPINNER_MIN_VAL); + mDateSpinner.setMaxValue(DATE_SPINNER_MAX_VAL); + mDateSpinner.setOnValueChangedListener(mOnDateChangedListener); + + mHourSpinner = (NumberPicker) findViewById + + 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; +} + +@Override +public void setEnabled(boolean enabled) { + // 设置控件是否可用 + if (mIsEnabled == enabled) { + return; + } + super.setEnabled(enabled); + mDateSpinner.setEnabled(enabled); + mMinuteSpinner.setEnabled(enabled); + mHourSpinner.setEnabled(enabled); + mAmPmSpinner.setEnabled(enabled); + mIsEnabled = enabled; +} + +@Override +public boolean isEnabled() { + // 获取控件是否可用 + return mIsEnabled; +} + +/** + * 获取当前日期的毫秒值 + * + * @return 当前日期的毫秒值 + */ +public long getCurrentDateInTimeMillis() { + return mDate.getTimeInMillis(); +} + +/** + * 设置当前日期 + * + * @param date 当前日期的毫秒值 + */ +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 当前月份 + */ +public int getCurrentMonth() { + return mDate.get(Calendar.MONTH); +} + +/** + * 设置当前月份 + * + * @param month 月份 + */ +public void setCurrentMonth(int month) { + if (!mInitialising && month == getCurrentMonth()) { + return; + } + mDate.set(Calendar.MONTH, month); + updateDateControl(); + onDateTimeChanged(); +} + +/** + * 获取当前月份中的日期 + * + * @return 月份中的日期 + */ +public int getCurrentDay() { + return mDate.get(Calendar.DAY_OF_MONTH); +} + +/** + * 设置当前月份中的日期 + * + * @param dayOfMonth 月份中的日期 + */ +public void setCurrentDay(int dayOfMonth) { + if (!mInitialising && dayOfMonth == getCurrentDay()) { + return; + } + mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); + updateDateControl(); + onDateTimeChanged(); +} + +/** + * 获取24小时制的当前小时 + * + * @return 24小时制的当前小时 + */ +public int getCurrentHourOfDay() { + return mDate.get(Calendar.HOUR_OF_DAY); +} + +private int getCurrentHour() { + if (mIs24HourView){ + return getCurrentHourOfDay(); + } else { + int hour = getCurrentHourOfDay(); + if (hour > HOURS_IN_HALF_DAY) { + return hour - HOURS_IN_HALF_DAY; + } else { + return hour == 0 ? HOURS_IN_HALF_DAY : hour; + } + } +} + +/** + * 设置24小时制的当前小时 + * + * @param hourOfDay 24小时制的小时 + */ +public void setCurrentHour(int hourOfDay) { + if (!mInitialising && hourOfDay == getCurrentHourOfDay()) { + return; + } + mDate.set(Calendar.HOUR_OF_DAY, hourOfDay); + if (!mIs24HourView) { + if (hourOfDay >= HOURS_IN_HALF_DAY) { + mIsAm = false; + if (hourOfDay > HOURS_IN_HALF_DAY) { + hourOfDay -= HOURS_IN_HALF_DAY; + } + } else { + mIsAm = true; + if (hourOfDay == 0) { + hourOfDay = HOURS_IN_HALF_DAY; + } + } + updateAmPmControl(); + } + mHourSpinner.setValue(hourOfDay); + onDateTimeChanged(); +} + +/** + * 获取当前分钟 + * + * @return 当前分钟 + */ +public int getCurrentMinute() { + return mDate.get(Calendar.MINUTE); +} + +/** + * 设置当前分钟 + */ +public void setCurrentMinute(int minute) { + if (!mInitialising && minute == getCurrentMinute()) { + return; + } + mMinuteSpinner.setValue(minute); + mDate.set(Calendar.MINUTE, minute); + onDateTimeChanged(); +} + +/** + * 判断是否是24小时视图 + * + * @return 如果是24小时视图返回true,否则返回false。 + */ +public boolean is24HourView () { + return mIs24HourView; +} + +/** + * 设置是否是24小时视图或AM/PM模式 + * + * @param is24HourView 如果是24小时视图传入true,否则传入false。 + */ +public void set24HourView(boolean is24HourView) { + if (mIs24HourView == is24HourView) { + return; + } + mIs24HourView = is24HourView; + mAmPmSpinner.setVisibility(is24HourView ? View.GONE : View.VISIBLE); + int hour = getCurrentHourOfDay(); + updateHourControl(); + setCurrentHour(hour); + updateAmPmControl(); +} + +/** + * 更新日期控制 + */ +private void updateDateControl() { + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, -DAYS_IN_ALL_WEEK / 2 - 1); + mDateSpinner.setDisplayedValues(null); + for (int i = 0; i < DAYS_IN_ALL_WEEK; ++i) { + cal.add(Calendar.DAY_OF_YEAR, 1); + mDateDisplayValues[i] = (String) DateFormat.format("MM.dd EEEE", cal); + } + mDateSpinner.setDisplayedValues(mDateDisplayValues); + mDateSpinner.setValue(DAYS_IN_ALL_WEEK / 2); + mDateSpinner.invalidate(); +} + +/** + * 更新上午/下午控制 + */ +private void updateAmPmControl() { + if (mIs24HourView) { + mAmPmSpinner.setVisibility(View.GONE); // 在24小时视图中隐藏AM/PM选择器 + } else { + int index = mIsAm ? Calendar.AM : Calendar.PM; // 根据当前时间是上午还是下午设置索引 + mAmPmSpinner.setValue(index); // 设置AM/PM选择器的值 + mAmPmSpinner.setVisibility(View.VISIBLE); // 显示AM/PM选择器 + } +} + +/** + * 更新小时控制 + */ +private void updateHourControl() { + if (mIs24HourView) { + mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW); // 设置24小时视图的小时最小值 + mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW); // 设置24小时视图的小时最大值 + } else { + mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW); // 设置12小时视图的小时最小值 + mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW); // 设置12小时视图的小时最大值 + } +} + +/** + * 设置日期时间改变的监听器 + * +/** + * 当日期时间改变时调用此方法 + */ +private void onDateTimeChanged() { + if (mOnDateTimeChangedListener != null) { + mOnDateTimeChangedListener.onDateTimeChanged(this, getCurrentYear(), + getCurrentMonth(), getCurrentDay(), getCurrentHourOfDay(), getCurrentMinute()); + } +} \ No newline at end of file diff --git a/src/ui(s)/DateTimePickerDialog.java b/src/ui(s)/DateTimePickerDialog.java new file mode 100644 index 0000000..ba22a76 --- /dev/null +++ b/src/ui(s)/DateTimePickerDialog.java @@ -0,0 +1,122 @@ +/** + * 文件名称:DateTimePickerDialog.java + * 版权声明:版权所有 (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * 许可证声明:根据Apache License, Version 2.0(“许可证”)获得许可; + * 除非遵守许可证,否则不得使用此文件。 + * 您可以在以下网址获得许可证的副本: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 除非适用法律要求或书面同意,否则按“现状”分发的软件是在没有任何明示或暗示的 + * 保证和条件的情况下提供的,包括但不限于关于所有权、不侵犯第三方权利、适销性或 + * 适用于特定用途的任何保证和条件。有关权限和限制的具体语言,请参见许可证。 + */ + +package net.micode.notes.ui; // 定义包名 + +import java.util.Calendar; // 导入日历类 + +import net.micode.notes.R; // 导入资源文件R,用于访问资源 +import net.micode.notes.ui.DateTimePicker; // 导入DateTimePicker类 +import net.micode.notes.ui.DateTimePicker.OnDateTimeChangedListener; // 导入DateTimePicker的监听器接口 + +import android.app.AlertDialog; // 导入AlertDialog类,用于创建对话框 +import android.content.Context; // 导入Context类 +import android.content.DialogInterface; // 导入DialogInterface类 +import android.content.DialogInterface.OnClickListener; // 导入点击事件监听器接口 +import android.text.format.DateFormat; // 导入日期格式化类 +import android.text.format.DateUtils; // 导入日期工具类 + +/** + * DateTimePickerDialog 类是一个继承自AlertDialog的对话框,用于选择日期和时间。 + */ +public class DateTimePickerDialog extends AlertDialog implements OnClickListener { + + // 成员变量 + private Calendar mDate = Calendar.getInstance(); // 当前日期时间的Calendar实例 + private boolean mIs24HourView; // 是否使用24小时制 + private OnDateTimeSetListener mOnDateTimeSetListener; // 日期时间设置监听器 + private DateTimePicker mDateTimePicker; // DateTimePicker组件 + + /** + * 日期时间设置监听器接口 + */ + public interface OnDateTimeSetListener { + void OnDateTimeSet(AlertDialog dialog, long date); // 当日期时间设置时的回调方法 + } + + /** + * DateTimePickerDialog构造函数 + * @param context 上下文环境 + * @param date 初始日期时间的毫秒值 + */ + public DateTimePickerDialog(Context context, long date) { + super(context); // 调用AlertDialog的构造函数 + mDateTimePicker = new DateTimePicker(context); // 创建DateTimePicker实例 + setView(mDateTimePicker); // 设置对话框的内容视图 + // 设置日期时间改变监听器 + 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; + // 根据是否使用24小时制更新DateTimePicker + mDateTimePicker.set24HourView(is24HourView); + } + + /** + * 设置日期时间设置监听器 + * @param callBack 监听器实例 + */ + public void setOnDateTimeSetListener(OnDateTimeSetListener callBack) { + mOnDateTimeSetListener = callBack; // 设置监听器 + } + + /** + * 更新对话框标题 + * @param date 毫秒值表示的日期时间 + */ + private void updateTitle(long date) { + int flag = DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME; + // 根据是否使用24小时制设置格式化标志 + flag |= mIs24HourView ? DateUtils.FORMAT_24HOUR : DateUtils.FORMAT_12HOUR; + // 设置对话框标题为日期时间字符串 + setTitle(DateUtils.formatDateTime(this.getContext(), date, flag)); + } + + /** + * 点击事件处理 + * @param arg0 对话框接口 + * @param arg1 按钮标识 + */ + public void onClick(DialogInterface arg0, int arg1) { + // 如果设置了监听器,则回调监听器 + if (mOnDateTimeSetListener != null) { + mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis()); + } + } + +} \ No newline at end of file diff --git a/src/ui(s)/DropdownMenu.java b/src/ui(s)/DropdownMenu.java new file mode 100644 index 0000000..46f6399 --- /dev/null +++ b/src/ui(s)/DropdownMenu.java @@ -0,0 +1,82 @@ +/** + * 文件名称:DropdownMenu.java + * 版权声明:版权所有 (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * 许可证声明:根据Apache License, Version 2.0(“许可证”)获得许可; + * 除非遵守许可证,否则不得使用此文件。 + * 您可以在以下网址获得许可证的副本: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 除非适用法律要求或书面同意,否则按“现状”分发的软件是在没有任何明示或暗示的 + * 保证和条件的情况下提供的,包括但不限于关于所有权、不侵犯第三方权利、适销性或 + * 适用于特定用途的任何保证和条件。有关权限和限制的具体语言,请参见许可证。 + */ + +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; // 导入R类,用于访问资源文件 + +/** + * DropdownMenu 类用于创建一个下拉菜单(Dropdown Menu)。 + */ +public class DropdownMenu { + // 成员变量 + private Button mButton; // 用于触发下拉菜单的按钮 + private PopupMenu mPopupMenu; // 弹出菜单对象 + private Menu mMenu; // 菜单对象 + + /** + * DropdownMenu 构造函数。 + * @param context 上下文环境,用于获取系统资源和服务。 + * @param button 用于触发下拉菜单的按钮。 + * @param menuId 菜单资源ID,用于从XML资源文件中加载菜单项。 + */ + 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(); // 获取PopupMenu的Menu对象 + mPopupMenu.getMenuInflater().inflate(menuId, mMenu); // 从XML资源文件中加载菜单项 + mButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + mPopupMenu.show(); // 设置按钮点击事件,显示PopupMenu + } + }); + } + + /** + * 设置下拉菜单项点击事件监听器。 + * @param listener 点击事件监听器。 + */ + public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) { + if (mPopupMenu != null) { + mPopupMenu.setOnMenuItemClickListener(listener); // 设置PopupMenu的菜单项点击事件 + } + } + + /** + * 查找具有指定ID的菜单项。 + * @param id 菜单项ID。 + * @return 返回找到的MenuItem对象,如果没有找到则返回null。 + */ + public MenuItem findItem(int id) { + return mMenu.findItem(id); // 在Menu中查找指定ID的菜单项 + } + + /** + * 设置触发下拉菜单的按钮文本。 + * @param title 按钮文本。 + */ + public void setTitle(CharSequence title) { + mButton.setText(title); // 设置按钮文本 + } +} \ No newline at end of file diff --git a/src/ui(s)/FoldersListAdapter.java b/src/ui(s)/FoldersListAdapter.java new file mode 100644 index 0000000..800d1cc --- /dev/null +++ b/src/ui(s)/FoldersListAdapter.java @@ -0,0 +1,118 @@ +/** + * 文件名称:FoldersListAdapter.java + * 版权声明:版权所有 (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * 许可证声明:根据Apache License, Version 2.0(“许可证”)获得许可; + * 除非遵守许可证,否则不得使用此文件。 + * 您可以在以下网址获得许可证的副本: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 除非适用法律要求或书面同意,否则按“现状”分发的软件是在没有任何明示或暗示的 + * 保证和条件的情况下提供的,包括但不限于关于所有权、不侵犯第三方权利、适销性或 + * 适用于特定用途的任何保证和条件。有关权限和限制的具体语言,请参见许可证。 + */ + +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; // 导入R类,用于访问资源文件 +import net.micode.notes.data.Notes; // 导入Notes类,用于访问笔记数据 +import net.micode.notes.data.Notes.NoteColumns; // 导入NoteColumns类,用于访问笔记列数据 + + +/** + * FoldersListAdapter 类是一个用于文件夹列表的适配器,继承自CursorAdapter。 + */ +public class FoldersListAdapter extends CursorAdapter { + // 定义查询数据库时需要的列 + public static final String [] PROJECTION = { + NoteColumns.ID, + NoteColumns.SNIPPET + }; + + // 定义列索引常量 + public static final int ID_COLUMN = 0; + public static final int NAME_COLUMN = 1; + + /** + * FoldersListAdapter 构造函数。 + * @param context 上下文环境,用于获取系统资源和服务。 + * @param c 数据库查询结果的Cursor对象。 + */ + public FoldersListAdapter(Context context, Cursor c) { + super(context, c); // 调用父类的构造函数 + } + + /** + * 创建新的视图。 + * @param context 上下文环境。 + * @param cursor Cursor对象。 + * @param parent 父视图组。 + * @return 返回新的视图对象。 + */ + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return new FolderListItem(context); // 创建FolderListItem视图 + } + + /** + * 绑定视图和数据。 + * @param view 视图对象。 + * @param context 上下文环境。 + * @param cursor Cursor对象。 + */ + @Override + public void bindView(View view, Context context, Cursor cursor) { + if (view instanceof FolderListItem) { + // 获取文件夹名称 + String folderName = (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context + .getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN); + ((FolderListItem) view).bind(folderName); // 绑定数据到视图 + } + } + + /** + * 获取文件夹名称。 + * @param context 上下文环境。 + * @param position 位置。 + * @return 返回文件夹名称。 + */ + public String getFolderName(Context context, int position) { + Cursor cursor = (Cursor) getItem(position); // 获取对应位置的Cursor对象 + 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 { + private TextView mName; // 用于显示文件夹名称的TextView + + /** + * FolderListItem 构造函数。 + * @param context 上下文环境。 + */ + public FolderListItem(Context context) { + super(context); // 调用父类的构造函数 + inflate(context, R.layout.folder_list_item, this); // 从XML布局文件中加载视图 + mName = (TextView) findViewById(R.id.tv_folder_name); // 获取TextView对象 + } + + /** + * 绑定文件夹名称数据到视图。 + * @param name 文件夹名称。 + */ + public void bind(String name) { + mName.setText(name); // 设置TextView的文本 + } + } + +} \ No newline at end of file From 888edd94dc9c6ca4cede7aea3292badc1b97bb63 Mon Sep 17 00:00:00 2001 From: 2201_75665698 <2201_75665698@noreply.gitcode.com> Date: Sun, 29 Dec 2024 20:57:31 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=E5=90=84=E7=B1=BB=E5=8F=8A=E4=B8=BB?= =?UTF-8?q?=E8=A6=81=E4=BD=9C=E7=94=A8=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/各类及主要作用说明.docx | Bin 0 -> 25083 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/各类及主要作用说明.docx diff --git a/src/各类及主要作用说明.docx b/src/各类及主要作用说明.docx new file mode 100644 index 0000000000000000000000000000000000000000..37b30a1ebcf2802fe6fea90572380eedfe3a06f2 GIT binary patch literal 25083 zcmeFYbC4}vvnSlPZQHi{wC&TjZQG}9+qP}nw(ZlNe&&Af#GSbB#1}FD-l>RHyLPH} zRs1q@<;u0?rGPD;WX2=YOIC~^USe&qk(@!$9jG$u{R4lp2yyoi4YAJR&0)(b$eUImOGQ7Q2CHj!6} ze8lp4fAS`fMIlhZ=1-BuGdte!W{W4x8Ci}S!4g`SIkq%*2QF zQz>vkb8ZM4CEPtNnKeHkAS-U9Hjw7~?(&S2Vjwb~^eGnE^+6OG{V(#3@FxizJ0?39p#mx4gtn_%0w^(#G{)I5DT#T{yR1l{{GmyOUvZv{koc&ZoUSQ` zeX;lNTu}MFr$B*c!5l~xLE0a`ntplooz~n|2M1&Ew5(>%{ zR@cGU%8{P#pY#87)&Iu+`JW!WGQQ7ZfB{C}Ch#+Gu1jIP52HYq-f(sUV+|ZaQ$iYf zW7T5i>yu}76-fI`Uu|FKV1zpApBXH$xW8ekQ@zMXRKK2&N}$b&C}$Q8Ng4{W?NwG@Qhk_n z(-@bQEyOiF!OM9?x@ zp|EZ{=gvI}dP;`EgJ~f1Aj9e3y-c)_G`>#$9Aomc-4Lob>h9TMXKD*R{3Ndb>K|<4 zNoKe|oNf>R02}}aKv!D_Bl`bv5+hqfXX~Hr_D@>-m%{-5diDoVEbOgHd}W)uNA2Yx^SVkvn)cECgT0?rr?a*zDbIz&4)A;$ zgscsEW*!S{tX}AynzW(m@L%_kjaV48Fs+W~Fh)wgpwL)|Ced-@JT#Tbcz6TUMsO@t z8cmML?!PQXJlF3)krkif3{rey&f*ZgP^`3?sVMmU-m4wCXYd0wuLRdZLSBM*&o}Bc zgOEWvA6#qjKbZKh@M=f7!M4K!08oqt1VH?e_@D6l&%nCO(saaTPxjksvG@*@lK3JvX~gtS&qqNto;1?qt?)}1@o3pgDb=;){WeJowXV^3AOB&Gj!OX3A6 zEeo<_z@c3}jg$THiTzbmITr3wAfH8-IACGtc5fx0^D!0i{bk$3=WWJCGp!O}VJJkZ zP=(LMtI5OTo5LMDAgpI< zx!*v(rsAY6_`FQWyFN?nPTICiW(BHbX^KmbNrgwDlr?}k!@Rc}IZx?1v5A$iF*Ilq zo2ZqQa?@4OO7g@)O2|kKvP{cbBK(@R=%f>8LsQbsQqs$oa<7U~-BZdufrGze>@pG^Z)8Oft{K!RA?wYHv-bz^PK1j&-`61yED2SiU@aQ#g828=F%3)@g_G zwoH-(H}US#gKBSYtWD!d{{2hWdb6g%hoY_Lv(87XD&`BosS-XG#7Uh1RMeczrg&dOQGivIol96HENO@=d|@rFaS6V+xG6 zT(M7c{o113%rp!cE&Wn|+uGEkWMI+ry~zfDRg)dQc~en!1L-38#N-@uu=*0I@}E$h z?6Q!i{<`|{xcEFsQZQtD*wgQFRWhf73@?Se{yMw2$}cM%>K6X!#dY0iw&~<0*ic#2 z@lbN;;IXpjJN{Bb$!^q`(i~IMC1NPYD7%BliKR1Ibm^S6&d;R&HM{CM`*XK5X@)L$ zCK$PDek24$a{Er@3U(cMl|S;|H0bL+Y;x`T?$!B(=AR#L{(s{w{%VTp&kxsp%lZg@ z&X!%P`IM=qFYB5vmPiu2!-u(fS-&xW&u~Xlh|F+CrirlQw|-@c%4Deov46IY}&o&bnjwuv@W4}?@epiv-ihA+8wuTyOtwb5z_k6Yw zxrhwkykwSBaY?qK_>vjSn*}93rO(x?!aedn@#AKb+@-7uF`A*o zeM{amY&>ZINYaHL`y1(sSsfQnNQ_bI#jSOpC;)u}56os3}9y zmXZ$1OUW*O)UKCSKtnwlNIYTA9Z^rk>}adEqi^*TF2E)(hI^FNK21U@RF&9X`6t82Zu8$st3_5i=ns6?KN~x%XU9>#qxTwta6BxH8UK{FEFhVrXg@dU$tYyPK%r1kv?oUbwQeL&Xoi^YC;BN-_o2Fw_EM5 z$8u-el2D9|wn9r?bR6-DOIE&nCZi070c+{GNrRM|&m+`pQoa`Ub+AyNq^0e`Z4>D?9Ch3$zfma;j=+c)Ee<7I zSkT_jG!A)5>=G)d4V`ZD9ln%%j0=h$%to8T#X6^N=f0`Re>2Qa|B8W`c4&gGnW1TnmxQPN-&V84luG}id} zanE~JU!SWZi$c+Xya;nPB~{An#$i*KD`5$g)ZG}+SI4WDJ|mcl}A zZDH~d6)V^<*yDC1mj`=M@zvW0cGTk1I1y4$-QqWaokj`)U7R9Qv9=M7$UDiib*2Xw zzV?S_qaCq^&*MVd!`u~gmF10hEjxj^y`1)=Nb|uebnKATJ}OeCh!TQeaDA|0d$5TV-=D z3=PqjG9$6*q)0)!;0y;4;Z@V-sHiPE^51EzSE|`t7TMp;?oaFE3)tLTi03hfvb1KO z2gG}$fIN84Fp&7*_5FyahS(RTPKRi}aWqF6iTE5yo5;g+#x&)2C>G_eeNA(k3^83v zACZIS=Ugy^=aZZWs@cx<-RfNlys6%?%K$Dp5& ztawzj(duQrig{UnB^YKYqG@YdZFe-(cj9PwF1=j}IUP+*mYn;Cdf}U;PM<0BHfgPp zKGO+f847GRkz)-h^9nXUGA=9`QQ5|hU&NGQBvfB#yWr3*oRl0S3)#j;#9-|0r){&G@i<>7wz>_)N@V0Tvym-cljLlCsBZeB(P_QA zgehaa^LB?lm44%MSsnw{9Rnc|9PAWj0U}BG#b1**rkF@k(oi<$I5A-pmi{0;HgWaU zx)Y`0T6NBoT!wY*gDJD%w=p%zr|TgQF#vM}_3U}Vvn1QJm{{8(`=nw<90XrJP?Nbp z*KPTL6^-s$4WE`ayQ&AL#@n&;s*Z{oo62ns>EpI{u$pVC9l*}qm_pM)?P7=#kN5<{ z2o9~w4=~=u@jh$z0g#$VgMsiyh=-M;mBssP%QiWo2GEDh#v{MhW^(yX+0Eu5zG2cz zc=#|@UJDXo-3}9aT!j=+4Z>2<6QbmmX?rkqJX9+~DL-AOle!44LzpTy)g$xmzum8Q zfv15(hv|c^G#Z(@pBtS4)#2YVq ztXT|nv1p#-~v@USBm4J7r=XMDBl!522x(~ zlbw6ysb-ddUbWVCs*#u)4HPMP^h(r{6cnwLE@6#^tSlb`XIWfO3UNSmf3@^zetyY5 z{;e`IH-Dd2^(ARJ6MKpA+FO*i1Fw3<{~mog;&7N}&z7~Y!WA{mD!XhX9r3J4EXVpx@@J3pbGtVJL+eR2nAlRhHX9>#qvxX;T8NE2)A8iciOKPKP{Npw_np7!mNl%8u{<+a10R7$WchQy4dXA(^q385{he4Z zX+d5YcL|8Gx!58)IVo_AAxC;qzEVu9%vp$&yx8`f8=_Zoj?*2}@P3zA>2P||`H6_K z{1v`7U^rZG`<%}x0)x%DyB)vRU_GwNZ$$g1wDjO}w~EE3kFGS_y{i`?Xv4L$b(Vr! zQ#ZZ@<_^GA9e}B8(8`0;=4>xvX~FeVn`?#67_NbMq4kXB{2m1BYRMSSDo&2s5jg8I zT|_`Wm1^d@^@Fp*(D-7yyHRLe7^{y@2Qje4aEF%HUDc9(!)!)&48yh|_9Yc{)!X9> zyZaXB`eU$7xoKPJJt2imC9?;gISLP&Ol4IOSF33`*fK66A@fa?Cl-rhIql^u%Z~Qy zXwcY(Ie3Ev;Nt0;z#P08KVq?cX0L(70@uqF?GLGa4bk=SIVf{n{sa*Om{YReo4Gyv zVFzqTdvX%48inS8xGI4rtg!Fc3;I>J=!W6tDE+Pe?L}mtE-w51@s}5NS8Yd^+m+Ab zZ{=%WS9Sc^dFrE>;x^aGXF{`(oMdcEglS)^m6{wb5n{`{W4p9`U+&z=RisgJnf1Di zZ+AX7Ys^1|Rtx&`)e)T~x`hB>kbPKa-^<<}H#(hYvUtWfMYEMT4$Z^dCN_O{GyBXf zpSL~kjy+Ez)|moi?!;j+1ct&GIfqyZzlQpoSbf*jdR6G!9YP2m5fkq_tJZ{JjAS@K zk~)3#E~6wReeyuR7V%|4G$mk|yiw(VFc^GDZK2P&K0LvONHtFBYE+g<{-ljut>f8LcFo@^e zlMKJJPk^+#Wp3F#x1ZAVB}%c4&!WGN4Uz8;I`t)j$i5_u_@$xEFUFa8v@IW3<5!8t zgDkVJk!9D33?Jw=b z`<_7TqUf zW3}a3kkfnDnuM84Su@YupsK0IK}9_w?N??02UpnV4v2GR0eEX zC=NUq8OBqulKg(}PH9p(^8ui6m>r?j)^5jT*=$HRwyPxf1aU!7M+P?T6-~@l1oz>c zlQ|d9RXJ2;@KNET3ABg`+j+HN-(79-HbB6tTh0fHV^pnoI9EXodNeiTb}Q9aQagpL ztJ3X6-<>n%xVYaRXXneWq9pPaX4De6-xt*ha2lhp)aT7}(MwwGE%%4&7Xuw0->sfG zjoq&n#%I3VeKy8r8z0M3^zjP@#9b&G8Beuu9fr1GL)~Z*T)Xv)ogUT(M`uwUo8M&E zTrNqnYVZ~O@$YQoh4j5**}qafd8_7E=a@~P{$h#*5}CY@GE!$mj)@agjM_+RNmM3+ z&MZ3YG;NmL;H98Bx={@y+0PIyvN0xu`;6Xt$itsl4DE{Nsp5Y^v5DLHVamLB>Ct%a z!Yo2M!IMTlIg#`)znR%zJ@kk^*tT?kN9b(4UG%W_v@RP9X#efVzg^LFYp^S?eY;#p zD)KEoKptxks`z%zd!^8Uy1Onqom;w`h2rKzbV@a2m5(i{g{YGlUj(B8z=Z+VXng@- zO+)0;eaW(3wgT81hZ1LZ#tSxXRYi2H6=< zxKRn&yG%(du18ND7xlLK^j%^wP9TVQe}a3>Qh{f9=+&g6Rss#=xmZ0qP(y#o4=W)= z1KbD8z}_F?x-av&x4L{@vE6IEv$eamybcIGe6&nayQbrRA$WVR3-{LfpJQ!hFF)hT z^$I3JN8~Game0j`UyROi+$;Q|VLzI-qN=$Wzi=egxds~DPlDl!$O1Bu;EN^B0nFS4 zm~QQbeOh`#C=yDC2#9($>pp{wlj{s|bNm#tu2LQnP`)?EC{Ay^}bI5*8O z=?YN?W2{2$lBJU@`*v+1@)OU9lAOGPp%7zd0()N}miHs9I%V>3rlK#>0h6EPWQYq;v+nQY-%QWf!1qitog*=?@R47k> zoJLCOD8UuG3imX5xo9ULW~(J^ovH($NNA?SGYhsG;&h+u_NeOZ(9XgZw}(Pi=YPhB z14)L7l;L2)L0(dv;b#B1d=R6S)170rokBRPN60JWfb&;|HU@y;(J@MP5_?u2TAeSV zMUy3oohP?1(i0#bWma$3Z9SPyybe2e40*OQ&%$SsVIW+9&QVd=f^+5se`K;C&YtuU*5Ay$77c&jzhMu-$^CNQ*o+V&hTue{kt0K?2|Hm%87dWczH!x-N9ttt)BW&_ zrAi0L)GC0 z2YGSIu$G8d(@NOi%|l)`85Dnc?%^7FiMqCuf!UK@nWj1`JC;}z(8 z5VkSleJw8AvZ;;M!$7t684Trl2}COsnV)82&=s#2`T2e5xn~6zAHw`Pq?00>zT>cR z*M(9~3wQ$HhBxhNSwgb*0^l5QAoM+oquas+!Bc1Jbm2}<&)OA~;QD%jU<#8B2=Vf+ zXqBTFj@6gjDNdFLU=+Y0hJfx4uUB)WMKdA|$Gr#)Z+HsW#AbLfmzV(94wTbns6$}H za2$IVB0%)fIf0mf#d=^kxD=tI!JBe5V1`YuBZx9cC%9b0NEIE->sG7!!i=+vs-q-U z&@x`GPkE0HmJl1B7fc}R!S-dX#Yb7f>im=<2nCdA{UjuDh4#ra&|(kJ?Alp)k)1*Z zF7XFpjSBPGij!NUSOWKIy^k5-w1nCguIt%M4#K7w8mBHt=mQ465MCg~EcVLgA2B9J z?1u>?Y8pa84cUyb?~PF8WkIXmE$yGq29EbN(`;(}c?5Zlj@-zVPa!n_R;SrxE8|C# z?B{d{QK|wn3x{CFFpwCOL`55b4}xYNr6-j_+3<(cMgrV^H>ei0}+az!2TE0O!6ECtDbY#B+a_&tw#+ ztdH7>Lu-LZGS`C&xlXjqY&YIY zl=8syWG8o`Ge!f!Ibru`Ey@}i{tdYC>!BqB`%+Eu*HuVe zzv)y0Jw9ljj>O9pl-q{wy6oZ|!g}zaOJ;_3m7yF_NjK=f`2THtDmNH6QU7OW02&kk z0MftOQzo{yPBykq#*Y6qJ{yxaqSqM^LT;#^aEW#T4WrFLDx0OsS}R;Oz;C(0@}uDE zVf|ZfI7kGr{|L#qF&JIGe$2QH8{IdetJfY37fZ?_o`q2C#+Q*KT~NL~InIU4>)|Fr zP9+{HIqjnHPVxDsAKpn)k|!Oo0faQ#SFfR1XD%=fCtJ7XHd8B_B&txa7)$CkX%s~s zXr+O?2qB^MG%7@DLSuK!w-ulzaWvbCO2JI^tpS6lT9?{;`I^Lb5DI^QiaE>IfI<*m z#&MPVvRedom1CLBLV=W96zX%&C+&(}iFa|CZ$>ZHB;>mkYH)^o_)|}jN>?W$F7_3u z8wyU1sJa4FmPzd_&^UwZg%m&uE4|JMA-ZKjN2hWj`hS$uBmuG}3x=mHpE3 z@Oq^0p@Lr^`6`&us;k*&QcS$%`3XN!HN!?VA-KrvRA*xtDN@uZ3@zs`l zFr=F4CA**ah`22MV2cZal^rCO#gifVXfrMPC4idEZj+QAaTp(hE)IIS1VEh3MsxPZ zev)huI2#>j;${GbI+8ccvEszz^LLXMyG6xJ<0Iqx{pNeyH+FZRit3gz`Rd4Er*&xv zwP3AVTk`nq3k-@NET}w~pgoXJkP@WVLf+a?hfwUub*_HfQ99OnqM+VA`3~Wb5Nvhe z>I`k4I9SBVE3|rUKNNf2!?OMBUG?pLe5b4eh7We^XZup6%MKX}9ivfZ&4hp?uA z5 z;`7x&BRpb653vA8&TC#on1 z#d-nqFGn+HXQiMy;%)?;2Rk}hLI44d429UCv ze_@fDLQLIm9NtazeUt~$F$KaNaz!=?3r{sEXe|yKokE4D{_b1bk%X7~T2i~pYajG= zP=}6Z9+$!7RAdR%f~ZbV&gRa1D(W**6n<_ASZ%1(Q4{Ym?D)9w(8y6-1Lzp7xu2;| zxZn7-b74n>X8(Ljv^T?RNeWY5yVj~^K^K4F+9_ZmFbgCtv0hhbIZT;>0Pm(_$I=DQ zT*mk|nyyNJcwJ}lTK$U-TjLGGdg0MW|2z0bPesLb4f2hV(H1hlmcD)c^BNLO*Y_!qtOwYVVZ1v;=us6 zhGZEEy^gYKX{5d1QtAV%E&GM%`9$}l!Ca<=V@BHZIFxm2q|fWQX;Xe}uM(^(xwNs5 zVxkVohZc;eRl;3o+tSu|?qGh=IG`u}-m{3l3Ws%u(ei=p_?Kl0POSD!XlM1$Qob^)mc}z zd7VtMpJZ}6HhR8}cR^@|#xnidPuA{MuBXL#HwxR&HQvE{gofQPz!?kDBLTn83nq(80faA=uLW!6}AMn#ALdJ+_L(D_m&6f(?j}0KsWA7i5EK{Zy z1sG~Z4HPf!;>04v*YNH6IGBQ$`}X)-^tXW>-SV;aFS;WcxR>d@50P!Ym0XFXpkR!l16HT8DwVDwo0M3@ zK21@#1?RV?ah&Y@KWV3uhlbhcmWUh#jr0KHZGHu-`wfsdfV#Un-ycI4JKv4X3J8B% zGyGB+7&Xch--^or();2rDZ+tbBNzoG5av|DN^qe)P^RA|d&`(LF~_RW>*Dcy*ngb1 z9E`-o;D0jddTQH14+^SYU&ZKtzhCA(&D8Alc;821&eXeETZ(e5n0dc{JVl(;WcxfF zmyml$U=3Um=P%m>y(BR(?Jwx+xd1R_m;~N6Sc-%+ zDUf{9(jcIGEt}Wc44om^%5M6FaVe+4ddZRyEKNG0igA944iogNAF}P{g&%H5zNPnJ zqG!Vqkg3{2U^0(w7BWX&I}^+&&s}l5p`D43&#eY+f(04HX)`?wA;?5#D&UyZ<sJ(1jLJT?-)F&;s_T9QT32Y*e;3w4*W57`^WE zJG@;>t0Kw93?mkJVA;XAAY9le*=I=KM}0S>O(LR#uSy$~K{J1!SZc=xqOCN_Njq>B zW&Q4&z(i&O$0q>Z(G(?lc5R*l(6jbu(E|fe-zavX{>AcALUhkTSy!fDQ;5I52s@ON zPA+sO0Xi#rc95-mMhq3Hw!_P;H&eXH2rQ>`dUzDSbWaA~qOs@}1UEOLyWrHvvK_pS z#V9~8nH~{;?5jI}m~|2%betdCMqxBbbRs`m*c9Ob(C9W<(DbnID4DVX6^Km@m%&mfl_GPoR-q-c zcfl+aCSlIO{Z3g40w6m&qTiB{uAC)1a{2u1prgoR33}0R85EH6osWbm;*6vm4C!fT z7m{RgdOApCp*Np!>7-sB-?1iaVhrfMt@VN=@>gqkES@Ak={@vKc3a6qWSMP;!K#ju z1249mB?;4Bal&YjJ_TLO(jN1(GqZDr|@Gm$m4G4wG94cWk%nK`>`4rB1Fp#Hof-aRa#{e zygr48=|wR!4(G%dMjTjzIVX{Y+Ked_jMLdjG~7m1;y1wq#FqGSXS4(`N6QoQe!_!) z=hKCMG52L@*pq5c#pFLXlk(;R$O$p@DAbSf_*Xo~%bMW-IXka@{PJRc_jj5vv)xJv zKKw;yeyl}szszzH^U!g~TshxaQzxnTVo_~L=b*h!YySpKC`_v@T|kD3U0PJY)x=B# z!8AcD%zP8gaj~JtYHKcG z8x<8^QWyxxenw(lBHJ?4kCq!n<4%Gp#Pz=OB3>^fzA3w&)lG8$IxQo1GMv%!f_imT z8ynPMzQh0U#ybh6gVdb%+_PyCDA7ZXYCxKwxo7_EXZ(FNl;hjEMvg=cYE=(Tl`36s zu)ag3LOzg3)7&a_AGwkW*m|5V1jRRG@|)0pWic>7jaAV|ghC-yf7kuMry@k|JxWia z!azPw$Zl*!GJ?dbiwxA}n-lU5t3%bOFQT|#8Z$!JpYm^S+Ge~4m-SMO1+d*k;-!yM zxV8xH&i0Fr{T46!QNf_)<^oXDE@(0mUTOZcV1IPT0Qw(1>m2{9=RcyTVei?TyiYuD zoS=Qspa%ho{3V8)=NJKp$h=73ndzftaiLH<+#>PaWQ}O;WYqpaGMjAGDsfuY80cmq zeYjz^@Is5s+h;xN2hbGh;L{b`d}VbM1dh4;JT?4_y-->KOj@__T1Qa`o%%j=?AxQbtjfte{%*k-aV6^@ybb^NuLQO8cx9rR^dWT6b8fE)YTpU{%6J z0-o~sva$EJG$@n^Q-pzgcUgzX&Tba`z!nL$d{uUYFi_Tb4K zt3>`#t>%_KaAY7Q(NZl)fN5W4`oR{`f|_mCl=rmdcm3N+I-Psri$Tx(^(|7jyq$n_ z;Q@4N3H3rRddtu=O(FUzf4u8MN|_-($nPGZJLlGTZmX_LjT~?G9Mvbs(eY;|}6k=sb5^3Wa!snwVV^i?8j|9ZBAu-^<$)E`Q&UYR}~!m#vf8&4j=SsJ{3hHp2=|{6vP?oD}{#q?0ree z5@*Yn?x!R9Ghnsoo@0+P{Lly;zTn1q-iSUy}8p?#7qy zO4#7e0LR7q_v^i|>}H1RfDK+Wj{o9Uz1^O(M;FZJSmo5JsraSLzKs!5;a)!SSpHlg z^4RdUmyM3i-_#V9d^H7LoJ$Gz79C^K<5CUC01tK#bzF<+q*f_N$G=bm^?^A=?hOvR zdU}BBgp$k@%;2ew)sw$NCCsNE!NR8$Y$pi0${1pH_|NAp5z%h+`pRhA(My|UChGp` zuSz0;A(uEftyMaZ59#rG0`)aN+LKFXM(?eWL#6wff32LhF6*)VjlZ}b5id_8FgT982aLAZ&_Cfsz{UXo-o8VgNwD0twD zDHXrpLrF`}zHe+c&=};2x88a=W|F9U(#_xXJ933GzSWA697}!9_COt>@Xm{E^cdO~ zAc_jBP~jZ**awAfWZMWaw6Di{s~vwH{rlf;Y(D`BR&V$zX}Bc;08su_xVRb{DE+56 zeVI9Bv%!Hdas&DX7jhkC%JIRHjxvg-jY5GoWb*=Oq#hkCnNL7zzfht(?vU=bAncX) z%z&OzJlQk06B#0${=DJ3lkQ=!_AHFLF#$tQpqXfm$gbk)nD+U8G=QB$8^UPI?vA^0 z&(uL5dbzc9p=SG1;obICf-~|stskenuD^tmvSx$RvE;ys(osJa8nK#W-QF7Ioe*$8^PtQdJ!ThYEJ zX#I`UW#DS(kULu`Fu){lunEzDf3C8H;pVm z17f9MJ-tvX7G-Sl`pcAJCV>&2X~0{_X97*8?#*psm``^_f-+CNaE=zPf8pR!Uu{g7 zZE4HSOXpHeX@?i=NIs8KOLt`I^L?PHqf>S}g|=k<5I<-9>aRRBzr}a8jSGL-7gEPb z(uzHf;X}{cV$6jMf70*>oAvz#O&3PD+)W0Qwoc-R1L8&MCSwoo0gVFx*_u1;3OO%V zjrF(F;R%a2ADjnWb~ClSJeapVyC(ZWL6QJo7=tZpTsJS`KJmRcJ0 z5N!WW>8^eI>z3E@Z#QEK>V4v4seZg18$73Y>ER za3S4I?sxSy1pQ-8$!btBa}y__-Hr)v0@;SmBrH?=sO|J{i1K1hqwi7XL8P!q3XERd zdl9@mQ0^!BZbpB4HI}bjK^Z*11I&|?GUGFN+)DyvDkuqOq_!;@RTJWGgj4&lO|u>- zHq+XO{e^@@UN31?(<23WcRlFbHW@UocbMmcS5ZpX1=2uH&yTyAs2W-|!rW~A|O z-SKDhXbM`#Jh2mrY9DSagL!lt1%c$ENmlIwU(!2O? z$Pm&G)+JpMecivp`GWLf(t1`8R<>9ZtS*Pk%eqtW4ScsoKINZ7B zbosPD!T;UrL{9gT;P}rhtTGk=;OFB%{$AkdF(D(ZEbWkQrG&S8hN}%KUO{CKA zvzU`<#ru3s)HI>vwQG`NgqnZH6J-xeXl+_YHMh8eW0741^qVwr6wdr^)tGIJeb&U3 z;UJQFaPORyYTTeRYoUoq#)LM{evqX{3T`2=Zx6wg1(+p*V@_^Rze{x$Z4no;f7omO zu#8g%;O6i{1XzVVfC#OtU~FzJ@=s90nUsZ0*r7e$5UhpB>28%&7cGVq+pmUny8(iv z`J$J7CWMa;3!XWRtc)i@=WlqB{$A`k+4ZCt zix}V`JDxuA$XMk^2WQg=ao4xv`%~anODgLj(~x-6O*q!4SIBmxTgeVHK|7MKZIQu8 zMR=yt7F8Uxt(AJkjp4=U>*PhoCi7Yb2S(!QWKRLp;^ey26M~$4p?hBHiFdF!8Tc+_ z9(hyLkV4a0JSWoUiUZR;RQ7k6oqoC@)mTdmJdj-OaTvNiOzwapzsz2bD~9-OEwk9x z0ZV0rpEFKl7F(!U@^$B+3wO*QUTkJ3#ECL-=%x4<$D=d`$^NN|R+d)!cR9#{k5yxi z64AUALnl08Moa((WW?sFHS_Vg&d!lSDRLD4`LBdfuDNO9EVGw9*~S-n@^#-*WQo)l z_k511Q%%xeZdiWpkR$n`EYTuXW;hYavm9t;x%M=Y0tdf=aH9;v4OyZ|r4czmp{*6n zK;Bo8uC7`$Mj+TXr-TErua6OXVO<^jT?e)@ced=gHN3s0$|YjQ+h&AuJ?x>8%2n?G zx?xPTdJTGLx>Za%9WT}#GPY*@6{mi`5Jw&$Qs)ek)Oq%i4l%RlB8v&VYvQ{1Xdq@u zWX)r%(Ujo(+DX*5Yt68n?@EC4Q)ud%L?R2Q~ZMFi$?cNd*E5 zRO033ew+FTNgF)X;E_mw?+f-n*5LkJ%=hh1rbMu$lgpX2$iXd`Wq%a!FQn}J5PE(H zJ_}}_pDvi_;nH`<5S*9Y?XH;&bL)PW*qn+Jvya zNAk`fMqCF-;Tx`Fjjc@ThKw@aIA>PBaW{q>OKDm1$lYrkxFfA%)$|PN+_^WT=4QQ7 z+R>>l=fY>3Kwy|Tf40wd?GBakOI=zj0sZhVOSN5ON zSG=o_Sw657hN_a$bUr79$lWBf92l=T+ZG}xlUyc?ej(8Ri7flgOv)dh=oJ6)FytV6 z|9e@IHSAj0+Z~k8O(eOC+bKJi<7r^yK@1v++`k8N@a(7}rH@qe50etdF#IkeHB|F$ zNZAUM?=xoB*QOjWygOp%w)^>2Uda;?qzjHGh7k%<1P|ezxk=`*&aiy=it|-jr_vTC zbY~@ucPG{1TS4Zzq@}OjX9*;0>z5M97#Tj^uMdw?E7)pZQ%k#UF3-$Zo|r0h^{hKD z=a#{Tt(~u-oMA)A36qM!9AGH{TRa-TuC`dx3X$~p%eujbRsHajjvhQ=(S7&d@d<7{ zqx+-Sm`Ic8$Ug)2XMiJ3QUiY8>@oJf?Ti}wJ@sUW`MakBVd!=5PhQ;KoKeL99#c<~ zq}FgG{E(yrUjH=QHTX@Ncp+WdhA|R&4`l+L+2%?>xBsq>G;+-*$RU6H|YT+88vso|wx`6g`(b4b~at_{Yl64mP0&W%AQ zn>S{Z#u^P?^9HYxMk`VNwJT`+7f3TmUY^zpjchhN)|v!Wv>8HnHMD;$6d~aTIg=MzsfcX;f-oeVicuM;`>)ft7Nn)|I`qGNR{=jNtD9^(5Qk0pizZ#gUj)FozVX;^r&i3|1a*>I8@RtWZp61 z6Q=vrxLv#RIiF;+X!#&vgKV*hTW*zvTk&21Ar`3(9yNm-ZQL~nOOj2-m>{*7TyaiT z-AN4Oy!CK!@ch$Ec+9pno{te4wJZkknqNYyER$Xr+HM39*)F7=myu$)$ZRy$T|j~f zOlU5oc2?g`=no~<+e|irImjF8q}9Yeo*lC0TxSTvvzeF-A#}3SSteqCB&%&`wu4^QHu3fui{m9<`smjGBAc~IbkC662f?t1xKQO zSqQ}Mazdcw^K9UH)Zr`(h$Cs%Acq1h+3Q+mVGx=n1i&9$!Ytgc!~exgAA)k$tZU~FGc$mVX4uom;HRNQa&gzQZ4^c%d*gFTKvUYtznVB z&da%hm|6ONV*lu}Dr`9lH21eEMO7qg{A0)E$AlvCQmaK-_`mGbEB=f5&rL|V|2L*Z z{(t!e^VBv-o^HU4Ljgu2yEo4EB_uiMh2I^JJB652>JGvM09#8{8|z|KgPe!thkx%t zZ%W-P+wLM23b)tG0gwFYIgsLZijblpA6r@}2tK1H`3?uw7+r52Z!J6~(NC2F;8u+Cjn z=hUv}-k3TvjyezN-+$K0I=sCqYq~7Nw3OO;4#*QX24hm}B^_U>)n*1A9?IOLFi_>3 z$41IVD|6FYVd#CsT^a8F?j+a(An@zd46^Tzmg}8MOLvu=9m#Ah6sfJ?`(#k%;8ZkZ z=o(SipsVE=RWvK-7%6Q;(pTh-VyTbKkGl|AHilQWlWjBm7HB8%e@fl4V~!^-NA)PS zI)A0}Z;J8RzS`xint$5qPSwBtTzNHq+3E6{l>=bb^z5sFhOHlC5g1zfoxT%f0vUBu z3Y63QiAfn7yZ-uuC(x1XX*v7!R%EYJEXi7s&Kgs4zX=mw-pYx5eu=iANgaq8dKs`j zvwnJr&*+e+L z(1n#`UFdMBy6c+?xvXxsj`p*g7A9D{iENZXZx$5ztpjemr{(FIz;;zdGuT;Q6>N7c z?|D{8i^feJWspvh{EAPnh*;I~mcU-_Kc`6x)cu`GHb$KvZ6fuaM>M#&z1N%xySc%y zQq$bDZdwmf4`j83UEtFU9{9Ez)y+L~Y{7A#ax-&zW^9#CWEU1EBL0-v$a-XeMf|E^d;BUtWu;XzT*yi4xYX=4H#zca z2g1y>gz%L8_~=NXFr=7C@hES90`*O#Mi&QWk-OC`8&yJsj7tGq6*$)}JYKC%nef4?b-b;|2o(IVs2kIY>2pi>>+?#j2eSKh1i$<9&2}m%g|&OE70#6Vz{R8M z`G2%?6>d>&eHR32Bpf=VVQ7#V1W8GyyIY2iLAnP7B!(I~Bt%4V=u$cb0g;fDl8_ET zgm<`~_o}@2`Tl};=6TN9vu3Z~ndh9n_w0Sn`mGt_k*3_7Gb(puMgyLaZ!uqX_|OL^ zdQND9d>^5Hdt-WuO%o)ld7*~p@`ewk#QBv+Oh-(X6jbMQFkZY`uHV~Ox@2>bsTLB` z^*wQBz)R|2A$ks@d@7{Z<28KBsbj=q+BXnkb0(F4fgOQCyT5aBbmIBua(`CWG~dz` zxlo2)PM$YpN9bmUK8v&?(UY}8bU;A6Xl^RL-ZgU;NxCZ#6=4ree^|GVYx0hS1w~Fn zu@T)STBKf%VBdh5&qpTn!`EQLiy}0Yae~;7j_$SG$JR%SwzTfC3_jCj3!1tp+P6xJ zzi7~myD5@fYJL~d@c)WX$26Cx>Fw5)cW}+sKXl!(9aFNtEuK|3Wwz=d+&;K%Q|I79Wi|~*CFRn z0_JK3#^9zrl=C@i`sCx*q0#7$X_Je#i)LNlTF=rwGB-4*KvwuDa<6Y_*-CYe`!P$m zzPtUreF(MT?Pc-|I*-666RaxJb?>+o-Nr_1TaN+4P0}n0(O9Zgl2JK{<~IZV#{PG! z&TODZF*c*K!V=TUwJWBlPcQ#<*)O4M_2xDn3d-u`Z?Y{X4@V1YH{>IjzZ40^vkCe2 zB(192bI%8~{VT~V8TaOE#0@cI>@*M=AGQnJ9yP5C37*$iG>K;oG(V)Jk4x5#kPLE+ zCLv7tlqfE|1eV0~&xu{QCog@rSLCedcR;zC!KS>!YWwP;Q#&M0TyFi~$Fj5OIl;$e z8wCDn58cq{ZoodN6N`lZ`O=+$nb|^~KAF@+T<3@whWmt%Dq%0BRo8T>OR`S+Muof5bGr1ksTl`2~Hz*o{QBq&R}CPh(goF3>X*Y&o=> z+Z;|0u5wLz9?hP4XV#qqhfO~E+K-6X=N$V7>NbaCPQGg1u3&ZO)?TgCu3(9ST6v$W zgWl~g(6b0K+=s}_EWB_C{%ktRF8rV z8mR_dtXcaD+{&pv+&wGz)~9cmJu)ZZm9ImyD#p)%ra;?13|dfK&NT(^Vpz?8TK-s!=Po+-K#0dObbFl9$L(YmG6 zae};gz=3i-$G~vpKqhU>sN;yAD(W7A!R9-O4FjR16wfa#L~*Qp5|`&GD3pA5Uh8?4 zV((;@=KIDUU{!$V_P$N(;f%NU!<$DUIlb_|e`iLFff+<;YB^boy1*Dkvo3-ZE*?ZK zibwh`17o3!8~h8D3=4Mb2WbY`w0ILR)CmBpu5Rk^)c&Da{R_N$IAJ}@fw$h(cL}Hy zq^J|`Q{!IkLkQ{-j1P45%=-le7Gd@ZX`7RV^#IzZMjbCI)iUREj1m`o_0T8dakeun z5>RwW@^HF=T!d155p;x7p^G6{H;5p~3@^Pj@yVnn_W@7`oH!YD(4?CGyuF@%dQmx+ zg$>gMSFWHOI2UmmF+Z2^l#~0}s8wfMaAg{nN#somew*-zBE>MiCLN_7aF=I6k}gcC z2vR`nFR$eNpd6wkZ#Q=Z>M0|^vH&F5)#Qh)d4dFmC1EP-z26q{l7_->(uXmTQ$x+R zzD??Xsg~;qCG4mboRGEt5oe4N0wa>@O;%&&*n%l$Cz;8hr&WdEr&%ds--W_}z(JVe zizJjQ@CM!$$R!H{a#UY|xzblPajrnttD2ctpyU;J1G~~{C_Id?i^|Rm%bXH-T%Fn= ze~})#xdCsI52 zV4uwgh@ka2IwTM6KhQv5&(ZFG*?TO<7D8h<=2z}+;=PEq>81Gc&~F&A>M!w~n0KuC z2u}U6f)&5}*o#smNwP0-NC#l$ejF_FDeQF3MLE5cO6I^ca)Ce*~9xCQdkH5DEatxA*5wZpdi zPt}#-$04#LOU#*N?zcoftyjz?gtgGAcd^W_pFP^YA7XhRM*7mAF=9+VG)u)%V%1kF z&u)cSWdfv|@fGrFYG;I3jSFA`GU5_>1{xLIE}p10w+kOx6>L6?}o{`MG5Od56rug1L8YB}lyCRkoET-Z7Ng)6FFe)QL2 zLejUC);pvaZ}+Kv>np^1x{ln1jr6iD#gLn&&*?DDG7>+$6#H;Tdfhd+eQT+?>d7m? zp<5Z4Zk@cPM~$7&T{NUPO*jf+cf&LH^hR(&ONb=tA#%6wE5OxtbBV4X)mT6bE6tLm zI9ACXS=xihNGaUebEM%+G71d~OW-M6XjGM+X9fdAw_S!qiuqYbFWjc~iQosTWdXjrON7C%a zoQ1HcFfGn2z;LFKB+SLppoo5aq@t}wtID3--G5OMa6_5d6x+K(i=Q(Y4_SNwMO9|T zjpi~dwhC6i$(hW_n;le`a8r(Wr%{Xl^YnrGU4P>V$8E(G7B{g#6;r96(!jP;>Qb_~ zirRZ%ids!i5!ae-Dm0zy({t(^R4+r49RhWPa@LF_=LIQtygfZgsB_&>(DJ23{}Y!w zf}35rT7|ipb9NILT8TFyK8A&~XO<6?Hb)F-2_tnxN7`Y^49^7EBVK5C`1q_S z)k~XjB0yhM!=rk)0lAFaF<_@XctjA*h&!gon~|J0MXbU;Xw#yjWG+-W+K@uqTDn%;>V~F(sVe%Yz3I5gkh3^Q8gTOELr-i z;*G1qyfvV`hA77=ZSp-{j1Jffke61wV*hLi-}h!U=(l?R5D3E#UjXu8o)OaZNTVtm zW9>&x>FQm4q#@$bV*R6)1V7`7+lH~B=fT1?{_p7Md4yMmQ|^4iGJIB#`Hz*0TBdbR ztH zqfYFE!|Zop4@U<-)~xz|10Se_(R=s34nWJHzhms)X9u#?oDMGVu#?U%feg6PK-Id` zwQK*gldRV89R{~G)jCUJ?!-A1#@=2KqFyEtU`+!$9Ua}~9( z7%TotCds3q)nnAYE-Im5eJ{)I*jE5=oXE(xo1*qPqhX1S&A|qip-fq2Y6@?x;b%#C za`?tZ?On1w#xj?$mCueCV7PQ-N+NV*aw0UnH(|JMEF08(_FWn(T(rhJW z-jo+u8}vh~DaSa8sKMc97xhqn4jyr!yl3A!=hOa~r8GCleA>1fOFs2%cVUxJ8i%-M ztW(5jO!;a_3H7XQ!S{gIZV>0#Z8yq%>ST%8mHy%N;UvUxilrc0UP5^WnGPuYO~tE7 z@{1}{t_7ZQa}_4*=e+huAGQ+@6?~?kM<D1ypT&eN(zfiGVezLEmpR1yZZ z8Y?@!I=aQ(<9u@Bo+^B>^IM|H$tiLXVvV@q#A+asg2T@6j(@<#)|Rb0Y1>_L4h}&% z0R}ddE)M!}zi95qiMuE_Kk_l_sRPOLne`w^JKI)9Ma}#!PVD&IiWR}S8bT{R(c4*L zz;5`wOrOIt_DY6lGBE9+IL-3iH^vOKSnR1DW)YTD%X%U_kMf9*2_Yv5XMKRupapUq zHYw0Eun&hWbDflRt;CZA|x_C=P73OAR>_)+Z9~S^q)nV@Gl5h<{6IV%Z z7O#y04!Yr8p~HCpMt_v;Mz@@AsZ?fV{HGfSjz@d6el5+ly)S0SFR%t0`P(c!@qb(Y+917%wahIX{%XC;@Oh^(b7kF;#_#p~i zH5qlY!LG8x@z$sSSRamq2{o@_CvG?AB~Dlba9ht?=(W~$mjPK@-vkpOyi919!Q#U9?3+f+?LmYue2 zVZ`cR3yF%24_;E#<(9i%CBi#L9smwdA z_by%2L*Kfd;Z<)$)?x3w$xAe|Vei2Z#bWO+yYCJ>J@V~?zvI01cvEAsK7iF067p^n zwspo8rWtCCaKTaN)||dmfP2;(D7!Q<@*R;le4t^IQZ>8yCZ5+D@cw;K1ob?@tKAx+ zsq~2Ze&;_{zD}LUn0Z*Xf1phWpHCf?yq~VR)J?1T-gbj!a5!Irb)LJmwL#>fI;fkp zT3`4QR7N6?PiZHcJlY;#*Uur6`A%iwN%KACr=^igTi*^>t8Lo@QmWto2-GpFuM=`J zj7eg(`@!?CU8QAAv&auQ&cDA(s``DLH+OOQ-!P9nWj|k;8h`m!H#6=LsBf1hqLAd4 za%e$Cb`vNU9Hufl+bg*~(Vd+*U_t4emaFe=W(`K;XPnGgIg{r-d6Jbfz%cw?si@j< z;?&Z=2C277g=)8pJlph(_Z)L4U-fK{Wp7wfOz%P_vWl|q@75VH$#>vFA2s6;Xp%Qwh z(eME+L+zz8@r#e*`)(dep~YdCBNod&Z@X_~8>_|Zj^j+~wb$O3>XoI~uM#Z@dWTZK zJ7)_d?iz5e!Q*aZu|CO3M+Boxo@2q{X!<8|$Fj-Zu5!)TD&r`H^h8zP9Kl#@>W#w) z*2KIA#B&1Pn8UhFG)aJBIN@s^8TW`}uccGov}u4oraXCmVQ>|pGCPC|iU0vsSHmAO zP7*~$Q9q<)K`C1|384s(Y^r^5t#fc<~oz}F*PH-7yU ziy!-Eq~C2|uhXw5=Ki9OF#e?fk*0f{e?5=#7vF;AC;#`X%Ioy&S#Q7S2W&s-*K^>m zhq#_3@GAr+@6Qmwrwm-DUze}`qQ4gWNxvp$y&mDZHttu1c+lTJ;2)*jb@KH)?7zqn zia*JJ+;6|m|8si%i;aRZppAm^Uz6?Y^y?FxUjZWYejfgh>CScjpQHXSHnIa4{&x7E Z{{d>sH<7Iy1?3j<9fa&4)+Se<{sWmSjFA8U literal 0 HcmV?d00001 From 7e1d4cf480d080d46366928d9dc8a58e8deef683 Mon Sep 17 00:00:00 2001 From: xcw <2901058208@qq.com> Date: Sun, 29 Dec 2024 22:07:12 +0800 Subject: [PATCH 4/5] code explanation --- src/ui(x)/NoteEditActivity.java | 929 ++++++++++++++++++++++++++++++++ 1 file changed, 929 insertions(+) create mode 100644 src/ui(x)/NoteEditActivity.java diff --git a/src/ui(x)/NoteEditActivity.java b/src/ui(x)/NoteEditActivity.java new file mode 100644 index 0000000..707f613 --- /dev/null +++ b/src/ui(x)/NoteEditActivity.java @@ -0,0 +1,929 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.app.Activity; +import android.app.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; + +// NoteEditActivity类继承自Activity类,并实现了三个接口,用于处理点击事件、笔记设置变化监听和文本视图变化监听。 +public class NoteEditActivity extends Activity implements OnClickListener, + NoteSettingChangedListener, OnTextViewChangeListener { + + // 内部类HeadViewHolder,用于持有笔记头部的视图元素引用。 + private class HeadViewHolder { + public TextView tvModified; // 修改时间的文本视图 + public ImageView ivAlertIcon; // 警告图标的图像视图 + public TextView tvAlertDate; // 警告日期的文本视图 + public ImageView ibSetBgColor; // 设置背景颜色的图像视图 + } + + // 静态代码块,用于初始化背景选择器按钮与颜色资源ID的映射关系。 + private static final Map sBgSelectorBtnsMap = new HashMap(); + 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); + } + + // 静态代码块,用于初始化背景选择器选中状态与选中图标资源ID的映射关系。 + private static final Map sBgSelectorSelectionMap = new HashMap(); + 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); + } + + // 静态代码块,用于初始化字体大小选择器按钮与字体大小资源ID的映射关系。 + private static final Map sFontSizeBtnsMap = new HashMap(); + 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); + } + + // 静态代码块,用于初始化字体大小选择器选中状态与选中图标资源ID的映射关系。 + private static final Map sFontSelectorSelectionMap = new HashMap(); + 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; + 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; + + // onCreate方法,用于初始化Activity。 + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + this.setContentView(R.layout.note_edit); + + // 如果没有保存的状态或者初始化Activity状态失败,则结束Activity。 + if (savedInstanceState == null && !initActivityState(getIntent())) { + finish(); + return; + } + initResources(); + } + + // onRestoreInstanceState方法,用于在Activity从杀死状态恢复时恢复之前的状态。 + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + // 如果保存的状态中包含Intent的额外数据,则尝试恢复Activity状态。 + if (savedInstanceState != null && savedInstanceState.containsKey(Intent.EXTRA_UID)) { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.putExtra(Intent.EXTRA_UID, savedInstanceState.getLong(Intent.EXTRA_UID)); + if (!initActivityState(intent)) { + finish(); + return; + } + Log.d(TAG, "Restoring from killed activity"); + } + } + + // initActivityState方法,用于初始化Activity的状态。 + private boolean initActivityState(Intent intent) { + // 如果Intent动作是VIEW,但是没有提供ID,则跳转到笔记列表Activity。 + 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); + } + + // 如果笔记ID在数据库中不可见,则跳转到笔记列表Activity并显示错误信息。 + 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 { + // 加载笔记对象。 + 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) { + 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 { + 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() { + // 设置笔记编辑器的文本外观,根据字体大小ID来决定。 + mNoteEditor.setTextAppearance(this, TextAppearanceResources + .getTexAppearanceResource(mFontSizeId)); + // 如果笔记是 checklist 模式,则切换到列表模式。 + 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)); + + // 显示或隐藏提醒头部。 + 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); + } +} + +// 当接收到新的Intent时调用,用于重新初始化Activity状态。 +@Override +protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + initActivityState(intent); +} + +// 当Activity状态需要被保存时调用,用于保存当前笔记的状态。 +@Override +protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + // 如果笔记还没有保存,先保存笔记,以确保有ID可以保存状态。 + if (!mWorkingNote.existInDatabase()) { + saveNote(); + } + outState.putLong(Intent.EXTRA_UID, mWorkingNote.getNoteId()); + Log.d(TAG, "Save working note id: " + mWorkingNote.getNoteId() + " onSaveInstanceState"); +} + +// 处理触摸事件,用于隐藏背景和字体大小选择器。 +@Override +public boolean dispatchTouchEvent(MotionEvent ev) { + if (mNoteBgColorSelector.getVisibility() == View.VISIBLE + && !inRangeOfView(mNoteBgColorSelector, ev)) { + mNoteBgColorSelector.setVisibility(View.GONE); + return true; + } + + if (mFontSizeSelector.getVisibility() == View.VISIBLE + && !inRangeOfView(mFontSizeSelector, ev)) { + mFontSizeSelector.setVisibility(View.GONE); + return true; + } + return super.dispatchTouchEvent(ev); +} + +// 判断触摸事件是否在指定视图范围内的方法。 +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在SharedPreferences中的错误。 + if(mFontSizeId >= TextAppearanceResources.getResourcesSize()) { + mFontSizeId = ResourceParser.BG_DEFAULT_FONT_SIZE; + } + mEditTextList = (LinearLayout) findViewById(R.id.note_edit_list); +} + +// 当Activity暂停时调用,用于保存笔记数据。 +@Override +protected void onPause() { + super.onPause(); + if(saveNote()) { + Log.d(TAG, "Note data was saved with length:" + mWorkingNote.getContent().length()); + } + clearSettingState(); +} + +// 更新小部件的方法。 +private void updateWidget() { + Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); + // 根据笔记的小部件类型发送更新广播。 + if (mWorkingNote.getWidgetType() == Notes.TYPE_WIDGET_2X) { + intent.setClass(this, NoteWidgetProvider_2x.class); + } else if (mWorkingNote.getWidgetType() == Notes.TYPE_WIDGET_4X) { + intent.setClass(this, NoteWidgetProvider_4x.class); + } else { + Log.e(TAG, "Unspported widget type"); + return; + } + + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] { + mWorkingNote.getWidgetId() + }); + + sendBroadcast(intent); + setResult(RESULT_OK, intent); +} + +// 处理点击事件的方法。 +public void onClick(View v) { + int id = v.getId(); + // 如果点击的是设置背景颜色按钮,则显示背景选择器。 + if (id == R.id.btn_set_bg_color) { + mNoteBgColorSelector.setVisibility(View.VISIBLE); + findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( + - View.VISIBLE); + } else if (sBgSelectorBtnsMap.containsKey(id)) { + // 如果点击的是背景颜色按钮,则更新笔记的背景颜色ID并隐藏选择器。 + findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( + View.GONE); + mWorkingNote.setBgColorId(sBgSelectorBtnsMap.get(id)); + mNoteBgColorSelector.setVisibility(View.GONE); + } else if (sFontSizeBtnsMap.containsKey(id)) { + // 如果点击的是字体大小按钮,则更新笔记的字体大小ID并应用新的字体大小。 + findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.GONE); + mFontSizeId = sFontSizeBtnsMap.get(id); + mSharedPrefs.edit().putInt(PREFERENCE_FONT_SIZE, mFontSizeId).commit(); + findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.VISIBLE); + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + getWorkingText(); + switchToListMode(mWorkingNote.getContent()); + } else { + mNoteEditor.setTextAppearance(this, + TextAppearanceResources.getTexAppearanceResource(mFontSizeId)); + } + mFontSizeSelector.setVisibility(View.GONE); + } +} +@Override +public void onBackPressed() { + // 当用户按下返回键时,首先检查是否有弹出的设置界面需要关闭。 + if(clearSettingState()) { + return; + } + + // 保存当前笔记的状态。 + saveNote(); + // 然后调用基类的onBackPressed方法,完成默认的返回操作。 + super.onBackPressed(); +} + +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; + } + // 如果没有弹出的设置界面需要关闭,则返回false。 + return false; +} + +public void onBackgroundColorChanged() { + // 当背景颜色改变时,更新界面上背景颜色选择器的状态。 + findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( + View.VISIBLE); + // 设置笔记编辑区域的背景。 + mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId()); + // 设置笔记标题区域的背景。 + mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId()); +} + +@Override +public boolean onPrepareOptionsMenu(Menu menu) { + // 在准备选项菜单时,首先检查Activity是否正在结束,如果是,则不进行任何操作。 + if (isFinishing()) { + return true; + } + // 清除任何弹出的设置界面。 + clearSettingState(); + // 清除菜单中的所有项目。 + menu.clear(); + // 根据笔记所在的文件夹ID决定加载哪个菜单资源。 + 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); + } + // 返回true表示菜单已经准备好。 + return true; +} + +@Override +public boolean onOptionsItemSelected(MenuItem item) { + // 根据选中的菜单项ID执行相应的操作。 + 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) { + // 删除当前笔记,并结束Activity。 + 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: + // 获取当前笔记的内容并通过Intent分享。 + 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; + } + // 返回true表示菜单项点击事件已处理。 + return true; +} + +private void setReminder() { + // 创建一个日期时间选择器对话框。 + DateTimePickerDialog d = new DateTimePickerDialog(this, System.currentTimeMillis()); + d.setOnDateTimeSetListener(new OnDateTimeSetListener() { + public void OnDateTimeSet(AlertDialog dialog, long date) { + // 当用户设置了日期时间后,更新笔记的提醒时间。 + mWorkingNote.setAlertDate(date, true); + } + }); + // 显示日期时间选择器对话框。 + d.show(); +} + +/** + * Share note to apps that support {@link Intent#ACTION_SEND} action + * and {@text/plain} type + */ +private void sendTo(Context context, String info) { + // 创建一个Intent,用于分享文本。 + Intent intent = new Intent(Intent.ACTION_SEND); + intent.putExtra(Intent.EXTRA_TEXT, info); + intent.setType("text/plain"); + // 启动一个新的Activity,分享笔记内容。 + context.startActivity(intent); +} + +private void createNewNote() { + // 首先保存当前编辑的笔记。 + saveNote(); + + // 结束当前Activity,准备启动一个新的Activity。 + finish(); + Intent intent = new Intent(this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_INSERT_OR_EDIT); + intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mWorkingNote.getFolderId()); + // 启动一个新的NoteEditActivity,用于创建新笔记。 + startActivity(intent); +} + +private void deleteCurrentNote() { + // 如果笔记存在于数据库中,则执行删除操作。 + if (mWorkingNote.existInDatabase()) { + HashSet ids = new HashSet(); + long id = mWorkingNote.getNoteId(); + if (id != Notes.ID_ROOT_FOLDER) { + ids.add(id); + } else { + Log.d(TAG, "Wrong note id, should not happen"); + } + // 如果不是同步模式,则直接删除笔记。 + if (!isSyncMode()) { + if (!DataUtils.batchDeleteNotes(getContentResolver(), ids)) { + Log.e(TAG, "Delete Note error"); + } + } else { + // 如果是同步模式,则将笔记移动到垃圾文件夹。 + if (!DataUtils.batchMoveToFolder(getContentResolver(), ids, Notes.ID_TRASH_FOLER)) { + Log.e(TAG, "Move notes to trash folder error, should not happens"); + } + } + } + // 标记笔记为已删除。 + mWorkingNote.markDeleted(true); +} + +private boolean isSyncMode() { + // 检查是否有同步账户名被设置,如果有,则返回true表示是同步模式。 + return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0; +} + +public void onClockAlertChanged(long date, boolean set) { + /** + * User could set clock to an unsaved note, so before setting the + * alert clock, we should save the note first + */ + // 如果用户为未保存的笔记设置了闹钟提醒,在设置闹钟提醒之前,应该先保存笔记。 + if (!mWorkingNote.existInDatabase()) { + saveNote(); + } + // 如果笔记的ID大于0,说明笔记已经保存在数据库中。 + 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) { + // 如果set为false,则取消闹钟。 + alarmManager.cancel(pendingIntent); + } else { + // 如果set为true,则设置闹钟。 + alarmManager.set(AlarmManager.RTC_WAKEUP, date, pendingIntent); + } + } else { + // 如果笔记ID不大于0,说明笔记未保存,记录错误日志,并提示用户输入内容。 + Log.e(TAG, "Clock alert setting error"); + showToast(R.string.error_note_empty_for_clock); + } +} +// 当小部件数据发生变化时调用此方法。 +public void onWidgetChanged() { + updateWidget(); // 更新小部件以反映数据的变化。 +} + +// 当编辑框中的文本被删除时调用此方法。 +public void onEditTextDelete(int index, String text) { + int childCount = mEditTextList.getChildCount(); // 获取编辑列表中的子项数量。 + if (childCount == 1) { + return; // 如果只有一个子项,不允许删除。 + } + + // 遍历从删除位置后的子项,更新它们的索引。 + for (int i = index + 1; i < childCount; i++) { + ((NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text)) + .setIndex(i - 1); + } + + // 从编辑列表中移除指定索引的子项。 + mEditTextList.removeViewAt(index); + NoteEditText edit = null; + // 如果删除的是第一项,则获取新的第一项;否则,获取前一项。 + if(index == 0) { + edit = (NoteEditText) mEditTextList.getChildAt(0).findViewById( + R.id.et_edit_text); + } else { + edit = (NoteEditText) mEditTextList.getChildAt(index - 1).findViewById( + R.id.et_edit_text); + } + int length = edit.length(); // 获取当前编辑项的文本长度。 + edit.append(text); // 将被删除的文本追加到当前编辑项。 + edit.requestFocus(); // 请求焦点到当前编辑项。 + edit.setSelection(length); // 将光标设置到追加文本的末尾。 +} + +// 当编辑框中按下回车键时调用此方法。 +public void onEditTextEnter(int index, String text) { + // 调试检查,确保索引没有超出编辑列表的范围。 + if(index > mEditTextList.getChildCount()) { + Log.e(TAG, "Index out of mEditTextList boundrary, should not happen"); + } + + // 创建一个新的列表项,并设置其文本和索引。 + View view = getListItem(text, index); + mEditTextList.addView(view, index); // 将新项添加到编辑列表中。 + NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text); + edit.requestFocus(); // 请求焦点到新项。 + edit.setSelection(0); // 将光标设置到新项的开头。 + // 更新新项之后所有子项的索引。 + for (int i = index + 1; i < mEditTextList.getChildCount(); i++) { + ((NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text)) + .setIndex(i); + } +} + +// 切换到列表模式,将文本分割成多个列表项。 +private void switchToListMode(String text) { + mEditTextList.removeAllViews(); // 移除编辑列表中的所有子项。 + String[] items = text.split("\n"); // 按行分割文本。 + int index = 0; // 初始化索引。 + for (String item : items) { + if(!TextUtils.isEmpty(item)) { // 如果行不为空,则添加到编辑列表中。 + mEditTextList.addView(getListItem(item, index)); + index++; + } + } + mEditTextList.addView(getListItem("", index)); // 添加一个新的空项。 + mEditTextList.getChildAt(index).findViewById(R.id.et_edit_text).requestFocus(); // 请求焦点到新项。 + + mNoteEditor.setVisibility(View.GONE); // 隐藏文本编辑框。 + mEditTextList.setVisibility(View.VISIBLE); // 显示编辑列表。 +} + +// 获取高亮显示搜索结果的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; // 返回高亮显示的Spannable对象。 +} + +// 获取列表项的视图,并设置其文本和样式。 +private View getListItem(String item, int index) { + View view = LayoutInflater.from(this).inflate(R.layout.note_edit_list_item, null); + final NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text); + edit.setTextAppearance(this, TextAppearanceResources.getTexAppearanceResource(mFontSizeId)); + CheckBox cb = ((CheckBox) view.findViewById(R.id.cb_edit_item)); + cb.setOnCheckedChangeListener(new OnCheckedChangeListener() { + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (isChecked) { + edit.setPaintFlags(edit.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); // 勾选时设置删除线。 + } else { + edit.setPaintFlags(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG); // 未勾选时取消删除线。 + } + } + }); + + // 根据标记判断列表项是否被勾选,并设置相应的文本和样式。 + if (item.startsWith(TAG_CHECKED)) { + cb.setChecked(true); + edit.setPaintFlags(edit.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); + item = item.substring(TAG_CHECKED.length(), item.length()).trim(); + } else if (item.startsWith(TAG_UNCHECKED)) { + cb.setChecked(false); + edit.setPaintFlags(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG); + item = item.substring(TAG_UNCHECKED.length(), item.length()).trim(); + } + + edit.setOnTextViewChangeListener(this); // 设置文本变化监听器。 + edit.setIndex(index); // 设置索引。 + edit.setText(getHighlightQueryResult(item, mUserQuery)); // 设置高亮显示的文本。 + return view; // 返回列表项视图。 +} + +// 文本变化时调用此方法,用于更新复选框的显示状态。 +public void onTextChange(int index, boolean hasText) { + if (index >= mEditTextList.getChildCount()) { + Log.e(TAG, "Wrong index, should not happen"); + return; + } + if(hasText) { + mEditTextList.getChildAt(index).findViewById(R.id.cb_edit_item).setVisibility(View.VISIBLE); + } else { + mEditTextList.getChildAt(index).findViewById(R.id.cb_edit_item).setVisibility(View.GONE); + } +} + +// 列表模式改变时调用此方法,用于在列表模式和编辑模式之间切换。 +public void onCheckListModeChanged(int oldMode, int newMode) { + if (newMode == TextNote.MODE_CHECK_LIST) { + switchToListMode(mNoteEditor.getText().toString()); // 切换到列表模式。 + } else { + if (!getWorkingText()) { + mWorkingNote.setWorkingText(mWorkingNote.getContent().replace(TAG_UNCHECKED + " ", + "")); // 移除未勾选项的标记。 + } + mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery)); // 设置高亮显示的文本。 + mEditTextList.setVisibility(View.GONE); // 隐藏编辑列表。 + mNoteEditor.setVisibility(View.VISIBLE); // 显示文本编辑框。 + } +} + +// 获取工作文本的方法,用于从列表项中获取文本并保存。 +private boolean getWorkingText() { + boolean hasChecked = false; + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < mEditTextList.getChildCount(); i++) { + View view = mEditTextList.getChildAt(i); + NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text); + if (!TextUtils.isEmpty(edit.getText())) { + if (((CheckBox) view.findViewById(R.id.cb_edit_item)).isChecked()) { + sb.append(TAG_CHECKED).append(" ").append(edit.getText()).append("\n"); + hasChecked = true; + } else { + sb.append(TAG_UNCHECKED).append(" ").append(edit.getText()).append("\n"); + } + } + } + mWorkingNote.setWorkingText(sb.toString()); // 设置工作文本。 + } else { + mWorkingNote.setWorkingText(mNoteEditor.getText().toString()); // 设置工作文本。 + } + return hasChecked; // 返回是否有勾选项。 +} + +// 保存笔记的方法,用于保存当前笔记的状态。 +private boolean saveNote() { + getWorkingText(); // 获取工作文本。 + boolean saved = mWorkingNote.saveNote(); // 保存笔记。 + if (saved) { + // 如果保存成功,设置结果码为RESULT_OK,表示笔记已创建或编辑。 + setResult(RESULT_OK); + } + return saved; // 返回保存是否成功。 +} + +// 发送到桌面的方法,用于创建桌面快捷方式。 +private void sendToDesktop() { + // 在发送消息到桌面之前,确保当前编辑的笔记存在于数据库中。对于新笔记,首先需要保存它。 + if (!mWorkingNote.existInDatabase()) { + saveNote(); + } + + if (mWorkingNote.getNoteId() > 0) { + Intent sender = new Intent(); // 创建一个Intent用于发送快捷方式。 + Intent shortcutIntent = new Intent(this, NoteEditActivity.class); // 创建一个Intent用于打开笔记。 + shortcutIntent.setAction(Intent.ACTION_VIEW); // 设置动作为查看。 + shortcutIntent.putExtra(Intent.EXTRA_UID, mWorkingNote.getNoteId()); // 传递笔记ID。 + sender.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent); // 设置快捷方式的Intent。 + 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 { + // 如果用户没有输入任何内容(笔记不值得保存),则没有笔记ID,提醒用户应该输入一些内容。 + Log.e(TAG, "Send to desktop error"); + showToast(R.string.error_note_empty_for_send_to_desktop); + } + } + + // 创建快捷方式图标标题的方法,用于生成快捷方式的标题。 + private String makeShortcutIconTitle(String content) { + content = content.replace(TAG_CHECKED, ""); + content = content.replace(TAG_UNCHECKED, ""); + return content.length() > SHORTCUT_ICON_TITLE_MAX_LEN ? content.substring(0, + SHORTCUT_ICON_TITLE_MAX_LEN) : content; + } + + // 显示短提示的方法,用于显示指定资源ID的提示。 + private void showToast(int resId) { + showToast(resId, Toast.LENGTH_SHORT); // 调用另一个重载方法,设置提示的持续时间为短。 + } + + // 显示提示的方法,用于显示指定资源ID和持续时间的提示。 + private void showToast(int resId, int duration) { + Toast.makeText(this, resId, duration).show();// 创建并显示提示。 + } +} From f9261be0279fbd10039d02b7ee0524abc19fcc3c Mon Sep 17 00:00:00 2001 From: machao <2184367881@qq.com> Date: Mon, 30 Dec 2024 08:34:12 +0800 Subject: [PATCH 5/5] ui_m --- src/ui(m)/NotesListActivity.java | 790 +++++++++++++++++++++++++ src/ui(m)/NotesPreferenceActivity.java | 471 +++++++++++++++ 2 files changed, 1261 insertions(+) create mode 100644 src/ui(m)/NotesListActivity.java create mode 100644 src/ui(m)/NotesPreferenceActivity.java diff --git a/src/ui(m)/NotesListActivity.java b/src/ui(m)/NotesListActivity.java new file mode 100644 index 0000000..b5291ec --- /dev/null +++ b/src/ui(m)/NotesListActivity.java @@ -0,0 +1,790 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +// ...(省略其他import) + +// NotesListActivity类继承自Activity,用于展示笔记列表 +public class NotesListActivity extends Activity implements OnClickListener, OnItemLongClickListener { + // 定义了一些查询操作的token,用于识别不同的查询请求 + 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; + private int mOriginY; + private int mDispatchY; + private TextView mTitleBar; + 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; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.note_list); + initResources(); + + /** + * 当用户首次使用应用程序时插入介绍信息 + */ + setAppInfoFromRawRes(); + } + + /** + * 从raw资源文件中读取介绍信息,并将其保存为笔记 + */ + private void setAppInfoFromRawRes() { + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); + if (!sp.getBoolean(PREFERENCE_ADD_INTRODUCTION, false)) { + StringBuilder sb = new StringBuilder(); + InputStream in = null; + try { + in = getResources().openRawResource(R.raw.introduction); + if (in != null) { + InputStreamReader isr = new InputStreamReader(in); + BufferedReader br = new BufferedReader(isr); + char [] buf = new char[1024]; + int len = 0; + while ((len = br.read(buf)) > 0) { + sb.append(buf, 0, len); + } + } else { + Log.e(TAG, "Read introduction file error"); + return; + } + } catch (IOException e) { + e.printStackTrace(); + return; + } finally { + if(in != null) { + try { + in.close(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + } + + WorkingNote note = WorkingNote.createEmptyNote(this, Notes.ID_ROOT_FOLDER, + AppWidgetManager.INVALID_APPWIDGET_ID, Notes.TYPE_WIDGET_INVALIDE, + ResourceParser.RED); + note.setWorkingText(sb.toString()); + if (note.saveNote()) { + sp.edit().putBoolean(PREFERENCE_ADD_INTRODUCTION, true).commit(); + } else { + Log.e(TAG, "Save introduction note error"); + return; + } + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode == RESULT_OK + && (requestCode == REQUEST_CODE_OPEN_NODE || requestCode == REQUEST_CODE_NEW_NODE)) { + mNotesListAdapter.changeCursor(null); + } else { + super.onActivityResult(requestCode, resultCode, data); + } + } + + /** + * 初始化活动所需的资源,包括内容解析器、适配器、列表视图等 + */ + private void initResources() { + mContentResolver = this.getContentResolver(); + mBackgroundQueryHandler = new BackgroundQueryHandler(this.getContentResolver()); + mCurrentFolderId = Notes.ID_ROOT_FOLDER; + 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 NewNoteOnTouchListener implements OnTouchListener { + 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(); + // 根据UI设计师的要求,对“新建笔记”按钮的透明部分进行特殊处理 + 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; + } + } + return false; + } + }; + + /** + * 启动异步查询,获取笔记列表 + */ + private void startAsyncNotesListQuery() { + 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"); + } + + // 异步查询处理器,处理查询完成事件 + private final class BackgroundQueryHandler extends AsyncQueryHandler { + public BackgroundQueryHandler(ContentResolver contentResolver) { + super(contentResolver); + } + + @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; + } + } + } + + /** + * 显示文件夹列表菜单,允许用户选择目标文件夹 + */ + private void showFolderListMenu(Cursor cursor) { + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + builder.setTitle(R.string.menu_title_select_folder); + final FoldersListAdapter adapter = new FoldersListAdapter(this, cursor); + builder.setAdapter(adapter, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + DataUtils.batchMoveToFolder(mContentResolver, + mNotesListAdapter.getSelectedItemIds(), adapter.getItemId(which)); + Toast.makeText( + NotesListActivity.this, + getString(R.string.format_move_notes_to_folder, + mNotesListAdapter.getSelectedCount(), + adapter.getFolderName(NotesListActivity.this, which)), + Toast.LENGTH_SHORT).show(); + mModeCallBack.finishActionMode(); + } + }); + builder.show(); + } + /** + * 创建新笔记的函数 + */ + private void createNewNote() { + Intent intent = new Intent(this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_INSERT_OR_EDIT); + intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mCurrentFolderId); + this.startActivityForResult(intent, REQUEST_CODE_NEW_NODE); + } + + /** + * 批量删除笔记的函数 + */ + private void batchDelete() { + new AsyncTask>() { + protected HashSet doInBackground(Void... unused) { + HashSet 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; + } + + @Override + protected void onPostExecute(HashSet widgets) { + if (widgets != null) { + for (AppWidgetAttribute widget : widgets) { + if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID + && widget.widgetType != Notes.TYPE_WIDGET_INVALIDE) { + updateWidget(widget.widgetId, widget.widgetType); + } + } + } + mModeCallBack.finishActionMode(); + } + }.execute(); + } + + /** + * 删除文件夹的函数 + */ + private void deleteFolder(long folderId) { + if (folderId == Notes.ID_ROOT_FOLDER) { + Log.e(TAG, "Wrong folder id, should not happen " + folderId); + return; + } + + HashSet ids = new HashSet(); + ids.add(folderId); + HashSet 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); + } + } + } + } + + /** + * 打开笔记项的函数 + */ + private void openNode(NoteItemData data) { + 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); + } + + /** + * 打开文件夹的函数 + */ + private void openFolder(NoteItemData data) { + mCurrentFolderId = data.getId(); + startAsyncNotesListQuery(); + if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { + mState = ListEditState.CALL_RECORD_FOLDER; + mAddNewNote.setVisibility(View.GONE); + } else { + mState = ListEditState.SUB_FOLDER; + } + if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { + mTitleBar.setText(R.string.call_record_folder_name); + } else { + mTitleBar.setText(data.getSnippet()); + } + mTitleBar.setVisibility(View.VISIBLE); + } + + @Override + public void onClick(View v) { + 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); + } + } + + /** + * 隐藏软键盘的函数 + */ + private void hideSoftInput(View view) { + InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + + /** + * 显示创建或修改文件夹对话框的函数 + */ + 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"); + 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) { + // TODO Auto-generated method stub + } + + public void onTextChanged(CharSequence s, int start, int before, int count) { + if (TextUtils.isEmpty(etName.getText())) { + positive.setEnabled(false); + } else { + positive.setEnabled(true); + } + } + + public void afterTextChanged(Editable s) { + // TODO Auto-generated method stub + } + }); + } + + @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; + } + } + + /** + * 更新Widget的函数 + */ + private void updateWidget(int appWidgetId, int appWidgetType) { + Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); + if (appWidgetType == Notes.TYPE_WIDGET_2X) { + intent.setClass(this, NoteWidgetProvider_2x.class); + } 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); + } + } + }; + + @Override + public void onContextMenuClosed(Menu menu) { + if (mNotesListView != null) { + mNotesListView.setOnCreateContextMenuListener(null); + } + super.onContextMenuClosed(menu); + } + + @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; + } + + @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; + } + + @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; + } + + @Override + public boolean onSearchRequested() { + startSearch(null, false, null /* appData */, false); + return true; + } + + /** + * 导出笔记到文本文件的函数 + */ + private void exportNoteToText() { + final BackupUtils backup = BackupUtils.getInstance(NotesListActivity.this); + new AsyncTask() { + + @Override + protected Integer doInBackground(Void... unused) { + return backup.exportToText(); + } + + @Override + protected void onPostExecute(Integer result) { + if (result == BackupUtils.STATE_SD_CARD_UNMOUONTED) { + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + builder.setTitle(NotesListActivity.this + .getString(R.string.failed_sdcard_export)); + builder.setMessage(NotesListActivity.this + .getString(R.string.error_sdcard_unmounted)); + builder.setPositiveButton(android.R.string.ok, null); + builder.show(); + } else if (result == BackupUtils.STATE_SUCCESS) { + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + builder.setTitle(NotesListActivity.this + .getString(R.string.success_sdcard_export)); + builder.setMessage(NotesListActivity.this.getString( + R.string.format_exported_file_location, backup + .getExportedTextFileName(), backup.getExportedTextFileDir())); + builder.setPositiveButton(android.R.string.ok, null); + builder.show(); + } else if (result == BackupUtils.STATE_SYSTEM_ERROR) { + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + builder.setTitle(NotesListActivity.this + .getString(R.string.failed_sdcard_export)); + builder.setMessage(NotesListActivity.this + .getString(R.string.error_sdcard_export)); + builder.setPositiveButton(android.R.string.ok, null); + builder.show(); + } + } + + }.execute(); + } + + /** + * 检查是否处于同步模式的函数 + */ + 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"); + } + + @Override + 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; + } +} + + \ No newline at end of file diff --git a/src/ui(m)/NotesPreferenceActivity.java b/src/ui(m)/NotesPreferenceActivity.java new file mode 100644 index 0000000..7a59d05 --- /dev/null +++ b/src/ui(m)/NotesPreferenceActivity.java @@ -0,0 +1,471 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.app.ActionBar; +import android.app.AlertDialog; +import android.content.BroadcastReceiver; +import android.content.ContentValues; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceClickListener; +import android.preference.PreferenceActivity; +import android.preference.PreferenceCategory; +import android.text.TextUtils; +import android.text.format.DateFormat; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; +import android.widget.Toast; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.gtask.remote.GTaskSyncService; + +/** + * 设置和偏好活动,用于配置应用程序的偏好设置。 + */ +public class NotesPreferenceActivity extends PreferenceActivity { + // 偏好设置文件的名称 + public static final String PREFERENCE_NAME = "notes_preferences"; + // 同步账户名称的键 + public static final String PREFERENCE_SYNC_ACCOUNT_NAME = "pref_key_account_name"; + // 最后一次同步时间的键 + public static final String PREFERENCE_LAST_SYNC_TIME = "pref_last_sync_time"; + // 设置背景颜色的键 + public static final String PREFERENCE_SET_BG_COLOR_KEY = "pref_key_bg_random_appear"; + // 同步账户的键 + private static final String PREFERENCE_SYNC_ACCOUNT_KEY = "pref_sync_account_key"; + // 权限过滤键 + private static final String AUTHORITIES_FILTER_KEY = "authorities"; + // 账户类别 + private PreferenceCategory mAccountCategory; + // GTask接收器 + private GTaskReceiver mReceiver; + // 原始账户数组 + private Account[] mOriAccounts; + // 是否添加了账户 + private boolean mHasAddedAccount; + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + + /* 使用应用程序图标进行导航 */ + getActionBar().setDisplayHomeAsUpEnabled(true); + + // 从资源文件中添加偏好设置 + addPreferencesFromResource(R.xml.preferences); + // 获取账户类别 + mAccountCategory = (PreferenceCategory) findPreference(PREFERENCE_SYNC_ACCOUNT_KEY); + // 创建GTask接收器 + mReceiver = new GTaskReceiver(); + // 创建意图过滤器 + IntentFilter filter = new IntentFilter(); + filter.addAction(GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME); + // 注册接收器 + registerReceiver(mReceiver, filter); + + // 初始化原始账户数组 + mOriAccounts = null; + // 从XML布局文件中加载设置头部 + View header = LayoutInflater.from(this).inflate(R.layout.settings_header, null); + // 将头部添加到偏好列表中 + getListView().addHeaderView(header, null, true); + } + + @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(); + } + + @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) { + // 如果GTask服务没有正在同步 + 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); + } + } + } + + /** + * 刷新UI。 + */ + private void refreshUI() { + // 加载账户偏好设置和同步按钮 + loadAccountPreference(); + loadSyncButton(); + } + + /** + * 显示选择账户的警告对话框。 + */ + private void showSelectAccountAlertDialog() { + // 创建警告对话框的构建器 + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); + + // 从XML布局文件中加载标题视图 + 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; + + // 如果账户数量大于0 + 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(); + } + }); + } + // 从XML布局文件中加载添加账户视图 + 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); + + // 从XML布局文件中加载标题视图 + View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null); + TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title); + titleTextView.setText(getString(R.string.preferences_dialog_change_account_title, + getSyncAccountName(this))); + TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle); + subtitleTextView.setText(getString(R.string.preferences_dialog_change_account_warn_msg)); + dialogBuilder.setCustomTitle(titleView); + + // 创建菜单项 + CharSequence[] menuItemArray = new CharSequence[] { + getString(R.string.preferences_menu_change_account), + getString(R.string.preferences_menu_remove_account), + getString(R.string.preferences_menu_cancel) + }; + dialogBuilder.setItems(menuItemArray, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + // 处理菜单项点击事件 + if (which == 0) { + showSelectAccountAlertDialog(); + } else if (which == 1) { + removeSyncAccount(); + refreshUI(); + } + } + }); + dialogBuilder.show(); +} + +/** + * 获取Google账户。 + */ +private Account[] getGoogleAccounts() { + // 获取账户管理器 + AccountManager accountManager = AccountManager.get(this); + // 获取Google账户 + return accountManager.getAccountsByType("com.google"); +} + +/** + * 设置同步账户。 + */ +private void setSyncAccount(String account) { + // 如果同步账户不为空且与当前账户不同 + if (!getSyncAccountName(this).equals(account)) { + // 获取共享首选项 + SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = settings.edit(); + if (account != null) { + editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, account); + } else { + editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, ""); + } + editor.commit(); + + // 清除最后同步时间 + 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(); + } +} + +/** + * 移除同步账户。 + */ +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(); +} + +/** + * 获取同步账户名称。 + */ +public static String getSyncAccountName(Context context) { + SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, + Context.MODE_PRIVATE); + return settings.getString(PREFERENCE_SYNC_ACCOUNT_NAME, ""); +} + +/** + * 设置最后同步时间。 + */ +public static void setLastSyncTime(Context context, long time) { + SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, + Context.MODE_PRIVATE); + SharedPreferences.Editor editor = settings.edit(); + editor.putLong(PREFERENCE_LAST_SYNC_TIME, time); + editor.commit(); +} + +/** + * 获取最后同步时间。 + */ +public static long getLastSyncTime(Context context) { + SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, + Context.MODE_PRIVATE); + return settings.getLong(PREFERENCE_LAST_SYNC_TIME, 0); +} + +/** + * GTask接收器,用于接收GTask服务的广播。 + */ +private class GTaskReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + // 刷新UI + refreshUI(); + if (intent.getBooleanExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_IS_SYNCING, false)) { + // 获取同步状态视图 + TextView syncStatus = (TextView) findViewById(R.id.prefenerece_sync_status_textview); + syncStatus.setText(intent + .getStringExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_PROGRESS_MSG)); + } + + } +} + +/** + * 处理选项菜单项点击事件。 + */ +public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + Intent intent = new Intent(this, NotesListActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + return true; + default: + return false; + } +}