diff --git a/AlarmAlertActivity.java b/AlarmAlertActivity.java new file mode 100644 index 0000000..25d2571 --- /dev/null +++ b/AlarmAlertActivity.java @@ -0,0 +1,377 @@ +/* + * 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; + + +public class AlarmAlertActivity extends Activity implements OnClickListener, OnDismissListener { + private long mNoteId; + private String mSnippet; + private static final int SNIPPET_PREW_MAX_LEN = 60; + 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); + } + + Intent intent = getIntent(); + + try { + mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1)); + mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId); + mSnippet = mSnippet.length() > SNIPPET_PREW_MAX_LEN ? mSnippet.substring(0, + SNIPPET_PREW_MAX_LEN) + getResources().getString(R.string.notelist_string_info) + : mSnippet; + } catch (IllegalArgumentException e) { + e.printStackTrace(); + return; + } + + mPlayer = new MediaPlayer(); + if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) { + showActionDialog(); + playAlarmSound(); + } else { + finish(); + } + } + + private boolean isScreenOn() { + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + return pm.isScreenOn(); + } + + private void playAlarmSound() { + Uri url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM); + + int silentModeStreams = Settings.System.getInt(getContentResolver(), + Settings.System.MODE_RINGER_STREAMS_AFFECTED, 0); + + if ((silentModeStreams & (1 << AudioManager.STREAM_ALARM)) != 0) { + mPlayer.setAudioStreamType(silentModeStreams); + } else { + mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM); + } + try { + mPlayer.setDataSource(this, url); + mPlayer.prepare(); + mPlayer.setLooping(true); + mPlayer.start(); + } catch (IllegalArgumentException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (SecurityException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IllegalStateException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + private void showActionDialog() { + AlertDialog.Builder dialog = new AlertDialog.Builder(this); + dialog.setTitle(R.string.app_name); + dialog.setMessage(mSnippet); + dialog.setPositiveButton(R.string.notealert_ok, this); + if (isScreenOn()) { + dialog.setNegativeButton(R.string.notealert_enter, this); + } + dialog.show().setOnDismissListener(this); + } + + public void onClick(DialogInterface dialog, int which) { + switch (which) { + case DialogInterface.BUTTON_NEGATIVE: + Intent intent = new Intent(this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.putExtra(Intent.EXTRA_UID, mNoteId); + startActivity(intent); + break; + default: + break; + } + } + + public void onDismiss(DialogInterface dialog) { + stopAlarmSound(); + finish(); + } + + private void stopAlarmSound() { + if (mPlayer != null) { + mPlayer.stop(); + mPlayer.release(); + mPlayer = null; + } + } +} +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,用于处理闹钟提醒相关的界面展示、声音播放以及用户交互操作 +// 同时实现了OnClickListener和OnDismissListener接口,分别处理对话框的点击事件和消失事件 +public class AlarmAlertActivity extends Activity implements OnClickListener, OnDismissListener { + + // 用于存储便签的唯一标识符,后续通过此标识符来获取对应的便签相关信息 + private long mNoteId; + // 用于存储便签内容的片段,会按照一定规则进行截取展示 + private String mSnippet; + // 定义便签内容片段展示的最大长度为60个字符 + private static final int SNIPPET_PREW_MAX_LEN = 60; + // MediaPlayer对象,用于播放闹钟提醒的声音 + MediaPlayer mPlayer; + + // onCreate方法是在Activity创建时调用的生命周期方法,进行一系列初始化操作 + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // 请求去除Activity的标题栏,使界面更加简洁,专注于展示提醒相关内容 + requestWindowFeature(Window.FEATURE_NO_TITLE); + + // 获取当前Activity的窗口对象,后续用于设置窗口的相关属性 + final Window win = getWindow(); + + // 添加FLAG_SHOW_WHEN_LOCKED标志,使得Activity窗口在屏幕锁定时也能够显示出来,方便用户看到提醒信息 + 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 intent = getIntent(); + + try { + // 尝试从Intent携带的数据中解析出便签的唯一标识符mNoteId + // 通过获取Intent中数据的路径片段(假设数据的格式符合一定规范,此处取路径中的第二个片段并转换为long类型) + mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1)); + // 使用DataUtils工具类的方法,根据mNoteId从内容解析器中获取便签内容的片段,并赋值给mSnippet + mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId); + // 如果获取到的便签内容片段长度大于预设的最大长度SNIPPET_PREW_MAX_LEN + if (mSnippet.length() > SNIPPET_PREW_MAX_LEN) { + // 则对内容进行截取,取前SNIPPET_PREW_MAX_LEN个字符,并拼接上特定的提示字符串(从资源文件中获取) + mSnippet = mSnippet.substring(0, SNIPPET_PREW_MAX_LEN) + getResources().getString(R.string.notelist_string_info); + } + } catch (IllegalArgumentException e) { + // 如果在解析过程中出现IllegalArgumentException异常(例如数据格式不正确等情况) + // 打印异常的栈信息,方便调试查找问题 + e.printStackTrace(); + // 直接返回,不再进行后续的初始化操作 + return; + } + + // 创建MediaPlayer对象,用于后续播放闹钟提醒的声音 + mPlayer = new MediaPlayer(); + + // 通过DataUtils工具类判断该便签在数据库中是否可见(根据便签的唯一标识符mNoteId和便签类型Notes.TYPE_NOTE来判断) + if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) { + // 如果便签可见,则调用showActionDialog方法弹出操作对话框,向用户展示相关信息并提供操作按钮 + showActionDialog(); + // 同时调用playAlarmSound方法开始播放闹钟提醒的声音 + playAlarmSound(); + } else { + // 如果便签不可见,直接调用finish方法结束当前的Activity,无需进行其他展示和操作 + finish(); + } + } + + // isScreenOn方法用于判断屏幕当前是否处于开启状态 + private boolean isScreenOn() { + // 获取系统的PowerManager服务,PowerManager用于管理电源相关的操作和获取电源状态信息 + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + // 调用PowerManager的isScreenOn方法来获取屏幕的开启状态,返回true表示屏幕开启,false表示屏幕关闭 + return pm.isScreenOn(); + } + + // playAlarmSound方法用于播放闹钟提醒的声音 + private void playAlarmSound() { + // 通过RingtoneManager获取系统默认的闹钟铃声的Uri,以便后续设置为MediaPlayer的数据源 + Uri url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM); + + // 从系统设置中获取受静音模式影响的音频流相关设置值,用于判断闹钟声音的播放设置情况 + int silentModeStreams = Settings.System.getInt(getContentResolver(), + Settings.System.MODE_RINGER_STREAMS_AFFECTED, 0); + + // 判断获取到的设置值中是否包含闹钟音频流对应的标志位(通过位运算进行判断) + if ((silentModeStreams & (1 << AudioManager.STREAM_ALARM))!= 0) { + // 如果包含,则将MediaPlayer的音频流类型设置为获取到的这个设置值,按照系统的静音相关设置来播放声音 + mPlayer.setAudioStreamType(silentModeStreams); + } else { + // 如果不包含,则将MediaPlayer的音频流类型设置为正常的闹钟音频流类型AudioManager.STREAM_ALARM + mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM); + } + + try { + // 使用获取到的Uri为MediaPlayer设置数据源,指定要播放的音频资源 + mPlayer.setDataSource(this, url); + // 调用prepare方法准备播放声音,进行必要的缓冲和初始化操作 + mPlayer.prepare(); + // 设置MediaPlayer为循环播放模式,使得闹钟声音会一直循环播放,直到被停止 + mPlayer.setLooping(true); + // 调用start方法开始播放声音,触发闹钟提醒的音频播放功能 + mPlayer.start(); + } catch (IllegalArgumentException e) { + // 如果在设置数据源、准备播放等过程中出现IllegalArgumentException异常(例如参数不合法等情况) + // 打印异常的栈信息,方便调试查找问题 + e.printStackTrace(); + } catch (SecurityException e) { + // 如果出现SecurityException异常(例如没有权限访问音频资源等情况) + // 打印异常的栈信息,方便调试查找问题 + e.printStackTrace(); + } catch (IllegalStateException e) { + // 如果出现IllegalStateException异常(例如MediaPlayer状态不正确等情况) + // 打印异常的栈信息,方便调试查找问题 + e.printStackTrace(); + } catch (IOException e) { + // 如果出现IOException异常(例如读取音频数据源出错等情况) + // 打印异常的栈信息,方便调试查找问题 + e.printStackTrace(); + } + } + + // showActionDialog方法用于创建并显示一个包含便签信息和操作按钮的对话框,方便用户进行相关操作 + private void showActionDialog() { + // 创建一个AlertDialog的构建器对象,传入当前Activity的Context,用于构建对话框 + AlertDialog.Builder dialog = new AlertDialog.Builder(this); + // 设置对话框的标题为应用的名称,从资源文件中获取对应的字符串作为标题内容 + dialog.setTitle(R.string.app_name); + // 设置对话框的消息内容为前面获取并处理后的便签内容片段mSnippet,展示给用户查看 + dialog.setMessage(mSnippet); + // 设置对话框的确定按钮(PositiveButton),按钮文本为R.string.notealert_ok,同时将当前类(实现了OnClickListener接口)作为点击事件的监听器传入 + // 点击确定按钮后会触发onClick方法中对应的逻辑处理 + dialog.setPositiveButton(R.string.notealert_ok, this); + + // 根据屏幕是否开启(通过isScreenOn方法判断)来决定是否添加“进入”按钮(NegativeButton) + if (isScreenOn()) { + // 如果屏幕开启,则添加“进入”按钮,按钮文本为R.string.notealert_enter,同样将当前类作为点击事件监听器传入 + dialog.setNegativeButton(R.string.notealert_enter, this); + } + + // 调用dialog.show()方法显示对话框,并为对话框设置消失监听器(通过setOnDismissListener方法传入实现了OnDismissListener接口的当前类) + // 这样在对话框消失时会触发onDismiss方法中的逻辑处理 + dialog.show().setOnDismissListener(this); + } + + // 实现OnClickListener接口的点击事件处理方法,根据用户点击对话框中不同按钮来执行相应的操作 + @Override + public void onClick(DialogInterface dialog, int which) { + switch (which) { + // 如果用户点击的是NegativeButton(即前面设置的“进入”按钮,如果屏幕开启时显示) + case DialogInterface.BUTTON_NEGATIVE: + // 创建一个Intent,用于启动NoteEditActivity,通常用于进入便签的编辑界面 + Intent intent = new Intent(this, NoteEditActivity.class); + // 设置Intent的动作(Action)为Intent.ACTION_VIEW,表示查看相关内容的操作 + intent.setAction(Intent.ACTION_VIEW); + // 通过putExtra方法将便签的唯一标识符mNoteId作为额外数据传递给要启动的NoteEditActivity + intent.putExtra(Intent.EXTRA_UID, mNoteId); + // 调用startActivity方法启动NoteEditActivity,实现界面跳转,进入便签编辑界面 + startActivity(intent); + break; + default: + // 如果点击的是其他按钮(这里目前只有PositiveButton情况),暂时不做额外处理 + break; + } + } + + // 实现OnDismissListener接口的对话框消失事件处理方法,在对话框消失时进行相关资源清理和Activity结束操作 + @Override + public void onDismiss(DialogInterface dialog) { + // 调用stopAlarmSound方法停止正在播放的闹钟声音,释放相关音频资源 + stopAlarmSound(); + // 调用finish方法结束当前的AlarmAlertActivity,关闭该提醒界面 + finish(); + } + + // stopAlarmSound方法用于停止并释放MediaPlayer资源,避免资源浪费和内存泄漏等问题 + private void stopAlarmSound() { + // 判断MediaPlayer对象是否为空,如果不为空,说明之前已经创建并正在使用 + if (mPlayer!= null) { + // 调用stop方法停止正在播放的音频,立即停止声音播放 + mPlayer.stop(); + // 调用release方法释放MediaPlayer占用的系统资源,如音频播放相关的内存等 + mPlayer.release(); + // 将mPlayer对象赋值为null,表示资源已经释放,避免后续误操作 + mPlayer = null; + } + } +} \ No newline at end of file diff --git a/AlarmAlertActivity12.java b/AlarmAlertActivity12.java new file mode 100644 index 0000000..25d2571 --- /dev/null +++ b/AlarmAlertActivity12.java @@ -0,0 +1,377 @@ +/* + * 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; + + +public class AlarmAlertActivity extends Activity implements OnClickListener, OnDismissListener { + private long mNoteId; + private String mSnippet; + private static final int SNIPPET_PREW_MAX_LEN = 60; + 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); + } + + Intent intent = getIntent(); + + try { + mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1)); + mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId); + mSnippet = mSnippet.length() > SNIPPET_PREW_MAX_LEN ? mSnippet.substring(0, + SNIPPET_PREW_MAX_LEN) + getResources().getString(R.string.notelist_string_info) + : mSnippet; + } catch (IllegalArgumentException e) { + e.printStackTrace(); + return; + } + + mPlayer = new MediaPlayer(); + if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) { + showActionDialog(); + playAlarmSound(); + } else { + finish(); + } + } + + private boolean isScreenOn() { + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + return pm.isScreenOn(); + } + + private void playAlarmSound() { + Uri url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM); + + int silentModeStreams = Settings.System.getInt(getContentResolver(), + Settings.System.MODE_RINGER_STREAMS_AFFECTED, 0); + + if ((silentModeStreams & (1 << AudioManager.STREAM_ALARM)) != 0) { + mPlayer.setAudioStreamType(silentModeStreams); + } else { + mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM); + } + try { + mPlayer.setDataSource(this, url); + mPlayer.prepare(); + mPlayer.setLooping(true); + mPlayer.start(); + } catch (IllegalArgumentException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (SecurityException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IllegalStateException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + private void showActionDialog() { + AlertDialog.Builder dialog = new AlertDialog.Builder(this); + dialog.setTitle(R.string.app_name); + dialog.setMessage(mSnippet); + dialog.setPositiveButton(R.string.notealert_ok, this); + if (isScreenOn()) { + dialog.setNegativeButton(R.string.notealert_enter, this); + } + dialog.show().setOnDismissListener(this); + } + + public void onClick(DialogInterface dialog, int which) { + switch (which) { + case DialogInterface.BUTTON_NEGATIVE: + Intent intent = new Intent(this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.putExtra(Intent.EXTRA_UID, mNoteId); + startActivity(intent); + break; + default: + break; + } + } + + public void onDismiss(DialogInterface dialog) { + stopAlarmSound(); + finish(); + } + + private void stopAlarmSound() { + if (mPlayer != null) { + mPlayer.stop(); + mPlayer.release(); + mPlayer = null; + } + } +} +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,用于处理闹钟提醒相关的界面展示、声音播放以及用户交互操作 +// 同时实现了OnClickListener和OnDismissListener接口,分别处理对话框的点击事件和消失事件 +public class AlarmAlertActivity extends Activity implements OnClickListener, OnDismissListener { + + // 用于存储便签的唯一标识符,后续通过此标识符来获取对应的便签相关信息 + private long mNoteId; + // 用于存储便签内容的片段,会按照一定规则进行截取展示 + private String mSnippet; + // 定义便签内容片段展示的最大长度为60个字符 + private static final int SNIPPET_PREW_MAX_LEN = 60; + // MediaPlayer对象,用于播放闹钟提醒的声音 + MediaPlayer mPlayer; + + // onCreate方法是在Activity创建时调用的生命周期方法,进行一系列初始化操作 + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // 请求去除Activity的标题栏,使界面更加简洁,专注于展示提醒相关内容 + requestWindowFeature(Window.FEATURE_NO_TITLE); + + // 获取当前Activity的窗口对象,后续用于设置窗口的相关属性 + final Window win = getWindow(); + + // 添加FLAG_SHOW_WHEN_LOCKED标志,使得Activity窗口在屏幕锁定时也能够显示出来,方便用户看到提醒信息 + 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 intent = getIntent(); + + try { + // 尝试从Intent携带的数据中解析出便签的唯一标识符mNoteId + // 通过获取Intent中数据的路径片段(假设数据的格式符合一定规范,此处取路径中的第二个片段并转换为long类型) + mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1)); + // 使用DataUtils工具类的方法,根据mNoteId从内容解析器中获取便签内容的片段,并赋值给mSnippet + mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId); + // 如果获取到的便签内容片段长度大于预设的最大长度SNIPPET_PREW_MAX_LEN + if (mSnippet.length() > SNIPPET_PREW_MAX_LEN) { + // 则对内容进行截取,取前SNIPPET_PREW_MAX_LEN个字符,并拼接上特定的提示字符串(从资源文件中获取) + mSnippet = mSnippet.substring(0, SNIPPET_PREW_MAX_LEN) + getResources().getString(R.string.notelist_string_info); + } + } catch (IllegalArgumentException e) { + // 如果在解析过程中出现IllegalArgumentException异常(例如数据格式不正确等情况) + // 打印异常的栈信息,方便调试查找问题 + e.printStackTrace(); + // 直接返回,不再进行后续的初始化操作 + return; + } + + // 创建MediaPlayer对象,用于后续播放闹钟提醒的声音 + mPlayer = new MediaPlayer(); + + // 通过DataUtils工具类判断该便签在数据库中是否可见(根据便签的唯一标识符mNoteId和便签类型Notes.TYPE_NOTE来判断) + if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) { + // 如果便签可见,则调用showActionDialog方法弹出操作对话框,向用户展示相关信息并提供操作按钮 + showActionDialog(); + // 同时调用playAlarmSound方法开始播放闹钟提醒的声音 + playAlarmSound(); + } else { + // 如果便签不可见,直接调用finish方法结束当前的Activity,无需进行其他展示和操作 + finish(); + } + } + + // isScreenOn方法用于判断屏幕当前是否处于开启状态 + private boolean isScreenOn() { + // 获取系统的PowerManager服务,PowerManager用于管理电源相关的操作和获取电源状态信息 + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + // 调用PowerManager的isScreenOn方法来获取屏幕的开启状态,返回true表示屏幕开启,false表示屏幕关闭 + return pm.isScreenOn(); + } + + // playAlarmSound方法用于播放闹钟提醒的声音 + private void playAlarmSound() { + // 通过RingtoneManager获取系统默认的闹钟铃声的Uri,以便后续设置为MediaPlayer的数据源 + Uri url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM); + + // 从系统设置中获取受静音模式影响的音频流相关设置值,用于判断闹钟声音的播放设置情况 + int silentModeStreams = Settings.System.getInt(getContentResolver(), + Settings.System.MODE_RINGER_STREAMS_AFFECTED, 0); + + // 判断获取到的设置值中是否包含闹钟音频流对应的标志位(通过位运算进行判断) + if ((silentModeStreams & (1 << AudioManager.STREAM_ALARM))!= 0) { + // 如果包含,则将MediaPlayer的音频流类型设置为获取到的这个设置值,按照系统的静音相关设置来播放声音 + mPlayer.setAudioStreamType(silentModeStreams); + } else { + // 如果不包含,则将MediaPlayer的音频流类型设置为正常的闹钟音频流类型AudioManager.STREAM_ALARM + mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM); + } + + try { + // 使用获取到的Uri为MediaPlayer设置数据源,指定要播放的音频资源 + mPlayer.setDataSource(this, url); + // 调用prepare方法准备播放声音,进行必要的缓冲和初始化操作 + mPlayer.prepare(); + // 设置MediaPlayer为循环播放模式,使得闹钟声音会一直循环播放,直到被停止 + mPlayer.setLooping(true); + // 调用start方法开始播放声音,触发闹钟提醒的音频播放功能 + mPlayer.start(); + } catch (IllegalArgumentException e) { + // 如果在设置数据源、准备播放等过程中出现IllegalArgumentException异常(例如参数不合法等情况) + // 打印异常的栈信息,方便调试查找问题 + e.printStackTrace(); + } catch (SecurityException e) { + // 如果出现SecurityException异常(例如没有权限访问音频资源等情况) + // 打印异常的栈信息,方便调试查找问题 + e.printStackTrace(); + } catch (IllegalStateException e) { + // 如果出现IllegalStateException异常(例如MediaPlayer状态不正确等情况) + // 打印异常的栈信息,方便调试查找问题 + e.printStackTrace(); + } catch (IOException e) { + // 如果出现IOException异常(例如读取音频数据源出错等情况) + // 打印异常的栈信息,方便调试查找问题 + e.printStackTrace(); + } + } + + // showActionDialog方法用于创建并显示一个包含便签信息和操作按钮的对话框,方便用户进行相关操作 + private void showActionDialog() { + // 创建一个AlertDialog的构建器对象,传入当前Activity的Context,用于构建对话框 + AlertDialog.Builder dialog = new AlertDialog.Builder(this); + // 设置对话框的标题为应用的名称,从资源文件中获取对应的字符串作为标题内容 + dialog.setTitle(R.string.app_name); + // 设置对话框的消息内容为前面获取并处理后的便签内容片段mSnippet,展示给用户查看 + dialog.setMessage(mSnippet); + // 设置对话框的确定按钮(PositiveButton),按钮文本为R.string.notealert_ok,同时将当前类(实现了OnClickListener接口)作为点击事件的监听器传入 + // 点击确定按钮后会触发onClick方法中对应的逻辑处理 + dialog.setPositiveButton(R.string.notealert_ok, this); + + // 根据屏幕是否开启(通过isScreenOn方法判断)来决定是否添加“进入”按钮(NegativeButton) + if (isScreenOn()) { + // 如果屏幕开启,则添加“进入”按钮,按钮文本为R.string.notealert_enter,同样将当前类作为点击事件监听器传入 + dialog.setNegativeButton(R.string.notealert_enter, this); + } + + // 调用dialog.show()方法显示对话框,并为对话框设置消失监听器(通过setOnDismissListener方法传入实现了OnDismissListener接口的当前类) + // 这样在对话框消失时会触发onDismiss方法中的逻辑处理 + dialog.show().setOnDismissListener(this); + } + + // 实现OnClickListener接口的点击事件处理方法,根据用户点击对话框中不同按钮来执行相应的操作 + @Override + public void onClick(DialogInterface dialog, int which) { + switch (which) { + // 如果用户点击的是NegativeButton(即前面设置的“进入”按钮,如果屏幕开启时显示) + case DialogInterface.BUTTON_NEGATIVE: + // 创建一个Intent,用于启动NoteEditActivity,通常用于进入便签的编辑界面 + Intent intent = new Intent(this, NoteEditActivity.class); + // 设置Intent的动作(Action)为Intent.ACTION_VIEW,表示查看相关内容的操作 + intent.setAction(Intent.ACTION_VIEW); + // 通过putExtra方法将便签的唯一标识符mNoteId作为额外数据传递给要启动的NoteEditActivity + intent.putExtra(Intent.EXTRA_UID, mNoteId); + // 调用startActivity方法启动NoteEditActivity,实现界面跳转,进入便签编辑界面 + startActivity(intent); + break; + default: + // 如果点击的是其他按钮(这里目前只有PositiveButton情况),暂时不做额外处理 + break; + } + } + + // 实现OnDismissListener接口的对话框消失事件处理方法,在对话框消失时进行相关资源清理和Activity结束操作 + @Override + public void onDismiss(DialogInterface dialog) { + // 调用stopAlarmSound方法停止正在播放的闹钟声音,释放相关音频资源 + stopAlarmSound(); + // 调用finish方法结束当前的AlarmAlertActivity,关闭该提醒界面 + finish(); + } + + // stopAlarmSound方法用于停止并释放MediaPlayer资源,避免资源浪费和内存泄漏等问题 + private void stopAlarmSound() { + // 判断MediaPlayer对象是否为空,如果不为空,说明之前已经创建并正在使用 + if (mPlayer!= null) { + // 调用stop方法停止正在播放的音频,立即停止声音播放 + mPlayer.stop(); + // 调用release方法释放MediaPlayer占用的系统资源,如音频播放相关的内存等 + mPlayer.release(); + // 将mPlayer对象赋值为null,表示资源已经释放,避免后续误操作 + mPlayer = null; + } + } +} \ No newline at end of file diff --git a/AlarmInitReceiver.java b/AlarmInitReceiver.java new file mode 100644 index 0000000..c26f03e --- /dev/null +++ b/AlarmInitReceiver.java @@ -0,0 +1,152 @@ +/* + * 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; + + +public class AlarmInitReceiver extends BroadcastReceiver { + + private static final String [] PROJECTION = new String [] { + NoteColumns.ID, + NoteColumns.ALERTED_DATE + }; + + private static final int COLUMN_ID = 0; + private static final int COLUMN_ALERTED_DATE = 1; + + @Override + public void onReceive(Context context, Intent intent) { + long currentDate = System.currentTimeMillis(); + Cursor c = context.getContentResolver().query(Notes.CONTENT_NOTE_URI, + PROJECTION, + NoteColumns.ALERTED_DATE + ">? AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE, + new String[] { String.valueOf(currentDate) }, + null); + + if (c != null) { + if (c.moveToFirst()) { + do { + long alertDate = c.getLong(COLUMN_ALERTED_DATE); + Intent sender = new Intent(context, AlarmReceiver.class); + sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, c.getLong(COLUMN_ID))); + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, 0); + AlarmManager alermManager = (AlarmManager) context + .getSystemService(Context.ALARM_SERVICE); + alermManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent); + } while (c.moveToNext()); + } + c.close(); + } + } +} +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 { + + // 定义一个字符串数组,用于指定从数据库中查询时需要获取的列名 + // 这里获取的是便签的唯一标识符(ID)和提醒日期(ALERTED_DATE)两列 + private static final String [] PROJECTION = new String [] { + NoteColumns.ID, + NoteColumns.ALERTED_DATE + }; + + // 定义常量,表示查询结果中ID列对应的索引位置,方便后续从Cursor中获取数据 + private static final int COLUMN_ID = 0; + // 定义常量,表示查询结果中ALERTED_DATE列对应的索引位置,方便后续从Cursor中获取数据 + private static final int COLUMN_ALERTED_DATE = 1; + + // onReceive方法是BroadcastReceiver的核心方法,当接收到相应广播时会被调用 + @Override + public void onReceive(Context context, Intent intent) { + // 获取当前系统的时间戳(以毫秒为单位),用于后续在数据库查询中作为比较条件,筛选出需要设置闹钟提醒的便签 + long currentDate = System.currentTimeMillis(); + + // 通过ContentResolver查询数据库,获取满足条件的便签记录信息 + // 参数说明: + // 1. Notes.CONTENT_NOTE_URI:指定查询的内容提供器的Uri,指向便签相关的数据表 + // 2. PROJECTION:前面定义的要查询的列名数组,即获取便签的ID和提醒日期两列数据 + // 3. NoteColumns.ALERTED_DATE + ">? AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE:查询条件,筛选出提醒日期大于当前日期且类型为普通便签(假设Notes.TYPE_NOTE代表普通便签类型)的记录 + // 4. new String[] { String.valueOf(currentDate) }:为查询条件中的占位符(?)提供实际的值,即当前日期 + // 5. null:表示按照默认的排序方式进行排序,如果有需要也可以指定具体的排序规则 + 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); + + // 判断查询结果的Cursor是否为空,如果不为空,说明有满足条件的便签记录 + if (c!= null) { + // 将游标移动到查询结果的第一条记录位置,如果有记录则返回true,否则返回false + if (c.moveToFirst()) { + // 使用do-while循环遍历查询结果集,处理每条满足条件的便签记录 + do { + // 从Cursor中获取提醒日期这一列的值(以长整型表示),对应之前定义的COLUMN_ALERTED_DATE索引位置 + long alertDate = c.getLong(COLUMN_ALERTED_DATE); + + // 创建一个Intent对象,用于启动AlarmReceiver(可能是处理闹钟提醒触发逻辑的广播接收器) + Intent sender = new Intent(context, AlarmReceiver.class); + // 为Intent设置数据部分,通过ContentUris工具类将便签的唯一标识符(ID)附加到便签内容提供器的Uri上 + // 这样接收方可以根据这个Uri获取到具体是哪个便签的闹钟提醒触发了 + sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, c.getLong(COLUMN_ID))); + + // 创建一个PendingIntent对象,用于包装前面创建的Intent,使得它可以在稍后被触发(例如作为闹钟触发时启动的目标意图) + // 参数说明: + // 1. context:当前的应用上下文环境 + // 2. 0:请求码,这里暂时设置为0,一般如果有多个不同用途的PendingIntent可以设置不同的请求码来区分 + // 3. sender:要包装的Intent对象,即前面创建的启动AlarmReceiver的Intent + // 4. 0:标志位,这里暂时设置为0,根据不同需求可以设置不同的标志来控制PendingIntent的行为 + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, 0); + + // 获取系统的AlarmManager服务,AlarmManager用于管理和设置闹钟相关的操作 + AlarmManager alermManager = (AlarmManager) context + .getSystemService(Context.ALARM_SERVICE); + + // 使用AlarmManager设置闹钟提醒,具体参数含义如下: + // 1. AlarmManager.RTC_WAKEUP:闹钟类型,表示在指定的时间(alertDate)唤醒设备并触发闹钟(如果设备处于睡眠状态) + // 2. alertDate:设置闹钟触发的时间,即从数据库中获取的该便签的提醒日期对应的时间戳(以毫秒为单位) + // 3. pendingIntent:前面创建的PendingIntent对象,当闹钟触发时会启动对应的Intent(这里是启动AlarmReceiver) + alermManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent); + } while (c.moveToNext()); + } + // 关闭游标,释放相关的系统资源,避免内存泄漏等问题,查询操作完成后需要及时关闭游标 + c.close(); + } + } +} \ No newline at end of file diff --git a/AlarmReceiver.java b/AlarmReceiver.java new file mode 100644 index 0000000..e503aca --- /dev/null +++ b/AlarmReceiver.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +public class AlarmReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + intent.setClass(context, AlarmAlertActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } +} +package net.micode.notes.ui; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +// AlarmReceiver类继承自BroadcastReceiver,是一个广播接收器,用于接收特定的广播消息并做出相应的响应。 +// 在这个场景下,大概率是接收与闹钟提醒相关的广播,然后启动对应的闹钟提醒界面Activity。 +public class AlarmReceiver extends BroadcastReceiver { + + // onReceive方法是BroadcastReceiver类中最重要的方法,当该广播接收器接收到与之匹配的广播时,此方法会被调用。 + // 它接收两个参数: + // 1. context:表示当前应用的上下文环境,通过它可以访问应用的各种资源、服务等内容。 + // 2. intent:包含了发送广播时所携带的信息,例如额外的数据、动作(Action)等,在这里它可能携带了与闹钟提醒相关的一些标识信息等。 + @Override + public void onReceive(Context context, Intent intent) { + // 通过intent.setClass方法,重新设置该Intent要启动的目标Activity类为AlarmAlertActivity.class。 + // 这意味着当这个Intent被启动时,将会启动AlarmAlertActivity这个界面,通常AlarmAlertActivity就是用于展示闹钟提醒具体内容(如提醒的便签信息、播放提醒声音等)的界面。 + intent.setClass(context, AlarmAlertActivity.class); + + // 为Intent添加FLAG_ACTIVITY_NEW_TASK标志。 + // 这个标志的作用是:如果当前应用处于后台或者没有任何任务栈存在的情况下,添加此标志可以确保新启动的Activity能够在一个新的任务栈中被启动, + // 避免出现因任务栈相关问题而导致Activity启动失败等异常情况,保证闹钟提醒界面能够正常展示给用户。 + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + // 使用传入的context上下文对象,调用startActivity方法来启动经过上述设置后的Intent。 + // 这样就会触发启动AlarmAlertActivity,使得用户可以看到闹钟提醒的相关界面,完成从接收到闹钟提醒广播到展示提醒界面的整个流程。 + context.startActivity(intent); + } +} \ No newline at end of file diff --git a/DateTimePicker.java b/DateTimePicker.java new file mode 100644 index 0000000..7e9007b --- /dev/null +++ b/DateTimePicker.java @@ -0,0 +1,321 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package net.micode.notes.ui; + +import java.text.DateFormatSymbols; +import java.util.Calendar; + +import net.micode.notes.R; + +import android.content.Context; +import android.text.format.DateFormat; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.NumberPicker; + +// DateTimePicker类继承自FrameLayout,它是一个自定义的视图组件,用于展示日期和时间选择的交互界面, +// 支持24小时制和12小时制(含AM/PM标识),用户可以通过该组件方便地选择具体的日期和时间,并能响应相应的变化事件。 +public class DateTimePicker extends FrameLayout { + + // 定义默认的启用状态,默认为启用,即组件默认是可以交互操作的 + private static final boolean DEFAULT_ENABLE_STATE = true; + + // 一天中半天(12小时制)包含的小时数 + private static final int HOURS_IN_HALF_DAY = 12; + // 一天(24小时制)包含的小时数 + private static final int HOURS_IN_ALL_DAY = 24; + // 一周包含的天数 + private static final int DAYS_IN_ALL_WEEK = 7; + // 日期选择器(NumberPicker)的最小值,通常对应一周的第一天(索引从0开始) + private static final int DATE_SPINNER_MIN_VAL = 0; + // 日期选择器(NumberPicker)的最大值,对应一周的最后一天(一周共7天,索引从0到6) + private static final int DATE_SPINNER_MAX_VAL = DAYS_IN_ALL_WEEK - 1; + // 24小时制视图下小时选择器(NumberPicker)的最小值,即0时 + private static final int HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW = 0; + // 24小时制视图下小时选择器(NumberPicker)的最大值,即23时 + private static final int HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW = 23; + // 12小时制视图下小时选择器(NumberPicker)的最小值,通常为1时(习惯上12小时制从1开始计数) + private static final int HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW = 1; + // 12小时制视图下小时选择器(NumberPicker)的最大值,即12时 + private static final int HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW = 12; + // 分钟选择器(NumberPicker)的最小值,即0分 + private static final int MINUT_SPINNER_MIN_VAL = 0; + // 分钟选择器(NumberPicker)的最大值,即59分 + private static final int MINUT_SPINNER_MAX_VAL = 59; + // AM/PM选择器(NumberPicker)的最小值,对应AM(通常用0表示) + private static final int AMPM_SPINNER_MIN_VAL = 0; + // AM/PM选择器(NumberPicker)的最大值,对应PM(通常用1表示) + private static final int AMPM_SPINNER_MAX_VAL = 1; + + // 用于显示日期的NumberPicker组件,用户可通过它选择具体的日期(以周为范围展示) + private final NumberPicker mDateSpinner; + // 用于显示小时的NumberPicker组件,根据设置的视图模式(24小时制或12小时制)展示不同范围的小时值 + private final NumberPicker mHourSpinner; + // 用于显示分钟的NumberPicker组件,可选择0到59分钟 + private final NumberPicker mMinuteSpinner; + // 用于显示AM/PM的NumberPicker组件,仅在12小时制下可见且用于切换上午或下午标识 + private final NumberPicker mAmPmSpinner; + + // 用于存储和操作当前选择的日期和时间信息,基于Calendar类实现,方便进行日期和时间的计算与设置 + private Calendar mDate; + + // 用于存储一周内各天要展示给用户的显示值(例如格式化后的"MM.dd EEEE"格式的字符串) + private String[] mDateDisplayValues = new String[DAYS_IN_ALL_WEEK]; + + // 用于标识当前时间是上午(true)还是下午(false),在12小时制下使用 + private boolean mIsAm; + + // 用于标识当前是否处于24小时制视图模式,true表示24小时制,false表示12小时制 + 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) { + // 根据新选择的日期与旧日期的差值,调整Calendar对象中的日期(增加或减少相应天数) + mDate.add(Calendar.DAY_OF_YEAR, newVal - oldVal); + // 更新日期相关的显示控制,例如刷新显示的日期字符串等 + updateDateControl(); + // 触发日期和时间变化的回调方法,通知外部可能关注此变化的地方进行相应处理 + onDateTimeChanged(); + } + }; + + // 内部类实现的小时选择器(NumberPicker)的值变化监听器,当用户在小时选择器上选择不同小时时触发 + 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小时制下,如果从上午11时切换到下午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; + } + // 反之,从下午12时(中午)切换到上午11时,日期需要减少一天 + 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; + } + // 如果是上午11时与下午12时(中午)之间切换,还需要更新AM/PM标识并刷新相关显示控制 + 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小时制下,如果从23时切换到0时,日期需要增加一天 + if (oldVal == HOURS_IN_ALL_DAY - 1 && newVal == 0) { + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, 1); + isDateChanged = true; + } + // 反之,从0时切换到23时,日期需要减少一天 + else if (oldVal == 0 && newVal == HOURS_IN_ALL_DAY - 1) { + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, -1); + isDateChanged = true; + } + } + // 根据当前选择的小时(考虑12小时制与24小时制转换情况),设置Calendar对象中的小时信息 + 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)); + } + } + }; + + // 内部类实现的分钟选择器(NumberPicker)的值变化监听器,当用户在分钟选择器上选择不同分钟时触发 + 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; + // 如果从最大分钟值(59分)切换到最小分钟值(0分),意味着小时需要增加1小时(时间进位) + if (oldVal == maxValue && newVal == minValue) { + offset += 1; + } + // 反之,从最小分钟值切换到最大分钟值,小时需要减少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(); + // 根据新的小时值判断是否切换AM/PM标识,并更新相关显示控制(仅在12小时制下相关) + if (newHour >= HOURS_IN_HALF_DAY) { + mIsAm = false; + updateAmPmControl(); + } else { + mIsAm = true; + updateAmPmControl(); + } + } + mDate.set(Calendar.MINUTE, newVal); + onDateTimeChanged(); + } + }; + + // 内部类实现的AM/PM选择器(NumberPicker)的值变化监听器,当用户在AM/PM选择器上切换时触发 + private NumberPicker.OnValueChangeListener mOnAmPmChangedListener = new NumberPicker.OnValueChangeListener() { + @Override + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + mIsAm =!mIsAm; + // 根据切换后的AM/PM标识,相应地调整Calendar对象中的小时信息(增加或减少12小时) + 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组件,默认根据系统设置判断是否采用24小时制视图 + public DateTimePicker(Context context) { + this(context, System.currentTimeMillis()); + } + + // 构造函数,使用指定的时间(以毫秒为单位的时间戳)初始化DateTimePicker组件,默认根据系统设置判断是否采用24小时制视图 + public DateTimePicker(Context context, long date) { + this(context, date, DateFormat.is24HourFormat(context)); + } + + // 完整的构造函数,用于初始化DateTimePicker组件,传入上下文、指定的时间以及是否采用24小时制视图的标识 + public DateTimePicker(Context context, long date, boolean is24HourView) { + super(context); + // 获取一个Calendar实例,用于存储和操作日期时间信息,初始化为当前时间(如果没有传入指定时间的话) + mDate = Calendar.getInstance(); + mInitialising = true; + // 根据当前小时数判断是上午还是下午(用于初始化AM/PM标识,仅在12小时制下相关) + mIsAm = getCurrentHourOfDay() >= HOURS_IN_HALF_DAY; + // 加载布局文件到该组件中,布局文件中应该包含了各个NumberPicker组件等用于展示和交互的视图元素 + inflate(context, R.layout.datetime_picker, this); + + // 获取布局文件中定义的日期选择器(NumberPicker)组件,并进行相关初始化设置 + mDateSpinner = (NumberPicker) findViewById(R.id.date); + mDateSpinner.setMinValue(DATE_SPINNER_MIN_VAL); + mDateSpinner.setMaxValue(DATE_SPINNER_MAX_VAL); + mDateSpinner.setOnValueChangedListener(mOnDateChangedListener); + + // 获取小时选择器(NumberPicker)组件,并设置其值变化监听器 + mHourSpinner = (NumberPicker) findViewById(R.id.hour); + mHourSpinner.setOnValueChangedListener(mOnHourChangedListener); + + // 获取分钟选择器(NumberPicker)组件,设置其最小值、最大值以及长按更新间隔等属性,并设置值变化监听器 + mMinuteSpinner = (NumberPicker) findViewById(R.id.minute); + mMinuteSpinner.setMinValue(MINUT_SPINNER_MIN_VAL); + mMinuteSpinner.setMaxValue(MINUT_SPINNER_MAX_VAL); + mMinuteSpinner.setOnLongPressUpdateInterval(100); + mMinuteSpinner.setOnValueChangedListener(mOnMinuteChangedListener); + + // 获取用于显示AM/PM的NumberPicker组件,设置其最小值、最大值,并设置显示的字符串值(从DateFormatSymbols获取AM/PM标识字符串),再设置其值变化监听器 + String[] stringsForAmPm = new DateFormatSymbols().getAmPmStrings(); + mAmPmSpinner = (NumberPicker) findViewById(R.id.amPm); + mAmPmSpinner.setMinValue(AMPM_SPINNER_MIN_VAL); + mAmPmSpinner.setMaxValue(AMPM_SPINNER_MAX_VAL); + mAmPmSpinner.setDisplayedValues(stringsForAmPm); + mAmPmSpinner.setOnValueChangedListener(mOnAmPmChangedListener); + + // 调用相关方法更新各个控件的初始显示状态,使其展示正确的初始日期、时间以及相关格式等信息 + updateDateControl(); + updateHourControl(); + updateAmPmControl(); + + // 根据传入的参数设置是否采用24小时制视图模式,并相应更新控件显示 + set24HourView(is24HourView); + + // 设置当前日期时间为传入的指定时间(如果有传入的话),或者保持默认的当前时间 + setCurrentDate(date); + + // 设置组件的初始启用状态 + setEnabled(isEnabled()); + + // 初始化完成,将初始化标记设置为false,后续操作不再按照初始化阶段处理 + mInitialising = false; + } + + // 重写父类的setEnabled方法,用于设置组件及其包含的各个子控件(NumberPicker组件)的启用状态,同时更新内部的启用状态记录变量 + @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; + } + + // 重写父类的isEnabled方法,返回组件当前的启用状态 + @Override + public boolean isEnabled() { + return mIsEnabled; + } + + /** + * 获取当前日期对应的时间戳(以毫秒为单位),通过Calendar对象获取其内部存储的时间信息并返回 + * + * @return the current date in millis(当前日期的毫秒时间戳) + */ + public long getCurrentDateInTimeMillis() { + return mDate.getTimeInMillis(); + } + + /** + * 设置当前日期,传入以毫秒为单位的时间戳,内部会解析并设置对应的年、月、日、时、分等信息到Calendar对象中 + * + * @param date The current date in millis(要设置的日期的毫秒时间戳) + */ + public void setCurrentDate(long date) { + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(date); + setCurrentDate(cal.get(); + } +} +} \ No newline at end of file diff --git a/DateTimePickerDialog.java b/DateTimePickerDialog.java new file mode 100644 index 0000000..8dc715d --- /dev/null +++ b/DateTimePickerDialog.java @@ -0,0 +1,204 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import java.util.Calendar; + +import net.micode.notes.R; +import net.micode.notes.ui.DateTimePicker; +import net.micode.notes.ui.DateTimePicker.OnDateTimeChangedListener; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.text.format.DateFormat; +import android.text.format.DateUtils; + +public class DateTimePickerDialog extends AlertDialog implements OnClickListener { + + private Calendar mDate = Calendar.getInstance(); + private boolean mIs24HourView; + private OnDateTimeSetListener mOnDateTimeSetListener; + private DateTimePicker mDateTimePicker; + + public interface OnDateTimeSetListener { + void OnDateTimeSet(AlertDialog dialog, long date); + } + + public DateTimePickerDialog(Context context, long date) { + super(context); + mDateTimePicker = new DateTimePicker(context); + 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); + mDateTimePicker.setCurrentDate(mDate.getTimeInMillis()); + setButton(context.getString(R.string.datetime_dialog_ok), this); + setButton2(context.getString(R.string.datetime_dialog_cancel), (OnClickListener)null); + set24HourView(DateFormat.is24HourFormat(this.getContext())); + updateTitle(mDate.getTimeInMillis()); + } + + public void set24HourView(boolean is24HourView) { + mIs24HourView = is24HourView; + } + + public void setOnDateTimeSetListener(OnDateTimeSetListener callBack) { + mOnDateTimeSetListener = callBack; + } + + private void updateTitle(long date) { + int flag = + DateUtils.FORMAT_SHOW_YEAR | + DateUtils.FORMAT_SHOW_DATE | + DateUtils.FORMAT_SHOW_TIME; + flag |= mIs24HourView ? DateUtils.FORMAT_24HOUR : DateUtils.FORMAT_24HOUR; + setTitle(DateUtils.formatDateTime(this.getContext(), date, flag)); + } + + public void onClick(DialogInterface arg0, int arg1) { + if (mOnDateTimeSetListener != null) { + mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis()); + } + } + +} + + + +package net.micode.notes.ui; + +import java.util.Calendar; + +import net.micode.notes.R; +import net.micode.notes.ui.DateTimePicker; +import net.micode.notes.ui.DateTimePicker.OnDateTimeChangedListener; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.text.format.DateFormat; +import android.text.format.DateUtils; + +// DateTimePickerDialog类继承自AlertDialog,它是一个自定义的对话框组件,用于展示日期和时间选择的交互界面, +// 内部包含了DateTimePicker组件来实现具体的日期时间选择功能,并提供了设置选择结果监听器等功能,方便用户在对话框中选择日期和时间并获取选择结果。 +public class DateTimePickerDialog extends AlertDialog implements OnClickListener { + + // 用于存储当前选择的日期和时间信息,基于Calendar类实现,初始化为当前系统时间,后续会根据用户在对话框中的选择进行更新 + private Calendar mDate = Calendar.getInstance(); + // 用于标识当前是否处于24小时制视图模式,true表示24小时制,false表示12小时制,初始值会根据系统设置来确定 + private boolean mIs24HourView; + // 定义一个接口类型的成员变量,用于设置当用户点击确定按钮完成日期和时间选择后触发的回调监听器,外部类可以实现该接口来获取选择结果 + private OnDateTimeSetListener mOnDateTimeSetListener; + // 用于展示和操作日期时间选择的具体组件,是DateTimePickerDialog的核心子组件,用户通过它来实际选择日期和时间 + private DateTimePicker mDateTimePicker; + + // 定义一个接口,用于外部类实现,以便在用户完成日期和时间选择并点击确定按钮后接收通知并获取选择的日期时间信息(以毫秒为单位的时间戳形式) + public interface OnDateTimeSetListener { + void OnDateTimeSet(AlertDialog dialog, long date); + } + + // 构造函数,用于创建DateTimePickerDialog实例,传入上下文和初始的日期时间(以毫秒为单位的时间戳) + public DateTimePickerDialog(Context context, long date) { + super(context); + + // 创建一个DateTimePicker实例,用于在对话框中展示日期和时间选择的交互界面,传入当前上下文,它会根据默认设置显示初始的日期时间等信息 + mDateTimePicker = new DateTimePicker(context); + + // 将创建的DateTimePicker组件设置为对话框的内容视图,这样对话框展示时就会显示日期时间选择的相关界面 + setView(mDateTimePicker); + + // 为DateTimePicker组件设置日期和时间变化的监听器,当用户在DateTimePicker中改变了日期、时间等信息时触发相应逻辑 + mDateTimePicker.setOnDateTimeChangedListener(new OnDateTimeChangedListener() { + public void onDateTimeChanged(DateTimePicker view, int year, int month, + int dayOfMonth, int hourOfDay, int minute) { + // 根据用户选择的新的年、月、日、时、分信息,更新内部存储的Calendar对象(mDate)中的对应字段,使mDate始终保持最新的选择状态 + 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方法更新对话框的标题,使其显示当前选择的日期和时间 + updateTitle(mDate.getTimeInMillis()); + } + }); + + // 设置对话框初始显示的日期和时间,将传入的时间戳解析并设置到内部的Calendar对象(mDate)中 + mDate.setTimeInMillis(date); + + // 将秒数设置为0,确保在处理日期时间时,秒数处于初始默认状态(可能是为了统一和简化选择的时间精度,只精确到分钟级别) + mDate.set(Calendar.SECOND, 0); + + // 将当前的日期和时间信息设置到DateTimePicker组件中,使其初始展示的内容与传入的初始日期时间一致 + mDateTimePicker.setCurrentDate(mDate.getTimeInMillis()); + + // 设置对话框的确定按钮,按钮文本从资源文件中获取(通过R.string.datetime_dialog_ok获取对应的字符串资源),并将当前类(实现了OnClickListener接口)作为点击事件的监听器传入 + setButton(context.getString(R.string.datetime_dialog_ok), this); + + // 设置对话框的取消按钮,按钮文本从资源文件中获取(通过R.string.datetime_dialog_cancel获取对应的字符串资源),传入null表示使用默认的取消按钮行为(通常是关闭对话框) + setButton2(context.getString(R.string.datetime_dialog_cancel), (OnClickListener)null); + + // 根据系统设置判断是否采用24小时制视图模式,调用set24HourView方法进行相应设置,并更新对话框标题等相关显示信息 + set24HourView(DateFormat.is24HourFormat(this.getContext())); + updateTitle(mDate.getTimeInMillis()); + } + + // 用于设置对话框的日期时间选择界面是否采用24小时制视图模式,更新内部的标识变量,并可能影响DateTimePicker组件的显示(例如AM/PM选择器的可见性等) + public void set24HourView(boolean is24HourView) { + mIs24HourView = is24HourView; + } + + // 用于设置当用户点击确定按钮完成日期和时间选择后触发的回调监听器,外部类可以通过实现OnDateTimeSetListener接口并传入相应实例来处理选择结果 + public void setOnDateTimeSetListener(OnDateTimeSetListener callBack) { + mOnDateTimeSetListener = callBack; + } + + // 私有方法,用于更新对话框的标题,根据传入的日期时间(以毫秒为单位的时间戳)以及当前的24小时制视图模式标识等信息,格式化并设置标题显示的内容 + private void updateTitle(long date) { + // 定义用于格式化日期时间显示的标志位,包括显示年、日期、时间等信息 + int flag = + DateUtils.FORMAT_SHOW_YEAR | + DateUtils.FORMAT_SHOW_DATE | + DateUtils.FORMAT_SHOW_TIME; + + // 根据当前是否处于24小时制视图模式,添加对应的格式化标志,以便正确格式化时间显示(是采用24小时制还是12小时制带AM/PM标识的格式) + flag |= mIs24HourView? DateUtils.FORMAT_24HOUR : DateUtils.FORMAT_12HOUR; + + // 使用DateUtils工具类的formatDateTime方法,根据传入的上下文、日期时间戳以及格式化标志,格式化日期时间信息并设置为对话框的标题内容 + setTitle(DateUtils.formatDateTime(this.getContext(), date, flag)); + } + + // 实现OnClickListener接口的点击事件处理方法,当用户点击对话框中的按钮(这里主要关注确定按钮)时触发 + // 根据按钮点击情况以及是否设置了OnDateTimeSetListener监听器,来决定是否触发监听器回调并传递选择的日期时间信息 + public void onClick(DialogInterface arg0, int arg1) { + if (mOnDateTimeSetListener!= null) { + mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis()); + } + } +} \ No newline at end of file diff --git a/DropdownMenu.java b/DropdownMenu.java new file mode 100644 index 0000000..bd540f8 --- /dev/null +++ b/DropdownMenu.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.Context; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.PopupMenu; +import android.widget.PopupMenu.OnMenuItemClickListener; + +import net.micode.notes.R; + +public class DropdownMenu { + private Button mButton; + private PopupMenu mPopupMenu; + private Menu mMenu; + + public DropdownMenu(Context context, Button button, int menuId) { + mButton = button; + mButton.setBackgroundResource(R.drawable.dropdown_icon); + mPopupMenu = new PopupMenu(context, mButton); + mMenu = mPopupMenu.getMenu(); + mPopupMenu.getMenuInflater().inflate(menuId, mMenu); + mButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + mPopupMenu.show(); + } + }); + } + + public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) { + if (mPopupMenu != null) { + mPopupMenu.setOnMenuItemClickListener(listener); + } + } + + public MenuItem findItem(int id) { + return mMenu.findItem(id); + } + + public void setTitle(CharSequence title) { + mButton.setText(title); + } +} + + + + + +package net.micode.notes.ui; + +import android.content.Context; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.PopupMenu; +import android.widget.PopupMenu.OnMenuItemClickListener; + +import net.micode.notes.R; + +// DropdownMenu类用于创建一个下拉菜单的功能组件,它将一个Button按钮与一个PopupMenu(弹出式菜单)相关联, +// 点击按钮时会弹出对应的菜单,并且可以设置菜单选项的点击监听器、查找特定菜单项以及设置按钮显示的标题等操作。 +public class DropdownMenu { + + // 用于存储关联的Button按钮对象,该按钮作为触发弹出菜单显示的交互元素 + private Button mButton; + // 用于存储创建的PopupMenu(弹出式菜单)对象,承载具体的菜单内容和交互逻辑 + private PopupMenu mPopupMenu; + // 用于存储PopupMenu对应的Menu对象,方便后续对菜单本身进行一些操作,比如查找菜单项等 + private Menu mMenu; + + // 构造函数,用于初始化DropdownMenu实例,传入上下文、要关联的Button按钮以及菜单资源的ID + public DropdownMenu(Context context, Button button, int menuId) { + // 将传入的Button按钮对象赋值给成员变量mButton,以便后续操作 + mButton = button; + + // 为Button按钮设置背景资源,这里使用了资源文件中定义的drawable资源(dropdown_icon)作为按钮的背景图片, + // 通常用于展示一个下拉箭头之类的图标,提示用户该按钮可以点击弹出菜单 + mButton.setBackgroundResource(R.drawable.dropdown_icon); + + // 创建一个PopupMenu实例,传入当前上下文和关联的Button按钮,使得弹出菜单能够在合适的位置(以按钮为锚点)显示出来 + mPopupMenu = new PopupMenu(context, mButton); + + // 获取PopupMenu对应的Menu对象,并赋值给成员变量mMenu,方便后续操作菜单内容 + mMenu = mPopupMenu.getMenu(); + + // 使用MenuInflater(菜单填充器)将指定资源ID对应的菜单布局资源填充到创建的Menu对象中, + // 这样就构建好了具体的菜单结构,包含了各个菜单项等内容 + mPopupMenu.getMenuInflater().inflate(menuId, mMenu); + + // 为Button按钮设置点击事件监听器,当按钮被点击时触发相应逻辑 + mButton.setOnClickListener(new OnClickListener() { + // 实现OnClickListener接口的点击事件处理方法,当按钮被点击时 + public void onClick(View v) { + // 调用PopupMenu的show方法,显示弹出式菜单,使其展示在屏幕上供用户选择菜单项 + mPopupMenu.show(); + } + }); + } + + // 用于设置PopupMenu中菜单项的点击事件监听器,外部可以传入实现了OnMenuItemClickListener接口的实例, + // 来处理用户点击菜单项后的相应逻辑 + public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) { + if (mPopupMenu!= null) { + // 如果PopupMenu对象不为空,为其设置菜单项点击事件监听器,将传入的监听器对象关联到PopupMenu上 + mPopupMenu.setOnMenuItemClickListener(listener); + } + } + + // 用于在已构建的Menu对象中查找指定ID的菜单项,返回对应的MenuItem对象,方便外部对特定菜单项进行操作,比如获取其属性、设置其状态等 + public MenuItem findItem(int id) { + return mMenu.findItem(id); + } + + // 用于设置Button按钮显示的标题文本内容,改变按钮上展示给用户的文字提示信息 + public void setTitle(CharSequence title) { + mButton.setText(title); + } +} diff --git a/FoldersListAdapter.java b/FoldersListAdapter.java new file mode 100644 index 0000000..e4c0d4b --- /dev/null +++ b/FoldersListAdapter.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.Context; +import android.database.Cursor; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CursorAdapter; +import android.widget.LinearLayout; +import android.widget.TextView; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; + + +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; + + public FoldersListAdapter(Context context, Cursor c) { + super(context, c); + // TODO Auto-generated constructor stub + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return new FolderListItem(context); + } + + @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); + } + } + + public String getFolderName(Context context, int position) { + Cursor cursor = (Cursor) getItem(position); + return (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context + .getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN); + } + + private class FolderListItem extends LinearLayout { + private TextView mName; + + public FolderListItem(Context context) { + super(context); + inflate(context, R.layout.folder_list_item, this); + mName = (TextView) findViewById(R.id.tv_folder_name); + } + + public void bind(String name) { + mName.setText(name); + } + } + +} +package net.micode.notes.ui; + +import android.content.Context; +import android.database.Cursor; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CursorAdapter; +import android.widget.LinearLayout; +import android.widget.TextView; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; + +// FoldersListAdapter类继承自CursorAdapter,它是一个用于将数据库游标(Cursor)中的数据适配到ListView等列表视图组件的适配器类, +// 主要用于展示文件夹相关信息的列表,每个列表项对应一个文件夹的名称或特定标识,同时提供了获取文件夹名称等相关功能。 +public class FoldersListAdapter extends CursorAdapter { + + // 定义一个字符串数组,用于指定从数据库查询时需要获取的列名。 + // 这里获取的是便签的唯一标识符(ID)和文件夹名称片段(SNIPPET,可能用于表示文件夹名称等相关信息)两列。 + public static final String [] PROJECTION = { + NoteColumns.ID, + NoteColumns.SNIPPET + }; + + // 定义常量,表示查询结果中ID列对应的索引位置,方便后续从Cursor中获取数据,这里索引为0。 + public static final int ID_COLUMN = 0; + // 定义常量,表示查询结果中用于表示文件夹名称(或名称片段)列对应的索引位置,方便后续从Cursor中获取数据,这里索引为1。 + public static final int NAME_COLUMN = 1; + + // 构造函数,用于初始化FoldersListAdapter实例,接收上下文和数据库游标(Cursor)作为参数,调用父类的构造函数完成基础的初始化工作。 + public FoldersListAdapter(Context context, Cursor c) { + super(context, c); + // TODO Auto-generated constructor stub:此处可能是预留的代码编写位置,目前为空,可根据后续需求添加额外的初始化逻辑。 + } + + // 重写CursorAdapter的newView方法,该方法用于创建一个新的视图(View)对象,作为列表中的一个新的列表项视图。 + // 在这里创建并返回一个自定义的FolderListItem视图对象,用于展示文件夹相关信息。 + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return new FolderListItem(context); + } + + // 重写CursorAdapter的bindView方法,该方法用于将游标(Cursor)中当前位置的数据绑定到指定的视图(View)上,进行数据展示等操作。 + @Override + public void bindView(View view, Context context, Cursor cursor) { + // 判断传入的视图是否是FolderListItem类型的实例,确保进行正确的绑定操作。 + if (view instanceof FolderListItem) { + // 获取文件夹名称相关信息,根据游标中获取的ID列的值进行判断,如果ID等于Notes.ID_ROOT_FOLDER(可能是代表根文件夹的特定标识), + // 则从资源文件中获取对应的特定字符串(可能是表示“上级文件夹”之类的提示文本)作为文件夹名称;否则从游标中获取对应的名称列(NAME_COLUMN)的值作为文件夹名称。 + String folderName = (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER)? context + .getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN); + // 调用FolderListItem的bind方法,将获取到的文件夹名称设置到对应的视图中的TextView组件上进行显示。 + ((FolderListItem) view).bind(folderName); + } + } + + // 定义一个方法,用于获取指定位置的文件夹名称,接收上下文和位置索引作为参数。 + // 通过调用getItem方法获取对应位置的游标(Cursor)对象,然后按照前面同样的逻辑判断并返回文件夹名称。 + public String getFolderName(Context context, int position) { + Cursor cursor = (Cursor) getItem(position); + return (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER)? context + .getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN); + } + + // 定义一个内部类FolderListItem,继承自LinearLayout,它代表了列表中的一个文件夹列表项的自定义视图结构,用于展示文件夹名称等信息。 + private class FolderListItem extends LinearLayout { + // 定义一个TextView组件,用于显示文件夹的名称信息。 + private TextView mName; + + // 构造函数,用于初始化FolderListItem实例,传入上下文对象,调用父类的构造函数完成基础初始化, + // 并加载对应的布局文件(folder_list_item.xml)到该视图中,然后获取布局中的TextView组件。 + public FolderListItem(Context context) { + super(context); + inflate(context, R.layout.folder_list_item, this); + mName = (TextView) findViewById(R.id.tv_folder_name); + } + + // 定义一个方法,用于将传入的文件夹名称设置到内部的TextView组件上进行显示,实现数据与视图的绑定展示。 + public void bind(String name) { + mName.setText(name); + } + } +} \ No newline at end of file diff --git a/NoteEditActivity.java b/NoteEditActivity.java new file mode 100644 index 0000000..95bc32e --- /dev/null +++ b/NoteEditActivity.java @@ -0,0 +1,335 @@ +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,实现了OnClickListener、NoteSettingChangedListener、OnTextViewChangeListener接口,用于编辑笔记相关的操作和界面交互 +public class NoteEditActivity extends Activity implements OnClickListener, + NoteSettingChangedListener, OnTextViewChangeListener { + + // 内部类,用于方便地持有笔记头部视图相关的控件引用 + private class HeadViewHolder { + public TextView tvModified; // 用于显示笔记修改时间的TextView + public ImageView ivAlertIcon; // 用于显示提醒图标(比如闹钟图标等)的ImageView + public TextView tvAlertDate; // 用于显示提醒时间相关文本的TextView + public ImageView ibSetBgColor; // 用于设置背景颜色的按钮ImageView + } + + // 以下是一些静态的映射关系,用于将界面上的按钮ID与对应的颜色资源ID等进行关联 + + // 将背景颜色选择按钮的ID映射到对应的颜色资源ID(如黄色、红色等颜色的资源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映射到对应的已选择该颜色时显示的标识视图的ID(比如选中黄色背景时对应的选中标识视图的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映射到对应的字体大小资源ID(如大字体、小字体等对应的资源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映射到对应的已选择该字体大小时显示的标识视图的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; // 用于编辑笔记内容的EditText控件 + 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; // 用于显示笔记内容为列表模式时的列表布局(LinearLayout) + private String mUserQuery; // 用户输入的查询字符串(可能用于搜索笔记内容等场景) + private Pattern mPattern; // 用于对用户查询内容进行正则匹配的模式对象 + + // Activity创建时调用的方法 + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // 设置该Activity对应的布局文件 + this.setContentView(R.layout.note_edit); + + // 如果savedInstanceState为null(即Activity首次创建,不是从内存恢复的情况),并且初始化Activity状态失败,则直接结束该Activity + if (savedInstanceState == null &&!initActivityState(getIntent())) { + finish(); + return; + } + // 初始化相关资源,如查找视图等操作 + initResources(); + } + + /** + * 当Activity因内存不足被系统销毁后,再次恢复时调用该方法,用于从保存的状态中恢复Activity的相关信息 + */ + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + // 如果保存的状态不为null且包含特定的额外数据(通过Intent.EXTRA_UID标识),则尝试重新初始化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"); + } + } + + // 根据传入的Intent初始化Activity的状态,比如确定是查看已有笔记还是新建笔记等情况,并加载相应的笔记数据 + private boolean initActivityState(Intent intent) { + /** + * 如果用户指定了Intent.ACTION_VIEW动作,但没有提供笔记的ID,那么跳转到NotesListActivity(可能是笔记列表页面) + */ + mWorkingNote = null; + if (TextUtils.equals(Intent.ACTION_VIEW, intent.getAction())) { + long noteId = intent.getLongExtra(Intent.EXTRA_UID, 0); + mUserQuery = ""; + + /** + * 如果Intent中包含搜索相关的额外数据(从搜索结果进入该Activity的情况),则获取对应的笔记ID和用户查询字符串 + */ + if (intent.hasExtra(SearchManager.EXTRA_DATA_KEY)) { + noteId = Long.parseLong(intent.getStringExtra(SearchManager.EXTRA_DATA_KEY)); + mUserQuery = intent.getStringExtra(SearchManager.USER_QUERY); + } + + // 检查指定的笔记ID对应的笔记在数据库中是否可见(是否存在且满足可见条件),如果不存在则跳转到笔记列表页面并提示错误信息 + if (!DataUtils.visibleInNoteDatabase(getContentResolver(), noteId, Notes.TYPE_NOTE)) { + Intent jump = new Intent(this, NotesListActivity.class); + startActivity(jump); + showToast(R.string.error_note_not_exist); + finish(); + return false; + } else { + // 从数据库加载指定ID的笔记对象,如果加载失败则记录错误日志并结束该Activity + 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; + } + // 设置笔记对象的设置状态改变监听器为当前Activity(实现了NoteSettingChangedListener接口) + mWorkingNote.setOnSettingStatusChangedListener(this); + return true; + } + + // Activity恢复可见时(如从暂停状态恢复)调用的方法,用于初始化笔记编辑界面的相关显示内容 + @Override + protected void onResume() { + super.onResume(); + initNoteScreen(); + } + + // 初始化笔记编辑界面的显示内容,包括设置字体样式、根据笔记模式显示不同内容、设置头部和编辑区域的背景等 + private void initNoteScreen() { + mNoteEditor.setTextAppearance(this, TextAppearanceResources + .getTexAppearanceResource(mFontSizeId)); + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + switchToListMode(mWorkingNote.getContent()); + } else { + mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery)); + mNoteEditor.setSelection(mNoteEditor.getText().length()); + } + for (Integer id : sBgSelectorSelectionMap.keySet()) { + findViewById(sBgSelectorSelectionMap.get(id)).setVisibility(View.GONE); + } + mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId()); + mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId()); + + mNoteHeaderHolder.tvModified.setText(DateUtils.formatDateTime(this, + mWorkingNote.getModifiedDate(), DateUtils.FORMAT_SHOW_DATE + | DateUtils.FORMAT_NUMERIC_DATE | DateUtils.FORMAT_SHOW_TIME + | DateUtils.FORMAT_SHOW_YEAR)); + + /** + * TODO: 添加用于设置提醒的菜单选项。目前禁用该功能,因为DateTimePicker还未准备好 + */ + showAlertHeader(); + } + + // 根据笔记是否设置了提醒时间,显示或隐藏提醒相关的视图(如提醒时间文本和图标)并设置相应的文本内容 + private void showAlertHeader() { + if (mWorkingNote.hasClockAlert()) { + long time = System.currentTimeMillis(); + if (time > mWorkingNote.getAlertDate()) { + mNoteHeaderHolder.tvAlertDate.setText(R.string.note_alert_expired); + } else { + mNoteHeaderHolder.tvAlertDate.setText(DateUtils.getRelativeTimeSpanString( + mWorkingNote.getAlertDate(), time, DateUtils.MINUTE_IN_MILLIS)); + } + mNoteHeaderHolder.tvAlertDate.setVisibility(View.VISIBLE); + mNoteHeaderHolder.ivAlertIcon.setVisibility(View.VISIBLE); + } else { + mNoteHeaderHolder.tvAlertDate.setVisibility(View.GONE); + mNoteHeaderHolder.ivAlertIcon.setVisibility(View.GONE); + }; + } + + // 当Activity接收到新的Intent时调用该方法(比如通过启动模式设置可以接收新的Intent情况),重新初始化Activity状态 + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + initActivityState(intent); + } + + // 在Activity可能被销毁前(如系统内存不足、用户切换应用等情况)保存当前Activity的相关状态信息(比如正在编辑的笔记的ID等) + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + /** + * 对于没有笔记ID的新笔记(可能还未保存),先保存笔记以生成一个ID。如果正在编辑的笔记不值得保存(可能内容为空等情况),没有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 \ No newline at end of file diff --git a/NoteEditText.java b/NoteEditText.java new file mode 100644 index 0000000..91d7dca --- /dev/null +++ b/NoteEditText.java @@ -0,0 +1,452 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.Context; +import android.graphics.Rect; +import android.text.Layout; +import android.text.Selection; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.style.URLSpan; +import android.util.AttributeSet; +import android.util.Log; +import android.view.ContextMenu; +import android.view.KeyEvent; +import android.view.MenuItem; +import android.view.MenuItem.OnMenuItemClickListener; +import android.view.MotionEvent; +import android.widget.EditText; + +import net.micode.notes.R; + +import java.util.HashMap; +import java.util.Map; + +public class NoteEditText extends EditText { + private static final String TAG = "NoteEditText"; + private int mIndex; + private int mSelectionStartBeforeDelete; + + private static final String SCHEME_TEL = "tel:" ; + private static final String SCHEME_HTTP = "http:" ; + private static final String SCHEME_EMAIL = "mailto:" ; + + private static final Map sSchemaActionResMap = new HashMap(); + static { + sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel); + sSchemaActionResMap.put(SCHEME_HTTP, R.string.note_link_web); + sSchemaActionResMap.put(SCHEME_EMAIL, R.string.note_link_email); + } + + /** + * Call by the {@link NoteEditActivity} to delete or add edit text + */ + public interface OnTextViewChangeListener { + /** + * Delete current edit text when {@link KeyEvent#KEYCODE_DEL} happens + * and the text is null + */ + void onEditTextDelete(int index, String text); + + /** + * Add edit text after current edit text when {@link KeyEvent#KEYCODE_ENTER} + * happen + */ + void onEditTextEnter(int index, String text); + + /** + * Hide or show item option when text change + */ + void onTextChange(int index, boolean hasText); + } + + private OnTextViewChangeListener mOnTextViewChangeListener; + + public NoteEditText(Context context) { + super(context, null); + mIndex = 0; + } + + public void setIndex(int index) { + mIndex = index; + } + + public void setOnTextViewChangeListener(OnTextViewChangeListener listener) { + mOnTextViewChangeListener = listener; + } + + public NoteEditText(Context context, AttributeSet attrs) { + super(context, attrs, android.R.attr.editTextStyle); + } + + public NoteEditText(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + // TODO Auto-generated constructor stub + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + + int x = (int) event.getX(); + int y = (int) event.getY(); + x -= getTotalPaddingLeft(); + y -= getTotalPaddingTop(); + x += getScrollX(); + y += getScrollY(); + + Layout layout = getLayout(); + int line = layout.getLineForVertical(y); + int off = layout.getOffsetForHorizontal(line, x); + Selection.setSelection(getText(), off); + break; + } + + return super.onTouchEvent(event); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_ENTER: + if (mOnTextViewChangeListener != null) { + return false; + } + break; + case KeyEvent.KEYCODE_DEL: + mSelectionStartBeforeDelete = getSelectionStart(); + break; + default: + break; + } + return super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + switch(keyCode) { + case KeyEvent.KEYCODE_DEL: + if (mOnTextViewChangeListener != null) { + if (0 == mSelectionStartBeforeDelete && mIndex != 0) { + mOnTextViewChangeListener.onEditTextDelete(mIndex, getText().toString()); + return true; + } + } else { + Log.d(TAG, "OnTextViewChangeListener was not seted"); + } + break; + case KeyEvent.KEYCODE_ENTER: + if (mOnTextViewChangeListener != null) { + int selectionStart = getSelectionStart(); + String text = getText().subSequence(selectionStart, length()).toString(); + setText(getText().subSequence(0, selectionStart)); + mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, text); + } else { + Log.d(TAG, "OnTextViewChangeListener was not seted"); + } + break; + default: + break; + } + return super.onKeyUp(keyCode, event); + } + + @Override + protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { + if (mOnTextViewChangeListener != null) { + if (!focused && TextUtils.isEmpty(getText())) { + mOnTextViewChangeListener.onTextChange(mIndex, false); + } else { + mOnTextViewChangeListener.onTextChange(mIndex, true); + } + } + super.onFocusChanged(focused, direction, previouslyFocusedRect); + } + + @Override + protected void onCreateContextMenu(ContextMenu menu) { + if (getText() instanceof Spanned) { + int selStart = getSelectionStart(); + int selEnd = getSelectionEnd(); + + int min = Math.min(selStart, selEnd); + int max = Math.max(selStart, selEnd); + + final URLSpan[] urls = ((Spanned) getText()).getSpans(min, max, URLSpan.class); + if (urls.length == 1) { + int defaultResId = 0; + for(String schema: sSchemaActionResMap.keySet()) { + if(urls[0].getURL().indexOf(schema) >= 0) { + defaultResId = sSchemaActionResMap.get(schema); + break; + } + } + + if (defaultResId == 0) { + defaultResId = R.string.note_link_other; + } + + menu.add(0, 0, 0, defaultResId).setOnMenuItemClickListener( + new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + // goto a new intent + urls[0].onClick(NoteEditText.this); + return true; + } + }); + } + } + super.onCreateContextMenu(menu); + } +} + + + + + +package net.micode.notes.ui; + +import android.content.Context; +import android.graphics.Rect; +import android.text.Layout; +import android.text.Selection; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.style.URLSpan; +import android.util.AttributeSet; +import android.util.Log; +import android.view.ContextMenu; +import android.view.KeyEvent; +import android.view.MenuItem; +import android.view.MenuItem.OnMenuItemClickListener; +import android.view.MotionEvent; +import android.widget.EditText; + +import net.micode.notes.R; + +import java.util.HashMap; +import java.util.Map; + +// NoteEditText类继承自EditText,是一个自定义的文本编辑框控件,在基础的文本编辑功能上进行了扩展, +// 用于处理与便签编辑相关的特定交互逻辑,比如响应回车键、删除键操作,根据文本内容变化显示或隐藏相关菜单项,以及处理文本中的链接点击等功能。 +public class NoteEditText extends EditText { + + // 用于日志记录的标签,方便在日志输出中识别该类相关的日志信息。 + private static final String TAG = "NoteEditText"; + + // 用于记录当前编辑文本框在整个编辑列表中的索引位置,方便在进行插入、删除等操作时确定其相对位置。 + private int mIndex; + + // 用于记录在按下删除键(KEYCODE_DEL)之前文本选择的起始位置,以便后续判断是否执行特定的删除逻辑(比如删除当前文本框等情况)。 + private int mSelectionStartBeforeDelete; + + // 定义电话链接的协议头(表示以拨打电话的方式打开链接)。 + private static final String SCHEME_TEL = "tel:"; + // 定义超文本传输协议链接的协议头(表示以网页浏览的方式打开链接)。 + private static final String SCHEME_HTTP = "http:"; + // 定义邮件链接的协议头(表示以发送邮件的方式打开链接)。 + private static final String SCHEME_EMAIL = "mailto:"; + + // 用于将不同的链接协议头与对应的字符串资源ID进行映射,这些字符串资源可能用于在菜单中显示对应链接类型的描述信息,方便用户识别链接的作用。 + private static final Map sSchemaActionResMap = new HashMap(); + static { + sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel); + sSchemaActionResMap.put(SCHEME_HTTP, R.string.note_link_web); + sSchemaActionResMap.put(SCHEME_EMAIL, R.string.note_link_email); + } + + // 定义一个接口,用于与外部类(如`NoteEditActivity`)进行交互,通知外部在文本编辑框中文本发生特定变化(删除、添加、内容变更等)时进行相应处理。 + public interface OnTextViewChangeListener { + /** + * 当按下删除键(KEYCODE_DEL)且文本内容为空时,删除当前编辑文本框的相关逻辑。 + */ + void onEditTextDelete(int index, String text); + + /** + * 当按下回车键(KEYCODE_ENTER)时,在当前编辑文本框之后添加新的编辑文本框的相关逻辑。 + */ + void onEditTextEnter(int index, String text); + + /** + * 当文本内容发生变化时,根据文本是否为空来隐藏或显示相关的菜单项等操作的逻辑。 + */ + void onTextChange(int index, boolean hasText); + } + + // 用于存储实现了`OnTextViewChangeListener`接口的监听器对象,外部类可以通过设置该监听器来响应文本编辑框中的相关文本变化事件。 + private OnTextViewChangeListener mOnTextViewChangeListener; + + // 构造函数,接收上下文对象,调用父类的构造函数进行初始化,同时初始化当前编辑文本框的索引为0。 + public NoteEditText(Context context) { + super(context, null); + mIndex = 0; + } + + // 用于设置当前编辑文本框在整个编辑列表中的索引位置。 + public void setIndex(int index) { + mIndex = index; + } + + // 用于设置文本变化监听器对象,外部类通过传入实现了`OnTextViewChangeListener`接口的实例,来接收文本编辑框中的相关文本变化通知并进行处理。 + public void setOnTextViewChangeListener(OnTextViewChangeListener listener) { + mOnTextViewChangeListener = listener; + } + + // 构造函数,接收上下文和属性集对象,调用父类对应的构造函数进行初始化,采用系统默认的编辑文本框样式(通过`android.R.attr.editTextStyle`指定)。 + public NoteEditText(Context context, AttributeSet attrs) { + super(context, attrs, android.R.attr.editTextStyle); + } + + // 构造函数,接收上下文、属性集和默认样式资源ID,调用父类对应的构造函数进行初始化,此处`TODO Auto-generated constructor stub`表示可能需要后续补充一些特定的初始化逻辑(目前为空)。 + public NoteEditText(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + // TODO Auto-generated constructor stub + } + + // 重写父类的`onTouchEvent`方法,用于处理触摸事件,主要实现了点击文本区域时根据触摸位置设置文本光标位置的功能,以便用户能方便地在指定位置进行文本编辑操作。 + @Override + public boolean onTouchEvent(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + // 获取触摸点相对于文本编辑框左边的距离(去除文本框的内边距等影响),并考虑滚动偏移量,得到在文本内容区域内的横坐标位置。 + int x = (int) event.getX(); + x -= getTotalPaddingLeft(); + x += getScrollX(); + + // 获取触摸点相对于文本编辑框上边的距离(去除文本框的内边距等影响),并考虑滚动偏移量,得到在文本内容区域内的纵坐标位置。 + int y = (int) event.getY(); + y -= getTotalPaddingTop(); + y += getScrollY(); + + // 根据触摸点的纵坐标位置获取所在的文本行数。 + Layout layout = getLayout(); + int line = layout.getLineForVertical(y); + + // 根据触摸点的横坐标位置和所在行数,获取对应的文本字符偏移量(即触摸位置对应的文本中的字符位置)。 + int off = layout.getOffsetForHorizontal(line, x); + + // 根据获取到的字符偏移量设置文本的光标位置,使得用户点击文本区域时能准确地定位到点击位置进行后续编辑操作。 + Selection.setSelection(getText(), off); + break; + } + + return super.onTouchEvent(event); + } + + // 重写父类的`onKeyDown`方法,用于处理按键按下事件,在这里主要是记录按下删除键(KEYCODE_DEL)时文本选择的起始位置,以及在按下回车键(KEYCODE_ENTER)时进行一些特定的逻辑处理(目前是返回false,可能后续会添加更多逻辑)。 + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_ENTER: + if (mOnTextViewChangeListener!= null) { + return false; + } + break; + case KeyEvent.KEYCODE_DEL: + mSelectionStartBeforeDelete = getSelectionStart(); + break; + default: + break; + } + return super.onKeyDown(keyCode, event); + } + + // 重写父类的`onKeyUp`方法,用于处理按键抬起事件,在这里根据抬起的按键不同执行不同的逻辑, + // 比如按下删除键抬起时,如果文本框索引不为0且文本选择起始位置为0(可能表示要删除当前文本框等情况),则通知监听器执行删除逻辑; + // 按下回车键抬起时,获取当前光标位置后的文本内容,将其作为新添加文本框的初始内容,并通知监听器执行添加文本框的逻辑。 + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_DEL: + if (mOnTextViewChangeListener!= null) { + if (0 == mSelectionStartBeforeDelete && mIndex!= 0) { + mOnTextViewChangeListener.onEditTextDelete(mIndex, getText().toString()); + return true; + } + } else { + Log.d(TAG, "OnTextViewChangeListener was not seted"); + } + break; + case KeyEvent.KEYCODE_ENTER: + if (mOnTextViewChangeListener!= null) { + int selectionStart = getSelectionStart(); + String text = getText().subSequence(selectionStart, length()).toString(); + setText(getText().subSequence(0, selectionStart)); + mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, text); + } else { + Log.d(TAG, "OnTextViewChangeListener was not seted"); + } + break; + default: + break; + } + return super.onKeyUp(keyCode, event); + } + + // 重写父类的`onFocusChanged`方法,用于处理文本编辑框焦点变化事件,在这里根据焦点是否失去以及文本内容是否为空,通知监听器执行相应的文本变化处理逻辑(比如隐藏或显示相关菜单项等)。 + @Override + protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { + if (mOnTextViewChangeListener!= null) { + if (!focused && TextUtils.isEmpty(getText())) { + mOnTextViewChangeListener.onTextChange(mIndex, false); + } else { + mOnTextViewChangeListener.onTextChange(mIndex, true); + } + } + super.onFocusChanged(focused, direction, previouslyFocusedRect); + } + + // 重写父类的`onCreateContextMenu`方法,用于创建上下文菜单(长按文本时弹出的菜单),在这里主要是处理文本中包含链接(通过`URLSpan`标识)的情况, + // 如果选中的文本区域内只有一个链接,根据链接的协议头查找对应的菜单描述资源ID,并添加一个菜单项用于点击链接执行相应操作(如打开网页、拨打电话、发送邮件等)。 + @Override + protected void onCreateContextMenu(ContextMenu menu) { + if (getText() instanceof Spanned) { + int selStart = getSelectionStart(); + int selEnd = getSelectionEnd(); + + int min = Math.min(selStart, selEnd); + int max = Math.max(selStart, selEnd); + + final URLSpan[] urls = ((Spanned) getText()).getSpans(min, max, URLSpan.class); + if (urls.length == 1) { + int defaultResId = 0; + for (String schema : sSchemaActionResMap.keySet()) { + if (urls[0].getURL().indexOf(schema) >= 0) { + defaultResId = sSchemaActionResMap.get(schema); + break; + } + } + + if (defaultResId == 0) { + defaultResId = R.string.note_link_other; + } + + menu.add(0, 0, 0, defaultResId).setOnMenuItemClickListener( + new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + // 点击菜单项时,执行链接对应的点击操作,比如打开相应的网页、拨打电话或者发送邮件等,调用URLSpan的onClick方法来触发具体操作。 + urls[0].onClick(NoteEditText.this); + return true; + } + }); + } + } + super.onCreateContextMenu(menu); + } +} \ No newline at end of file diff --git a/NoteItemData.java b/NoteItemData.java new file mode 100644 index 0000000..0f5a878 --- /dev/null +++ b/NoteItemData.java @@ -0,0 +1,224 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.Context; +import android.database.Cursor; +import android.text.TextUtils; + +import net.micode.notes.data.Contact; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.tool.DataUtils; + + +public class NoteItemData { + static final String [] PROJECTION = new String [] { + NoteColumns.ID, + NoteColumns.ALERTED_DATE, + NoteColumns.BG_COLOR_ID, + NoteColumns.CREATED_DATE, + NoteColumns.HAS_ATTACHMENT, + NoteColumns.MODIFIED_DATE, + NoteColumns.NOTES_COUNT, + NoteColumns.PARENT_ID, + NoteColumns.SNIPPET, + NoteColumns.TYPE, + NoteColumns.WIDGET_ID, + NoteColumns.WIDGET_TYPE, + }; + + private static final int ID_COLUMN = 0; + private static final int ALERTED_DATE_COLUMN = 1; + private static final int BG_COLOR_ID_COLUMN = 2; + private static final int CREATED_DATE_COLUMN = 3; + private static final int HAS_ATTACHMENT_COLUMN = 4; + private static final int MODIFIED_DATE_COLUMN = 5; + private static final int NOTES_COUNT_COLUMN = 6; + private static final int PARENT_ID_COLUMN = 7; + private static final int SNIPPET_COLUMN = 8; + private static final int TYPE_COLUMN = 9; + private static final int WIDGET_ID_COLUMN = 10; + private static final int WIDGET_TYPE_COLUMN = 11; + + private long mId; + private long mAlertDate; + private int mBgColorId; + private long mCreatedDate; + private boolean mHasAttachment; + private long mModifiedDate; + private int mNotesCount; + private long mParentId; + private String mSnippet; + private int mType; + private int mWidgetId; + private int mWidgetType; + private String mName; + private String mPhoneNumber; + + private boolean mIsLastItem; + private boolean mIsFirstItem; + private boolean mIsOnlyOneItem; + private boolean mIsOneNoteFollowingFolder; + private boolean mIsMultiNotesFollowingFolder; + + public NoteItemData(Context context, Cursor cursor) { + mId = cursor.getLong(ID_COLUMN); + mAlertDate = cursor.getLong(ALERTED_DATE_COLUMN); + mBgColorId = cursor.getInt(BG_COLOR_ID_COLUMN); + mCreatedDate = cursor.getLong(CREATED_DATE_COLUMN); + mHasAttachment = (cursor.getInt(HAS_ATTACHMENT_COLUMN) > 0) ? true : false; + mModifiedDate = cursor.getLong(MODIFIED_DATE_COLUMN); + mNotesCount = cursor.getInt(NOTES_COUNT_COLUMN); + mParentId = cursor.getLong(PARENT_ID_COLUMN); + mSnippet = cursor.getString(SNIPPET_COLUMN); + mSnippet = mSnippet.replace(NoteEditActivity.TAG_CHECKED, "").replace( + NoteEditActivity.TAG_UNCHECKED, ""); + mType = cursor.getInt(TYPE_COLUMN); + mWidgetId = cursor.getInt(WIDGET_ID_COLUMN); + mWidgetType = cursor.getInt(WIDGET_TYPE_COLUMN); + + mPhoneNumber = ""; + if (mParentId == Notes.ID_CALL_RECORD_FOLDER) { + mPhoneNumber = DataUtils.getCallNumberByNoteId(context.getContentResolver(), mId); + if (!TextUtils.isEmpty(mPhoneNumber)) { + mName = Contact.getContact(context, mPhoneNumber); + if (mName == null) { + mName = mPhoneNumber; + } + } + } + + if (mName == null) { + mName = ""; + } + checkPostion(cursor); + } + + private void checkPostion(Cursor cursor) { + mIsLastItem = cursor.isLast() ? true : false; + mIsFirstItem = cursor.isFirst() ? true : false; + mIsOnlyOneItem = (cursor.getCount() == 1); + mIsMultiNotesFollowingFolder = false; + mIsOneNoteFollowingFolder = false; + + if (mType == Notes.TYPE_NOTE && !mIsFirstItem) { + int position = cursor.getPosition(); + if (cursor.moveToPrevious()) { + if (cursor.getInt(TYPE_COLUMN) == Notes.TYPE_FOLDER + || cursor.getInt(TYPE_COLUMN) == Notes.TYPE_SYSTEM) { + if (cursor.getCount() > (position + 1)) { + mIsMultiNotesFollowingFolder = true; + } else { + mIsOneNoteFollowingFolder = true; + } + } + if (!cursor.moveToNext()) { + throw new IllegalStateException("cursor move to previous but can't move back"); + } + } + } + } + + public boolean isOneFollowingFolder() { + return mIsOneNoteFollowingFolder; + } + + public boolean isMultiFollowingFolder() { + return mIsMultiNotesFollowingFolder; + } + + public boolean isLast() { + return mIsLastItem; + } + + public String getCallName() { + return mName; + } + + public boolean isFirst() { + return mIsFirstItem; + } + + public boolean isSingle() { + return mIsOnlyOneItem; + } + + public long getId() { + return mId; + } + + public long getAlertDate() { + return mAlertDate; + } + + public long getCreatedDate() { + return mCreatedDate; + } + + public boolean hasAttachment() { + return mHasAttachment; + } + + public long getModifiedDate() { + return mModifiedDate; + } + + public int getBgColorId() { + return mBgColorId; + } + + public long getParentId() { + return mParentId; + } + + public int getNotesCount() { + return mNotesCount; + } + + public long getFolderId () { + return mParentId; + } + + public int getType() { + return mType; + } + + public int getWidgetType() { + return mWidgetType; + } + + public int getWidgetId() { + return mWidgetId; + } + + public String getSnippet() { + return mSnippet; + } + + public boolean hasAlert() { + return (mAlertDate > 0); + } + + public boolean isCallRecord() { + return (mParentId == Notes.ID_CALL_RECORD_FOLDER && !TextUtils.isEmpty(mPhoneNumber)); + } + + public static int getNoteType(Cursor cursor) { + return cursor.getInt(TYPE_COLUMN); + } +} diff --git a/NotesListActivity.java b/NotesListActivity.java new file mode 100644 index 0000000..d300324 --- /dev/null +++ b/NotesListActivity.java @@ -0,0 +1,1281 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.appwidget.AppWidgetManager; +import android.content.AsyncQueryHandler; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.os.AsyncTask; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.util.Log; +import android.view.ActionMode; +import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.Display; +import android.view.HapticFeedbackConstants; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MenuItem.OnMenuItemClickListener; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnCreateContextMenuListener; +import android.view.View.OnTouchListener; +import android.view.inputmethod.InputMethodManager; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.AdapterView.OnItemLongClickListener; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.PopupMenu; +import android.widget.TextView; +import android.widget.Toast; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.gtask.remote.GTaskSyncService; +import net.micode.notes.model.WorkingNote; +import net.micode.notes.tool.BackupUtils; +import net.micode.notes.tool.DataUtils; +import net.micode.notes.tool.ResourceParser; +import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute; +import net.micode.notes.widget.NoteWidgetProvider_2x; +import net.micode.notes.widget.NoteWidgetProvider_4x; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.HashSet; + +public class NotesListActivity extends Activity implements OnClickListener, OnItemLongClickListener { + private static final int FOLDER_NOTE_LIST_QUERY_TOKEN = 0; + + private static final int FOLDER_LIST_QUERY_TOKEN = 1; + + private static final int MENU_FOLDER_DELETE = 0; + + private static final int MENU_FOLDER_VIEW = 1; + + private static final int MENU_FOLDER_CHANGE_NAME = 2; + + private static final String PREFERENCE_ADD_INTRODUCTION = "net.micode.notes.introduction"; + + private enum ListEditState { + NOTE_LIST, SUB_FOLDER, CALL_RECORD_FOLDER + }; + + private 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(); + + /** + * Insert an introduction when user firstly use this application + */ + setAppInfoFromRawRes(); + } + + @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 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 onStart() { + super.onStart(); + startAsyncNotesListQuery(); + } + + 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 ModeCallback implements ListView.MultiChoiceModeListener, OnMenuItemClickListener { + private DropdownMenu mDropDownMenu; + private ActionMode mActionMode; + private MenuItem mMoveMenu; + + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + getMenuInflater().inflate(R.menu.note_list_options, menu); + menu.findItem(R.id.delete).setOnMenuItemClickListener(this); + mMoveMenu = menu.findItem(R.id.move); + if (mFocusNoteDataItem.getParentId() == Notes.ID_CALL_RECORD_FOLDER + || DataUtils.getUserFolderCount(mContentResolver) == 0) { + mMoveMenu.setVisible(false); + } else { + mMoveMenu.setVisible(true); + mMoveMenu.setOnMenuItemClickListener(this); + } + mActionMode = mode; + mNotesListAdapter.setChoiceMode(true); + mNotesListView.setLongClickable(false); + mAddNewNote.setVisibility(View.GONE); + + View customView = LayoutInflater.from(NotesListActivity.this).inflate( + R.layout.note_list_dropdown_menu, null); + mode.setCustomView(customView); + mDropDownMenu = new DropdownMenu(NotesListActivity.this, + (Button) customView.findViewById(R.id.selection_menu), + R.menu.note_list_dropdown); + mDropDownMenu.setOnDropdownMenuItemClickListener(new PopupMenu.OnMenuItemClickListener(){ + public boolean onMenuItemClick(MenuItem item) { + mNotesListAdapter.selectAll(!mNotesListAdapter.isAllSelected()); + updateMenu(); + return true; + } + + }); + return true; + } + + private void updateMenu() { + int selectedCount = mNotesListAdapter.getSelectedCount(); + // Update dropdown menu + String format = getResources().getString(R.string.menu_select_title, selectedCount); + mDropDownMenu.setTitle(format); + MenuItem item = mDropDownMenu.findItem(R.id.action_select_all); + if (item != null) { + if (mNotesListAdapter.isAllSelected()) { + item.setChecked(true); + item.setTitle(R.string.menu_deselect_all); + } else { + item.setChecked(false); + item.setTitle(R.string.menu_select_all); + } + } + } + + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + // TODO Auto-generated method stub + return false; + } + + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + // TODO Auto-generated method stub + return false; + } + + public void onDestroyActionMode(ActionMode mode) { + mNotesListAdapter.setChoiceMode(false); + mNotesListView.setLongClickable(true); + mAddNewNote.setVisibility(View.VISIBLE); + } + + public void finishActionMode() { + mActionMode.finish(); + } + + public void onItemCheckedStateChanged(ActionMode mode, int position, long id, + boolean checked) { + mNotesListAdapter.setCheckedItem(position, checked); + updateMenu(); + } + + public boolean onMenuItemClick(MenuItem item) { + if (mNotesListAdapter.getSelectedCount() == 0) { + Toast.makeText(NotesListActivity.this, getString(R.string.menu_select_none), + Toast.LENGTH_SHORT).show(); + return true; + } + + switch (item.getItemId()) { + case R.id.delete: + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + builder.setTitle(getString(R.string.alert_title_delete)); + builder.setIcon(android.R.drawable.ic_dialog_alert); + builder.setMessage(getString(R.string.alert_message_delete_notes, + mNotesListAdapter.getSelectedCount())); + builder.setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, + int which) { + batchDelete(); + } + }); + builder.setNegativeButton(android.R.string.cancel, null); + builder.show(); + break; + case R.id.move: + startQueryDestinationFolders(); + break; + default: + return false; + } + return true; + } + } + + private class NewNoteOnTouchListener implements OnTouchListener { + + 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(); + /** + * Minus TitleBar's height + */ + if (mState == ListEditState.SUB_FOLDER) { + eventY -= mTitleBar.getHeight(); + start -= mTitleBar.getHeight(); + } + /** + * HACKME:When click the transparent part of "New Note" button, dispatch + * the event to the list view behind this button. The transparent part of + * "New Note" button could be expressed by formula y=-0.12x+94(Unit:pixel) + * and the line top of the button. The coordinate based on left of the "New + * Note" button. The 94 represents maximum height of the transparent part. + * Notice that, if the background of the button changes, the formula should + * also change. This is very bad, just for the UI designer's strong requirement. + */ + 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 not synced, delete notes directly + if (DataUtils.batchDeleteNotes(mContentResolver, mNotesListAdapter + .getSelectedItemIds())) { + } else { + Log.e(TAG, "Delete notes error, should not happens"); + } + } else { + // in sync mode, we'll move the deleted note into the trash + // folder + 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()) { + // if not synced, delete folder directly + DataUtils.batchDeleteNotes(mContentResolver, ids); + } else { + // in sync mode, we'll move the deleted folder into the trash folder + 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); + } + + 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); + } + /** + * When the name edit text is null, disable the positive button + */ + 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; + } + } + + 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); + // set sync or sync_cancel + 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"); + } + + 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; + } +} + +package net.micode.notes.ui; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.appwidget.AppWidgetManager; +import android.content.AsyncQueryHandler; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.os.AsyncTask; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.util.Log; +import android.view.ActionMode; +import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.Display; +import android.view.HapticFeedbackConstants; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MenuItem.OnMenuItemClickListener; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnCreateContextMenuListener; +import android.view.View.OnTouchListener; +import android.view.inputmethod.InputMethodManager; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.AdapterView.OnItemLongClickListener; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.PopupMenu; +import android.widget.TextView; +import android.widget.Toast; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.gtask.remote.GTaskSyncService; +import net.micode.notes.model.WorkingNote; +import net.micode.notes.tool.BackupUtils; +import net.micode.notes.tool.DataUtils; +import net.micode.notes.tool.ResourceParser; +import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute; +import net.micode.notes.widget.NoteWidgetProvider_2x; +import net.micode.notes.widget.NoteWidgetProvider_4x; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.HashSet; + +// NotesListActivity类是一个Android中的Activity,主要用于展示便签列表,实现了便签的查询、添加、删除、移动、文件夹相关操作以及与小部件更新等功能交互, +// 同时处理了各种用户交互事件,如点击、长按、触摸等操作对应的逻辑。 +public class NotesListActivity extends Activity implements OnClickListener, OnItemLongClickListener { + + // 用于标识查询文件夹下便签列表的操作标记,在异步查询处理中作为区分不同查询任务的标识。 + private static final int FOLDER_NOTE_LIST_QUERY_TOKEN = 0; + // 用于标识查询文件夹列表的操作标记,在异步查询处理中作为区分不同查询任务的标识。 + private static final int FOLDER_LIST_QUERY_TOKEN = 1; + // 菜单中删除文件夹选项对应的ID。 + private static final int MENU_FOLDER_DELETE = 0; + // 菜单中查看文件夹选项对应的ID。 + private static final int MENU_FOLDER_VIEW = 1; + // 菜单中修改文件夹名称选项对应的ID。 + 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 + }; + + // 用于记录当前列表编辑的状态,初始化为普通便签列表状态(`NOTE_LIST`)。 + private ListEditState mState; + // 用于处理后台数据库查询操作的异步查询处理器对象,通过它可以在后台线程中执行数据库查询任务,并在查询完成后在主线程处理结果。 + private BackgroundQueryHandler mBackgroundQueryHandler; + // 用于适配便签列表数据并展示在列表视图中的适配器对象,负责将从数据库获取的便签数据转换为适合在列表中展示的视图项。 + private NotesListAdapter mNotesListAdapter; + // 用于展示便签列表的列表视图组件,用户可以在该视图中看到便签的各项信息,并进行相应的交互操作(如点击、长按等)。 + private ListView mNotesListView; + // 用于添加新便签的按钮组件,用户点击该按钮可以创建新的便签。 + private Button mAddNewNote; + // 用于标记是否进行事件分发的布尔变量,在处理触摸事件时根据条件判断是否将事件分发给列表视图进行处理。 + private boolean mDispatch; + // 用于记录触摸事件起始的Y坐标,在处理触摸相关逻辑时使用,比如判断触摸位置的变化等情况。 + private int mOriginY; + // 用于记录分发触摸事件时的Y坐标,在触摸事件处理过程中进行坐标相关的计算和调整时使用。 + private int mDispatchY; + // 用于显示标题栏的文本视图组件,根据不同的列表状态(如进入不同文件夹时)显示相应的标题内容。 + private TextView mTitleBar; + // 用于记录当前所在文件夹的ID,通过改变该值可以切换显示不同文件夹下的便签列表内容等操作。 + private long mCurrentFolderId; + // 用于与内容提供器进行交互,访问和操作应用的数据(如便签相关的数据存储等),通过它可以进行数据库查询、插入、更新、删除等操作。 + private ContentResolver mContentResolver; + // 用于处理列表视图进入多选模式时的回调逻辑,实现了相关的接口方法,来处理多选模式下的菜单创建、选项点击、状态改变等操作。 + private ModeCallback mModeCallBack; + + // 用于日志记录的标签,方便在日志输出中识别该类相关的日志信息。 + private static final String TAG = "NotesListActivity"; + // 定义便签列表视图滚动的速率,用于控制列表滚动的速度等相关操作(具体使用场景可能在列表滚动相关逻辑中体现)。 + public static final int NOTES_LISTVIEW_SCROLL_RATE = 30; + // 用于临时存储当前获得焦点的便签数据项对象,在处理长按等操作获取便签相关信息后进行暂存,方便后续使用(比如进行文件夹操作等关联逻辑时)。 + private NoteItemData mFocusNoteDataItem; + + // 定义查询普通文件夹下便签的选择条件字符串,用于数据库查询语句中指定筛选符合条件的便签记录,这里表示筛选父ID等于指定值的便签。 + private static final String NORMAL_SELECTION = NoteColumns.PARENT_ID + "=?"; + // 定义查询根文件夹下便签的选择条件字符串,用于数据库查询语句中指定筛选符合条件的便签记录,这里的条件相对复杂,涉及类型、父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)"; + + // 用于标识打开便签节点(查看或编辑便签)的请求码,在`onActivityResult`方法中通过该请求码判断返回结果对应的操作情况。 + private final static int REQUEST_CODE_OPEN_NODE = 102; + // 用于标识新建便签的请求码,在`onActivityResult`方法中通过该请求码判断返回结果对应的操作情况。 + private final static int REQUEST_CODE_NEW_NODE = 103; + + // Activity创建时调用的方法,进行一些初始化操作,如设置布局、初始化资源等,并处理首次使用应用时插入应用介绍的逻辑。 + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.note_list); + initResources(); + + /** + * Insert an introduction when user firstly use this application + */ + setAppInfoFromRawRes(); + } + + // 当Activity结束并返回结果时调用的方法,根据请求码和结果码判断是否需要更新便签列表适配器的数据(比如在新建或打开便签操作返回后),如果不符合条件则调用父类的相应方法处理。 + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode == RESULT_OK + && (requestCode == REQUEST_CODE_OPEN_NODE || requestCode == REQUEST_CODE_NEW_NODE)) { + mNotesListAdapter.changeCursor(null); + } else { + super.onActivityResult(requestCode, resultCode, data); + } + } + + // 用于在用户首次使用应用时,从原始资源文件(`R.raw.introduction`)中读取应用介绍内容,并创建一个空的便签将介绍内容存入,保存到数据库中,同时设置对应的偏好设置表示已添加介绍内容。 + 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) { + // 捕获关闭输入流时可能出现的异常,进行打印栈信息处理(通常是进行一些错误日志记录等)。 + e.printStackTrace(); + } + } + } + + // 创建一个空的工作便签对象,传入相关参数(如所在文件夹ID、小部件ID、便签类型、颜色等信息),用于存储应用介绍内容。 + WorkingNote note = WorkingNote.createEmptyNote(this, Notes.ID_ROOT_FOLDER, + AppWidgetManager.INVALID_APPWIDGET_ID, Notes.TYPE_WIDGET_INVALIDE, + ResourceParser.RED); + note.setWorkingText(sb.toString()); + if (note.saveNote()) { + sp.edit().putBoolean(PREFERENCE_ADD_INTRODUCTION, true).commit(); + } else { + Log.e(TAG, "Save introduction note error"); + return; + } + } + } + + // Activity启动时调用的方法,在这里主要启动异步的便签列表查询任务,获取并展示相应的便签列表数据。 + @Override + protected void onStart() { + super.onStart(); + startAsyncNotesListQuery(); + } + + // 用于初始化各种资源相关的组件和变量,如获取内容解析器、创建异步查询处理器、找到布局中的列表视图、按钮等组件,并设置相应的监听器和初始状态等。 + 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(); + } + + // 内部类,实现了`ListView.MultiChoiceModeListener`和`OnMenuItemClickListener`接口,用于处理列表视图进入多选模式时的各种逻辑, + // 包括创建多选菜单、处理菜单选项点击、更新菜单状态以及响应列表项选中状态改变等操作。 + private class ModeCallback implements ListView.MultiChoiceModeListener, OnMenuItemClickListener { + private DropdownMenu mDropDownMenu; + private ActionMode mActionMode; + private MenuItem mMoveMenu; + + // 当多选模式被创建时调用的方法,用于初始化多选模式下的菜单,设置菜单项的可见性、点击监听器等,并进行一些相关的界面显示调整(如隐藏添加新便签按钮等)。 + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + getMenuInflater().inflate(R.menu.note_list_options, menu); + menu.findItem(R.id.delete).setOnMenuItemClickListener(this); + mMoveMenu = menu.findItem(R.id.move); + if (mFocusNoteDataItem.getParentId() == Notes.ID_CALL_RECORD_FOLDER + || DataUtils.getUserFolderCount(mContentResolver) == 0) { + mMoveMenu.setVisible(false); + } else { + mMoveMenu.setVisible(true); + mMoveMenu.setOnMenuItemClickListener(this); + } + mActionMode = mode; + mNotesListAdapter.setChoiceMode(true); + mNotesListView.setLongClickable(false); + mAddNewNote.setVisibility(View.GONE); + + View customView = LayoutInflater.from(NotesListActivity.this).inflate( + R.layout.note_list_dropdown_menu, null); + mode.setCustomView(customView); + mDropDownMenu = new DropdownMenu(NotesListActivity.this, + (Button) customView.findViewById(R.id.selection_menu), + R.menu.note_list_dropdown); + mDropDownMenu.setOnDropdownMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + mNotesListAdapter.selectAll(!mNotesListAdapter.isAllSelected()); + updateMenu(); + return true; + } + + }); + return true; + } + + // 用于更新多选模式下的下拉菜单状态,根据已选中的便签数量更新菜单标题以及全选/取消全选菜单项的显示状态和标题文本。 + private void updateMenu() { + int selectedCount = mNotesListAdapter.getSelectedCount(); + // Update dropdown menu + String format = getResources().getString(R.string.menu_select_title, selectedCount); + mDropDownMenu.setTitle(format); + MenuItem item = mDropDownMenu.findItem(R.id.action_select_all); + if (item!= null) { + if (mNotesListAdapter.isAllSelected()) { + item.setChecked(true); + item.setTitle(R.string.menu_deselect_all); + } else { + item.setChecked(false); + item.setTitle(R.string.menu_select_all); + } + } + } + + // 在准备多选模式下的菜单时调用的方法(目前该方法未实现具体逻辑,只是按照接口要求返回`false`),可用于根据当前状态动态调整菜单选项等情况。 + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + // TODO Auto-generated method stub + return false; + } + + // 当多选模式下的菜单项被点击时调用的方法(目前该方法未实现具体的点击逻辑处理,只是按照接口要求返回`false`),用于处理具体的菜单项点击操作对应的业务逻辑。 + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + // TODO Auto-generated method stub + return false; + } + + // 当多选模式被销毁时调用的方法,用于恢复列表的相关设置,如取消选择模式、恢复长按可点击状态、重新显示添加新便签按钮等。 + public void onDestroyActionMode(ActionMode mode) { + mNotesListAdapter.setChoiceMode(false); + mNotesListView.setLongClickable(true); + mAddNewNote.setVisibility(View.VISIBLE); + } + + // 用于手动结束多选模式,调用`ActionMode`的`finish`方法来关闭多选模式相关的界面显示等。 + public void finishActionMode() { + mActionMode.finish(); + } + + // 当列表项的选中状态发生改变时调用的方法,用于更新适配器中对应项的选中状态,并调用`updateMenu`方法更新菜单状态显示。 + public void onItemCheckedStateChanged(ActionMode mode, int position, long id, + boolean checked) { + mNotesListAdapter.setCheckedItem(position, checked); + updateMenu(); + } + + // 处理菜单项点击事件的方法,根据点击的 \ No newline at end of file diff --git a/NotesListAdapter.java b/NotesListAdapter.java new file mode 100644 index 0000000..f90c5e6 --- /dev/null +++ b/NotesListAdapter.java @@ -0,0 +1,382 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.Context; +import android.database.Cursor; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CursorAdapter; + +import net.micode.notes.data.Notes; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; + + +public class NotesListAdapter extends CursorAdapter { + private static final String TAG = "NotesListAdapter"; + private Context mContext; + private HashMap mSelectedIndex; + private int mNotesCount; + private boolean mChoiceMode; + + public static class AppWidgetAttribute { + public int widgetId; + public int widgetType; + }; + + public NotesListAdapter(Context context) { + super(context, null); + mSelectedIndex = new HashMap(); + mContext = context; + mNotesCount = 0; + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return new NotesListItem(context); + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + if (view instanceof NotesListItem) { + NoteItemData itemData = new NoteItemData(context, cursor); + ((NotesListItem) view).bind(context, itemData, mChoiceMode, + isSelectedItem(cursor.getPosition())); + } + } + + public void setCheckedItem(final int position, final boolean checked) { + mSelectedIndex.put(position, checked); + notifyDataSetChanged(); + } + + public boolean isInChoiceMode() { + return mChoiceMode; + } + + public void setChoiceMode(boolean mode) { + mSelectedIndex.clear(); + mChoiceMode = mode; + } + + public void selectAll(boolean checked) { + Cursor cursor = getCursor(); + for (int i = 0; i < getCount(); i++) { + if (cursor.moveToPosition(i)) { + if (NoteItemData.getNoteType(cursor) == Notes.TYPE_NOTE) { + setCheckedItem(i, checked); + } + } + } + } + + public HashSet getSelectedItemIds() { + HashSet itemSet = new HashSet(); + for (Integer position : mSelectedIndex.keySet()) { + if (mSelectedIndex.get(position) == true) { + Long id = getItemId(position); + if (id == Notes.ID_ROOT_FOLDER) { + Log.d(TAG, "Wrong item id, should not happen"); + } else { + itemSet.add(id); + } + } + } + + return itemSet; + } + + public HashSet getSelectedWidget() { + HashSet itemSet = new HashSet(); + for (Integer position : mSelectedIndex.keySet()) { + if (mSelectedIndex.get(position) == true) { + Cursor c = (Cursor) getItem(position); + if (c != null) { + AppWidgetAttribute widget = new AppWidgetAttribute(); + NoteItemData item = new NoteItemData(mContext, c); + widget.widgetId = item.getWidgetId(); + widget.widgetType = item.getWidgetType(); + itemSet.add(widget); + /** + * Don't close cursor here, only the adapter could close it + */ + } else { + Log.e(TAG, "Invalid cursor"); + return null; + } + } + } + return itemSet; + } + + public int getSelectedCount() { + Collection values = mSelectedIndex.values(); + if (null == values) { + return 0; + } + Iterator iter = values.iterator(); + int count = 0; + while (iter.hasNext()) { + if (true == iter.next()) { + count++; + } + } + return count; + } + + public boolean isAllSelected() { + int checkedCount = getSelectedCount(); + return (checkedCount != 0 && checkedCount == mNotesCount); + } + + public boolean isSelectedItem(final int position) { + if (null == mSelectedIndex.get(position)) { + return false; + } + return mSelectedIndex.get(position); + } + + @Override + protected void onContentChanged() { + super.onContentChanged(); + calcNotesCount(); + } + + @Override + public void changeCursor(Cursor cursor) { + super.changeCursor(cursor); + calcNotesCount(); + } + + private void calcNotesCount() { + mNotesCount = 0; + for (int i = 0; i < getCount(); i++) { + Cursor c = (Cursor) getItem(i); + if (c != null) { + if (NoteItemData.getNoteType(c) == Notes.TYPE_NOTE) { + mNotesCount++; + } + } else { + Log.e(TAG, "Invalid cursor"); + return; + } + } + } +} + + + +package net.micode.notes.ui; + +import android.content.Context; +import android.database.Cursor; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CursorAdapter; + +import net.micode.notes.data.Notes; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; + +// NotesListAdapter类继承自CursorAdapter,是一个用于适配便签列表数据到ListView等列表视图组件的适配器类, +// 它负责将从数据库查询获取的Cursor数据转换为适合展示的视图项,并处理了列表项的选择、计数以及与小部件相关属性获取等功能逻辑。 +public class NotesListAdapter extends CursorAdapter { + + // 用于日志记录的标签,方便在日志输出中识别该类相关的日志信息。 + private static final String TAG = "NotesListAdapter"; + // 用于存储上下文信息,方便后续在创建视图、获取资源等操作中使用。 + private Context mContext; + // 使用HashMap来存储列表项的选中状态,键为列表项的位置索引,值为布尔类型表示是否被选中,以此记录用户在多选模式下对各列表项的选择情况。 + private HashMap mSelectedIndex; + // 用于记录便签的数量,通过遍历游标数据统计得出,便于后续判断全选等相关逻辑时使用。 + private int mNotesCount; + // 用于标记当前列表是否处于多选模式,控制相关的界面交互和数据处理逻辑,例如是否显示多选菜单、是否响应多选相关操作等。 + private boolean mChoiceMode; + + // 内部静态类,用于封装与便签关联的小部件的属性信息,包含小部件的ID和类型两个成员变量,方便在处理便签与小部件关联操作时传递和使用相关数据。 + public static class AppWidgetAttribute { + public int widgetId; + public int widgetType; + }; + + // 构造函数,接收上下文对象作为参数,调用父类构造函数传入上下文和空游标初始化,同时初始化选中状态的HashMap和记录便签数量的变量,并保存传入的上下文信息。 + public NotesListAdapter(Context context) { + super(context, null); + mSelectedIndex = new HashMap(); + mContext = context; + mNotesCount = 0; + } + + // 重写父类的方法,用于创建一个新的视图对象来展示列表项,在这里返回一个自定义的`NotesListItem`视图对象,该对象负责具体的便签数据展示布局和渲染。 + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return new NotesListItem(context); + } + + // 重写父类的方法,用于将数据绑定到已创建的视图对象上进行展示,在这里从游标中获取便签数据封装成`NoteItemData`对象, + // 然后调用`NotesListItem`的`bind`方法将数据、多选模式状态以及该项是否被选中的信息传递进去,完成数据与视图的绑定展示。 + @Override + public void bindView(View view, Context context, Cursor cursor) { + if (view instanceof NotesListItem) { + NoteItemData itemData = new NoteItemData(context, cursor); + ((NotesListItem) view).bind(context, itemData, mChoiceMode, + isSelectedItem(cursor.getPosition())); + } + } + + // 用于设置指定位置的列表项的选中状态,将位置索引和对应的选中状态存入`mSelectedIndex` HashMap中,并通知数据集已改变,使得列表视图能够更新相应的显示状态。 + public void setCheckedItem(final int position, final boolean checked) { + mSelectedIndex.put(position, checked); + notifyDataSetChanged(); + } + + // 用于判断当前列表是否处于多选模式,外部代码可以调用该方法获取相应的状态信息,以决定是否显示多选相关的界面元素或执行相应逻辑。 + public boolean isInChoiceMode() { + return mChoiceMode; + } + + // 用于设置列表的多选模式状态,当进入或退出多选模式时调用该方法,会先清空之前的选中状态记录,然后更新`mChoiceMode`变量,以切换相应的操作逻辑和界面显示。 + public void setChoiceMode(boolean mode) { + mSelectedIndex.clear(); + mChoiceMode = mode; + } + + // 用于全选或取消全选所有符合条件(通常是便签类型为普通便签)的列表项,通过遍历游标中的所有记录,根据便签类型判断是否进行选中状态设置,实现批量选中或取消选中的功能。 + public void selectAll(boolean checked) { + Cursor cursor = getCursor(); + for (int i = 0; i < getCount(); i++) { + if (cursor.moveToPosition(i)) { + if (NoteItemData.getNoteType(cursor) == Notes.TYPE_NOTE) { + setCheckedItem(i, checked); + } + } + } + } + + // 用于获取所有已选中列表项的ID集合,遍历`mSelectedIndex`中所有被标记为选中的位置索引,通过`getItemId`方法获取对应项的ID, + // 并添加到`HashSet`集合中返回,同时对特殊的根文件夹ID情况进行日志记录(因为可能不符合预期的选中逻辑)。 + public HashSet getSelectedItemIds() { + HashSet itemSet = new HashSet(); + for (Integer position : mSelectedIndex.keySet()) { + if (mSelectedIndex.get(position) == true) { + Long id = getItemId(position); + if (id == Notes.ID_ROOT_FOLDER) { + Log.d(TAG, "Wrong item id, should not happen"); + } else { + itemSet.add(id); + } + } + } + + return itemSet; + } + + // 用于获取所有已选中列表项关联的小部件属性集合,遍历`mSelectedIndex`中所有被标记为选中的位置索引,通过`getItem`方法获取对应位置的游标数据, + // 进而创建`AppWidgetAttribute`对象并填充小部件的ID和类型信息,添加到`HashSet`集合中返回,同时对游标无效的情况进行日志记录并返回`null`。 + public HashSet getSelectedWidget() { + HashSet itemSet = new HashSet(); + for (Integer position : mSelectedIndex.keySet()) { + if (mSelectedIndex.get(position) == true) { + Cursor c = (Cursor) getItem(position); + if (c!= null) { + AppWidgetAttribute widget = new AppWidgetAttribute(); + NoteItemData item = new NoteItemData(mContext, c); + widget.widgetId = item.getWidgetId(); + widget.widgetType = item.getWidgetType(); + itemSet.add(widget); + /** + * Don't close cursor here, only the adapter could close it + */ + } else { + Log.e(TAG, "Invalid cursor"); + return null; + } + } + } + return itemSet; + } + + // 用于获取当前已选中列表项的数量,通过获取`mSelectedIndex`中所有值(即各位置的选中状态)的集合,然后遍历该集合统计值为`true`(表示选中)的个数并返回。 + public int getSelectedCount() { + Collection values = mSelectedIndex.values(); + if (null == values) { + return 0; + } + Iterator iter = values.iterator(); + int count = 0; + while (iter.hasNext()) { + if (true == iter.next()) { + count++; + } + } + return count; + } + + // 用于判断是否所有符合条件(通常是便签类型相关限定)的列表项都已被选中,通过比较已选中的数量和总的便签数量来确定,若已选中数量不为0且等于便签数量则返回`true`,表示全选状态。 + public boolean isAllSelected() { + int checkedCount = getSelectedCount(); + return (checkedCount!= 0 && checkedCount == mNotesCount); + } + + // 用于判断指定位置的列表项是否被选中,先判断对应位置在`mSelectedIndex`中的值是否为`null`(未记录则视为未选中), + // 若不为`null`则返回该位置记录的选中状态(`true`表示选中,`false`表示未选中)。 + public boolean isSelectedItem(final int position) { + if (null == mSelectedIndex.get(position)) { + return false; + } + return mSelectedIndex.get(position); + } + + // 重写父类的方法,当数据集内容发生变化时会被调用,在这里调用`calcNotesCount`方法重新计算便签的数量,以保证相关统计数据的准确性。 + @Override + protected void onContentChanged() { + super.onContentChanged(); + calcNotesCount(); + } + + // 重写父类的方法,当游标数据发生改变(比如重新查询获取了新的数据)时会被调用,同样在这里调用`calcNotesCount`方法重新计算便签的数量,以适配新的数据情况。 + @Override + public void changeCursor(Cursor cursor) { + super.changeCursor(cursor); + calcNotesCount(); + } + + // 私有方法,用于计算便签的数量,通过遍历游标中的所有记录,根据记录对应的便签类型判断是否为普通便签,若是则数量加1,最终得到总的便签数量并更新`mNotesCount`变量。 + private void calcNotesCount() { + mNotesCount = 0; + for (int i = 0; i < getCount(); i++) { + Cursor c = (Cursor) getItem(i); + if (c!= null) { + if (NoteItemData.getNoteType(c) == Notes.TYPE_NOTE) { + mNotesCount++; + } + } else { + Log.e(TAG, "Invalid cursor"); + return; + } + } + } +} \ No newline at end of file diff --git a/NotesListItem.java b/NotesListItem.java new file mode 100644 index 0000000..9fd411d --- /dev/null +++ b/NotesListItem.java @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.Context; +import android.text.format.DateUtils; +import android.view.View; +import android.widget.CheckBox; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.tool.DataUtils; +import net.micode.notes.tool.ResourceParser.NoteItemBgResources; + +// NotesListItem类继承自LinearLayout,代表了便签列表中的每一个列表项视图,负责将便签相关的数据展示在对应的UI元素上, +// 包括标题、时间、提醒图标、联系人姓名等信息,并根据不同的便签类型、状态等设置相应的显示样式和背景。 +public class NotesListItem extends LinearLayout { + + // 用于显示提醒图标的ImageView组件,根据便签是否有提醒等情况来决定其显示与否以及显示的图标资源。 + private ImageView mAlert; + // 用于显示便签标题的TextView组件,展示便签的主要内容摘要或者文件夹名称等信息,会根据不同的便签类型和状态设置合适的文本样式和内容。 + private TextView mTitle; + // 用于显示便签相关时间信息(如修改时间)的TextView组件,通过格式化处理将时间戳转换为相对时间的文本展示给用户。 + private TextView mTime; + // 用于显示联系人姓名(如果便签与通话记录等相关)的TextView组件,在对应的业务场景下展示相关联系人的名字。 + private TextView mCallName; + // 用于存储当前列表项对应的便签数据对象,方便在其他方法中获取便签的各项属性来进行界面元素的设置和判断逻辑。 + private NoteItemData mItemData; + // 用于在多选模式下显示选择状态的CheckBox组件,根据是否处于多选模式以及当前便签类型等条件来控制其显示和选中状态。 + private CheckBox mCheckBox; + + // 构造函数,接收上下文对象作为参数,调用父类构造函数初始化,然后通过`inflate`方法将布局文件(`R.layout.note_item`)填充到当前的LinearLayout中, + // 并找到布局中的各个UI组件实例,方便后续进行数据绑定和操作。 + public NotesListItem(Context context) { + super(context); + inflate(context, R.layout.note_item, this); + mAlert = (ImageView) findViewById(R.id.iv_alert_icon); + mTitle = (TextView) findViewById(R.id.tv_title); + mTime = (TextView) findViewById(R.id.tv_time); + mCallName = (TextView) findViewById(R.id.tv_name); + mCheckBox = (CheckBox) findViewById(android.R.id.checkbox); + } + + // 用于将便签数据绑定到列表项的各个UI组件上进行展示,根据便签的类型、是否处于多选模式以及是否有提醒等不同情况,设置相应组件的显示状态、文本内容、图标资源等信息, + // 同时还会调用`setBackground`方法根据便签的状态设置合适的背景样式。 + public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked) { + // 如果处于多选模式且当前便签类型为普通便签(`Notes.TYPE_NOTE`),则显示CheckBox组件,并根据传入的选中状态参数设置其选中状态。 + if (choiceMode && data.getType() == Notes.TYPE_NOTE) { + mCheckBox.setVisibility(View.VISIBLE); + mCheckBox.setChecked(checked); + } else { + // 否则隐藏CheckBox组件,例如在非多选模式或者不是普通便签类型时不需要显示选择框。 + mCheckBox.setVisibility(View.GONE); + } + + // 将传入的便签数据对象赋值给成员变量,方便后续在其他方法中获取便签的各项属性信息进行操作。 + mItemData = data; + + // 如果便签的ID是通话记录文件夹的ID(`Notes.ID_CALL_RECORD_FOLDER`),进行以下针对通话记录文件夹类型的UI设置。 + if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { + // 隐藏联系人姓名的TextView组件,因为对于通话记录文件夹本身不需要展示联系人姓名。 + mCallName.setVisibility(View.GONE); + // 显示提醒图标(这里的提醒图标可能代表通话记录相关的特定含义),并设置其可见状态为显示。 + mAlert.setVisibility(View.VISIBLE); + // 设置标题的文本样式为主要项目的文本外观样式(通过资源ID `R.style.TextAppearancePrimaryItem`指定)。 + mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); + // 设置标题的文本内容,包括通话记录文件夹的名称以及该文件夹下便签数量的格式化文本,展示给用户文件夹相关的信息。 + mTitle.setText(context.getString(R.string.call_record_folder_name) + + context.getString(R.string.format_folder_files_count, data.getNotesCount())); + // 设置提醒图标的资源为通话记录相关的图标(`R.drawable.call_record`),使其显示对应的图标样式。 + mAlert.setImageResource(R.drawable.call_record); + } else if (data.getParentId() == Notes.ID_CALL_RECORD_FOLDER) { + // 如果便签的父ID是通话记录文件夹的ID,意味着该便签属于通话记录文件夹下的具体便签,进行以下设置。 + mCallName.setVisibility(View.VISIBLE); + // 设置联系人姓名的TextView组件显示对应的联系人姓名,通过获取便签数据中的联系人姓名属性进行展示。 + mCallName.setText(data.getCallName()); + // 设置标题的文本样式为次要项目的文本外观样式(通过资源ID `R.style.TextAppearanceSecondaryItem`指定)。 + mTitle.setTextAppearance(context, R.style.TextAppearanceSecondaryItem); + // 设置标题的文本内容为格式化后的便签片段内容(通过`DataUtils`工具类的方法进行格式化处理),展示便签的主要摘要信息。 + mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); + // 如果便签有提醒设置,则显示提醒图标(这里是时钟图标代表提醒),并设置其可见状态为显示。 + if (data.hasAlert()) { + mAlert.setImageResource(R.drawable.clock); + mAlert.setVisibility(View.VISIBLE); + } else { + // 如果没有提醒设置,则隐藏提醒图标。 + mAlert.setVisibility(View.GONE); + } + } else { + // 如果便签不属于通话记录文件夹相关情况,进行以下通用的便签UI设置。 + mCallName.setVisibility(View.GONE); + mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); + + // 如果便签类型是文件夹类型(`Notes.TYPE_FOLDER`),设置标题的文本内容为文件夹名称加上该文件夹下便签数量的格式化文本,展示文件夹相关信息,同时隐藏提醒图标(文件夹一般无提醒概念)。 + if (data.getType() == Notes.TYPE_FOLDER) { + mTitle.setText(data.getSnippet() + + context.getString(R.string.format_folder_files_count, + data.getNotesCount())); + mAlert.setVisibility(View.GONE); + } else { + // 如果是普通便签类型,设置标题的文本内容为格式化后的便签片段内容,展示便签的主要摘要信息,同时根据是否有提醒设置来决定提醒图标的显示与否(和前面逻辑类似)。 + mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); + if (data.hasAlert()) { + mAlert.setImageResource(R.drawable.clock); + mAlert.setVisibility(View.VISIBLE); + } else { + mAlert.setVisibility(View.GONE); + } + } + } + + // 设置时间的TextView组件显示的文本内容,通过`DateUtils`工具类的方法将便签的修改时间戳转换为相对时间的文本格式(如“刚刚”“10分钟前”等)展示给用户。 + mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate())); + + // 调用`setBackground`方法,根据便签的数据信息(如类型、状态等)设置列表项的背景样式,以区分不同情况的便签显示效果。 + setBackground(data); + } + + // 私有方法,根据便签的数据信息(主要是类型、是否单独、是否是第一个、最后一个等状态信息)来设置列表项的背景资源, + // 通过`NoteItemBgResources`工具类的相关方法获取对应的背景资源ID,并设置为当前列表项的背景。 + private void setBackground(NoteItemData data) { + int id = data.getBgColorId(); + if (data.getType() == Notes.TYPE_NOTE) { + // 如果是普通便签类型,根据便签的不同状态设置不同的背景资源。 + if (data.isSingle() || data.isOneFollowingFolder()) { + setBackgroundResource(NoteItemBgResources.getNoteBgSingleRes(id)); + } else if (data.isLast()) { + setBackgroundResource(NoteItemBgResources.getNoteBgLastRes(id)); + } else if (data.isFirst() || data.isMultiFollowingFolder()) { + setBackgroundResource(NoteItemBgResources.getNoteBgFirstRes(id)); + } else { + setBackgroundResource(NoteItemBgResources.getNoteBgNormalRes(id)); + } + } else { + // 如果是文件夹类型,则统一设置为文件夹对应的背景资源(通过`NoteItemBgResources`工具类的相关方法获取)。 + setBackgroundResource(NoteItemBgResources.getFolderBgRes()); + } + } + + // 用于获取当前列表项对应的便签数据对象,外部代码可以调用该方法获取便签的详细信息,方便进行其他相关的业务逻辑处理(比如点击列表项后根据便签数据进行相应操作等)。 + public NoteItemData getItemData() { + return mItemData; + } +} \ No newline at end of file diff --git a/NotesPreferenceActivity.java b/NotesPreferenceActivity.java new file mode 100644 index 0000000..2d895b5 --- /dev/null +++ b/NotesPreferenceActivity.java @@ -0,0 +1,743 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.app.ActionBar; +import android.app.AlertDialog; +import android.content.BroadcastReceiver; +import android.content.ContentValues; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceClickListener; +import android.preference.PreferenceActivity; +import android.preference.PreferenceCategory; +import android.text.TextUtils; +import android.text.format.DateFormat; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; +import android.widget.Toast; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.gtask.remote.GTaskSyncService; + + +public class NotesPreferenceActivity extends PreferenceActivity { + public static final String PREFERENCE_NAME = "notes_preferences"; + + public static final String PREFERENCE_SYNC_ACCOUNT_NAME = "pref_key_account_name"; + + public static final String PREFERENCE_LAST_SYNC_TIME = "pref_last_sync_time"; + + public static final String PREFERENCE_SET_BG_COLOR_KEY = "pref_key_bg_random_appear"; + + private static final String PREFERENCE_SYNC_ACCOUNT_KEY = "pref_sync_account_key"; + + private static final String AUTHORITIES_FILTER_KEY = "authorities"; + + private PreferenceCategory mAccountCategory; + + private GTaskReceiver mReceiver; + + private Account[] mOriAccounts; + + private boolean mHasAddedAccount; + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + + /* using the app icon for navigation */ + getActionBar().setDisplayHomeAsUpEnabled(true); + + addPreferencesFromResource(R.xml.preferences); + mAccountCategory = (PreferenceCategory) findPreference(PREFERENCE_SYNC_ACCOUNT_KEY); + mReceiver = new GTaskReceiver(); + IntentFilter filter = new IntentFilter(); + filter.addAction(GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME); + registerReceiver(mReceiver, filter); + + mOriAccounts = null; + View header = LayoutInflater.from(this).inflate(R.layout.settings_header, null); + getListView().addHeaderView(header, null, true); + } + + @Override + protected void onResume() { + super.onResume(); + + // need to set sync account automatically if user has added a new + // account + if (mHasAddedAccount) { + Account[] accounts = getGoogleAccounts(); + if (mOriAccounts != null && accounts.length > mOriAccounts.length) { + for (Account accountNew : accounts) { + boolean found = false; + for (Account accountOld : mOriAccounts) { + if (TextUtils.equals(accountOld.name, accountNew.name)) { + found = true; + break; + } + } + if (!found) { + setSyncAccount(accountNew.name); + break; + } + } + } + } + + refreshUI(); + } + + @Override + protected void onDestroy() { + if (mReceiver != null) { + unregisterReceiver(mReceiver); + } + super.onDestroy(); + } + + private void loadAccountPreference() { + mAccountCategory.removeAll(); + + Preference accountPref = new Preference(this); + final String defaultAccount = getSyncAccountName(this); + accountPref.setTitle(getString(R.string.preferences_account_title)); + accountPref.setSummary(getString(R.string.preferences_account_summary)); + accountPref.setOnPreferenceClickListener(new OnPreferenceClickListener() { + public boolean onPreferenceClick(Preference preference) { + if (!GTaskSyncService.isSyncing()) { + if (TextUtils.isEmpty(defaultAccount)) { + // the first time to set account + showSelectAccountAlertDialog(); + } else { + // if the account has already been set, we need to promp + // user about the risk + showChangeAccountConfirmAlertDialog(); + } + } else { + Toast.makeText(NotesPreferenceActivity.this, + R.string.preferences_toast_cannot_change_account, Toast.LENGTH_SHORT) + .show(); + } + return true; + } + }); + + mAccountCategory.addPreference(accountPref); + } + + private void loadSyncButton() { + Button syncButton = (Button) findViewById(R.id.preference_sync_button); + TextView lastSyncTimeView = (TextView) findViewById(R.id.prefenerece_sync_status_textview); + + // set button state + if (GTaskSyncService.isSyncing()) { + syncButton.setText(getString(R.string.preferences_button_sync_cancel)); + syncButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + GTaskSyncService.cancelSync(NotesPreferenceActivity.this); + } + }); + } else { + syncButton.setText(getString(R.string.preferences_button_sync_immediately)); + syncButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + GTaskSyncService.startSync(NotesPreferenceActivity.this); + } + }); + } + syncButton.setEnabled(!TextUtils.isEmpty(getSyncAccountName(this))); + + // set last sync time + if (GTaskSyncService.isSyncing()) { + lastSyncTimeView.setText(GTaskSyncService.getProgressString()); + lastSyncTimeView.setVisibility(View.VISIBLE); + } else { + long lastSyncTime = getLastSyncTime(this); + if (lastSyncTime != 0) { + lastSyncTimeView.setText(getString(R.string.preferences_last_sync_time, + DateFormat.format(getString(R.string.preferences_last_sync_time_format), + lastSyncTime))); + lastSyncTimeView.setVisibility(View.VISIBLE); + } else { + lastSyncTimeView.setVisibility(View.GONE); + } + } + } + + private void refreshUI() { + loadAccountPreference(); + loadSyncButton(); + } + + private void showSelectAccountAlertDialog() { + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); + + View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null); + TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title); + titleTextView.setText(getString(R.string.preferences_dialog_select_account_title)); + TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle); + subtitleTextView.setText(getString(R.string.preferences_dialog_select_account_tips)); + + dialogBuilder.setCustomTitle(titleView); + dialogBuilder.setPositiveButton(null, null); + + Account[] accounts = getGoogleAccounts(); + String defAccount = getSyncAccountName(this); + + mOriAccounts = accounts; + mHasAddedAccount = false; + + if (accounts.length > 0) { + CharSequence[] items = new CharSequence[accounts.length]; + final CharSequence[] itemMapping = items; + int checkedItem = -1; + int index = 0; + for (Account account : accounts) { + if (TextUtils.equals(account.name, defAccount)) { + checkedItem = index; + } + items[index++] = account.name; + } + dialogBuilder.setSingleChoiceItems(items, checkedItem, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + setSyncAccount(itemMapping[which].toString()); + dialog.dismiss(); + refreshUI(); + } + }); + } + + View addAccountView = LayoutInflater.from(this).inflate(R.layout.add_account_text, null); + dialogBuilder.setView(addAccountView); + + final AlertDialog dialog = dialogBuilder.show(); + addAccountView.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + mHasAddedAccount = true; + Intent intent = new Intent("android.settings.ADD_ACCOUNT_SETTINGS"); + intent.putExtra(AUTHORITIES_FILTER_KEY, new String[] { + "gmail-ls" + }); + startActivityForResult(intent, -1); + dialog.dismiss(); + } + }); + } + + private void showChangeAccountConfirmAlertDialog() { + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); + + View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null); + TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title); + titleTextView.setText(getString(R.string.preferences_dialog_change_account_title, + getSyncAccountName(this))); + TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle); + subtitleTextView.setText(getString(R.string.preferences_dialog_change_account_warn_msg)); + dialogBuilder.setCustomTitle(titleView); + + CharSequence[] menuItemArray = new CharSequence[] { + getString(R.string.preferences_menu_change_account), + getString(R.string.preferences_menu_remove_account), + getString(R.string.preferences_menu_cancel) + }; + dialogBuilder.setItems(menuItemArray, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + if (which == 0) { + showSelectAccountAlertDialog(); + } else if (which == 1) { + removeSyncAccount(); + refreshUI(); + } + } + }); + dialogBuilder.show(); + } + + private Account[] getGoogleAccounts() { + AccountManager accountManager = AccountManager.get(this); + return accountManager.getAccountsByType("com.google"); + } + + private void setSyncAccount(String account) { + if (!getSyncAccountName(this).equals(account)) { + SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = settings.edit(); + if (account != null) { + editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, account); + } else { + editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, ""); + } + editor.commit(); + + // clean up last sync time + setLastSyncTime(this, 0); + + // clean up local gtask related info + new Thread(new Runnable() { + public void run() { + ContentValues values = new ContentValues(); + values.put(NoteColumns.GTASK_ID, ""); + values.put(NoteColumns.SYNC_ID, 0); + getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null); + } + }).start(); + + Toast.makeText(NotesPreferenceActivity.this, + getString(R.string.preferences_toast_success_set_accout, account), + Toast.LENGTH_SHORT).show(); + } + } + + private void removeSyncAccount() { + SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = settings.edit(); + if (settings.contains(PREFERENCE_SYNC_ACCOUNT_NAME)) { + editor.remove(PREFERENCE_SYNC_ACCOUNT_NAME); + } + if (settings.contains(PREFERENCE_LAST_SYNC_TIME)) { + editor.remove(PREFERENCE_LAST_SYNC_TIME); + } + editor.commit(); + + // clean up local gtask related info + new Thread(new Runnable() { + public void run() { + ContentValues values = new ContentValues(); + values.put(NoteColumns.GTASK_ID, ""); + values.put(NoteColumns.SYNC_ID, 0); + getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null); + } + }).start(); + } + + public static String getSyncAccountName(Context context) { + SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, + Context.MODE_PRIVATE); + return settings.getString(PREFERENCE_SYNC_ACCOUNT_NAME, ""); + } + + public static void setLastSyncTime(Context context, long time) { + SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, + Context.MODE_PRIVATE); + SharedPreferences.Editor editor = settings.edit(); + editor.putLong(PREFERENCE_LAST_SYNC_TIME, time); + editor.commit(); + } + + public static long getLastSyncTime(Context context) { + SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, + Context.MODE_PRIVATE); + return settings.getLong(PREFERENCE_LAST_SYNC_TIME, 0); + } + + private class GTaskReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + refreshUI(); + if (intent.getBooleanExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_IS_SYNCING, false)) { + TextView syncStatus = (TextView) findViewById(R.id.prefenerece_sync_status_textview); + syncStatus.setText(intent + .getStringExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_PROGRESS_MSG)); + } + + } + } + + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + Intent intent = new Intent(this, NotesListActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + return true; + default: + return false; + } + } +} +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; + +// NotesPreferenceActivity类继承自PreferenceActivity,是用于管理便签应用偏好设置的Activity, +// 涉及与账户相关的设置(如选择同步账户、切换账户、移除账户等)、同步操作的控制(开始同步、取消同步)以及根据不同状态更新界面显示等功能逻辑。 +public class NotesPreferenceActivity extends PreferenceActivity { + + // 用于存储偏好设置名称的常量,作为获取SharedPreferences对象时的名称参数,用于保存和读取应用的各种偏好设置数据。 + public static final String PREFERENCE_NAME = "notes_preferences"; + // 用于存储同步账户名称偏好设置的键名,通过该键可以在SharedPreferences中存取当前选择的同步账户名称信息。 + 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"; + // 用于在偏好设置中标识账户相关分类的键名,用于在布局资源中找到对应的PreferenceCategory组件,方便进行账户相关偏好设置项的管理操作。 + private static final String PREFERENCE_SYNC_ACCOUNT_KEY = "pref_sync_account_key"; + // 用于在添加账户意图中传递权限过滤相关信息的键名(从代码中看是与添加谷歌账户相关操作中使用,具体过滤“gmail-ls”权限相关情况),用于控制账户添加时的一些权限筛选逻辑。 + private static final String AUTHORITIES_FILTER_KEY = "authorities"; + + // 用于存储偏好设置中账户分类的PreferenceCategory对象,通过它可以对账户相关的偏好设置项进行添加、移除等操作,方便管理账户设置相关的UI展示和逻辑处理。 + private PreferenceCategory mAccountCategory; + // 用于接收同步服务相关广播的广播接收器对象,当同步服务有状态变化(如开始同步、取消同步、同步进度更新等)时会接收到广播并进行相应的界面更新等操作。 + private GTaskReceiver mReceiver; + // 用于存储原始的账户列表信息,在判断是否新增账户等逻辑中使用,通过对比前后账户列表来确定是否有账户添加的情况发生。 + private Account[] mOriAccounts; + // 用于标记是否添加了新账户的布尔变量,在账户相关操作逻辑中(如在`onResume`方法中判断是否需要自动设置同步账户)根据其值来执行相应的处理逻辑。 + private boolean mHasAddedAccount; + + // Activity创建时调用的方法,进行一些初始化操作,如设置ActionBar的返回按钮可用、加载偏好设置资源文件、获取账户分类组件、注册广播接收器以及添加列表头部视图等操作。 + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + + /* using the app icon for navigation */ + getActionBar().setDisplayHomeAsUpEnabled(true); + + addPreferencesFromResource(R.xml.preferences); + mAccountCategory = (PreferenceCategory) findPreference(PREFERENCE_SYNC_ACCOUNT_KEY); + mReceiver = new GTaskReceiver(); + IntentFilter filter = new IntentFilter(); + filter.addAction(GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME); + registerReceiver(mReceiver, filter); + + mOriAccounts = null; + View header = LayoutInflater.from(this).inflate(R.layout.settings_header, null); + getListView().addHeaderView(header, null, true); + } + + // Activity重新恢复到前台时调用的方法,在这里主要判断是否有新添加的账户,如果有则尝试自动设置同步账户,然后调用`refreshUI`方法更新整个偏好设置界面的显示内容,以反映最新的状态。 + @Override + protected void onResume() { + super.onResume(); + + // need to set sync account automatically if user has added a new + // account + if (mHasAddedAccount) { + Account[] accounts = getGoogleAccounts(); + if (mOriAccounts!= null && accounts.length > mOriAccounts.length) { + for (Account accountNew : accounts) { + boolean found = false; + for (Account accountOld : mOriAccounts) { + if (TextUtils.equals(accountOld.name, accountNew.name)) { + found = true; + break; + } + } + if (!found) { + setSyncAccount(accountNew.name); + break; + } + } + } + } + + refreshUI(); + } + + // Activity销毁时调用的方法,在这里主要进行广播接收器的注销操作,避免出现内存泄漏等问题,然后调用父类的销毁方法完成其他相关的清理工作。 + @Override + protected void onDestroy() { + if (mReceiver!= null) { + unregisterReceiver(mReceiver); + } + super.onDestroy(); + } + + // 用于加载账户偏好设置相关的UI展示内容,先移除账户分类下已有的所有偏好设置项,然后创建一个新的偏好设置项用于显示账户相关信息,并设置其标题、摘要以及点击监听器等,最后将其添加到账户分类中进行展示。 + private void loadAccountPreference() { + mAccountCategory.removeAll(); + + Preference accountPref = new Preference(this); + final String defaultAccount = getSyncAccountName(this); + accountPref.setTitle(getString(R.string.preferences_account_title)); + accountPref.setSummary(getString(R.string.preferences_account_summary)); + accountPref.setOnPreferenceClickListener(new OnPreferenceClickListener() { + public boolean onPreferenceClick(Preference preference) { + if (!GTaskSyncService.isSyncing()) { + if (TextUtils.isEmpty(defaultAccount)) { + // the first time to set account + showSelectAccountAlertDialog(); + } else { + // if the account has already been set, we need to promp + // user about the risk + showChangeAccountConfirmAlertDialog(); + } + } else { + Toast.makeText(NotesPreferenceActivity.this, + R.string.preferences_toast_cannot_change_account, Toast.LENGTH_SHORT) + .show(); + } + return true; + } + }); + + mAccountCategory.addPreference(accountPref); + } + + // 用于加载同步按钮相关的UI展示和点击事件逻辑设置,根据同步服务当前是否正在同步来设置按钮的文本(如显示“同步”或“取消同步”)以及对应的点击监听器(启动或取消同步操作), + // 同时根据同步情况和上次同步时间来设置显示同步状态文本视图的内容和可见性,展示给用户同步相关的状态信息。 + private void loadSyncButton() { + Button syncButton = (Button) findViewById(R.id.preference_sync_button); + TextView lastSyncTimeView = (TextView) findViewById(R.id.prefenerece_sync_status_textview); + + // set button state + if (GTaskSyncService.isSyncing()) { + syncButton.setText(getString(R.string.preferences_button_sync_cancel)); + syncButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + GTaskSyncService.cancelSync(NotesPreferenceActivity.this); + } + }); + } else { + syncButton.setText(getString(R.string.preferences_button_sync_immediately)); + syncButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + GTaskSyncService.startSync(NotesPreferenceActivity.this); + } + }); + } + syncButton.setEnabled(!TextUtils.isEmpty(getSyncAccountName(this))); + + // set last sync time + if (GTaskSyncService.isSyncing()) { + lastSyncTimeView.setText(GTaskSyncService.getProgressString()); + lastSyncTimeView.setVisibility(View.VISIBLE); + } else { + long lastSyncTime = getLastSyncTime(this); + if (lastSyncTime!= 0) { + lastSyncTimeView.setText(getString(R.string.preferences_last_sync_time, + DateFormat.format(getString(R.string.preferences_last_sync_time_format), + lastSyncTime))); + lastSyncTimeView.setVisibility(View.VISIBLE); + } else { + lastSyncTimeView.setVisibility(View.GONE); + } + } + } + + // 用于更新整个偏好设置界面的显示内容,通过调用`loadAccountPreference`和`loadSyncButton`方法分别更新账户相关设置和同步按钮相关的UI展示,以反映最新的应用状态和设置信息。 + private void refreshUI() { + loadAccountPreference(); + loadSyncButton(); + } + + // 用于显示选择账户的提醒对话框,在首次设置同步账户或者需要重新选择账户时弹出,对话框中展示可选的谷歌账户列表供用户选择, + // 同时提供添加新账户的入口,用户选择账户后会设置相应的同步账户并更新界面显示,选择添加新账户则跳转到系统的添加账户设置页面。 + private void showSelectAccountAlertDialog() { + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); + + View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null); + TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title); + titleTextView.setText(getString(R.string.preferences_dialog_select_account_title)); + TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle); + subtitleTextView.setText(getString(R.string.preferences_dialog_select_account_tips)); + + dialogBuilder.setCustomTitle(titleView); + dialogBuilder.setPositiveButton(null, null); + + Account[] accounts = getGoogleAccounts(); + String defAccount = getSyncAccountName(this); + + mOriAccounts = accounts; + mHasAddedAccount = false; + + if (accounts.length > 0) { + CharSequence[] items = new CharSequence[accounts.length]; + final CharSequence[] itemMapping = items; + int checkedItem = -1; + int index = 0; + for (Account account : accounts) { + if (TextUtils.equals(account.name, defAccount)) { + checkedItem = index; + } + items[index++] = account.name; + } + dialogBuilder.setSingleChoiceItems(items, checkedItem, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + setSyncAccount(itemMapping[which].toString()); + dialog.dismiss(); + refreshUI(); + } + }); + } + + View addAccountView = LayoutInflater.from(this).inflate(R.layout.add_account_text, null); + dialogBuilder.setView(addAccountView); + + final AlertDialog dialog = dialogBuilder.show(); + addAccountView.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + mHasAddedAccount = true; + Intent intent = new Intent("android.settings.ADD_ACCOUNT_SETTINGS"); + intent.putExtra(AUTHORITIES_FILTER_KEY, new String[] { + "gmail-ls" + }); + startActivityForResult(intent, -1); + dialog.dismiss(); + } + }); + } + + // 用于显示确认更改账户的提醒对话框,当已经设置了同步账户后再次点击账户设置项时弹出,对话框中提示用户更改账户的风险,并提供更改账户、移除账户和取消操作的选项, + // 用户选择相应操作后会执行对应的逻辑(如重新选择账户、移除账户并更新界面等)。 + private void showChangeAccountConfirmAlertDialog() { + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); + + View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null); + TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title); + titleTextView.setText(getString(R.string.preferences_dialog_change_account_title, + getSyncAccountName(this))); + TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle); + subtitleTextView.setText(getString(R.string.preferences_dialog_change_account_warn_msg)); + dialogBuilder.setCustomTitle(titleView); + + CharSequence[] menuItemArray = new CharSequence[] { + getString(R.string.preferences_menu_change_account), + getString(R.string.preferences_menu_remove_account), + getString(R.string.preferences_menu_cancel) + }; + dialogBuilder.setItems(menuItemArray, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + if (which == 0) { + showSelectAccountAlertDialog(); + } else if (which == 1) { + removeSyncAccount(); + refreshUI(); + } + } + }); + dialogBuilder.show(); + } + + // 用于获取设备上已登录的谷歌账户列表,通过`AccountManager`获取指定类型(“com.google”)的账户信息,返回账户数组,方便在账户选择等相关操作中展示和使用这些账户数据。 + private Account[] getGoogleAccounts() { + AccountManager accountManager = AccountManager.get(this); + return accountManager.getAccountsByType("com.google"); + } + + // 用于设置同步账户名称,将传入的账户名称保存到SharedPreferences中,同时进行一些相关的清理操作,如重置上次同步时间、清除本地与同步任务(`gtask`)相关的信息等, + // 最后通过Toast提示用户设置成功的信息,并且只有当传入的账户名称与当前保存的不一致时才执行实际的设置操作。 + private void setSyncAccount(String account) { + if (!getSyncAccountName(this).equals(account)) { + SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = settings.edit(); + if (account!= null) { + editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, account); + } else { + editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, ""); + } + editor.commit(); + + // clean up last sync time + setLastSyncTime(this, 0); + + // clean up local gtask related info + new Thread(new Runnable() { + public void run() { + ContentValues values = new ContentValues(); + values.put(NoteColumns.GTASK_ID, ""); + values.put(NoteColumns.SYNC_ID, 0); + getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null); + } + }).start(); + + Toast.makeText(NotesPreferenceActivity.this, + getString(R.string.preferences_toast_success_set_accout, account), + Toast.LENGTH_SHORT).show(); + } + } + + // 用于移除当前设置的同步账户相关信息,从SharedPreferences中移除同步账户名称和上次同步时间的记录,同时清除本地与同步任务(`gtask`)相关的信息,以达到彻底移除账户相关设置的效果。 + private void removeSyncAccount() { + SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = settings.edit(); + if (settings.contains(PREFERENCE_SYNC_ACCOUNT_NAME)) { + editor.remove(PREFERENCE_SYNC_ACCOUNT_NAME); + } + if (settings.contains(PREFERENCE_LAST_SYNC_TIME)) { + editor.remove(PREFERENCE_LAST_SYNC_TIME); + } + editor.commit(); + + // clean up local gtask related info + new Thread(new Runnable() { + public void run() { + ContentValues values = new ContentValues(); + values.put(NoteColumns.GTASK_ID, ""); + values.put(NoteColumns.SYNC_ID, 0); + getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null); + } + }).start(); + } + + // 静态方法,用于获取当前设置的同步账户名称,从SharedPreferences中根据对应的键(`PREFERENCE_SYNC_ACCOUNT_NAME`)读取账户名称信息,如果不存在则返回空字符串。 + public static String getSyncAccountName(Context context) { + SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, + Context.MODE_PRIVATE); + return settings.getString(PREFERENCE_SYNC_ACCOUNT_NAME, ""); + } + + // 静态方法,用于设置上次同步时间,将传入的时间戳保存到SharedPreferences中,通过编辑对象提交更改,方便后续在界面展示等地方获取并显示上次同步的时间信息。 + public static void setLastSyncTime(Context context, long time) { + SharedPreferences settings = context \ No newline at end of file