diff --git a/src/ui(m)/NotesListActivity.java b/src/ui(m)/NotesListActivity.java new file mode 100644 index 0000000..b5291ec --- /dev/null +++ b/src/ui(m)/NotesListActivity.java @@ -0,0 +1,790 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +// ...(省略其他import) + +// NotesListActivity类继承自Activity,用于展示笔记列表 +public class NotesListActivity extends Activity implements OnClickListener, OnItemLongClickListener { + // 定义了一些查询操作的token,用于识别不同的查询请求 + private static final int FOLDER_NOTE_LIST_QUERY_TOKEN = 0; + private static final int FOLDER_LIST_QUERY_TOKEN = 1; + // 定义了一些菜单项的常量,用于识别不同的菜单操作 + private static final int MENU_FOLDER_DELETE = 0; + private static final int MENU_FOLDER_VIEW = 1; + private static final int MENU_FOLDER_CHANGE_NAME = 2; + // 偏好设置的键,用于检查是否显示过介绍信息 + private static final String PREFERENCE_ADD_INTRODUCTION = "net.micode.notes.introduction"; + // 枚举,表示不同的列表编辑状态 + private enum ListEditState { + NOTE_LIST, SUB_FOLDER, CALL_RECORD_FOLDER + }; + // 成员变量声明 + private ListEditState mState; + private BackgroundQueryHandler mBackgroundQueryHandler; + private NotesListAdapter mNotesListAdapter; + private ListView mNotesListView; + private Button mAddNewNote; + private boolean mDispatch; + private int mOriginY; + private int mDispatchY; + private TextView mTitleBar; + private long mCurrentFolderId; + private ContentResolver mContentResolver; + private ModeCallback mModeCallBack; + private static final String TAG = "NotesListActivity"; + public static final int NOTES_LISTVIEW_SCROLL_RATE = 30; + private NoteItemData mFocusNoteDataItem; + private static final String NORMAL_SELECTION = NoteColumns.PARENT_ID + "=?"; + private static final String ROOT_FOLDER_SELECTION = "(" + NoteColumns.TYPE + "<>" + + Notes.TYPE_SYSTEM + " AND " + NoteColumns.PARENT_ID + "=?)" + " OR (" + + NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER + " AND " + + NoteColumns.NOTES_COUNT + ">0)"; + private final static int REQUEST_CODE_OPEN_NODE = 102; + private final static int REQUEST_CODE_NEW_NODE = 103; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.note_list); + initResources(); + + /** + * 当用户首次使用应用程序时插入介绍信息 + */ + setAppInfoFromRawRes(); + } + + /** + * 从raw资源文件中读取介绍信息,并将其保存为笔记 + */ + private void setAppInfoFromRawRes() { + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); + if (!sp.getBoolean(PREFERENCE_ADD_INTRODUCTION, false)) { + StringBuilder sb = new StringBuilder(); + InputStream in = null; + try { + in = getResources().openRawResource(R.raw.introduction); + if (in != null) { + InputStreamReader isr = new InputStreamReader(in); + BufferedReader br = new BufferedReader(isr); + char [] buf = new char[1024]; + int len = 0; + while ((len = br.read(buf)) > 0) { + sb.append(buf, 0, len); + } + } else { + Log.e(TAG, "Read introduction file error"); + return; + } + } catch (IOException e) { + e.printStackTrace(); + return; + } finally { + if(in != null) { + try { + in.close(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + } + + WorkingNote note = WorkingNote.createEmptyNote(this, Notes.ID_ROOT_FOLDER, + AppWidgetManager.INVALID_APPWIDGET_ID, Notes.TYPE_WIDGET_INVALIDE, + ResourceParser.RED); + note.setWorkingText(sb.toString()); + if (note.saveNote()) { + sp.edit().putBoolean(PREFERENCE_ADD_INTRODUCTION, true).commit(); + } else { + Log.e(TAG, "Save introduction note error"); + return; + } + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode == RESULT_OK + && (requestCode == REQUEST_CODE_OPEN_NODE || requestCode == REQUEST_CODE_NEW_NODE)) { + mNotesListAdapter.changeCursor(null); + } else { + super.onActivityResult(requestCode, resultCode, data); + } + } + + /** + * 初始化活动所需的资源,包括内容解析器、适配器、列表视图等 + */ + private void initResources() { + mContentResolver = this.getContentResolver(); + mBackgroundQueryHandler = new BackgroundQueryHandler(this.getContentResolver()); + mCurrentFolderId = Notes.ID_ROOT_FOLDER; + mNotesListView = (ListView) findViewById(R.id.notes_list); + mNotesListView.addFooterView(LayoutInflater.from(this).inflate(R.layout.note_list_footer, null), null, false); + mNotesListView.setOnItemClickListener(new OnListItemClickListener()); + mNotesListView.setOnItemLongClickListener(this); + mNotesListAdapter = new NotesListAdapter(this); + mNotesListView.setAdapter(mNotesListAdapter); + mAddNewNote = (Button) findViewById(R.id.btn_new_note); + mAddNewNote.setOnClickListener(this); + mAddNewNote.setOnTouchListener(new NewNoteOnTouchListener()); + mDispatch = false; + mDispatchY = 0; + mOriginY = 0; + mTitleBar = (TextView) findViewById(R.id.tv_title_bar); + mState = ListEditState.NOTE_LIST; + mModeCallBack = new ModeCallback(); + } + // 内部类,用于处理多点触控事件,当用户触摸“新建笔记”按钮时的特殊处理 + private class NewNoteOnTouchListener implements OnTouchListener { + public boolean onTouch(View v, MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: { + // 计算屏幕高度和“新建笔记”按钮的高度差 + Display display = getWindowManager().getDefaultDisplay(); + int screenHeight = display.getHeight(); + int newNoteViewHeight = mAddNewNote.getHeight(); + int start = screenHeight - newNoteViewHeight; + int eventY = start + (int) event.getY(); + // 根据UI设计师的要求,对“新建笔记”按钮的透明部分进行特殊处理 + if (event.getY() < (event.getX() * (-0.12) + 94)) { + View view = mNotesListView.getChildAt(mNotesListView.getChildCount() - 1 + - mNotesListView.getFooterViewsCount()); + if (view != null && view.getBottom() > start + && (view.getTop() < (start + 94))) { + mOriginY = (int) event.getY(); + mDispatchY = eventY; + event.setLocation(event.getX(), mDispatchY); + mDispatch = true; + return mNotesListView.dispatchTouchEvent(event); + } + } + break; + } + case MotionEvent.ACTION_MOVE: { + if (mDispatch) { + mDispatchY += (int) event.getY() - mOriginY; + event.setLocation(event.getX(), mDispatchY); + return mNotesListView.dispatchTouchEvent(event); + } + break; + } + default: { + if (mDispatch) { + event.setLocation(event.getX(), mDispatchY); + mDispatch = false; + return mNotesListView.dispatchTouchEvent(event); + } + break; + } + } + return false; + } + }; + + /** + * 启动异步查询,获取笔记列表 + */ + private void startAsyncNotesListQuery() { + String selection = (mCurrentFolderId == Notes.ID_ROOT_FOLDER) ? ROOT_FOLDER_SELECTION + : NORMAL_SELECTION; + mBackgroundQueryHandler.startQuery(FOLDER_NOTE_LIST_QUERY_TOKEN, null, + Notes.CONTENT_NOTE_URI, NoteItemData.PROJECTION, selection, new String[] { + String.valueOf(mCurrentFolderId) + }, NoteColumns.TYPE + " DESC," + NoteColumns.MODIFIED_DATE + " DESC"); + } + + // 异步查询处理器,处理查询完成事件 + private final class BackgroundQueryHandler extends AsyncQueryHandler { + public BackgroundQueryHandler(ContentResolver contentResolver) { + super(contentResolver); + } + + @Override + protected void onQueryComplete(int token, Object cookie, Cursor cursor) { + switch (token) { + case FOLDER_NOTE_LIST_QUERY_TOKEN: + mNotesListAdapter.changeCursor(cursor); + break; + case FOLDER_LIST_QUERY_TOKEN: + if (cursor != null && cursor.getCount() > 0) { + showFolderListMenu(cursor); + } else { + Log.e(TAG, "Query folder failed"); + } + break; + default: + return; + } + } + } + + /** + * 显示文件夹列表菜单,允许用户选择目标文件夹 + */ + private void showFolderListMenu(Cursor cursor) { + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + builder.setTitle(R.string.menu_title_select_folder); + final FoldersListAdapter adapter = new FoldersListAdapter(this, cursor); + builder.setAdapter(adapter, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + DataUtils.batchMoveToFolder(mContentResolver, + mNotesListAdapter.getSelectedItemIds(), adapter.getItemId(which)); + Toast.makeText( + NotesListActivity.this, + getString(R.string.format_move_notes_to_folder, + mNotesListAdapter.getSelectedCount(), + adapter.getFolderName(NotesListActivity.this, which)), + Toast.LENGTH_SHORT).show(); + mModeCallBack.finishActionMode(); + } + }); + builder.show(); + } + /** + * 创建新笔记的函数 + */ + private void createNewNote() { + Intent intent = new Intent(this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_INSERT_OR_EDIT); + intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mCurrentFolderId); + this.startActivityForResult(intent, REQUEST_CODE_NEW_NODE); + } + + /** + * 批量删除笔记的函数 + */ + private void batchDelete() { + new AsyncTask>() { + protected HashSet doInBackground(Void... unused) { + HashSet widgets = mNotesListAdapter.getSelectedWidget(); + if (!isSyncMode()) { + // 如果不是同步模式,直接删除笔记 + if (DataUtils.batchDeleteNotes(mContentResolver, mNotesListAdapter + .getSelectedItemIds())) { + } else { + Log.e(TAG, "Delete notes error, should not happens"); + } + } else { + // 如果是同步模式,将删除的笔记移动到回收站文件夹 + if (!DataUtils.batchMoveToFolder(mContentResolver, mNotesListAdapter + .getSelectedItemIds(), Notes.ID_TRASH_FOLER)) { + Log.e(TAG, "Move notes to trash folder error, should not happens"); + } + } + return widgets; + } + + @Override + protected void onPostExecute(HashSet widgets) { + if (widgets != null) { + for (AppWidgetAttribute widget : widgets) { + if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID + && widget.widgetType != Notes.TYPE_WIDGET_INVALIDE) { + updateWidget(widget.widgetId, widget.widgetType); + } + } + } + mModeCallBack.finishActionMode(); + } + }.execute(); + } + + /** + * 删除文件夹的函数 + */ + private void deleteFolder(long folderId) { + if (folderId == Notes.ID_ROOT_FOLDER) { + Log.e(TAG, "Wrong folder id, should not happen " + folderId); + return; + } + + HashSet ids = new HashSet(); + ids.add(folderId); + HashSet widgets = DataUtils.getFolderNoteWidget(mContentResolver, + folderId); + if (!isSyncMode()) { + // 如果不是同步模式,直接删除文件夹 + DataUtils.batchDeleteNotes(mContentResolver, ids); + } else { + // 如果是同步模式,将删除的文件夹移动到回收站文件夹 + DataUtils.batchMoveToFolder(mContentResolver, ids, Notes.ID_TRASH_FOLER); + } + if (widgets != null) { + for (AppWidgetAttribute widget : widgets) { + if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID + && widget.widgetType != Notes.TYPE_WIDGET_INVALIDE) { + updateWidget(widget.widgetId, widget.widgetType); + } + } + } + } + + /** + * 打开笔记项的函数 + */ + private void openNode(NoteItemData data) { + Intent intent = new Intent(this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.putExtra(Intent.EXTRA_UID, data.getId()); + this.startActivityForResult(intent, REQUEST_CODE_OPEN_NODE); + } + + /** + * 打开文件夹的函数 + */ + private void openFolder(NoteItemData data) { + mCurrentFolderId = data.getId(); + startAsyncNotesListQuery(); + if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { + mState = ListEditState.CALL_RECORD_FOLDER; + mAddNewNote.setVisibility(View.GONE); + } else { + mState = ListEditState.SUB_FOLDER; + } + if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { + mTitleBar.setText(R.string.call_record_folder_name); + } else { + mTitleBar.setText(data.getSnippet()); + } + mTitleBar.setVisibility(View.VISIBLE); + } + + @Override + public void onClick(View v) { + switch (v.getId()) { + case R.id.btn_new_note: + createNewNote(); + break; + default: + break; + } + } + + /** + * 显示软键盘的函数 + */ + private void showSoftInput() { + InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + if (inputMethodManager != null) { + inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); + } + } + + /** + * 隐藏软键盘的函数 + */ + private void hideSoftInput(View view) { + InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + + /** + * 显示创建或修改文件夹对话框的函数 + */ + private void showCreateOrModifyFolderDialog(final boolean create) { + final AlertDialog.Builder builder = new AlertDialog.Builder(this); + View view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_text, null); + final EditText etName = (EditText) view.findViewById(R.id.et_foler_name); + showSoftInput(); + if (!create) { + if (mFocusNoteDataItem != null) { + etName.setText(mFocusNoteDataItem.getSnippet()); + builder.setTitle(getString(R.string.menu_folder_change_name)); + } else { + Log.e(TAG, "The long click data item is null"); + return; + } + } else { + etName.setText(""); + builder.setTitle(this.getString(R.string.menu_create_folder)); + } + + builder.setPositiveButton(android.R.string.ok, null); + builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + hideSoftInput(etName); + } + }); + + final Dialog dialog = builder.setView(view).show(); + final Button positive = (Button)dialog.findViewById(android.R.id.button1); + positive.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + hideSoftInput(etName); + String name = etName.getText().toString(); + if (DataUtils.checkVisibleFolderName(mContentResolver, name)) { + Toast.makeText(NotesListActivity.this, getString(R.string.folder_exist, name), + Toast.LENGTH_LONG).show(); + etName.setSelection(0, etName.length()); + return; + } + if (!create) { + if (!TextUtils.isEmpty(name)) { + ContentValues values = new ContentValues(); + values.put(NoteColumns.SNIPPET, name); + values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); + values.put(NoteColumns.LOCAL_MODIFIED, 1); + mContentResolver.update(Notes.CONTENT_NOTE_URI, values, NoteColumns.ID + + "=?", new String[] { + String.valueOf(mFocusNoteDataItem.getId()) + }); + } + } else if (!TextUtils.isEmpty(name)) { + ContentValues values = new ContentValues(); + values.put(NoteColumns.SNIPPET, name); + values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); + mContentResolver.insert(Notes.CONTENT_NOTE_URI, values); + } + dialog.dismiss(); + } + }); + + if (TextUtils.isEmpty(etName.getText())) { + positive.setEnabled(false); + } + etName.addTextChangedListener(new TextWatcher() { + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // TODO Auto-generated method stub + } + + public void onTextChanged(CharSequence s, int start, int before, int count) { + if (TextUtils.isEmpty(etName.getText())) { + positive.setEnabled(false); + } else { + positive.setEnabled(true); + } + } + + public void afterTextChanged(Editable s) { + // TODO Auto-generated method stub + } + }); + } + + @Override + public void onBackPressed() { + switch (mState) { + case SUB_FOLDER: + mCurrentFolderId = Notes.ID_ROOT_FOLDER; + mState = ListEditState.NOTE_LIST; + startAsyncNotesListQuery(); + mTitleBar.setVisibility(View.GONE); + break; + case CALL_RECORD_FOLDER: + mCurrentFolderId = Notes.ID_ROOT_FOLDER; + mState = ListEditState.NOTE_LIST; + mAddNewNote.setVisibility(View.VISIBLE); + mTitleBar.setVisibility(View.GONE); + startAsyncNotesListQuery(); + break; + case NOTE_LIST: + super.onBackPressed(); + break; + default: + break; + } + } + + /** + * 更新Widget的函数 + */ + private void updateWidget(int appWidgetId, int appWidgetType) { + Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); + if (appWidgetType == Notes.TYPE_WIDGET_2X) { + intent.setClass(this, NoteWidgetProvider_2x.class); + } else if (appWidgetType == Notes.TYPE_WIDGET_4X) { + intent.setClass(this, NoteWidgetProvider_4x.class); + } else { + Log.e(TAG, "Unspported widget type"); + return; + } + + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] { + appWidgetId + }); + + sendBroadcast(intent); + setResult(RESULT_OK, intent); + } + /** + * 为文件夹创建上下文菜单的监听器 + */ + private final OnCreateContextMenuListener mFolderOnCreateContextMenuListener = new OnCreateContextMenuListener() { + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + if (mFocusNoteDataItem != null) { + menu.setHeaderTitle(mFocusNoteDataItem.getSnippet()); + menu.add(0, MENU_FOLDER_VIEW, 0, R.string.menu_folder_view); + menu.add(0, MENU_FOLDER_DELETE, 0, R.string.menu_folder_delete); + menu.add(0, MENU_FOLDER_CHANGE_NAME, 0, R.string.menu_folder_change_name); + } + } + }; + + @Override + public void onContextMenuClosed(Menu menu) { + if (mNotesListView != null) { + mNotesListView.setOnCreateContextMenuListener(null); + } + super.onContextMenuClosed(menu); + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + if (mFocusNoteDataItem == null) { + Log.e(TAG, "The long click data item is null"); + return false; + } + switch (item.getItemId()) { + case MENU_FOLDER_VIEW: + openFolder(mFocusNoteDataItem); + break; + case MENU_FOLDER_DELETE: + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.alert_title_delete)); + builder.setIcon(android.R.drawable.ic_dialog_alert); + builder.setMessage(getString(R.string.alert_message_delete_folder)); + builder.setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + deleteFolder(mFocusNoteDataItem.getId()); + } + }); + builder.setNegativeButton(android.R.string.cancel, null); + builder.show(); + break; + case MENU_FOLDER_CHANGE_NAME: + showCreateOrModifyFolderDialog(false); + break; + default: + break; + } + + return true; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + menu.clear(); + if (mState == ListEditState.NOTE_LIST) { + getMenuInflater().inflate(R.menu.note_list, menu); + // 设置同步或取消同步的菜单项标题 + menu.findItem(R.id.menu_sync).setTitle( + GTaskSyncService.isSyncing() ? R.string.menu_sync_cancel : R.string.menu_sync); + } else if (mState == ListEditState.SUB_FOLDER) { + getMenuInflater().inflate(R.menu.sub_folder, menu); + } else if (mState == ListEditState.CALL_RECORD_FOLDER) { + getMenuInflater().inflate(R.menu.call_record_folder, menu); + } else { + Log.e(TAG, "Wrong state:" + mState); + } + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_new_folder: { + showCreateOrModifyFolderDialog(true); + break; + } + case R.id.menu_export_text: { + exportNoteToText(); + break; + } + case R.id.menu_sync: { + if (isSyncMode()) { + if (TextUtils.equals(item.getTitle(), getString(R.string.menu_sync))) { + GTaskSyncService.startSync(this); + } else { + GTaskSyncService.cancelSync(this); + } + } else { + startPreferenceActivity(); + } + break; + } + case R.id.menu_setting: { + startPreferenceActivity(); + break; + } + case R.id.menu_new_note: { + createNewNote(); + break; + } + case R.id.menu_search: + onSearchRequested(); + break; + default: + break; + } + return true; + } + + @Override + public boolean onSearchRequested() { + startSearch(null, false, null /* appData */, false); + return true; + } + + /** + * 导出笔记到文本文件的函数 + */ + private void exportNoteToText() { + final BackupUtils backup = BackupUtils.getInstance(NotesListActivity.this); + new AsyncTask() { + + @Override + protected Integer doInBackground(Void... unused) { + return backup.exportToText(); + } + + @Override + protected void onPostExecute(Integer result) { + if (result == BackupUtils.STATE_SD_CARD_UNMOUONTED) { + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + builder.setTitle(NotesListActivity.this + .getString(R.string.failed_sdcard_export)); + builder.setMessage(NotesListActivity.this + .getString(R.string.error_sdcard_unmounted)); + builder.setPositiveButton(android.R.string.ok, null); + builder.show(); + } else if (result == BackupUtils.STATE_SUCCESS) { + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + builder.setTitle(NotesListActivity.this + .getString(R.string.success_sdcard_export)); + builder.setMessage(NotesListActivity.this.getString( + R.string.format_exported_file_location, backup + .getExportedTextFileName(), backup.getExportedTextFileDir())); + builder.setPositiveButton(android.R.string.ok, null); + builder.show(); + } else if (result == BackupUtils.STATE_SYSTEM_ERROR) { + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + builder.setTitle(NotesListActivity.this + .getString(R.string.failed_sdcard_export)); + builder.setMessage(NotesListActivity.this + .getString(R.string.error_sdcard_export)); + builder.setPositiveButton(android.R.string.ok, null); + builder.show(); + } + } + + }.execute(); + } + + /** + * 检查是否处于同步模式的函数 + */ + private boolean isSyncMode() { + return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0; + } + + /** + * 启动偏好设置活动的函数 + */ + private void startPreferenceActivity() { + Activity from = getParent() != null ? getParent() : this; + Intent intent = new Intent(from, NotesPreferenceActivity.class); + from.startActivityIfNeeded(intent, -1); + } + + // 列表项点击事件的监听器 + private class OnListItemClickListener implements OnItemClickListener { + + public void onItemClick(AdapterView parent, View view, int position, long id) { + if (view instanceof NotesListItem) { + NoteItemData item = ((NotesListItem) view).getItemData(); + if (mNotesListAdapter.isInChoiceMode()) { + if (item.getType() == Notes.TYPE_NOTE) { + position = position - mNotesListView.getHeaderViewsCount(); + mModeCallBack.onItemCheckedStateChanged(null, position, id, + !mNotesListAdapter.isSelectedItem(position)); + } + return; + } + + switch (mState) { + case NOTE_LIST: + if (item.getType() == Notes.TYPE_FOLDER + || item.getType() == Notes.TYPE_SYSTEM) { + openFolder(item); + } else if (item.getType() == Notes.TYPE_NOTE) { + openNode(item); + } else { + Log.e(TAG, "Wrong note type in NOTE_LIST"); + } + break; + case SUB_FOLDER: + case CALL_RECORD_FOLDER: + if (item.getType() == Notes.TYPE_NOTE) { + openNode(item); + } else { + Log.e(TAG, "Wrong note type in SUB_FOLDER"); + } + break; + default: + break; + } + } + } + + } + + /** + * 开始查询目标文件夹的函数 + */ + private void startQueryDestinationFolders() { + String selection = NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>? AND " + NoteColumns.ID + "<>?"; + selection = (mState == ListEditState.NOTE_LIST) ? selection: + "(" + selection + ") OR (" + NoteColumns.ID + "=" + Notes.ID_ROOT_FOLDER + ")"; + + mBackgroundQueryHandler.startQuery(FOLDER_LIST_QUERY_TOKEN, + null, + Notes.CONTENT_NOTE_URI, + FoldersListAdapter.PROJECTION, + selection, + new String[] { + String.valueOf(Notes.TYPE_FOLDER), + String.valueOf(Notes.ID_TRASH_FOLER), + String.valueOf(mCurrentFolderId) + }, + NoteColumns.MODIFIED_DATE + " DESC"); + } + + @Override + public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { + if (view instanceof NotesListItem) { + mFocusNoteDataItem = ((NotesListItem) view).getItemData(); + if (mFocusNoteDataItem.getType() == Notes.TYPE_NOTE && !mNotesListAdapter.isInChoiceMode()) { + if (mNotesListView.startActionMode(mModeCallBack) != null) { + mModeCallBack.onItemCheckedStateChanged(null, position, id, true); + mNotesListView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + } else { + Log.e(TAG, "startActionMode fails"); + } + } else if (mFocusNoteDataItem.getType() == Notes.TYPE_FOLDER) { + mNotesListView.setOnCreateContextMenuListener(mFolderOnCreateContextMenuListener); + } + } + return false; + } +} + + \ No newline at end of file diff --git a/src/ui(m)/NotesPreferenceActivity.java b/src/ui(m)/NotesPreferenceActivity.java new file mode 100644 index 0000000..7a59d05 --- /dev/null +++ b/src/ui(m)/NotesPreferenceActivity.java @@ -0,0 +1,471 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.app.ActionBar; +import android.app.AlertDialog; +import android.content.BroadcastReceiver; +import android.content.ContentValues; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceClickListener; +import android.preference.PreferenceActivity; +import android.preference.PreferenceCategory; +import android.text.TextUtils; +import android.text.format.DateFormat; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; +import android.widget.Toast; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.gtask.remote.GTaskSyncService; + +/** + * 设置和偏好活动,用于配置应用程序的偏好设置。 + */ +public class NotesPreferenceActivity extends PreferenceActivity { + // 偏好设置文件的名称 + public static final String PREFERENCE_NAME = "notes_preferences"; + // 同步账户名称的键 + public static final String PREFERENCE_SYNC_ACCOUNT_NAME = "pref_key_account_name"; + // 最后一次同步时间的键 + public static final String PREFERENCE_LAST_SYNC_TIME = "pref_last_sync_time"; + // 设置背景颜色的键 + public static final String PREFERENCE_SET_BG_COLOR_KEY = "pref_key_bg_random_appear"; + // 同步账户的键 + private static final String PREFERENCE_SYNC_ACCOUNT_KEY = "pref_sync_account_key"; + // 权限过滤键 + private static final String AUTHORITIES_FILTER_KEY = "authorities"; + // 账户类别 + private PreferenceCategory mAccountCategory; + // GTask接收器 + private GTaskReceiver mReceiver; + // 原始账户数组 + private Account[] mOriAccounts; + // 是否添加了账户 + private boolean mHasAddedAccount; + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + + /* 使用应用程序图标进行导航 */ + getActionBar().setDisplayHomeAsUpEnabled(true); + + // 从资源文件中添加偏好设置 + addPreferencesFromResource(R.xml.preferences); + // 获取账户类别 + mAccountCategory = (PreferenceCategory) findPreference(PREFERENCE_SYNC_ACCOUNT_KEY); + // 创建GTask接收器 + mReceiver = new GTaskReceiver(); + // 创建意图过滤器 + IntentFilter filter = new IntentFilter(); + filter.addAction(GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME); + // 注册接收器 + registerReceiver(mReceiver, filter); + + // 初始化原始账户数组 + mOriAccounts = null; + // 从XML布局文件中加载设置头部 + View header = LayoutInflater.from(this).inflate(R.layout.settings_header, null); + // 将头部添加到偏好列表中 + getListView().addHeaderView(header, null, true); + } + + @Override + protected void onResume() { + super.onResume(); + + // 自动设置同步账户,如果用户添加了新账户 + if (mHasAddedAccount) { + Account[] accounts = getGoogleAccounts(); + if (mOriAccounts != null && accounts.length > mOriAccounts.length) { + for (Account accountNew : accounts) { + boolean found = false; + for (Account accountOld : mOriAccounts) { + if (TextUtils.equals(accountOld.name, accountNew.name)) { + found = true; + break; + } + } + if (!found) { + setSyncAccount(accountNew.name); + break; + } + } + } + } + + // 刷新UI + refreshUI(); + } + + @Override + protected void onDestroy() { + // 如果接收器不为空,注销接收器 + if (mReceiver != null) { + unregisterReceiver(mReceiver); + } + super.onDestroy(); + } + + /** + * 加载账户偏好设置。 + */ + private void loadAccountPreference() { + // 移除所有账户偏好设置 + mAccountCategory.removeAll(); + + // 创建一个新的偏好设置 + Preference accountPref = new Preference(this); + // 获取默认账户 + final String defaultAccount = getSyncAccountName(this); + accountPref.setTitle(getString(R.string.preferences_account_title)); + accountPref.setSummary(getString(R.string.preferences_account_summary)); + accountPref.setOnPreferenceClickListener(new OnPreferenceClickListener() { + public boolean onPreferenceClick(Preference preference) { + // 如果GTask服务没有正在同步 + if (!GTaskSyncService.isSyncing()) { + // 如果默认账户为空 + if (TextUtils.isEmpty(defaultAccount)) { + // 第一次设置账户 + showSelectAccountAlertDialog(); + } else { + // 如果账户已经设置,提示用户风险 + showChangeAccountConfirmAlertDialog(); + } + } else { + // 提示用户不能更改账户 + Toast.makeText(NotesPreferenceActivity.this, + R.string.preferences_toast_cannot_change_account, Toast.LENGTH_SHORT) + .show(); + } + return true; + } + }); + + // 将账户偏好设置添加到账户类别中 + mAccountCategory.addPreference(accountPref); + } + + /** + * 加载同步按钮。 + */ + private void loadSyncButton() { + // 获取同步按钮和最后同步时间视图 + Button syncButton = (Button) findViewById(R.id.preference_sync_button); + TextView lastSyncTimeView = (TextView) findViewById(R.id.prefenerece_sync_status_textview); + + // 设置按钮状态 + if (GTaskSyncService.isSyncing()) { + syncButton.setText(getString(R.string.preferences_button_sync_cancel)); + syncButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + // 取消同步 + GTaskSyncService.cancelSync(NotesPreferenceActivity.this); + } + }); + } else { + syncButton.setText(getString(R.string.preferences_button_sync_immediately)); + syncButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + // 立即同步 + GTaskSyncService.startSync(NotesPreferenceActivity.this); + } + }); + } + syncButton.setEnabled(!TextUtils.isEmpty(getSyncAccountName(this))); + + // 设置最后同步时间 + if (GTaskSyncService.isSyncing()) { + lastSyncTimeView.setText(GTaskSyncService.getProgressString()); + lastSyncTimeView.setVisibility(View.VISIBLE); + } else { + long lastSyncTime = getLastSyncTime(this); + if (lastSyncTime != 0) { + lastSyncTimeView.setText(getString(R.string.preferences_last_sync_time, + DateFormat.format(getString(R.string.preferences_last_sync_time_format), + lastSyncTime))); + lastSyncTimeView.setVisibility(View.VISIBLE); + } else { + lastSyncTimeView.setVisibility(View.GONE); + } + } + } + + /** + * 刷新UI。 + */ + private void refreshUI() { + // 加载账户偏好设置和同步按钮 + loadAccountPreference(); + loadSyncButton(); + } + + /** + * 显示选择账户的警告对话框。 + */ + private void showSelectAccountAlertDialog() { + // 创建警告对话框的构建器 + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); + + // 从XML布局文件中加载标题视图 + View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null); + TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title); + titleTextView.setText(getString(R.string.preferences_dialog_select_account_title)); + TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle); + subtitleTextView.setText(getString(R.string.preferences_dialog_select_account_tips)); + + // 设置自定义标题 + dialogBuilder.setCustomTitle(titleView); + dialogBuilder.setPositiveButton(null, null); + + // 获取Google账户 + Account[] accounts = getGoogleAccounts()); + String defAccount = getSyncAccountName(this); + + // 初始化原始账户数组和添加账户标志 + mOriAccounts = accounts; + mHasAddedAccount = false; + + // 如果账户数量大于0 + if (accounts.length > 0) { + // 创建账户项 + CharSequence[] items = new CharSequence[accounts.length]; + final CharSequence[] itemMapping = items; + int checkedItem = -1; + int index = 0; + for (Account account : accounts) { + if (TextUtils.equals(account.name, defAccount)) { + checkedItem = index; + } + items[index++] = account.name; + } + // 设置单选项目 + dialogBuilder.setSingleChoiceItems(items, checkedItem, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + //设置同步账户 + setSyncAccount(itemMapping[which].toString()); + dialog.dismiss(); + refreshUI(); + } + }); + } + // 从XML布局文件中加载添加账户视图 + View addAccountView = LayoutInflater.from(this).inflate(R.layout.add_account_text, null); + dialogBuilder.setView(addAccountView); + + // 显示警告对话框 + final AlertDialog dialog = dialogBuilder.show(); + addAccountView.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + // 添加账户 + mHasAddedAccount = true; + Intent intent = new Intent("android.settings.ADD_ACCOUNT_SETTINGS"); + intent.putExtra(AUTHORITIES_FILTER_KEY, new String[] { + "gmail-ls" + }); + startActivityForResult(intent, -1); + dialog.dismiss(); + } + }); +} + +/** + * 显示更改账户的确认警告对话框。 + */ +private void showChangeAccountConfirmAlertDialog() { + // 创建警告对话框的构建器 + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); + + // 从XML布局文件中加载标题视图 + View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null); + TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title); + titleTextView.setText(getString(R.string.preferences_dialog_change_account_title, + getSyncAccountName(this))); + TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle); + subtitleTextView.setText(getString(R.string.preferences_dialog_change_account_warn_msg)); + dialogBuilder.setCustomTitle(titleView); + + // 创建菜单项 + CharSequence[] menuItemArray = new CharSequence[] { + getString(R.string.preferences_menu_change_account), + getString(R.string.preferences_menu_remove_account), + getString(R.string.preferences_menu_cancel) + }; + dialogBuilder.setItems(menuItemArray, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + // 处理菜单项点击事件 + if (which == 0) { + showSelectAccountAlertDialog(); + } else if (which == 1) { + removeSyncAccount(); + refreshUI(); + } + } + }); + dialogBuilder.show(); +} + +/** + * 获取Google账户。 + */ +private Account[] getGoogleAccounts() { + // 获取账户管理器 + AccountManager accountManager = AccountManager.get(this); + // 获取Google账户 + return accountManager.getAccountsByType("com.google"); +} + +/** + * 设置同步账户。 + */ +private void setSyncAccount(String account) { + // 如果同步账户不为空且与当前账户不同 + if (!getSyncAccountName(this).equals(account)) { + // 获取共享首选项 + SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = settings.edit(); + if (account != null) { + editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, account); + } else { + editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, ""); + } + editor.commit(); + + // 清除最后同步时间 + setLastSyncTime(this, 0); + + // 清除本地gtask相关信息 + new Thread(new Runnable() { + public void run() { + ContentValues values = new ContentValues(); + values.put(NoteColumns.GTASK_ID, ""); + values.put(NoteColumns.SYNC_ID, 0); + getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null); + } + }).start(); + + // 提示用户设置账户成功 + Toast.makeText(NotesPreferenceActivity.this, + getString(R.string.preferences_toast_success_set_accout, account), + Toast.LENGTH_SHORT).show(); + } +} + +/** + * 移除同步账户。 + */ +private void removeSyncAccount() { + // 获取共享首选项 + SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = settings.edit(); + if (settings.contains(PREFERENCE_SYNC_ACCOUNT_NAME)) { + editor.remove(PREFERENCE_SYNC_ACCOUNT_NAME); + } + if (settings.contains(PREFERENCE_LAST_SYNC_TIME)) { + editor.remove(PREFERENCE_LAST_SYNC_TIME); + } + editor.commit(); + + // 清除本地gtask相关信息 + new Thread(new Runnable() { + public void run() { + ContentValues values = new ContentValues(); + values.put(NoteColumns.GTASK_ID, ""); + values.put(NoteColumns.SYNC_ID, 0); + getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null); + } + }).start(); +} + +/** + * 获取同步账户名称。 + */ +public static String getSyncAccountName(Context context) { + SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, + Context.MODE_PRIVATE); + return settings.getString(PREFERENCE_SYNC_ACCOUNT_NAME, ""); +} + +/** + * 设置最后同步时间。 + */ +public static void setLastSyncTime(Context context, long time) { + SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, + Context.MODE_PRIVATE); + SharedPreferences.Editor editor = settings.edit(); + editor.putLong(PREFERENCE_LAST_SYNC_TIME, time); + editor.commit(); +} + +/** + * 获取最后同步时间。 + */ +public static long getLastSyncTime(Context context) { + SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, + Context.MODE_PRIVATE); + return settings.getLong(PREFERENCE_LAST_SYNC_TIME, 0); +} + +/** + * GTask接收器,用于接收GTask服务的广播。 + */ +private class GTaskReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + // 刷新UI + refreshUI(); + if (intent.getBooleanExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_IS_SYNCING, false)) { + // 获取同步状态视图 + TextView syncStatus = (TextView) findViewById(R.id.prefenerece_sync_status_textview); + syncStatus.setText(intent + .getStringExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_PROGRESS_MSG)); + } + + } +} + +/** + * 处理选项菜单项点击事件。 + */ +public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + Intent intent = new Intent(this, NotesListActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + return true; + default: + return false; + } +} diff --git a/src/ui(s)/AlarmAlertActivity.java b/src/ui(s)/AlarmAlertActivity.java new file mode 100644 index 0000000..dc4b618 --- /dev/null +++ b/src/ui(s)/AlarmAlertActivity.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.DialogInterface.OnDismissListener; +import android.content.Intent; +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.media.RingtoneManager; +import android.net.Uri; +import android.os.Bundle; +import android.os.PowerManager; +import android.provider.Settings; +import android.view.Window; +import android.view.WindowManager; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.tool.DataUtils; + +import java.io.IOException; + +// AlarmAlertActivity类继承自Activity,用于处理闹钟提醒的UI和逻辑。 +public class AlarmAlertActivity extends Activity implements OnClickListener, OnDismissListener { + // 用于存储笔记ID和笔记预览文本 + private long mNoteId; + private String mSnippet; + // 定义预览文本的最大长度 + private static final int SNIPPET_PREW_MAX_LEN = 60; + // 用于播放闹钟声音的MediaPlayer对象 + MediaPlayer mPlayer; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_NO_TITLE); // 请求无标题栏的窗口特性 + + final Window win = getWindow(); // 获取窗口对象 + win.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); // 设置窗口在锁屏时也能显示 + + // 如果屏幕不是开启状态,则添加额外的窗口参数以点亮屏幕 + if (!isScreenOn()) { + win.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON + | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON + | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON + | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR); + } + + // 获取启动该Activity的Intent对象 + Intent intent = getIntent(); + + try { + // 从Intent中解析笔记ID + mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1)); + // 根据笔记ID获取笔记预览文本 + mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId); + // 如果预览文本超过最大长度,则截断并添加省略号 + mSnippet = mSnippet.length() > SNIPPET_PREW_MAX_LEN ? mSnippet.substring(0, + SNIPPET_PREW_MAX_LEN) + getResources().getString(R.string.notelist_string_info) + : mSnippet; + } catch (IllegalArgumentException e) { + e.printStackTrace(); + return; + } + + // 初始化MediaPlayer对象 + mPlayer = new MediaPlayer(); + // 如果笔记存在于数据库中,则显示操作对话框并播放闹钟声音 + if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) { + showActionDialog(); + playAlarmSound(); + } else { + finish(); + } + } + + // 检查屏幕是否开启 + private boolean isScreenOn() { + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + return pm.isScreenOn(); + } + + // 播放闹钟声音 + private void playAlarmSound() { + // 获取默认闹钟铃声的Uri + Uri url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM); + + // 获取受影响的静音模式流类型 + int silentModeStreams = Settings.System.getInt(getContentResolver(), + Settings.System.MODE_RINGER_STREAMS_AFFECTED, 0); + + // 设置MediaPlayer的音频流类型 + if ((silentModeStreams & (1 << AudioManager.STREAM_ALARM)) != 0) { + mPlayer.setAudioStreamType(silentModeStreams); + } else { + mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM); + } + try { + // 设置MediaPlayer的数据源为闹钟铃声,并准备播放 + mPlayer.setDataSource(this, url); + mPlayer.prepare(); + mPlayer.setLooping(true); // 设置循环播放 + mPlayer.start(); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + } catch (SecurityException e) { + e.printStackTrace(); + } catch (IllegalStateException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + // 显示操作对话框 + private void showActionDialog() { + AlertDialog.Builder dialog = new AlertDialog.Builder(this); + dialog.setTitle(R.string.app_name); // 设置对话框标题 + dialog.setMessage(mSnippet); // 设置对话框消息 + dialog.setPositiveButton(R.string.notealert_ok, this); // 设置确定按钮 + if (isScreenOn()) { // 如果屏幕开启,则设置编辑按钮 + dialog.setNegativeButton(R.string.notealert_enter, this); + } + dialog.show().setOnDismissListener(this); // 显示对话框并设置对话框消失监听器 + } + + // DialogInterface.OnClickListener接口的实现,处理对话框按钮点击事件 + public void onClick(DialogInterface dialog, int which) { + switch (which) { + case DialogInterface.BUTTON_NEGATIVE: // 如果点击编辑按钮 + Intent intent = new Intent(this, NoteEditActivity.class); // 创建Intent,跳转到编辑笔记界面 + intent.setAction(Intent.ACTION_VIEW); + intent.putExtra(Intent.EXTRA_UID, mNoteId); // 传递笔记ID + startActivity(intent); // 启动编辑笔记界面 + break; + default: + break; + } + } + + // DialogInterface.OnDismissListener接口的实现,处理对话框消失事件 + public void onDismiss(DialogInterface dialog) { + stopAlarmSound(); // 停止闹钟声音 + finish(); // 结束当前Activity + } + + // 停止闹钟声音 + private void stopAlarmSound() { + if (mPlayer != null) { + mPlayer.stop(); // 停止播放 + mPlayer.release(); // 释放资源 + mPlayer = null; // 置空MediaPlayer对象 + } + } +} \ No newline at end of file diff --git a/src/ui(s)/AlarmInitReceiver.java b/src/ui(s)/AlarmInitReceiver.java new file mode 100644 index 0000000..6f99ea7 --- /dev/null +++ b/src/ui(s)/AlarmInitReceiver.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.ContentUris; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; + +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; + + +// AlarmInitReceiver类继承自BroadcastReceiver,用于初始化闹钟提醒。 +public class AlarmInitReceiver extends BroadcastReceiver { + + // 定义查询数据库时需要返回的列 + private static final String [] PROJECTION = new String [] { + NoteColumns.ID, // 笔记ID + NoteColumns.ALERTED_DATE // 笔记的已提醒日期 + }; + + // 定义PROJECTION数组中各列的索引 + private static final int COLUMN_ID = 0; // 笔记ID索引 + private static final int COLUMN_ALERTED_DATE = 1; // 已提醒日期索引 + + // onReceive方法在接收到广播时被调用,这里用于设置闹钟提醒。 + @Override + public void onReceive(Context context, Intent intent) { + // 获取当前日期时间 + long currentDate = System.currentTimeMillis(); + // 查询数据库,获取所有未提醒且类型为笔记的笔记 + Cursor c = context.getContentResolver().query(Notes.CONTENT_NOTE_URI, + PROJECTION, + NoteColumns.ALERTED_DATE + ">? AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE, + new String[] { String.valueOf(currentDate) }, + null); + + // 如果查询结果不为空 + if (c != null) { + // 如果查询结果至少有一条记录 + if (c.moveToFirst()) { + // 遍历查询结果 + do { + // 获取笔记的提醒日期 + long alertDate = c.getLong(COLUMN_ALERTED_DATE); + // 创建一个Intent,用于触发闹钟提醒 + Intent sender = new Intent(context, AlarmReceiver.class); + // 为Intent设置数据,标识具体的笔记 + sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, c.getLong(COLUMN_ID))); + // 创建PendingIntent,用于AlarmManager设置闹钟 + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, 0); + // 获取AlarmManager实例 + AlarmManager alermManager = (AlarmManager) context + .getSystemService(Context.ALARM_SERVICE); + // 设置闹钟,使用RTC_WAKEUP模式在指定时间唤醒设备 + alermManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent); + } while (c.moveToNext()); + } + // 关闭游标 + c.close(); + } + } +} \ No newline at end of file diff --git a/src/ui(s)/AlarmReceiver.java b/src/ui(s)/AlarmReceiver.java new file mode 100644 index 0000000..6356bd5 --- /dev/null +++ b/src/ui(s)/AlarmReceiver.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +// AlarmReceiver类继承自BroadcastReceiver,用于处理闹钟提醒的广播事件。 +public class AlarmReceiver extends BroadcastReceiver { + // onReceive方法在接收到广播时被调用,这里用于启动闹钟提醒的Activity。 + @Override + public void onReceive(Context context, Intent intent) { + // 设置intent的目标类为AlarmAlertActivity,即闹钟提醒的Activity。 + intent.setClass(context, AlarmAlertActivity.class); + // 为intent添加标志,表示需要创建一个新的任务栈。 + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + // 使用context启动AlarmAlertActivity,展示闹钟提醒界面。 + context.startActivity(intent); + } +} \ No newline at end of file diff --git a/src/ui(s)/DateTimePicker.java b/src/ui(s)/DateTimePicker.java new file mode 100644 index 0000000..3744de1 --- /dev/null +++ b/src/ui(s)/DateTimePicker.java @@ -0,0 +1,511 @@ +/** + * 文件名称:DateTimePicker.java + * 版权声明:版权所有 (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * 许可证声明:根据Apache License, Version 2.0(“许可证”)获得许可; + * 除非遵守许可证,否则不得使用此文件。 + * 您可以在以下网址获得许可证的副本: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 除非适用法律要求或书面同意,否则按“现状”分发的软件是在没有任何明示或暗示的 + * 保证和条件的情况下提供的,包括但不限于关于所有权、不侵犯第三方权利、适销性或 + * 适用于特定用途的任何保证和条件。有关权限和限制的具体语言,请参见许可证。 + */ + +package net.micode.notes.ui; // 定义包名 + +import java.text.DateFormatSymbols; // 用于日期时间格式化的符号 +import java.util.Calendar; // 用于日期时间操作的日历类 + +import net.micode.notes.R; // 导入资源文件R,用于访问资源 + +import android.content.Context; // Android上下文环境 +import android.text.format.DateFormat; // 用于日期时间格式化 +import android.view.View; // 用于视图操作 +import android.widget.FrameLayout; // 布局容器FrameLayout +import android.widget.NumberPicker; // 自定义视图,用于选择数字 + +/** + * DateTimePicker 类是一个自定义视图组件,用于在Android应用中选择日期和时间。 + */ +public class DateTimePicker extends FrameLayout { + // 类成员变量声明 + private static final boolean DEFAULT_ENABLE_STATE = true; // 默认启用状态 + + // 时间单位常量 + private static final int HOURS_IN_HALF_DAY = 12; + private static final int HOURS_IN_ALL_DAY = 24; + private static final int DAYS_IN_ALL_WEEK = 7; + // NumberPicker组件的值范围常量 + private static final int DATE_SPINNER_MIN_VAL = 0; + private static final int DATE_SPINNER_MAX_VAL = DAYS_IN_ALL_WEEK - 1; + private static final int HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW = 0; + private static final int HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW = 23; + private static final int HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW = 1; + private static final int HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW = 12; + private static final int MINUT_SPINNER_MIN_VAL = 0; + private static final int MINUT_SPINNER_MAX_VAL = 59; + private static final int AMPM_SPINNER_MIN_VAL = 0; + private static final int AMPM_SPINNER_MAX_VAL = 1; + + // 控件声明 + private final NumberPicker mDateSpinner; // 用于选择日期的NumberPicker + private final NumberPicker mHourSpinner; // 用于选择小时的NumberPicker + private final NumberPicker mMinuteSpinner; // 用于选择分钟的NumberPicker + private final NumberPicker mAmPmSpinner; // 用于选择上午/下午的NumberPicker + private Calendar mDate; // 用于管理日期和时间的Calendar实例 + + // 日期显示值数组 + private String[] mDateDisplayValues = new String[DAYS_IN_ALL_WEEK]; + + // 上午/下午状态标志 + private boolean mIsAm; + + // 是否为24小时视图 + private boolean mIs24HourView; + + // 控件启用状态 + private boolean mIsEnabled = DEFAULT_ENABLE_STATE; + + // 控件初始化状态 + private boolean mInitialising; + + // 日期时间改变监听器 + private OnDateTimeChangedListener mOnDateTimeChangedListener; + + // NumberPicker值变化监听器 + private NumberPicker.OnValueChangeListener mOnDateChangedListener = new NumberPicker.OnValueChangeListener() { + @Override + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + // 当日期值变化时,更新日历并通知监听器 + mDate.add(Calendar.DAY_OF_YEAR, newVal - oldVal); + updateDateControl(); + onDateTimeChanged(); + } + }; + + // 小时变化监听器 + private NumberPicker.OnValueChangeListener mOnHourChangedListener = new NumberPicker.OnValueChangeListener() { + @Override + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + // 当小时值变化时,更新日历并通知监听器 + boolean isDateChanged = false; + Calendar cal = Calendar.getInstance(); + if (!mIs24HourView) { + // 处理12小时制逻辑 + if (!mIsAm && oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) { + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, 1); + isDateChanged = true; + } else if (mIsAm && oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) { + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, -1); + isDateChanged = true; + } + if (oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY || + oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) { + mIsAm = !mIsAm; + updateAmPmControl(); + } + } else { + // 处理24小时制逻辑 + if (oldVal == HOURS_IN_ALL_DAY - 1 && newVal == 0) { + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, 1); + isDateChanged = true; + } else if (oldVal == 0 && newVal == HOURS_IN_ALL_DAY - 1) { + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, -1); + isDateChanged = true; + } + } + int newHour = mHourSpinner.getValue() % HOURS_IN_HALF_DAY + (mIsAm ? 0 : HOURS_IN_HALF_DAY); + mDate.set(Calendar.HOUR_OF_DAY, newHour); + onDateTimeChanged(); + if (isDateChanged) { + setCurrentYear(cal.get(Calendar.YEAR)); + setCurrentMonth(cal.get(Calendar.MONTH)); + setCurrentDay(cal.get(Calendar.DAY_OF_MONTH)); + } + } + }; + + // 分钟变化监听器 + private NumberPicker.OnValueChangeListener mOnMinuteChangedListener = new NumberPicker.OnValueChangeListener() { + @Override + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + // 当分钟值变化时,更新日历并通知监听器 + int minValue = mMinuteSpinner.getMinValue(); + int maxValue = mMinuteSpinner.getMaxValue(); + int offset = 0; + if (oldVal == maxValue && newVal == minValue) { + offset += 1; + } else if (oldVal == minValue && newVal == maxValue) { + offset -= 1; + } + if (offset != 0) { + mDate.add(Calendar.HOUR_OF_DAY, offset); + mHourSpinner.setValue(getCurrentHour()); + updateDateControl(); + int newHour = getCurrentHourOfDay(); + if (newHour >= HOURS_IN_HALF_DAY) { + mIsAm = false; + updateAmPmControl(); + } else { + mIsAm = true; + updateAmPmControl(); + } + } + mDate.set(Calendar.MINUTE, newVal); + onDateTimeChanged(); + } + }; + + // 上午/下午变化监听器 + private NumberPicker.OnValueChangeListener mOnAmPmChangedListener = new NumberPicker.OnValueChangeListener() { + @Override + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + // 当上午/下午变化时,更新日历并通知监听器 + mIsAm = !mIsAm; + if (mIsAm) { + mDate.add(Calendar.HOUR_OF_DAY, -HOURS_IN_HALF_DAY); + } else { + mDate.add(Calendar.HOUR_OF_DAY, HOURS_IN_HALF_DAY); + } + updateAmPmControl(); + onDateTimeChanged(); + } + }; + + // 定义日期时间变化监听器接口 + public interface OnDateTimeChangedListener { + void onDateTimeChanged(DateTimePicker view, int year, int month, + int dayOfMonth, int hourOfDay, int minute); + } + + // DateTimePicker的构造函数 + public DateTimePicker(Context context) { + this(context, System.currentTimeMillis()); + } + + public DateTimePicker(Context context, long date) { + this(context, date, DateFormat.is24HourFormat(context)); + } + + public DateTimePicker(Context context, long date, boolean is24HourView) { + super(context); + mDate = Calendar.getInstance(); + mInitialising = true; + mIsAm = getCurrentHourOfDay() >= HOURS_IN_HALF_DAY; + inflate(context, R.layout.datetime_picker, this); + + mDateSpinner = (NumberPicker) findViewById(R.id.date); + mDateSpinner.setMinValue(DATE_SPINNER_MIN_VAL); + mDateSpinner.setMaxValue(DATE_SPINNER_MAX_VAL); + mDateSpinner.setOnValueChangedListener(mOnDateChangedListener); + + mHourSpinner = (NumberPicker) findViewById + + String[] stringsForAmPm = new DateFormatSymbols().getAmPmStrings(); + mAmPmSpinner = (NumberPicker) findViewById(R.id.amPm); + mAmPmSpinner.setMinValue(AMPM_SPINNER_MIN_VAL); + mAmPmSpinner.setMaxValue(AMPM_SPINNER_MAX_VAL); + mAmPmSpinner.setDisplayedValues(stringsForAmPm); + mAmPmSpinner.setOnValueChangedListener(mOnAmPmChangedListener); + + // 初始化控件状态 + updateDateControl(); + updateHourControl(); + updateAmPmControl(); + + set24HourView(is24HourView); + + // 设置当前时间 + setCurrentDate(date); + + setEnabled(isEnabled()); + + // 设置内容描述 + mInitialising = false; +} + +@Override +public void setEnabled(boolean enabled) { + // 设置控件是否可用 + if (mIsEnabled == enabled) { + return; + } + super.setEnabled(enabled); + mDateSpinner.setEnabled(enabled); + mMinuteSpinner.setEnabled(enabled); + mHourSpinner.setEnabled(enabled); + mAmPmSpinner.setEnabled(enabled); + mIsEnabled = enabled; +} + +@Override +public boolean isEnabled() { + // 获取控件是否可用 + return mIsEnabled; +} + +/** + * 获取当前日期的毫秒值 + * + * @return 当前日期的毫秒值 + */ +public long getCurrentDateInTimeMillis() { + return mDate.getTimeInMillis(); +} + +/** + * 设置当前日期 + * + * @param date 当前日期的毫秒值 + */ +public void setCurrentDate(long date) { + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(date); + setCurrentDate(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH), + cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE)); +} + +/** + * 设置当前日期 + * + * @param year 当前年 + * @param month 当前月 + * @param dayOfMonth 当前日 + * @param hourOfDay 当前小时 + * @param minute 当前分钟 + */ +public void setCurrentDate(int year, int month, + int dayOfMonth, int hourOfDay, int minute) { + setCurrentYear(year); + setCurrentMonth(month); + setCurrentDay(dayOfMonth); + setCurrentHour(hourOfDay); + setCurrentMinute(minute); +} + +/** + * 获取当前年份 + * + * @return 当前年份 + */ +public int getCurrentYear() { + return mDate.get(Calendar.YEAR); +} + +/** + * 设置当前年份 + * + * @param year 当前年份 + */ +public void setCurrentYear(int year) { + if (!mInitialising && year == getCurrentYear()) { + return; + } + mDate.set(Calendar.YEAR, year); + updateDateControl(); + onDateTimeChanged(); +} + +/** + * 获取当前月份 + * + * @return 当前月份 + */ +public int getCurrentMonth() { + return mDate.get(Calendar.MONTH); +} + +/** + * 设置当前月份 + * + * @param month 月份 + */ +public void setCurrentMonth(int month) { + if (!mInitialising && month == getCurrentMonth()) { + return; + } + mDate.set(Calendar.MONTH, month); + updateDateControl(); + onDateTimeChanged(); +} + +/** + * 获取当前月份中的日期 + * + * @return 月份中的日期 + */ +public int getCurrentDay() { + return mDate.get(Calendar.DAY_OF_MONTH); +} + +/** + * 设置当前月份中的日期 + * + * @param dayOfMonth 月份中的日期 + */ +public void setCurrentDay(int dayOfMonth) { + if (!mInitialising && dayOfMonth == getCurrentDay()) { + return; + } + mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); + updateDateControl(); + onDateTimeChanged(); +} + +/** + * 获取24小时制的当前小时 + * + * @return 24小时制的当前小时 + */ +public int getCurrentHourOfDay() { + return mDate.get(Calendar.HOUR_OF_DAY); +} + +private int getCurrentHour() { + if (mIs24HourView){ + return getCurrentHourOfDay(); + } else { + int hour = getCurrentHourOfDay(); + if (hour > HOURS_IN_HALF_DAY) { + return hour - HOURS_IN_HALF_DAY; + } else { + return hour == 0 ? HOURS_IN_HALF_DAY : hour; + } + } +} + +/** + * 设置24小时制的当前小时 + * + * @param hourOfDay 24小时制的小时 + */ +public void setCurrentHour(int hourOfDay) { + if (!mInitialising && hourOfDay == getCurrentHourOfDay()) { + return; + } + mDate.set(Calendar.HOUR_OF_DAY, hourOfDay); + if (!mIs24HourView) { + if (hourOfDay >= HOURS_IN_HALF_DAY) { + mIsAm = false; + if (hourOfDay > HOURS_IN_HALF_DAY) { + hourOfDay -= HOURS_IN_HALF_DAY; + } + } else { + mIsAm = true; + if (hourOfDay == 0) { + hourOfDay = HOURS_IN_HALF_DAY; + } + } + updateAmPmControl(); + } + mHourSpinner.setValue(hourOfDay); + onDateTimeChanged(); +} + +/** + * 获取当前分钟 + * + * @return 当前分钟 + */ +public int getCurrentMinute() { + return mDate.get(Calendar.MINUTE); +} + +/** + * 设置当前分钟 + */ +public void setCurrentMinute(int minute) { + if (!mInitialising && minute == getCurrentMinute()) { + return; + } + mMinuteSpinner.setValue(minute); + mDate.set(Calendar.MINUTE, minute); + onDateTimeChanged(); +} + +/** + * 判断是否是24小时视图 + * + * @return 如果是24小时视图返回true,否则返回false。 + */ +public boolean is24HourView () { + return mIs24HourView; +} + +/** + * 设置是否是24小时视图或AM/PM模式 + * + * @param is24HourView 如果是24小时视图传入true,否则传入false。 + */ +public void set24HourView(boolean is24HourView) { + if (mIs24HourView == is24HourView) { + return; + } + mIs24HourView = is24HourView; + mAmPmSpinner.setVisibility(is24HourView ? View.GONE : View.VISIBLE); + int hour = getCurrentHourOfDay(); + updateHourControl(); + setCurrentHour(hour); + updateAmPmControl(); +} + +/** + * 更新日期控制 + */ +private void updateDateControl() { + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, -DAYS_IN_ALL_WEEK / 2 - 1); + mDateSpinner.setDisplayedValues(null); + for (int i = 0; i < DAYS_IN_ALL_WEEK; ++i) { + cal.add(Calendar.DAY_OF_YEAR, 1); + mDateDisplayValues[i] = (String) DateFormat.format("MM.dd EEEE", cal); + } + mDateSpinner.setDisplayedValues(mDateDisplayValues); + mDateSpinner.setValue(DAYS_IN_ALL_WEEK / 2); + mDateSpinner.invalidate(); +} + +/** + * 更新上午/下午控制 + */ +private void updateAmPmControl() { + if (mIs24HourView) { + mAmPmSpinner.setVisibility(View.GONE); // 在24小时视图中隐藏AM/PM选择器 + } else { + int index = mIsAm ? Calendar.AM : Calendar.PM; // 根据当前时间是上午还是下午设置索引 + mAmPmSpinner.setValue(index); // 设置AM/PM选择器的值 + mAmPmSpinner.setVisibility(View.VISIBLE); // 显示AM/PM选择器 + } +} + +/** + * 更新小时控制 + */ +private void updateHourControl() { + if (mIs24HourView) { + mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW); // 设置24小时视图的小时最小值 + mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW); // 设置24小时视图的小时最大值 + } else { + mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW); // 设置12小时视图的小时最小值 + mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW); // 设置12小时视图的小时最大值 + } +} + +/** + * 设置日期时间改变的监听器 + * +/** + * 当日期时间改变时调用此方法 + */ +private void onDateTimeChanged() { + if (mOnDateTimeChangedListener != null) { + mOnDateTimeChangedListener.onDateTimeChanged(this, getCurrentYear(), + getCurrentMonth(), getCurrentDay(), getCurrentHourOfDay(), getCurrentMinute()); + } +} \ No newline at end of file diff --git a/src/ui(s)/DateTimePickerDialog.java b/src/ui(s)/DateTimePickerDialog.java new file mode 100644 index 0000000..ba22a76 --- /dev/null +++ b/src/ui(s)/DateTimePickerDialog.java @@ -0,0 +1,122 @@ +/** + * 文件名称:DateTimePickerDialog.java + * 版权声明:版权所有 (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * 许可证声明:根据Apache License, Version 2.0(“许可证”)获得许可; + * 除非遵守许可证,否则不得使用此文件。 + * 您可以在以下网址获得许可证的副本: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 除非适用法律要求或书面同意,否则按“现状”分发的软件是在没有任何明示或暗示的 + * 保证和条件的情况下提供的,包括但不限于关于所有权、不侵犯第三方权利、适销性或 + * 适用于特定用途的任何保证和条件。有关权限和限制的具体语言,请参见许可证。 + */ + +package net.micode.notes.ui; // 定义包名 + +import java.util.Calendar; // 导入日历类 + +import net.micode.notes.R; // 导入资源文件R,用于访问资源 +import net.micode.notes.ui.DateTimePicker; // 导入DateTimePicker类 +import net.micode.notes.ui.DateTimePicker.OnDateTimeChangedListener; // 导入DateTimePicker的监听器接口 + +import android.app.AlertDialog; // 导入AlertDialog类,用于创建对话框 +import android.content.Context; // 导入Context类 +import android.content.DialogInterface; // 导入DialogInterface类 +import android.content.DialogInterface.OnClickListener; // 导入点击事件监听器接口 +import android.text.format.DateFormat; // 导入日期格式化类 +import android.text.format.DateUtils; // 导入日期工具类 + +/** + * DateTimePickerDialog 类是一个继承自AlertDialog的对话框,用于选择日期和时间。 + */ +public class DateTimePickerDialog extends AlertDialog implements OnClickListener { + + // 成员变量 + private Calendar mDate = Calendar.getInstance(); // 当前日期时间的Calendar实例 + private boolean mIs24HourView; // 是否使用24小时制 + private OnDateTimeSetListener mOnDateTimeSetListener; // 日期时间设置监听器 + private DateTimePicker mDateTimePicker; // DateTimePicker组件 + + /** + * 日期时间设置监听器接口 + */ + public interface OnDateTimeSetListener { + void OnDateTimeSet(AlertDialog dialog, long date); // 当日期时间设置时的回调方法 + } + + /** + * DateTimePickerDialog构造函数 + * @param context 上下文环境 + * @param date 初始日期时间的毫秒值 + */ + public DateTimePickerDialog(Context context, long date) { + super(context); // 调用AlertDialog的构造函数 + mDateTimePicker = new DateTimePicker(context); // 创建DateTimePicker实例 + setView(mDateTimePicker); // 设置对话框的内容视图 + // 设置日期时间改变监听器 + mDateTimePicker.setOnDateTimeChangedListener(new OnDateTimeChangedListener() { + public void onDateTimeChanged(DateTimePicker view, int year, int month, + int dayOfMonth, int hourOfDay, int minute) { + mDate.set(Calendar.YEAR, year); // 设置年 + mDate.set(Calendar.MONTH, month); // 设置月 + mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); // 设置日 + mDate.set(Calendar.HOUR_OF_DAY, hourOfDay); // 设置小时 + mDate.set(Calendar.MINUTE, minute); // 设置分钟 + updateTitle(mDate.getTimeInMillis()); // 更新对话框标题 + } + }); + mDate.setTimeInMillis(date); // 设置初始日期时间 + mDate.set(Calendar.SECOND, 0); // 设置秒为0 + mDateTimePicker.setCurrentDate(mDate.getTimeInMillis()); // 设置DateTimePicker的初始日期时间 + // 设置对话框按钮 + setButton(context.getString(R.string.datetime_dialog_ok), this); + setButton2(context.getString(R.string.datetime_dialog_cancel), (OnClickListener)null); + // 设置24小时制视图 + set24HourView(DateFormat.is24HourFormat(this.getContext())); + updateTitle(mDate.getTimeInMillis()); // 更新对话框标题 + } + + /** + * 设置是否使用24小时制视图 + * @param is24HourView 是否使用24小时制 + */ + public void set24HourView(boolean is24HourView) { + mIs24HourView = is24HourView; + // 根据是否使用24小时制更新DateTimePicker + mDateTimePicker.set24HourView(is24HourView); + } + + /** + * 设置日期时间设置监听器 + * @param callBack 监听器实例 + */ + public void setOnDateTimeSetListener(OnDateTimeSetListener callBack) { + mOnDateTimeSetListener = callBack; // 设置监听器 + } + + /** + * 更新对话框标题 + * @param date 毫秒值表示的日期时间 + */ + private void updateTitle(long date) { + int flag = DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME; + // 根据是否使用24小时制设置格式化标志 + flag |= mIs24HourView ? DateUtils.FORMAT_24HOUR : DateUtils.FORMAT_12HOUR; + // 设置对话框标题为日期时间字符串 + setTitle(DateUtils.formatDateTime(this.getContext(), date, flag)); + } + + /** + * 点击事件处理 + * @param arg0 对话框接口 + * @param arg1 按钮标识 + */ + public void onClick(DialogInterface arg0, int arg1) { + // 如果设置了监听器,则回调监听器 + if (mOnDateTimeSetListener != null) { + mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis()); + } + } + +} \ No newline at end of file diff --git a/src/ui(s)/DropdownMenu.java b/src/ui(s)/DropdownMenu.java new file mode 100644 index 0000000..46f6399 --- /dev/null +++ b/src/ui(s)/DropdownMenu.java @@ -0,0 +1,82 @@ +/** + * 文件名称:DropdownMenu.java + * 版权声明:版权所有 (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * 许可证声明:根据Apache License, Version 2.0(“许可证”)获得许可; + * 除非遵守许可证,否则不得使用此文件。 + * 您可以在以下网址获得许可证的副本: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 除非适用法律要求或书面同意,否则按“现状”分发的软件是在没有任何明示或暗示的 + * 保证和条件的情况下提供的,包括但不限于关于所有权、不侵犯第三方权利、适销性或 + * 适用于特定用途的任何保证和条件。有关权限和限制的具体语言,请参见许可证。 + */ + +package net.micode.notes.ui; // 定义包名 + +import android.content.Context; // 导入Context类,用于获取系统资源和服务 +import android.view.Menu; // 导入Menu类,用于操作菜单 +import android.view.MenuItem; // 导入MenuItem类,用于操作菜单项 +import android.view.View; // 导入View类,用于操作视图 +import android.view.View.OnClickListener; // 导入OnClickListener接口,用于设置点击事件 +import android.widget.Button; // 导入Button类,用于操作按钮 +import android.widget.PopupMenu; // 导入PopupMenu类,用于创建弹出菜单 +import android.widget.PopupMenu.OnMenuItemClickListener; // 导入OnMenuItemClickListener接口,用于设置菜单项点击事件 + +import net.micode.notes.R; // 导入R类,用于访问资源文件 + +/** + * DropdownMenu 类用于创建一个下拉菜单(Dropdown Menu)。 + */ +public class DropdownMenu { + // 成员变量 + private Button mButton; // 用于触发下拉菜单的按钮 + private PopupMenu mPopupMenu; // 弹出菜单对象 + private Menu mMenu; // 菜单对象 + + /** + * DropdownMenu 构造函数。 + * @param context 上下文环境,用于获取系统资源和服务。 + * @param button 用于触发下拉菜单的按钮。 + * @param menuId 菜单资源ID,用于从XML资源文件中加载菜单项。 + */ + public DropdownMenu(Context context, Button button, int menuId) { + mButton = button; // 初始化按钮 + mButton.setBackgroundResource(R.drawable.dropdown_icon); // 设置按钮背景图标 + mPopupMenu = new PopupMenu(context, mButton); // 创建PopupMenu对象 + mMenu = mPopupMenu.getMenu(); // 获取PopupMenu的Menu对象 + mPopupMenu.getMenuInflater().inflate(menuId, mMenu); // 从XML资源文件中加载菜单项 + mButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + mPopupMenu.show(); // 设置按钮点击事件,显示PopupMenu + } + }); + } + + /** + * 设置下拉菜单项点击事件监听器。 + * @param listener 点击事件监听器。 + */ + public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) { + if (mPopupMenu != null) { + mPopupMenu.setOnMenuItemClickListener(listener); // 设置PopupMenu的菜单项点击事件 + } + } + + /** + * 查找具有指定ID的菜单项。 + * @param id 菜单项ID。 + * @return 返回找到的MenuItem对象,如果没有找到则返回null。 + */ + public MenuItem findItem(int id) { + return mMenu.findItem(id); // 在Menu中查找指定ID的菜单项 + } + + /** + * 设置触发下拉菜单的按钮文本。 + * @param title 按钮文本。 + */ + public void setTitle(CharSequence title) { + mButton.setText(title); // 设置按钮文本 + } +} \ No newline at end of file diff --git a/src/ui(s)/FoldersListAdapter.java b/src/ui(s)/FoldersListAdapter.java new file mode 100644 index 0000000..800d1cc --- /dev/null +++ b/src/ui(s)/FoldersListAdapter.java @@ -0,0 +1,118 @@ +/** + * 文件名称:FoldersListAdapter.java + * 版权声明:版权所有 (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * 许可证声明:根据Apache License, Version 2.0(“许可证”)获得许可; + * 除非遵守许可证,否则不得使用此文件。 + * 您可以在以下网址获得许可证的副本: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 除非适用法律要求或书面同意,否则按“现状”分发的软件是在没有任何明示或暗示的 + * 保证和条件的情况下提供的,包括但不限于关于所有权、不侵犯第三方权利、适销性或 + * 适用于特定用途的任何保证和条件。有关权限和限制的具体语言,请参见许可证。 + */ + +package net.micode.notes.ui; // 定义包名 + +import android.content.Context; // 导入Context类,用于获取系统资源和服务 +import android.database.Cursor; // 导入Cursor类,用于访问数据库查询结果 +import android.view.View; // 导入View类,用于操作视图 +import android.view.ViewGroup; // 导入ViewGroup类,用于操作视图组 +import android.widget.CursorAdapter; // 导入CursorAdapter类,用于适配器 +import android.widget.LinearLayout; // 导入LinearLayout类,用于线性布局 +import android.widget.TextView; // 导入TextView类,用于文本视图 + +import net.micode.notes.R; // 导入R类,用于访问资源文件 +import net.micode.notes.data.Notes; // 导入Notes类,用于访问笔记数据 +import net.micode.notes.data.Notes.NoteColumns; // 导入NoteColumns类,用于访问笔记列数据 + + +/** + * FoldersListAdapter 类是一个用于文件夹列表的适配器,继承自CursorAdapter。 + */ +public class FoldersListAdapter extends CursorAdapter { + // 定义查询数据库时需要的列 + public static final String [] PROJECTION = { + NoteColumns.ID, + NoteColumns.SNIPPET + }; + + // 定义列索引常量 + public static final int ID_COLUMN = 0; + public static final int NAME_COLUMN = 1; + + /** + * FoldersListAdapter 构造函数。 + * @param context 上下文环境,用于获取系统资源和服务。 + * @param c 数据库查询结果的Cursor对象。 + */ + public FoldersListAdapter(Context context, Cursor c) { + super(context, c); // 调用父类的构造函数 + } + + /** + * 创建新的视图。 + * @param context 上下文环境。 + * @param cursor Cursor对象。 + * @param parent 父视图组。 + * @return 返回新的视图对象。 + */ + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return new FolderListItem(context); // 创建FolderListItem视图 + } + + /** + * 绑定视图和数据。 + * @param view 视图对象。 + * @param context 上下文环境。 + * @param cursor Cursor对象。 + */ + @Override + public void bindView(View view, Context context, Cursor cursor) { + if (view instanceof FolderListItem) { + // 获取文件夹名称 + String folderName = (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context + .getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN); + ((FolderListItem) view).bind(folderName); // 绑定数据到视图 + } + } + + /** + * 获取文件夹名称。 + * @param context 上下文环境。 + * @param position 位置。 + * @return 返回文件夹名称。 + */ + public String getFolderName(Context context, int position) { + Cursor cursor = (Cursor) getItem(position); // 获取对应位置的Cursor对象 + return (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context + .getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN); // 获取文件夹名称 + } + + /** + * FolderListItem 类是文件夹列表项的视图。 + */ + private class FolderListItem extends LinearLayout { + private TextView mName; // 用于显示文件夹名称的TextView + + /** + * FolderListItem 构造函数。 + * @param context 上下文环境。 + */ + public FolderListItem(Context context) { + super(context); // 调用父类的构造函数 + inflate(context, R.layout.folder_list_item, this); // 从XML布局文件中加载视图 + mName = (TextView) findViewById(R.id.tv_folder_name); // 获取TextView对象 + } + + /** + * 绑定文件夹名称数据到视图。 + * @param name 文件夹名称。 + */ + public void bind(String name) { + mName.setText(name); // 设置TextView的文本 + } + } + +} \ No newline at end of file diff --git a/src/ui(x)/NoteEditActivity.java b/src/ui(x)/NoteEditActivity.java new file mode 100644 index 0000000..707f613 --- /dev/null +++ b/src/ui(x)/NoteEditActivity.java @@ -0,0 +1,929 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.app.Activity; +import android.app.AlarmManager; +import android.app.AlertDialog; +import android.app.PendingIntent; +import android.app.SearchManager; +import android.appwidget.AppWidgetManager; +import android.content.ContentUris; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.Paint; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.TextUtils; +import android.text.format.DateUtils; +import android.text.style.BackgroundColorSpan; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.WindowManager; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.TextNote; +import net.micode.notes.model.WorkingNote; +import net.micode.notes.model.WorkingNote.NoteSettingChangedListener; +import net.micode.notes.tool.DataUtils; +import net.micode.notes.tool.ResourceParser; +import net.micode.notes.tool.ResourceParser.TextAppearanceResources; +import net.micode.notes.ui.DateTimePickerDialog.OnDateTimeSetListener; +import net.micode.notes.ui.NoteEditText.OnTextViewChangeListener; +import net.micode.notes.widget.NoteWidgetProvider_2x; +import net.micode.notes.widget.NoteWidgetProvider_4x; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +// NoteEditActivity类继承自Activity类,并实现了三个接口,用于处理点击事件、笔记设置变化监听和文本视图变化监听。 +public class NoteEditActivity extends Activity implements OnClickListener, + NoteSettingChangedListener, OnTextViewChangeListener { + + // 内部类HeadViewHolder,用于持有笔记头部的视图元素引用。 + private class HeadViewHolder { + public TextView tvModified; // 修改时间的文本视图 + public ImageView ivAlertIcon; // 警告图标的图像视图 + public TextView tvAlertDate; // 警告日期的文本视图 + public ImageView ibSetBgColor; // 设置背景颜色的图像视图 + } + + // 静态代码块,用于初始化背景选择器按钮与颜色资源ID的映射关系。 + private static final Map sBgSelectorBtnsMap = new HashMap(); + static { + sBgSelectorBtnsMap.put(R.id.iv_bg_yellow, ResourceParser.YELLOW); + sBgSelectorBtnsMap.put(R.id.iv_bg_red, ResourceParser.RED); + sBgSelectorBtnsMap.put(R.id.iv_bg_blue, ResourceParser.BLUE); + sBgSelectorBtnsMap.put(R.id.iv_bg_green, ResourceParser.GREEN); + sBgSelectorBtnsMap.put(R.id.iv_bg_white, ResourceParser.WHITE); + } + + // 静态代码块,用于初始化背景选择器选中状态与选中图标资源ID的映射关系。 + private static final Map sBgSelectorSelectionMap = new HashMap(); + static { + sBgSelectorSelectionMap.put(ResourceParser.YELLOW, R.id.iv_bg_yellow_select); + sBgSelectorSelectionMap.put(ResourceParser.RED, R.id.iv_bg_red_select); + sBgSelectorSelectionMap.put(ResourceParser.BLUE, R.id.iv_bg_blue_select); + sBgSelectorSelectionMap.put(ResourceParser.GREEN, R.id.iv_bg_green_select); + sBgSelectorSelectionMap.put(ResourceParser.WHITE, R.id.iv_bg_white_select); + } + + // 静态代码块,用于初始化字体大小选择器按钮与字体大小资源ID的映射关系。 + private static final Map sFontSizeBtnsMap = new HashMap(); + static { + sFontSizeBtnsMap.put(R.id.ll_font_large, ResourceParser.TEXT_LARGE); + sFontSizeBtnsMap.put(R.id.ll_font_small, ResourceParser.TEXT_SMALL); + sFontSizeBtnsMap.put(R.id.ll_font_normal, ResourceParser.TEXT_MEDIUM); + sFontSizeBtnsMap.put(R.id.ll_font_super, ResourceParser.TEXT_SUPER); + } + + // 静态代码块,用于初始化字体大小选择器选中状态与选中图标资源ID的映射关系。 + private static final Map sFontSelectorSelectionMap = new HashMap(); + static { + sFontSelectorSelectionMap.put(ResourceParser.TEXT_LARGE, R.id.iv_large_select); + sFontSelectorSelectionMap.put(ResourceParser.TEXT_SMALL, R.id.iv_small_select); + sFontSelectorSelectionMap.put(ResourceParser.TEXT_MEDIUM, R.id.iv_medium_select); + sFontSelectorSelectionMap.put(ResourceParser.TEXT_SUPER, R.id.iv_super_select); + } + + // 类的标签,用于日志记录。 + private static final String TAG = "NoteEditActivity"; + + // 成员变量,用于持有笔记头部视图的引用。 + private HeadViewHolder mNoteHeaderHolder; + private View mHeadViewPanel; // 笔记头部面板视图 + private View mNoteBgColorSelector; // 笔记背景颜色选择器视图 + private View mFontSizeSelector; // 字体大小选择器视图 + private EditText mNoteEditor; // 笔记编辑框 + private View mNoteEditorPanel; // 笔记编辑面板视图 + private WorkingNote mWorkingNote; // 当前正在编辑的笔记对象 + private SharedPreferences mSharedPrefs; // 共享偏好设置 + private int mFontSizeId; // 字体大小的ID + + // 用于存储用户偏好字体大小的键。 + private static final String PREFERENCE_FONT_SIZE = "pref_font_size"; + + // 快捷方式图标标题的最大长度限制。 + private static final int SHORTCUT_ICON_TITLE_MAX_LEN = 10; + + // 用于标记笔记项的选中和未选中状态的字符串。 + public static final String TAG_CHECKED = String.valueOf('\u221A'); + public static final String TAG_UNCHECKED = String.valueOf('\u25A1'); + + // 用于存储编辑文本列表的线性布局。 + private LinearLayout mEditTextList; + + // 用户查询的字符串和对应的模式对象。 + private String mUserQuery; + private Pattern mPattern; + + // onCreate方法,用于初始化Activity。 + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + this.setContentView(R.layout.note_edit); + + // 如果没有保存的状态或者初始化Activity状态失败,则结束Activity。 + if (savedInstanceState == null && !initActivityState(getIntent())) { + finish(); + return; + } + initResources(); + } + + // onRestoreInstanceState方法,用于在Activity从杀死状态恢复时恢复之前的状态。 + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + // 如果保存的状态中包含Intent的额外数据,则尝试恢复Activity状态。 + if (savedInstanceState != null && savedInstanceState.containsKey(Intent.EXTRA_UID)) { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.putExtra(Intent.EXTRA_UID, savedInstanceState.getLong(Intent.EXTRA_UID)); + if (!initActivityState(intent)) { + finish(); + return; + } + Log.d(TAG, "Restoring from killed activity"); + } + } + + // initActivityState方法,用于初始化Activity的状态。 + private boolean initActivityState(Intent intent) { + // 如果Intent动作是VIEW,但是没有提供ID,则跳转到笔记列表Activity。 + mWorkingNote = null; + if (TextUtils.equals(Intent.ACTION_VIEW, intent.getAction())) { + long noteId = intent.getLongExtra(Intent.EXTRA_UID, 0); + mUserQuery = ""; + + // 如果是从搜索结果开始的。 + if (intent.hasExtra(SearchManager.EXTRA_DATA_KEY)) { + noteId = Long.parseLong(intent.getStringExtra(SearchManager.EXTRA_DATA_KEY)); + mUserQuery = intent.getStringExtra(SearchManager.USER_QUERY); + } + + // 如果笔记ID在数据库中不可见,则跳转到笔记列表Activity并显示错误信息。 + if (!DataUtils.visibleInNoteDatabase(getContentResolver(), noteId, Notes.TYPE_NOTE)) { + Intent jump = new Intent(this, NotesListActivity.class); + startActivity(jump); + showToast(R.string.error_note_not_exist); + finish(); + return false; + } else { + // 加载笔记对象。 + mWorkingNote = WorkingNote.load(this, noteId); + if (mWorkingNote == null) { + Log.e(TAG, "load note failed with note id" + noteId); + finish(); + return false; + } + } + // 设置窗口的软键盘模式。 + getWindow().setSoftInputMode( + WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN + | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); + } else if(TextUtils.equals(Intent.ACTION_INSERT_OR_EDIT, intent.getAction())) { + // 新建笔记 + long folderId = intent.getLongExtra(Notes.INTENT_EXTRA_FOLDER_ID, 0); + int widgetId = intent.getIntExtra(Notes.INTENT_EXTRA_WIDGET_ID, + AppWidgetManager.INVALID_APPWIDGET_ID); + int widgetType = intent.getIntExtra(Notes.INTENT_EXTRA_WIDGET_TYPE, + Notes.TYPE_WIDGET_INVALIDE); + int bgResId = intent.getIntExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, + ResourceParser.getDefaultBgId(this)); + + // 解析通话记录笔记 + String phoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER); + long callDate = intent.getLongExtra(Notes.INTENT_EXTRA_CALL_DATE, 0); + if (callDate != 0 && phoneNumber != null) { + if (TextUtils.isEmpty(phoneNumber)) { + Log.w(TAG, "The call record number is null"); + } + long noteId = 0; + if ((noteId = DataUtils.getNoteIdByPhoneNumberAndCallDate(getContentResolver(), + phoneNumber, callDate)) > 0) { + mWorkingNote = WorkingNote.load(this, noteId); + if (mWorkingNote == null) { + Log.e(TAG, "load call note failed with note id" + noteId); + finish(); + return false; + } + } else { + mWorkingNote = WorkingNote.createEmptyNote(this, folderId, widgetId, + widgetType, bgResId); + mWorkingNote.convertToCallNote(phoneNumber, callDate); + } + } else { + mWorkingNote = WorkingNote.createEmptyNote(this, folderId, widgetId, widgetType, + bgResId); + } + + // 设置窗口的软键盘模式。 + getWindow().setSoftInputMode( + WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE + | WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); + } else { + Log.e(TAG, "Intent not specified action, should not support"); + finish(); + return false; + } + // 设置笔记设置状态变化监听器。 + mWorkingNote.setOnSettingStatusChangedListener(this); + return true; + } + + // 当Activity重新获得焦点时调用,用于初始化笔记屏幕。 +@Override +protected void onResume() { + super.onResume(); + initNoteScreen(); +} + +// 初始化笔记屏幕的方法。 +private void initNoteScreen() { + // 设置笔记编辑器的文本外观,根据字体大小ID来决定。 + mNoteEditor.setTextAppearance(this, TextAppearanceResources + .getTexAppearanceResource(mFontSizeId)); + // 如果笔记是 checklist 模式,则切换到列表模式。 + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + switchToListMode(mWorkingNote.getContent()); + } else { + // 高亮显示搜索结果,并设置光标到最后。 + mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery)); + mNoteEditor.setSelection(mNoteEditor.getText().length()); + } + // 隐藏所有背景选择器的选中状态。 + for (Integer id : sBgSelectorSelectionMap.keySet()) { + findViewById(sBgSelectorSelectionMap.get(id)).setVisibility(View.GONE); + } + // 设置头部和编辑面板的背景资源。 + mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId()); + mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId()); + + // 设置修改时间的文本。 + mNoteHeaderHolder.tvModified.setText(DateUtils.formatDateTime(this, + mWorkingNote.getModifiedDate(), DateUtils.FORMAT_SHOW_DATE + | DateUtils.FORMAT_NUMERIC_DATE | DateUtils.FORMAT_SHOW_TIME + | DateUtils.FORMAT_SHOW_YEAR)); + + // 显示或隐藏提醒头部。 + showAlertHeader(); +} + +// 显示或隐藏提醒头部的方法。 +private void showAlertHeader() { + // 如果笔记有闹钟提醒,根据提醒时间是否已过显示不同的文本。 + if (mWorkingNote.hasClockAlert()) { + long time = System.currentTimeMillis(); + if (time > mWorkingNote.getAlertDate()) { + mNoteHeaderHolder.tvAlertDate.setText(R.string.note_alert_expired); + } else { + mNoteHeaderHolder.tvAlertDate.setText(DateUtils.getRelativeTimeSpanString( + mWorkingNote.getAlertDate(), time, DateUtils.MINUTE_IN_MILLIS)); + } + mNoteHeaderHolder.tvAlertDate.setVisibility(View.VISIBLE); + mNoteHeaderHolder.ivAlertIcon.setVisibility(View.VISIBLE); + } else { + mNoteHeaderHolder.tvAlertDate.setVisibility(View.GONE); + mNoteHeaderHolder.ivAlertIcon.setVisibility(View.GONE); + } +} + +// 当接收到新的Intent时调用,用于重新初始化Activity状态。 +@Override +protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + initActivityState(intent); +} + +// 当Activity状态需要被保存时调用,用于保存当前笔记的状态。 +@Override +protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + // 如果笔记还没有保存,先保存笔记,以确保有ID可以保存状态。 + if (!mWorkingNote.existInDatabase()) { + saveNote(); + } + outState.putLong(Intent.EXTRA_UID, mWorkingNote.getNoteId()); + Log.d(TAG, "Save working note id: " + mWorkingNote.getNoteId() + " onSaveInstanceState"); +} + +// 处理触摸事件,用于隐藏背景和字体大小选择器。 +@Override +public boolean dispatchTouchEvent(MotionEvent ev) { + if (mNoteBgColorSelector.getVisibility() == View.VISIBLE + && !inRangeOfView(mNoteBgColorSelector, ev)) { + mNoteBgColorSelector.setVisibility(View.GONE); + return true; + } + + if (mFontSizeSelector.getVisibility() == View.VISIBLE + && !inRangeOfView(mFontSizeSelector, ev)) { + mFontSizeSelector.setVisibility(View.GONE); + return true; + } + return super.dispatchTouchEvent(ev); +} + +// 判断触摸事件是否在指定视图范围内的方法。 +private boolean inRangeOfView(View view, MotionEvent ev) { + int []location = new int[2]; + view.getLocationOnScreen(location); + int x = location[0]; + int y = location[1]; + if (ev.getX() < x + || ev.getX() > (x + view.getWidth()) + || ev.getY() < y + || ev.getY() > (y + view.getHeight())) { + return false; + } + return true; +} + +// 初始化资源的方法。 +private void initResources() { + // 初始化头部视图、头部持有者、编辑器、编辑器面板等资源。 + mHeadViewPanel = findViewById(R.id.note_title); + mNoteHeaderHolder = new HeadViewHolder(); + mNoteHeaderHolder.tvModified = (TextView) findViewById(R.id.tv_modified_date); + mNoteHeaderHolder.ivAlertIcon = (ImageView) findViewById(R.id.iv_alert_icon); + mNoteHeaderHolder.tvAlertDate = (TextView) findViewById(R.id.tv_alert_date); + mNoteHeaderHolder.ibSetBgColor = (ImageView) findViewById(R.id.btn_set_bg_color); + mNoteHeaderHolder.ibSetBgColor.setOnClickListener(this); + mNoteEditor = (EditText) findViewById(R.id.note_edit_view); + mNoteEditorPanel = findViewById(R.id.sv_note_edit); + mNoteBgColorSelector = findViewById(R.id.note_bg_color_selector); + for (int id : sBgSelectorBtnsMap.keySet()) { + ImageView iv = (ImageView) findViewById(id); + iv.setOnClickListener(this); + } + + mFontSizeSelector = findViewById(R.id.font_size_selector); + for (int id : sFontSizeBtnsMap.keySet()) { + View view = findViewById(id); + view.setOnClickListener(this); + }; + mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); + mFontSizeId = mSharedPrefs.getInt(PREFERENCE_FONT_SIZE, ResourceParser.BG_DEFAULT_FONT_SIZE); + // 修复存储资源ID在SharedPreferences中的错误。 + if(mFontSizeId >= TextAppearanceResources.getResourcesSize()) { + mFontSizeId = ResourceParser.BG_DEFAULT_FONT_SIZE; + } + mEditTextList = (LinearLayout) findViewById(R.id.note_edit_list); +} + +// 当Activity暂停时调用,用于保存笔记数据。 +@Override +protected void onPause() { + super.onPause(); + if(saveNote()) { + Log.d(TAG, "Note data was saved with length:" + mWorkingNote.getContent().length()); + } + clearSettingState(); +} + +// 更新小部件的方法。 +private void updateWidget() { + Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); + // 根据笔记的小部件类型发送更新广播。 + if (mWorkingNote.getWidgetType() == Notes.TYPE_WIDGET_2X) { + intent.setClass(this, NoteWidgetProvider_2x.class); + } else if (mWorkingNote.getWidgetType() == Notes.TYPE_WIDGET_4X) { + intent.setClass(this, NoteWidgetProvider_4x.class); + } else { + Log.e(TAG, "Unspported widget type"); + return; + } + + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] { + mWorkingNote.getWidgetId() + }); + + sendBroadcast(intent); + setResult(RESULT_OK, intent); +} + +// 处理点击事件的方法。 +public void onClick(View v) { + int id = v.getId(); + // 如果点击的是设置背景颜色按钮,则显示背景选择器。 + if (id == R.id.btn_set_bg_color) { + mNoteBgColorSelector.setVisibility(View.VISIBLE); + findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( + - View.VISIBLE); + } else if (sBgSelectorBtnsMap.containsKey(id)) { + // 如果点击的是背景颜色按钮,则更新笔记的背景颜色ID并隐藏选择器。 + findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( + View.GONE); + mWorkingNote.setBgColorId(sBgSelectorBtnsMap.get(id)); + mNoteBgColorSelector.setVisibility(View.GONE); + } else if (sFontSizeBtnsMap.containsKey(id)) { + // 如果点击的是字体大小按钮,则更新笔记的字体大小ID并应用新的字体大小。 + findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.GONE); + mFontSizeId = sFontSizeBtnsMap.get(id); + mSharedPrefs.edit().putInt(PREFERENCE_FONT_SIZE, mFontSizeId).commit(); + findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.VISIBLE); + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + getWorkingText(); + switchToListMode(mWorkingNote.getContent()); + } else { + mNoteEditor.setTextAppearance(this, + TextAppearanceResources.getTexAppearanceResource(mFontSizeId)); + } + mFontSizeSelector.setVisibility(View.GONE); + } +} +@Override +public void onBackPressed() { + // 当用户按下返回键时,首先检查是否有弹出的设置界面需要关闭。 + if(clearSettingState()) { + return; + } + + // 保存当前笔记的状态。 + saveNote(); + // 然后调用基类的onBackPressed方法,完成默认的返回操作。 + super.onBackPressed(); +} + +private boolean clearSettingState() { + // 检查背景颜色选择器是否可见,如果是,则隐藏它,并返回true表示状态已清除。 + if (mNoteBgColorSelector.getVisibility() == View.VISIBLE) { + mNoteBgColorSelector.setVisibility(View.GONE); + return true; + } else if (mFontSizeSelector.getVisibility() == View.VISIBLE) { + // 检查字体大小选择器是否可见,如果是,则隐藏它,并返回true表示状态已清除。 + mFontSizeSelector.setVisibility(View.GONE); + return true; + } + // 如果没有弹出的设置界面需要关闭,则返回false。 + return false; +} + +public void onBackgroundColorChanged() { + // 当背景颜色改变时,更新界面上背景颜色选择器的状态。 + findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( + View.VISIBLE); + // 设置笔记编辑区域的背景。 + mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId()); + // 设置笔记标题区域的背景。 + mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId()); +} + +@Override +public boolean onPrepareOptionsMenu(Menu menu) { + // 在准备选项菜单时,首先检查Activity是否正在结束,如果是,则不进行任何操作。 + if (isFinishing()) { + return true; + } + // 清除任何弹出的设置界面。 + clearSettingState(); + // 清除菜单中的所有项目。 + menu.clear(); + // 根据笔记所在的文件夹ID决定加载哪个菜单资源。 + if (mWorkingNote.getFolderId() == Notes.ID_CALL_RECORD_FOLDER) { + getMenuInflater().inflate(R.menu.call_note_edit, menu); + } else { + getMenuInflater().inflate(R.menu.note_edit, menu); + } + // 如果笔记处于列表模式,则更改“列表模式”菜单项的标题为“普通模式”。 + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + menu.findItem(R.id.menu_list_mode).setTitle(R.string.menu_normal_mode); + } else { + menu.findItem(R.id.menu_list_mode).setTitle(R.string.menu_list_mode); + } + // 如果笔记已经有闹钟提醒,则隐藏“设置提醒”菜单项。 + if (mWorkingNote.hasClockAlert()) { + menu.findItem(R.id.menu_alert).setVisible(false); + } else { + // 如果笔记没有闹钟提醒,则隐藏“删除提醒”菜单项。 + menu.findItem(R.id.menu_delete_remind).setVisible(false); + } + // 返回true表示菜单已经准备好。 + return true; +} + +@Override +public boolean onOptionsItemSelected(MenuItem item) { + // 根据选中的菜单项ID执行相应的操作。 + switch (item.getItemId()) { + case R.id.menu_new_note: + // 创建新笔记。 + createNewNote(); + break; + case R.id.menu_delete: + // 显示删除笔记的对话框。 + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.alert_title_delete)); + builder.setIcon(android.R.drawable.ic_dialog_alert); + builder.setMessage(getString(R.string.alert_message_delete_note)); + builder.setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + // 删除当前笔记,并结束Activity。 + deleteCurrentNote(); + finish(); + } + }); + builder.setNegativeButton(android.R.string.cancel, null); + builder.show(); + break; + case R.id.menu_font_size: + // 显示字体大小选择器。 + mFontSizeSelector.setVisibility(View.VISIBLE); + findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.VISIBLE); + break; + case R.id.menu_list_mode: + // 切换笔记的列表模式和普通模式。 + mWorkingNote.setCheckListMode(mWorkingNote.getCheckListMode() == 0 ? + TextNote.MODE_CHECK_LIST : 0); + break; + case R.id.menu_share: + // 获取当前笔记的内容并通过Intent分享。 + getWorkingText(); + sendTo(this, mWorkingNote.getContent()); + break; + case R.id.menu_send_to_desktop: + // 发送到桌面,具体实现未给出。 + sendToDesktop(); + break; + case R.id.menu_alert: + // 设置提醒。 + setReminder(); + break; + case R.id.menu_delete_remind: + // 删除闹钟提醒。 + mWorkingNote.setAlertDate(0, false); + break; + default: + // 默认不执行任何操作。 + break; + } + // 返回true表示菜单项点击事件已处理。 + return true; +} + +private void setReminder() { + // 创建一个日期时间选择器对话框。 + DateTimePickerDialog d = new DateTimePickerDialog(this, System.currentTimeMillis()); + d.setOnDateTimeSetListener(new OnDateTimeSetListener() { + public void OnDateTimeSet(AlertDialog dialog, long date) { + // 当用户设置了日期时间后,更新笔记的提醒时间。 + mWorkingNote.setAlertDate(date, true); + } + }); + // 显示日期时间选择器对话框。 + d.show(); +} + +/** + * Share note to apps that support {@link Intent#ACTION_SEND} action + * and {@text/plain} type + */ +private void sendTo(Context context, String info) { + // 创建一个Intent,用于分享文本。 + Intent intent = new Intent(Intent.ACTION_SEND); + intent.putExtra(Intent.EXTRA_TEXT, info); + intent.setType("text/plain"); + // 启动一个新的Activity,分享笔记内容。 + context.startActivity(intent); +} + +private void createNewNote() { + // 首先保存当前编辑的笔记。 + saveNote(); + + // 结束当前Activity,准备启动一个新的Activity。 + finish(); + Intent intent = new Intent(this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_INSERT_OR_EDIT); + intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mWorkingNote.getFolderId()); + // 启动一个新的NoteEditActivity,用于创建新笔记。 + startActivity(intent); +} + +private void deleteCurrentNote() { + // 如果笔记存在于数据库中,则执行删除操作。 + if (mWorkingNote.existInDatabase()) { + HashSet ids = new HashSet(); + long id = mWorkingNote.getNoteId(); + if (id != Notes.ID_ROOT_FOLDER) { + ids.add(id); + } else { + Log.d(TAG, "Wrong note id, should not happen"); + } + // 如果不是同步模式,则直接删除笔记。 + if (!isSyncMode()) { + if (!DataUtils.batchDeleteNotes(getContentResolver(), ids)) { + Log.e(TAG, "Delete Note error"); + } + } else { + // 如果是同步模式,则将笔记移动到垃圾文件夹。 + if (!DataUtils.batchMoveToFolder(getContentResolver(), ids, Notes.ID_TRASH_FOLER)) { + Log.e(TAG, "Move notes to trash folder error, should not happens"); + } + } + } + // 标记笔记为已删除。 + mWorkingNote.markDeleted(true); +} + +private boolean isSyncMode() { + // 检查是否有同步账户名被设置,如果有,则返回true表示是同步模式。 + return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0; +} + +public void onClockAlertChanged(long date, boolean set) { + /** + * User could set clock to an unsaved note, so before setting the + * alert clock, we should save the note first + */ + // 如果用户为未保存的笔记设置了闹钟提醒,在设置闹钟提醒之前,应该先保存笔记。 + if (!mWorkingNote.existInDatabase()) { + saveNote(); + } + // 如果笔记的ID大于0,说明笔记已经保存在数据库中。 + if (mWorkingNote.getNoteId() > 0) { + Intent intent = new Intent(this, AlarmReceiver.class); + intent.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mWorkingNote.getNoteId())); + PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0); + AlarmManager alarmManager = ((AlarmManager) getSystemService(ALARM_SERVICE)); + showAlertHeader(); + if(!set) { + // 如果set为false,则取消闹钟。 + alarmManager.cancel(pendingIntent); + } else { + // 如果set为true,则设置闹钟。 + alarmManager.set(AlarmManager.RTC_WAKEUP, date, pendingIntent); + } + } else { + // 如果笔记ID不大于0,说明笔记未保存,记录错误日志,并提示用户输入内容。 + Log.e(TAG, "Clock alert setting error"); + showToast(R.string.error_note_empty_for_clock); + } +} +// 当小部件数据发生变化时调用此方法。 +public void onWidgetChanged() { + updateWidget(); // 更新小部件以反映数据的变化。 +} + +// 当编辑框中的文本被删除时调用此方法。 +public void onEditTextDelete(int index, String text) { + int childCount = mEditTextList.getChildCount(); // 获取编辑列表中的子项数量。 + if (childCount == 1) { + return; // 如果只有一个子项,不允许删除。 + } + + // 遍历从删除位置后的子项,更新它们的索引。 + for (int i = index + 1; i < childCount; i++) { + ((NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text)) + .setIndex(i - 1); + } + + // 从编辑列表中移除指定索引的子项。 + mEditTextList.removeViewAt(index); + NoteEditText edit = null; + // 如果删除的是第一项,则获取新的第一项;否则,获取前一项。 + if(index == 0) { + edit = (NoteEditText) mEditTextList.getChildAt(0).findViewById( + R.id.et_edit_text); + } else { + edit = (NoteEditText) mEditTextList.getChildAt(index - 1).findViewById( + R.id.et_edit_text); + } + int length = edit.length(); // 获取当前编辑项的文本长度。 + edit.append(text); // 将被删除的文本追加到当前编辑项。 + edit.requestFocus(); // 请求焦点到当前编辑项。 + edit.setSelection(length); // 将光标设置到追加文本的末尾。 +} + +// 当编辑框中按下回车键时调用此方法。 +public void onEditTextEnter(int index, String text) { + // 调试检查,确保索引没有超出编辑列表的范围。 + if(index > mEditTextList.getChildCount()) { + Log.e(TAG, "Index out of mEditTextList boundrary, should not happen"); + } + + // 创建一个新的列表项,并设置其文本和索引。 + View view = getListItem(text, index); + mEditTextList.addView(view, index); // 将新项添加到编辑列表中。 + NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text); + edit.requestFocus(); // 请求焦点到新项。 + edit.setSelection(0); // 将光标设置到新项的开头。 + // 更新新项之后所有子项的索引。 + for (int i = index + 1; i < mEditTextList.getChildCount(); i++) { + ((NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text)) + .setIndex(i); + } +} + +// 切换到列表模式,将文本分割成多个列表项。 +private void switchToListMode(String text) { + mEditTextList.removeAllViews(); // 移除编辑列表中的所有子项。 + String[] items = text.split("\n"); // 按行分割文本。 + int index = 0; // 初始化索引。 + for (String item : items) { + if(!TextUtils.isEmpty(item)) { // 如果行不为空,则添加到编辑列表中。 + mEditTextList.addView(getListItem(item, index)); + index++; + } + } + mEditTextList.addView(getListItem("", index)); // 添加一个新的空项。 + mEditTextList.getChildAt(index).findViewById(R.id.et_edit_text).requestFocus(); // 请求焦点到新项。 + + mNoteEditor.setVisibility(View.GONE); // 隐藏文本编辑框。 + mEditTextList.setVisibility(View.VISIBLE); // 显示编辑列表。 +} + +// 获取高亮显示搜索结果的Spannable对象。 +private Spannable getHighlightQueryResult(String fullText, String userQuery) { + SpannableString spannable = new SpannableString(fullText == null ? "" : fullText); + if (!TextUtils.isEmpty(userQuery)) { + mPattern = Pattern.compile(userQuery); // 编译搜索关键字为正则表达式。 + Matcher m = mPattern.matcher(fullText); // 在全文中匹配正则表达式。 + int start = 0; + while (m.find(start)) { // 找到匹配项。 + spannable.setSpan( // 设置背景颜色为高亮显示。 + new BackgroundColorSpan(this.getResources().getColor( + R.color.user_query_highlight)), m.start(), m.end(), + Spannable.SPAN_INCLUSIVE_EXCLUSIVE); + start = m.end(); // 更新匹配起始位置。 + } + } + return spannable; // 返回高亮显示的Spannable对象。 +} + +// 获取列表项的视图,并设置其文本和样式。 +private View getListItem(String item, int index) { + View view = LayoutInflater.from(this).inflate(R.layout.note_edit_list_item, null); + final NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text); + edit.setTextAppearance(this, TextAppearanceResources.getTexAppearanceResource(mFontSizeId)); + CheckBox cb = ((CheckBox) view.findViewById(R.id.cb_edit_item)); + cb.setOnCheckedChangeListener(new OnCheckedChangeListener() { + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (isChecked) { + edit.setPaintFlags(edit.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); // 勾选时设置删除线。 + } else { + edit.setPaintFlags(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG); // 未勾选时取消删除线。 + } + } + }); + + // 根据标记判断列表项是否被勾选,并设置相应的文本和样式。 + if (item.startsWith(TAG_CHECKED)) { + cb.setChecked(true); + edit.setPaintFlags(edit.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); + item = item.substring(TAG_CHECKED.length(), item.length()).trim(); + } else if (item.startsWith(TAG_UNCHECKED)) { + cb.setChecked(false); + edit.setPaintFlags(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG); + item = item.substring(TAG_UNCHECKED.length(), item.length()).trim(); + } + + edit.setOnTextViewChangeListener(this); // 设置文本变化监听器。 + edit.setIndex(index); // 设置索引。 + edit.setText(getHighlightQueryResult(item, mUserQuery)); // 设置高亮显示的文本。 + return view; // 返回列表项视图。 +} + +// 文本变化时调用此方法,用于更新复选框的显示状态。 +public void onTextChange(int index, boolean hasText) { + if (index >= mEditTextList.getChildCount()) { + Log.e(TAG, "Wrong index, should not happen"); + return; + } + if(hasText) { + mEditTextList.getChildAt(index).findViewById(R.id.cb_edit_item).setVisibility(View.VISIBLE); + } else { + mEditTextList.getChildAt(index).findViewById(R.id.cb_edit_item).setVisibility(View.GONE); + } +} + +// 列表模式改变时调用此方法,用于在列表模式和编辑模式之间切换。 +public void onCheckListModeChanged(int oldMode, int newMode) { + if (newMode == TextNote.MODE_CHECK_LIST) { + switchToListMode(mNoteEditor.getText().toString()); // 切换到列表模式。 + } else { + if (!getWorkingText()) { + mWorkingNote.setWorkingText(mWorkingNote.getContent().replace(TAG_UNCHECKED + " ", + "")); // 移除未勾选项的标记。 + } + mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery)); // 设置高亮显示的文本。 + mEditTextList.setVisibility(View.GONE); // 隐藏编辑列表。 + mNoteEditor.setVisibility(View.VISIBLE); // 显示文本编辑框。 + } +} + +// 获取工作文本的方法,用于从列表项中获取文本并保存。 +private boolean getWorkingText() { + boolean hasChecked = false; + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < mEditTextList.getChildCount(); i++) { + View view = mEditTextList.getChildAt(i); + NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text); + if (!TextUtils.isEmpty(edit.getText())) { + if (((CheckBox) view.findViewById(R.id.cb_edit_item)).isChecked()) { + sb.append(TAG_CHECKED).append(" ").append(edit.getText()).append("\n"); + hasChecked = true; + } else { + sb.append(TAG_UNCHECKED).append(" ").append(edit.getText()).append("\n"); + } + } + } + mWorkingNote.setWorkingText(sb.toString()); // 设置工作文本。 + } else { + mWorkingNote.setWorkingText(mNoteEditor.getText().toString()); // 设置工作文本。 + } + return hasChecked; // 返回是否有勾选项。 +} + +// 保存笔记的方法,用于保存当前笔记的状态。 +private boolean saveNote() { + getWorkingText(); // 获取工作文本。 + boolean saved = mWorkingNote.saveNote(); // 保存笔记。 + if (saved) { + // 如果保存成功,设置结果码为RESULT_OK,表示笔记已创建或编辑。 + setResult(RESULT_OK); + } + return saved; // 返回保存是否成功。 +} + +// 发送到桌面的方法,用于创建桌面快捷方式。 +private void sendToDesktop() { + // 在发送消息到桌面之前,确保当前编辑的笔记存在于数据库中。对于新笔记,首先需要保存它。 + if (!mWorkingNote.existInDatabase()) { + saveNote(); + } + + if (mWorkingNote.getNoteId() > 0) { + Intent sender = new Intent(); // 创建一个Intent用于发送快捷方式。 + Intent shortcutIntent = new Intent(this, NoteEditActivity.class); // 创建一个Intent用于打开笔记。 + shortcutIntent.setAction(Intent.ACTION_VIEW); // 设置动作为查看。 + shortcutIntent.putExtra(Intent.EXTRA_UID, mWorkingNote.getNoteId()); // 传递笔记ID。 + sender.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent); // 设置快捷方式的Intent。 + sender.putExtra(Intent.EXTRA_SHORTCUT_NAME, makeShortcutIconTitle(mWorkingNote.getContent())); // 设置快捷方式的名称。 + sender.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, Intent.ShortcutIconResource.fromContext(this, R.drawable.icon_app)); // 设置快捷方式的图标。 + sender.putExtra("duplicate", true); // 设置快捷方式的重复标志。 + sender.setAction("com.android.launcher.action.INSTALL_SHORTCUT"); // 设置动作为安装快捷方式。 + showToast(R.string.info_note_enter_desktop); // 显示提示。 + sendBroadcast(sender); // 发送广播以创建快捷方式。 + } else { + // 如果用户没有输入任何内容(笔记不值得保存),则没有笔记ID,提醒用户应该输入一些内容。 + Log.e(TAG, "Send to desktop error"); + showToast(R.string.error_note_empty_for_send_to_desktop); + } + } + + // 创建快捷方式图标标题的方法,用于生成快捷方式的标题。 + private String makeShortcutIconTitle(String content) { + content = content.replace(TAG_CHECKED, ""); + content = content.replace(TAG_UNCHECKED, ""); + return content.length() > SHORTCUT_ICON_TITLE_MAX_LEN ? content.substring(0, + SHORTCUT_ICON_TITLE_MAX_LEN) : content; + } + + // 显示短提示的方法,用于显示指定资源ID的提示。 + private void showToast(int resId) { + showToast(resId, Toast.LENGTH_SHORT); // 调用另一个重载方法,设置提示的持续时间为短。 + } + + // 显示提示的方法,用于显示指定资源ID和持续时间的提示。 + private void showToast(int resId, int duration) { + Toast.makeText(this, resId, duration).show();// 创建并显示提示。 + } +} diff --git a/src/widget/NoteWidgetProvider.java b/src/widget/NoteWidgetProvider.java new file mode 100644 index 0000000..a39041a --- /dev/null +++ b/src/widget/NoteWidgetProvider.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.widget; +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.util.Log; +import android.widget.RemoteViews; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.tool.ResourceParser; +import net.micode.notes.ui.NoteEditActivity; +import net.micode.notes.ui.NotesListActivity; + +// 继承自AppWidgetProvider,用于管理Note小部件的抽象类 +public abstract class NoteWidgetProvider extends AppWidgetProvider { + // 定义查询数据库时需要返回的列 + public static final String [] PROJECTION = new String [] { + NoteColumns.ID, // 笔记ID + NoteColumns.BG_COLOR_ID, // 笔记背景颜色ID + NoteColumns.SNIPPET // 笔记摘要 + }; + + // 定义PROJECTION数组中各列的索引,方便后续操作 + public static final int COLUMN_ID = 0; + public static final int COLUMN_BG_COLOR_ID = 1; + public static final int COLUMN_SNIPPET = 2; + + // 用于日志输出的标签 + private static final String TAG = "NoteWidgetProvider"; + + // 当小部件被删除时,更新数据库中对应的笔记,将WIDGET_ID设置为无效 + @Override + public void onDeleted(Context context, int[] appWidgetIds) { + // 创建ContentValues对象,用于更新数据库 + ContentValues values = new ContentValues(); + values.put(NoteColumns.WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); // 设置WIDGET_ID为无效值 + // 遍历所有被删除的小部件ID + for (int i = 0; i < appWidgetIds.length; i++) { + // 更新数据库中对应小部件ID的笔记,将WIDGET_ID设置为无效 + context.getContentResolver().update(Notes.CONTENT_NOTE_URI, + values, + NoteColumns.WIDGET_ID + "=?", + new String[] { String.valueOf(appWidgetIds[i])}); + } + } + + // 获取与小部件相关的笔记信息 + private Cursor getNoteWidgetInfo(Context context, int widgetId) { + // 查询数据库,获取与小部件ID相关的笔记信息 + return context.getContentResolver().query(Notes.CONTENT_NOTE_URI, + PROJECTION, + NoteColumns.WIDGET_ID + "=? AND " + NoteColumns.PARENT_ID + "<>?", + new String[] { String.valueOf(widgetId), String.valueOf(Notes.ID_TRASH_FOLER) }, + null); + } + + // 更新小部件的显示内容,该方法被外部调用 + protected void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + update(context, appWidgetManager, appWidgetIds, false); // 默认不开启隐私模式 + } + + // 更新小部件的显示内容,考虑隐私模式 + private void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds, + boolean privacyMode) { + // 遍历所有小部件ID + for (int i = 0; i < appWidgetIds.length; i++) { + // 如果小部件ID有效 + if (appWidgetIds[i] != AppWidgetManager.INVALID_APPWIDGET_ID) { + // 获取默认背景ID + int bgId = ResourceParser.getDefaultBgId(context); + // 初始化笔记摘要为空字符串 + String snippet = ""; + // 创建意图,用于启动NoteEditActivity + Intent intent = new Intent(context, NoteEditActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); // 设置意图标志,确保Activity单实例 + intent.putExtra(Notes.INTENT_EXTRA_WIDGET_ID, appWidgetIds[i]); // 传递小部件ID + intent.putExtra(Notes.INTENT_EXTRA_WIDGET_TYPE, getWidgetType()); // 传递小部件类型 + + // 获取与小部件相关的笔记信息 + Cursor c = getNoteWidgetInfo(context, appWidgetIds[i]); + if (c != null && c.moveToFirst()) { + // 如果查询结果有多条,记录错误日志并返回 + if (c.getCount() > 1) { + Log.e(TAG, "Multiple message with same widget id:" + appWidgetIds[i]); + c.close(); + return; + } + // 获取笔记摘要和背景ID + snippet = c.getString(COLUMN_SNIPPET); + bgId = c.getInt(COLUMN_BG_COLOR_ID); + intent.putExtra(Intent.EXTRA_UID, c.getLong(COLUMN_ID)); + intent.setAction(Intent.ACTION_VIEW); // 设置意图动作为查看 + } else { + // 如果没有查询到笔记信息,设置默认文本和意图动作 + snippet = context.getResources().getString(R.string.widget_havenot_content); + intent.setAction(Intent.ACTION_INSERT_OR_EDIT); + } + + // 关闭游标 + if (c != null) { + c.close(); + } + + // 创建RemoteViews对象,用于更新小部件视图 + RemoteViews rv = new RemoteViews(context.getPackageName(), getLayoutId()); + // 设置背景图片资源 + rv.setImageViewResource(R.id.widget_bg_image, getBgResourceId(bgId)); + intent.putExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, bgId); + + // 生成PendingIntent,用于启动宿主Activity + PendingIntent pendingIntent = null; + if (privacyMode) { + // 隐私模式下,显示默认文本,并启动笔记列表Activity + rv.setTextViewText(R.id.widget_text, + context.getString(R.string.widget_under_visit_mode)); + pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], new Intent( + context, NotesListActivity.class), PendingIntent.FLAG_UPDATE_CURRENT); + } else { + // 非隐私模式下,显示笔记摘要,并启动编辑笔记Activity + rv.setTextViewText(R.id.widget_text, snippet); + pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], intent, + PendingIntent.FLAG_UPDATE_CURRENT); + } + + // 设置点击事件,点击小部件时启动对应的Activity + rv.setOnClickPendingIntent(R.id.widget_text, pendingIntent); + // 更新小部件视图 + appWidgetManager.updateAppWidget(appWidgetIds[i], rv); + } + } + } + + // 获取背景图片资源ID的方法,需要子类实现 + protected abstract int getBgResourceId(int bgId); + + // 获取小部件布局资源ID的方法,需要子类实现 + protected abstract int getLayoutId(); + + // 获取小部件类型的方法,需要子类实现 + protected abstract int getWidgetType(); +} \ No newline at end of file diff --git a/src/widget/NoteWidgetProvider_2x.java b/src/widget/NoteWidgetProvider_2x.java new file mode 100644 index 0000000..db93365 --- /dev/null +++ b/src/widget/NoteWidgetProvider_2x.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.widget; + +import android.appwidget.AppWidgetManager; +import android.content.Context; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.tool.ResourceParser; + +// NoteWidgetProvider_2x类继承自NoteWidgetProvider类,专门用于实现2x2网格大小的笔记小部件(Note Widget)。 +// 这个类提供了小部件的更新逻辑,以及如何根据笔记数据渲染小部件的UI。 +public class NoteWidgetProvider_2x extends NoteWidgetProvider { + + // 当AppWidgetManager需要更新小部件时,会调用此方法。 + // 这是AppWidgetProvider生命周期中的一个重要方法,用于响应小部件的更新请求。 + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + // 调用父类(NoteWidgetProvider)的update方法,传入必要的参数,以更新小部件的UI。 + // 这里使用super关键字来访问父类的update方法,确保父类的逻辑被正确执行。 + super.update(context, appWidgetManager, appWidgetIds); + } + + // getLayoutId方法被用来获取小部件的布局资源ID。 + // 每个小部件都有自己的布局文件,这里返回的是2x2网格小部件的布局资源ID。 + @Override + protected int getLayoutId() { + // 返回R.layout.widget_2x,这是2x2网格小部件对应的布局资源ID。 + return R.layout.widget_2x; + } + + // getBgResourceId方法根据传入的背景ID(bgId),返回对应的背景资源ID。 + // 这个方法使用ResourceParser工具类中的WidgetBgResources来获取正确的背景资源ID。 + @Override + protected int getBgResourceId(int bgId) { + // 调用ResourceParser.WidgetBgResources的getWidget2xBgResource方法,传入bgId。 + // 这个方法负责根据bgId返回2x2网格小部件的背景资源ID。 + return ResourceParser.WidgetBgResources.getWidget2xBgResource(bgId); + } + + // getWidgetType方法返回当前小部件的类型。 + // 小部件类型用于区分不同的小部件,例如,不同的网格大小可能会有不同的类型。 + @Override + protected int getWidgetType() { + // 返回Notes.TYPE_WIDGET_2X,这是一个常量,表示当前小部件是2x2网格大小的笔记小部件。 + return Notes.TYPE_WIDGET_2X; + } +} \ No newline at end of file diff --git a/src/widget/NoteWidgetProvider_4x.java b/src/widget/NoteWidgetProvider_4x.java new file mode 100644 index 0000000..d325630 --- /dev/null +++ b/src/widget/NoteWidgetProvider_4x.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.widget; + +import android.appwidget.AppWidgetManager; +import android.content.Context; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.tool.ResourceParser; + +// NoteWidgetProvider_4x类继承自NoteWidgetProvider类,专门用于实现4x4网格大小的Note小部件功能。 +public class NoteWidgetProvider_4x extends NoteWidgetProvider { + + // 当AppWidgetManager需要更新小部件时,会调用此方法。 + // 这是AppWidgetProvider生命周期中的一个重要方法,用于响应小部件的更新请求。 + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + // 调用父类(NoteWidgetProvider)的update方法,传入必要的参数,以更新小部件的UI。 + // 这里使用super关键字来访问父类的update方法,确保父类的逻辑被正确执行。 + super.update(context, appWidgetManager, appWidgetIds); + } + + // getLayoutId方法被用来获取小部件的布局资源ID。 + // 每个小部件都有自己的布局文件,这里返回的是4x4网格小部件的布局资源ID。 + // 注意:该方法没有@Override注解,这是一个遗漏,因为方法体与父类中的方法签名相同, + // 应该加上@Override注解以明确表示这是一个重写的方法。 + protected int getLayoutId() { + // 返回R.layout.widget_4x,这是4x4网格小部件对应的布局资源ID。 + return R.layout.widget_4x; + } + + // getBgResourceId方法根据传入的背景ID(bgId),返回对应的背景资源ID。 + // 这个方法使用ResourceParser工具类中的WidgetBgResources来获取正确的背景资源ID。 + @Override + protected int getBgResourceId(int bgId) { + // 调用ResourceParser.WidgetBgResources的getWidget4xBgResource方法,传入bgId。 + // 这个方法负责根据bgId返回4x4网格小部件的背景资源ID。 + return ResourceParser.WidgetBgResources.getWidget4xBgResource(bgId); + } + + // getWidgetType方法返回当前小部件的类型。 + // 小部件类型用于区分不同的小部件,例如,不同的网格大小可能会有不同的类型。 + @Override + protected int getWidgetType() { + // 返回Notes.TYPE_WIDGET_4X,这是一个常量,表示当前小部件是4x4网格大小的笔记小部件。 + return Notes.TYPE_WIDGET_4X; + } +} \ No newline at end of file diff --git a/src/各类及主要作用说明.docx b/src/各类及主要作用说明.docx new file mode 100644 index 0000000..37b30a1 Binary files /dev/null and b/src/各类及主要作用说明.docx differ