parent
f85591f659
commit
01d5d60055
@ -0,0 +1,143 @@
|
||||
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; // 笔记ID
|
||||
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 {
|
||||
// 从意图中获取笔记ID
|
||||
mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1));
|
||||
// 根据ID获取笔记片段
|
||||
mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId);
|
||||
// 如果片段过长,截断并添加提示
|
||||
mSnippet = mSnippet.length() > SNIPPET_PREW_MAX_LEN ? mSnippet.substring(0,
|
||||
SNIPPET_PREW_MAX_LEN) + getResources().getString(R.string.notelist_string_info)
|
||||
: mSnippet;
|
||||
} catch (IllegalArgumentException e) {
|
||||
e.printStackTrace(); // 打印异常
|
||||
return; // 退出
|
||||
}
|
||||
|
||||
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); // 获取闹钟铃声URI
|
||||
|
||||
int silentModeStreams = Settings.System.getInt(getContentResolver(),
|
||||
Settings.System.MODE_RINGER_STREAMS_AFFECTED, 0);
|
||||
|
||||
// 设置音频流类型
|
||||
if ((silentModeStreams & (1 << AudioManager.STREAM_ALARM)) != 0) {
|
||||
mPlayer.setAudioStreamType(silentModeStreams);
|
||||
} else {
|
||||
mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM);
|
||||
}
|
||||
try {
|
||||
mPlayer.setDataSource(this, url); // 设置数据源
|
||||
mPlayer.prepare(); // 准备播放器
|
||||
mPlayer.setLooping(true); // 循环播放
|
||||
mPlayer.start(); // 开始播放
|
||||
} catch (IllegalArgumentException | SecurityException | IllegalStateException | IOException e) {
|
||||
e.printStackTrace(); // 打印异常
|
||||
}
|
||||
}
|
||||
|
||||
// 显示操作对话框
|
||||
private void showActionDialog() {
|
||||
AlertDialog.Builder dialog = new AlertDialog.Builder(this);
|
||||
dialog.setTitle(R.string.app_name); // 设置对话框标题
|
||||
dialog.setMessage(mSnippet); // 设置对话框消息
|
||||
dialog.setPositiveButton(R.string.notealert_ok, this); // 设置确认按钮
|
||||
if (isScreenOn()) {
|
||||
dialog.setNegativeButton(R.string.notealert_enter, this); // 设置取消按钮
|
||||
}
|
||||
dialog.show().setOnDismissListener(this); // 显示对话框并设置消失监听
|
||||
}
|
||||
|
||||
// 处理对话框按钮点击事件
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
switch (which) {
|
||||
case DialogInterface.BUTTON_NEGATIVE:
|
||||
Intent intent = new Intent(this, NoteEditActivity.class);
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.putExtra(Intent.EXTRA_UID, mNoteId); // 传递笔记ID
|
||||
startActivity(intent); // 启动编辑活动
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理对话框消失事件
|
||||
public void onDismiss(DialogInterface dialog) {
|
||||
stopAlarmSound(); // 停止闹钟声音
|
||||
finish(); // 结束活动
|
||||
}
|
||||
|
||||
// 停止闹钟声音
|
||||
private void stopAlarmSound() {
|
||||
if (mPlayer != null) {
|
||||
mPlayer.stop(); // 停止播放
|
||||
mPlayer.release(); // 释放资源
|
||||
mPlayer = null; // 清空播放器引用
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
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) {
|
||||
// 设置意图的目标类为 AlarmAlertActivity
|
||||
intent.setClass(context, AlarmAlertActivity.class);
|
||||
// 添加标志以启动新任务
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
// 启动 AlarmAlertActivity
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,81 @@
|
||||
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; // 是否为24小时制
|
||||
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); // 将秒数设置为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())); // 设置24小时制
|
||||
updateTitle(mDate.getTimeInMillis()); // 更新对话框标题
|
||||
}
|
||||
|
||||
// 设置24小时制
|
||||
public void set24HourView(boolean is24HourView) {
|
||||
mIs24HourView = is24HourView; // 更新24小时制状态
|
||||
}
|
||||
|
||||
// 设置日期时间设置监听器
|
||||
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_12HOUR; // 根据24小时制设置格式
|
||||
setTitle(DateUtils.formatDateTime(this.getContext(), date, flag)); // 设置对话框标题
|
||||
}
|
||||
|
||||
// 确认按钮点击事件
|
||||
public void onClick(DialogInterface arg0, int arg1) {
|
||||
if (mOnDateTimeSetListener != null) {
|
||||
mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis()); // 通知监听器设置的日期时间
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
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); // 设置菜单项点击监听器
|
||||
}
|
||||
}
|
||||
|
||||
// 根据ID查找菜单项
|
||||
public MenuItem findItem(int id) {
|
||||
return mMenu.findItem(id); // 返回指定ID的菜单项
|
||||
}
|
||||
|
||||
// 设置按钮标题
|
||||
public void setTitle(CharSequence title) {
|
||||
mButton.setText(title); // 更新按钮文本
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
package net.micode.notes.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.***;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.***Adapter;
|
||||
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 ***Adapter {
|
||||
// 定义要查询的列
|
||||
public static final String[] PROJECTION = {
|
||||
NoteColumns.ID, // 文件夹ID
|
||||
NoteColumns.SNIPPET // 文件夹名称
|
||||
};
|
||||
|
||||
// 列索引常量
|
||||
public static final int ID_COLUMN = 0; // ID列索引
|
||||
public static final int NAME_COLUMN = 1; // 名称列索引
|
||||
|
||||
// 构造函数,接收上下文和***对象
|
||||
public FoldersListAdapter(Context context, *** c) {
|
||||
super(context, c); // 调用父类构造函数
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
// 创建新视图的方法
|
||||
@Override
|
||||
public View newView(Context context, *** cursor, ViewGroup parent) {
|
||||
return new FolderListItem(context); // 返回新的文件夹列表项视图
|
||||
}
|
||||
|
||||
// 绑定数据到视图的方法
|
||||
@Override
|
||||
public void bindView(View view, Context context, *** cursor) {
|
||||
if (view instanceof FolderListItem) { // 确保视图是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 = (***) 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; // 文件夹名称的TextView
|
||||
|
||||
// 构造函数,接收上下文
|
||||
public FolderListItem(Context context) {
|
||||
super(context);
|
||||
// 从布局文件中加载视图
|
||||
inflate(context, R.layout.folder_list_item, this);
|
||||
// 获取文件夹名称的TextView
|
||||
mName = (TextView) findViewById(R.id.tv_folder_name);
|
||||
}
|
||||
|
||||
// 绑定文件夹名称到TextView的方法
|
||||
public void bind(String name) {
|
||||
mName.setText(name); // 设置TextView的文本为文件夹名称
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,665 @@
|
||||
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.***;
|
||||
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 first uses 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) {
|
||||
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();
|
||||
// 更新下拉菜单
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
private void showCreateOrModifyFolderDialog(final boolean create) {
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
|
||||
// ActionMode的回调方法
|
||||
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;
|
||||
}
|
||||
|
||||
// 创建或修改文件夹的对话框视图
|
||||
View view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_text, null);
|
||||
final EditText etName = (EditText) view.findViewById(R.id.et_foler_name);
|
||||
showSoftInput(); // 显示软键盘
|
||||
|
||||
if (!create) {
|
||||
if (mFocusNoteDataItem != null) {
|
||||
etName.setText(mFocusNoteDataItem.getSnippet()); // 设置当前文件夹名称
|
||||
builder.setTitle(getString(R.string.menu_folder_change_name));
|
||||
} else {
|
||||
Log.e(TAG, "The long click data item is null");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
etName.setText(""); // 新建文件夹时清空输入框
|
||||
builder.setTitle(this.getString(R.string.menu_create_folder));
|
||||
}
|
||||
|
||||
builder.setPositiveButton(android.R.string.ok, null);
|
||||
builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
hideSoftInput(etName); // 隐藏软键盘
|
||||
}
|
||||
});
|
||||
|
||||
final Dialog dialog = builder.setView(view).show();
|
||||
final Button positive = (Button) dialog.findViewById(android.R.id.button1);
|
||||
positive.setOnClickListener(new OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
hideSoftInput(etName); // 隐藏软键盘
|
||||
String name = etName.getText().toString();
|
||||
if (DataUtils.checkVisibleFolderName(mContentResolver, name)) {
|
||||
Toast.makeText(NotesListActivity.this, getString(R.string.folder_exist, name), Toast.LENGTH_LONG).show();
|
||||
etName.setSelection(0, etName.length()); // 重新选择文本
|
||||
return;
|
||||
}
|
||||
if (!create) {
|
||||
if (!TextUtils.isEmpty(name)) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(NoteColumns.SNIPPET, name);
|
||||
values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER);
|
||||
values.put(NoteColumns.LOCAL_MODIFIED, 1);
|
||||
mContentResolver.update(Notes.CONTENT_NOTE_URI, values, NoteColumns.ID + "=?", new String[] {
|
||||
String.valueOf(mFocusNoteDataItem.getId())
|
||||
});
|
||||
}
|
||||
} else if (!TextUtils.isEmpty(name)) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(NoteColumns.SNIPPET, name);
|
||||
values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER);
|
||||
mContentResolver.insert(Notes.CONTENT_NOTE_URI, values); // 插入新文件夹
|
||||
}
|
||||
dialog.dismiss(); // 关闭对话框
|
||||
}
|
||||
});
|
||||
}
|
||||
if (TextUtils.isEmpty(etName.getText())) {
|
||||
positive.setEnabled(false); // 如果输入框为空,禁用确认按钮
|
||||
}
|
||||
/**
|
||||
* 当名称编辑框为空时,禁用确认按钮
|
||||
*/
|
||||
etName.addTextChangedListener(new TextWatcher() {
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
// TODO Auto-generated method stub
|
||||
}
|
||||
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
// 根据输入框内容启用或禁用确认按钮
|
||||
if (TextUtils.isEmpty(etName.getText())) {
|
||||
positive.setEnabled(false);
|
||||
} else {
|
||||
positive.setEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
public void afterTextChanged(Editable s) {
|
||||
// TODO Auto-generated method stub
|
||||
}
|
||||
});
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
switch (mState) {
|
||||
case SUB_FOLDER:
|
||||
mCurrentFolderId = Notes.ID_ROOT_FOLDER;
|
||||
mState = ListEditState.NOTE_LIST;
|
||||
startAsyncNotesListQuery(); // 查询笔记列表
|
||||
mTitleBar.setVisibility(View.GONE);
|
||||
break;
|
||||
case CALL_RECORD_FOLDER:
|
||||
mCurrentFolderId = Notes.ID_ROOT_FOLDER;
|
||||
mState = ListEditState.NOTE_LIST;
|
||||
mAddNewNote.setVisibility(View.VISIBLE);
|
||||
mTitleBar.setVisibility(View.GONE);
|
||||
startAsyncNotesListQuery(); // 查询笔记列表
|
||||
break;
|
||||
case NOTE_LIST:
|
||||
super.onBackPressed(); // 调用父类的返回处理
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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, "Unsupported widget type");
|
||||
return;
|
||||
}
|
||||
|
||||
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] { appWidgetId });
|
||||
sendBroadcast(intent); // 发送更新小部件的广播
|
||||
setResult(RESULT_OK, intent); // 设置结果为成功
|
||||
}
|
||||
|
||||
private final OnCreateContextMenuListener mFolderOnCreateContextMenuListener = new OnCreateContextMenuListener() {
|
||||
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
|
||||
if (mFocusNoteDataItem != null) {
|
||||
menu.setHeaderTitle(mFocusNoteDataItem.getSnippet());
|
||||
menu.add(0, MENU_FOLDER_VIEW, 0, R.string.menu_folder_view);
|
||||
menu.add(0, MENU_FOLDER_DELETE, 0, R.string.menu_folder_delete);
|
||||
menu.add(0, MENU_FOLDER_CHANGE_NAME, 0, R.string.menu_folder_change_name);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onContextMenuClosed(Menu menu) {
|
||||
if (mNotesListView != null) {
|
||||
mNotesListView.setOnCreateContextMenuListener(null); // 清除上下文菜单监听器
|
||||
}
|
||||
super.onContextMenuClosed(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContextItemSelected(MenuItem item) {
|
||||
if (mFocusNoteDataItem == null) {
|
||||
Log.e(TAG, "The long click data item is null");
|
||||
return false;
|
||||
}
|
||||
switch (item.getItemId()) {
|
||||
case MENU_FOLDER_VIEW:
|
||||
openFolder(mFocusNoteDataItem); // 打开文件夹
|
||||
break;
|
||||
case MENU_FOLDER_DELETE:
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setTitle(getString(R.string.alert_title_delete));
|
||||
builder.setIcon(android.R.drawable.ic_dialog_alert);
|
||||
builder.setMessage(getString(R.string.alert_message_delete_folder));
|
||||
builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
deleteFolder(mFocusNoteDataItem.getId()); // 删除文件夹
|
||||
}
|
||||
});
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
builder.show();
|
||||
break;
|
||||
case MENU_FOLDER_CHANGE_NAME:
|
||||
showCreateOrModifyFolderDialog(false); // 显示修改文件夹名称对话框
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
menu.clear(); // 清空菜单
|
||||
if (mState == ListEditState.NOTE_LIST) {
|
||||
getMenuInflater().inflate(R.menu.note_list, menu);
|
||||
// 设置同步或取消同步
|
||||
menu.findItem(R.id.menu_sync).setTitle(
|
||||
GTaskSyncService.isSyncing() ? R.string.menu_sync_cancel : R.string.menu_sync);
|
||||
} else if (mState == ListEditState.SUB_FOLDER) {
|
||||
getMenuInflater().inflate(R.menu.sub_folder, menu);
|
||||
} else if (mState == ListEditState.CALL_RECORD_FOLDER) {
|
||||
getMenuInflater().inflate(R.menu.call_record_folder, menu);
|
||||
} else {
|
||||
Log.e(TAG, "Wrong state:" + mState);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_new_folder: {
|
||||
showCreateOrModifyFolderDialog(true); // 显示创建新文件夹对话框
|
||||
break;
|
||||
}
|
||||
case R.id.menu_export_text: {
|
||||
exportNoteToText(); // 导出笔记为文本
|
||||
break;
|
||||
}
|
||||
case R.id.menu_sync: {
|
||||
if (isSyncMode()) {
|
||||
if (TextUtils.equals(item.getTitle(), getString(R.string.menu_sync))) {
|
||||
GTaskSyncService.startSync(this); // 开始同步
|
||||
} else {
|
||||
GTaskSyncService.cancelSync(this); // 取消同步
|
||||
}
|
||||
} else {
|
||||
startPreferenceActivity(); // 启动设置活动
|
||||
}
|
||||
break;
|
||||
}
|
||||
case R.id.menu_setting: {
|
||||
startPreferenceActivity(); // 启动设置活动
|
||||
break;
|
||||
}
|
||||
case R.id.menu_new_note: {
|
||||
createNewNote(); // 创建新笔记
|
||||
break;
|
||||
}
|
||||
case R.id.menu_search:
|
||||
onSearchRequested(); // 请求搜索
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSearchRequested() {
|
||||
startSearch(null, false, null /* appData */, false); // 启动搜索
|
||||
return true;
|
||||
}
|
||||
|
||||
private void exportNoteToText() {
|
||||
final BackupUtils backup = BackupUtils.getInstance(NotesListActivity.this);
|
||||
new AsyncTask<Void, Void, Integer>() {
|
||||
|
||||
@Override
|
||||
protected Integer doInBackground(Void... unused) {
|
||||
return backup.exportToText(); // 导出笔记到文本
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Integer result) {
|
||||
// 根据导出结果显示相应的对话框
|
||||
if (result == BackupUtils.STATE_SD_CARD_UNMOUONTED) {
|
||||
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_FOLDER),
|
||||
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; // 返回false以允许其他处理
|
||||
}
|
||||
@ -0,0 +1,105 @@
|
||||
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;
|
||||
|
||||
public class NotesListItem extends LinearLayout {
|
||||
private ImageView mAlert; // 警告图标
|
||||
private TextView mTitle; // 笔记标题
|
||||
private TextView mTime; // 修改时间
|
||||
private TextView mCallName; // 通话名称
|
||||
private NoteItemData mItemData; // 笔记项数据
|
||||
private CheckBox mCheckBox; // 选择框
|
||||
|
||||
public NotesListItem(Context context) {
|
||||
super(context);
|
||||
inflate(context, R.layout.note_item, this); // 加载布局
|
||||
mAlert = (ImageView) findViewById(R.id.iv_alert_icon);
|
||||
mTitle = (TextView) findViewById(R.id.tv_title);
|
||||
mTime = (TextView) findViewById(R.id.tv_time);
|
||||
mCallName = (TextView) findViewById(R.id.tv_name);
|
||||
mCheckBox = (CheckBox) findViewById(android.R.id.checkbox);
|
||||
}
|
||||
|
||||
public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked) {
|
||||
if (choiceMode && data.getType() == Notes.TYPE_NOTE) {
|
||||
mCheckBox.setVisibility(View.VISIBLE); // 显示选择框
|
||||
mCheckBox.setChecked(checked); // 设置选择框状态
|
||||
} else {
|
||||
mCheckBox.setVisibility(View.GONE); // 隐藏选择框
|
||||
}
|
||||
|
||||
mItemData = data; // 绑定数据
|
||||
if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) {
|
||||
mCallName.setVisibility(View.GONE); // 隐藏通话名称
|
||||
mAlert.setVisibility(View.VISIBLE); // 显示警告图标
|
||||
mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem);
|
||||
mTitle.setText(context.getString(R.string.call_record_folder_name)
|
||||
+ context.getString(R.string.format_folder_files_count, data.getNotesCount()));
|
||||
mAlert.setImageResource(R.drawable.call_record); // 设置警告图标
|
||||
} else if (data.getParentId() == Notes.ID_CALL_RECORD_FOLDER) {
|
||||
mCallName.setVisibility(View.VISIBLE); // 显示通话名称
|
||||
mCallName.setText(data.getCallName());
|
||||
mTitle.setTextAppearance(context, R.style.TextAppearanceSecondaryItem);
|
||||
mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet()));
|
||||
if (data.hasAlert()) {
|
||||
mAlert.setImageResource(R.drawable.clock); // 设置时钟图标
|
||||
mAlert.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mAlert.setVisibility(View.GONE); // 隐藏警告图标
|
||||
}
|
||||
} else {
|
||||
mCallName.setVisibility(View.GONE); // 隐藏通话名称
|
||||
mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem);
|
||||
|
||||
if (data.getType() == Notes.TYPE_FOLDER) {
|
||||
mTitle.setText(data.getSnippet()
|
||||
+ context.getString(R.string.format_folder_files_count,
|
||||
data.getNotesCount())); // 设置文件夹标题
|
||||
mAlert.setVisibility(View.GONE); // 隐藏警告图标
|
||||
} else {
|
||||
mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet()));
|
||||
if (data.hasAlert()) {
|
||||
mAlert.setImageResource(R.drawable.clock); // 设置时钟图标
|
||||
mAlert.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mAlert.setVisibility(View.GONE); // 隐藏警告图标
|
||||
}
|
||||
}
|
||||
}
|
||||
mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate())); // 设置修改时间
|
||||
|
||||
setBackground(data); // 设置背景
|
||||
}
|
||||
|
||||
private void setBackground(NoteItemData data) {
|
||||
int id = data.getBgColorId(); // 获取背景颜色ID
|
||||
if (data.getType() == Notes.TYPE_NOTE) {
|
||||
if (data.isSingle() || data.isOneFollowingFolder()) {
|
||||
setBackgroundResource(NoteItemBgResources.getNoteBgSingleRes(id)); // 设置单个笔记背景
|
||||
} else if (data.isLast()) {
|
||||
setBackgroundResource(NoteItemBgResources.getNoteBgLastRes(id)); // 设置最后一个笔记背景
|
||||
} else if (data.isFirst() || data.isMultiFollowingFolder()) {
|
||||
setBackgroundResource(NoteItemBgResources.getNoteBgFirstRes(id)); // 设置第一个笔记背景
|
||||
} else {
|
||||
setBackgroundResource(NoteItemBgResources.getNoteBgNormalRes(id)); // 设置普通笔记背景
|
||||
}
|
||||
} else {
|
||||
setBackgroundResource(NoteItemBgResources.getFolderBgRes()); // 设置文件夹背景
|
||||
}
|
||||
}
|
||||
|
||||
public NoteItemData getItemData() {
|
||||
return mItemData; // 获取笔记项数据
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,358 @@
|
||||
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);
|
||||
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();
|
||||
|
||||
// 如果用户添加了新账户,则需要自动设置同步账户
|
||||
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(); // 刷新UI
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
if (mReceiver != null) {
|
||||
unregisterReceiver(mReceiver); // 注销广播接收器
|
||||
}
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
private void loadAccountPreference() {
|
||||
mAccountCategory.removeAll(); // 清空账户类别
|
||||
|
||||
Preference accountPref = new Preference(this);
|
||||
final String defaultAccount = getSyncAccountName(this);
|
||||
accountPref.setTitle(getString(R.string.preferences_account_title));
|
||||
accountPref.setSummary(getString(R.string.preferences_account_summary));
|
||||
accountPref.setOnPreferenceClickListener(new OnPreferenceClickListener() {
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
if (!GTaskSyncService.isSyncing()) {
|
||||
if (TextUtils.isEmpty(defaultAccount)) {
|
||||
// 第一次设置账户
|
||||
showSelectAccountAlertDialog();
|
||||
} else {
|
||||
// 如果账户已经设置,需要提示用户风险
|
||||
showChangeAccountConfirmAlertDialog();
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(NotesPreferenceActivity.this,
|
||||
R.string.preferences_toast_cannot_change_account, Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
mAccountCategory.addPreference(accountPref); // 添加账户偏好设置
|
||||
}
|
||||
|
||||
private void loadSyncButton() {
|
||||
Button syncButton = (Button) findViewById(R.id.preference_sync_button);
|
||||
TextView lastSyncTimeView = (TextView) findViewById(R.id.prefenerece_sync_status_textview);
|
||||
|
||||
// 设置按钮状态
|
||||
if (GTaskSyncService.isSyncing()) {
|
||||
syncButton.setText(getString(R.string.preferences_button_sync_cancel));
|
||||
syncButton.setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
GTaskSyncService.cancelSync(NotesPreferenceActivity.this); // 取消同步
|
||||
}
|
||||
});
|
||||
} else {
|
||||
syncButton.setText(getString(R.string.preferences_button_sync_immediately));
|
||||
syncButton.setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
GTaskSyncService.startSync(NotesPreferenceActivity.this); // 立即同步
|
||||
}
|
||||
});
|
||||
}
|
||||
syncButton.setEnabled(!TextUtils.isEmpty(getSyncAccountName(this))); // 根据账户状态启用按钮
|
||||
|
||||
// 设置最后同步时间
|
||||
if (GTaskSyncService.isSyncing()) {
|
||||
lastSyncTimeView.setText(GTaskSyncService.getProgressString());
|
||||
lastSyncTimeView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
long lastSyncTime = getLastSyncTime(this);
|
||||
if (lastSyncTime != 0) {
|
||||
lastSyncTimeView.setText(getString(R.string.preferences_last_sync_time,
|
||||
DateFormat.format(getString(R.string.preferences_last_sync_time_format),
|
||||
lastSyncTime)));
|
||||
lastSyncTimeView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
lastSyncTimeView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshUI() {
|
||||
loadAccountPreference(); // 加载账户偏好设置
|
||||
loadSyncButton(); // 加载同步按钮
|
||||
}
|
||||
|
||||
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(); // 刷新UI
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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(); // 刷新UI
|
||||
}
|
||||
}
|
||||
});
|
||||
dialogBuilder.show();
|
||||
}
|
||||
|
||||
private Account[] getGoogleAccounts() {
|
||||
AccountManager accountManager = AccountManager.get(this);
|
||||
return accountManager.getAccountsByType("com.google"); // 获取Google账户
|
||||
}
|
||||
|
||||
private void setSyncAccount(String account) {
|
||||
if (!getSyncAccountName(this).equals(account)) {
|
||||
SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
|
||||
SharedPreferences.Editor editor = settings.edit();
|
||||
if (account != null) {
|
||||
editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, account); // 设置同步账户名称
|
||||
} else {
|
||||
editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, "");
|
||||
}
|
||||
editor.commit();
|
||||
|
||||
// 清除最后同步时间
|
||||
setLastSyncTime(this, 0);
|
||||
|
||||
// 清除本地GTask相关信息
|
||||
new Thread(new Runnable() {
|
||||
public void run() {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(NoteColumns.GTASK_ID, "");
|
||||
values.put(NoteColumns.SYNC_ID, 0);
|
||||
getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null);
|
||||
}
|
||||
}).start();
|
||||
|
||||
Toast.makeText(NotesPreferenceActivity.this,
|
||||
getString(R.string.preferences_toast_success_set_accout, account),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
private void removeSyncAccount() {
|
||||
SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
|
||||
SharedPreferences.Editor editor = settings.edit();
|
||||
if (settings.contains(PREFERENCE_SYNC_ACCOUNT_NAME)) {
|
||||
editor.remove(PREFERENCE_SYNC_ACCOUNT_NAME); // 移除同步账户名称
|
||||
}
|
||||
if (settings.contains(PREFERENCE_LAST_SYNC_TIME)) {
|
||||
editor.remove(PREFERENCE_LAST_SYNC_TIME); // 移除最后同步时间
|
||||
}
|
||||
editor.commit();
|
||||
|
||||
// 清除本地GTask相关信息
|
||||
new Thread(new Runnable() {
|
||||
public void run() {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(NoteColumns.GTASK_ID, "");
|
||||
values.put(NoteColumns.SYNC_ID, 0);
|
||||
getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
public static String getSyncAccountName(Context context) {
|
||||
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
|
||||
Context.MODE_PRIVATE);
|
||||
return settings.getString(PREFERENCE_SYNC_ACCOUNT_NAME, ""); // 获取同步账户名称
|
||||
}
|
||||
|
||||
public static void setLastSyncTime(Context context, long time) {
|
||||
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
|
||||
Context.MODE_PRIVATE);
|
||||
SharedPreferences.Editor editor = settings.edit();
|
||||
editor.putLong(PREFERENCE_LAST_SYNC_TIME, time); // 设置最后同步时间
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
public static long getLastSyncTime(Context context) {
|
||||
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
|
||||
Context.MODE_PRIVATE);
|
||||
return settings.getLong(PREFERENCE_LAST_SYNC_TIME, 0); // 获取最后同步时间
|
||||
}
|
||||
|
||||
private class GTaskReceiver extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
refreshUI(); // 刷新UI
|
||||
if (intent.getBooleanExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_IS_SYNCING, false)) {
|
||||
TextView syncStatus = (TextView) findViewById(R.id.prefenerece_sync_status_textview);
|
||||
syncStatus.setText(intent
|
||||
.getStringExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_PROGRESS_MSG)); // 更新同步状态
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue