From f0d70ab2155a030a1dc6438766758627ef5486a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E9=87=8D=E9=98=B3?= Date: Sun, 6 Apr 2025 19:55:22 +0800 Subject: [PATCH] =?UTF-8?q?liuchongyang=5Fbranch=E7=B2=BE=E8=AF=BB?= =?UTF-8?q?=E6=8A=A5=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/精读报告.txt | 2121 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2121 insertions(+) create mode 100644 doc/精读报告.txt diff --git a/doc/精读报告.txt b/doc/精读报告.txt new file mode 100644 index 0000000..6999239 --- /dev/null +++ b/doc/精读报告.txt @@ -0,0 +1,2121 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + package net.micode.notes.ui; + + import android.app.Activity; + import android.app.AlertDialog; + import android.app.Dialog; + import android.appwidget.AppWidgetManager; + import android.content.AsyncQueryHandler; + import android.content.ContentResolver; + import android.content.ContentValues; + import android.content.Context; + import android.content.DialogInterface; + import android.content.Intent; + import android.content.SharedPreferences; + import android.database.Cursor; + import android.os.AsyncTask; + import android.os.Bundle; + import android.preference.PreferenceManager; + import android.text.Editable; + import android.text.TextUtils; + import android.text.TextWatcher; + import android.util.Log; + import android.view.ActionMode; + import android.view.ContextMenu; + import android.view.ContextMenu.ContextMenuInfo; + import android.view.Display; + import android.view.HapticFeedbackConstants; + import android.view.LayoutInflater; + import android.view.Menu; + import android.view.MenuItem; + import android.view.MenuItem.OnMenuItemClickListener; + import android.view.MotionEvent; + import android.view.View; + import android.view.View.OnClickListener; + import android.view.View.OnCreateContextMenuListener; + import android.view.View.OnTouchListener; + import android.view.inputmethod.InputMethodManager; + import android.widget.AdapterView; + import android.widget.AdapterView.OnItemClickListener; + import android.widget.AdapterView.OnItemLongClickListener; + import android.widget.Button; + import android.widget.EditText; + import android.widget.ListView; + import android.widget.PopupMenu; + import android.widget.TextView; + import android.widget.Toast; + + import net.micode.notes.R; + import net.micode.notes.data.Notes; + import net.micode.notes.data.Notes.NoteColumns; + import net.micode.notes.gtask.remote.GTaskSyncService; + import net.micode.notes.model.WorkingNote; + import net.micode.notes.tool.BackupUtils; + import net.micode.notes.tool.DataUtils; + import net.micode.notes.tool.ResourceParser; + import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute; + import net.micode.notes.widget.NoteWidgetProvider_2x; + import net.micode.notes.widget.NoteWidgetProvider_4x; + + import java.io.BufferedReader; + import java.io.IOException; + import java.io.InputStream; + import java.io.InputStreamReader; + import java.util.HashSet; + + public class NotesListActivity extends Activity implements OnClickListener, OnItemLongClickListener { + private static final int FOLDER_NOTE_LIST_QUERY_TOKEN = 0; + + private static final int FOLDER_LIST_QUERY_TOKEN = 1; + + private static final int MENU_FOLDER_DELETE = 0; + + private static final int MENU_FOLDER_VIEW = 1; + + private static final int MENU_FOLDER_CHANGE_NAME = 2; + + private static final String PREFERENCE_ADD_INTRODUCTION = "net.micode.notes.introduction"; + + private enum ListEditState { + NOTE_LIST, SUB_FOLDER, CALL_RECORD_FOLDER + }; + + private ListEditState mState; + + private BackgroundQueryHandler mBackgroundQueryHandler; + + private NotesListAdapter mNotesListAdapter; + + private ListView mNotesListView; + + private Button mAddNewNote; + + private boolean mDispatch; + + private int mOriginY; + + private int mDispatchY; + + private TextView mTitleBar; + + private long mCurrentFolderId; + + private ContentResolver mContentResolver; + + private ModeCallback mModeCallBack; + + private static final String TAG = "NotesListActivity"; + + public static final int NOTES_LISTVIEW_SCROLL_RATE = 30; + + private NoteItemData mFocusNoteDataItem; + + private static final String NORMAL_SELECTION = NoteColumns.PARENT_ID + "=?"; + + private static final String ROOT_FOLDER_SELECTION = "(" + NoteColumns.TYPE + "<>" + + Notes.TYPE_SYSTEM + " AND " + NoteColumns.PARENT_ID + "=?)" + " OR (" + + NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER + " AND " + + NoteColumns.NOTES_COUNT + ">0)"; + + private final static int REQUEST_CODE_OPEN_NODE = 102; + private final static int REQUEST_CODE_NEW_NODE = 103; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.note_list); + initResources(); + + /** + * Insert an introduction when user firstly use this application + */ + setAppInfoFromRawRes(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode == RESULT_OK + && (requestCode == REQUEST_CODE_OPEN_NODE || requestCode == REQUEST_CODE_NEW_NODE)) { + mNotesListAdapter.changeCursor(null); + } else { + super.onActivityResult(requestCode, resultCode, data); + } + } + + private void setAppInfoFromRawRes() { + // 从SharedPreferences中获取默认的Preference + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); + // 如果没有添加过介绍 + if (!sp.getBoolean(PREFERENCE_ADD_INTRODUCTION, false)) { + // 创建一个StringBuilder对象 + StringBuilder sb = new StringBuilder(); + // 创建一个InputStream对象 + InputStream in = null; + try { + // 打开raw资源中的introduction文件 + in = getResources().openRawResource(R.raw.introduction); + // 如果文件不为空 + if (in != null) { + // 创建一个InputStreamReader对象 + InputStreamReader isr = new InputStreamReader(in); + // 创建一个BufferedReader对象 + BufferedReader br = new BufferedReader(isr); + // 创建一个字符数组 + char [] buf = new char[1024]; + // 读取文件的长度 + int len = 0; + // 循环读取文件内容 + while ((len = br.read(buf)) > 0) { + // 将读取的内容添加到StringBuilder中 + sb.append(buf, 0, len); + } + } else { + // 如果文件为空,打印错误日志 + Log.e(TAG, "Read introduction file error"); + return; + } + } catch (IOException e) { + // 打印异常信息 + e.printStackTrace(); + return; + } finally { + // 如果InputStream不为空 + if(in != null) { + try { + // 关闭InputStream + 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; + } + } + } + + // 重写onStart方法 + @Override + protected void onStart() { + // 调用父类的onStart方法 + super.onStart(); + // 启动异步笔记列表查询 + startAsyncNotesListQuery(); + } + + private void initResources() { + mContentResolver = this.getContentResolver(); + mBackgroundQueryHandler = new BackgroundQueryHandler(this.getContentResolver()); + mCurrentFolderId = Notes.ID_ROOT_FOLDER; + mNotesListView = (ListView) findViewById(R.id.notes_list); + mNotesListView.addFooterView(LayoutInflater.from(this).inflate(R.layout.note_list_footer, null), + null, false); + mNotesListView.setOnItemClickListener(new OnListItemClickListener()); + mNotesListView.setOnItemLongClickListener(this); + mNotesListAdapter = new NotesListAdapter(this); + mNotesListView.setAdapter(mNotesListAdapter); + mAddNewNote = (Button) findViewById(R.id.btn_new_note); + mAddNewNote.setOnClickListener(this); + mAddNewNote.setOnTouchListener(new NewNoteOnTouchListener()); + mDispatch = false; + mDispatchY = 0; + mOriginY = 0; + mTitleBar = (TextView) findViewById(R.id.tv_title_bar); + mState = ListEditState.NOTE_LIST; + mModeCallBack = new ModeCallback(); + } + + private class ModeCallback implements ListView.MultiChoiceModeListener, OnMenuItemClickListener { + private DropdownMenu mDropDownMenu; + private ActionMode mActionMode; + private MenuItem mMoveMenu; + + // 创建ActionMode时调用 + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + // 将note_list_options菜单填充到menu中 + getMenuInflater().inflate(R.menu.note_list_options, menu); + // 设置delete菜单项的点击事件为当前Activity + menu.findItem(R.id.delete).setOnMenuItemClickListener(this); + // 获取move菜单项 + mMoveMenu = menu.findItem(R.id.move); + // 如果当前焦点笔记的父文件夹是通话记录文件夹,或者用户文件夹数量为0,则隐藏move菜单项 + if (mFocusNoteDataItem.getParentId() == Notes.ID_CALL_RECORD_FOLDER + || DataUtils.getUserFolderCount(mContentResolver) == 0) { + mMoveMenu.setVisible(false); + } else { + // 否则显示move菜单项,并设置点击事件为当前Activity + mMoveMenu.setVisible(true); + mMoveMenu.setOnMenuItemClickListener(this); + } + // 设置ActionMode + mActionMode = mode; + // 设置NotesListAdapter为多选模式 + mNotesListAdapter.setChoiceMode(true); + // 设置NotesListView不可长按 + mNotesListView.setLongClickable(false); + // 隐藏添加新笔记按钮 + mAddNewNote.setVisibility(View.GONE); + + // 创建自定义视图 + View customView = LayoutInflater.from(NotesListActivity.this).inflate( + R.layout.note_list_dropdown_menu, null); + // 设置ActionMode的自定义视图 + mode.setCustomView(customView); + // 创建DropdownMenu + mDropDownMenu = new DropdownMenu(NotesListActivity.this, + (Button) customView.findViewById(R.id.selection_menu), + R.menu.note_list_dropdown); + // 设置DropdownMenu的点击事件 + mDropDownMenu.setOnDropdownMenuItemClickListener(new PopupMenu.OnMenuItemClickListener(){ + public boolean onMenuItemClick(MenuItem item) { + // 全选或取消全选 + mNotesListAdapter.selectAll(!mNotesListAdapter.isAllSelected()); + // 更新菜单 + updateMenu(); + return true; + } + + }); + return true; + } + + private void updateMenu() { + int selectedCount = mNotesListAdapter.getSelectedCount(); + // Update dropdown menu + String format = getResources().getString(R.string.menu_select_title, selectedCount); + mDropDownMenu.setTitle(format); + MenuItem item = mDropDownMenu.findItem(R.id.action_select_all); + if (item != null) { + if (mNotesListAdapter.isAllSelected()) { + item.setChecked(true); + item.setTitle(R.string.menu_deselect_all); + } else { + item.setChecked(false); + item.setTitle(R.string.menu_select_all); + } + } + } + + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + // TODO Auto-generated method stub + return false; + } + + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + // TODO Auto-generated method stub + return false; + } + + public void onDestroyActionMode(ActionMode mode) { + mNotesListAdapter.setChoiceMode(false); + mNotesListView.setLongClickable(true); + mAddNewNote.setVisibility(View.VISIBLE); + } + + public void finishActionMode() { + mActionMode.finish(); + } + + public void onItemCheckedStateChanged(ActionMode mode, int position, long id, + boolean checked) { + mNotesListAdapter.setCheckedItem(position, checked); + updateMenu(); + } + + public boolean onMenuItemClick(MenuItem item) { + if (mNotesListAdapter.getSelectedCount() == 0) { + Toast.makeText(NotesListActivity.this, getString(R.string.menu_select_none), + Toast.LENGTH_SHORT).show(); + return true; + } + + switch (item.getItemId()) { + case R.id.delete: + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + builder.setTitle(getString(R.string.alert_title_delete)); + builder.setIcon(android.R.drawable.ic_dialog_alert); + builder.setMessage(getString(R.string.alert_message_delete_notes, + mNotesListAdapter.getSelectedCount())); + builder.setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, + int which) { + batchDelete(); + } + }); + builder.setNegativeButton(android.R.string.cancel, null); + builder.show(); + break; + case R.id.move: + startQueryDestinationFolders(); + break; + default: + return false; + } + return true; + } + } + + private class NewNoteOnTouchListener implements OnTouchListener { + + public boolean onTouch(View v, MotionEvent event) { + // 根据触摸事件的不同类型,执行不同的操作 + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: { + // 获取屏幕的高度 + Display display = getWindowManager().getDefaultDisplay(); + int screenHeight = display.getHeight(); + // 获取新增笔记视图的高度 + int newNoteViewHeight = mAddNewNote.getHeight(); + // 计算新增笔记视图的起始位置 + int start = screenHeight - newNoteViewHeight; + // 计算触摸事件的Y坐标 + int eventY = start + (int) event.getY(); + /** + * Minus TitleBar's height + */ + // 如果当前状态为子文件夹状态,则减去标题栏的高度 + if (mState == ListEditState.SUB_FOLDER) { + eventY -= mTitleBar.getHeight(); + start -= mTitleBar.getHeight(); + } + /** + * HACKME:When click the transparent part of "New Note" button, dispatch + * the event to the list view behind this button. The transparent part of + * "New Note" button could be expressed by formula y=-0.12x+94(Unit:pixel) + * and the line top of the button. The coordinate based on left of the "New + * Note" button. The 94 represents maximum height of the transparent part. + * Notice that, if the background of the button changes, the formula should + * also change. This is very bad, just for the UI designer's strong requirement. + */ + // 如果触摸事件的Y坐标小于公式y=-0.12x+94的结果,则将事件分派给列表视图 + if (event.getY() < (event.getX() * (-0.12) + 94)) { + // 获取列表视图的最后一个子视图 + View view = mNotesListView.getChildAt(mNotesListView.getChildCount() - 1 + - mNotesListView.getFooterViewsCount()); + // 如果子视图不为空,且子视图的底部大于起始位置,且子视图的顶部小于起始位置加上94,则将事件分派给列表视图 + if (view != null && view.getBottom() > start + && (view.getTop() < (start + 94))) { + mOriginY = (int) event.getY(); + mDispatchY = eventY; + event.setLocation(event.getX(), mDispatchY); + mDispatch = true; + return mNotesListView.dispatchTouchEvent(event); + } + } + break; + } + case MotionEvent.ACTION_MOVE: { + // 如果事件被分派,则更新事件的位置 + if (mDispatch) { + mDispatchY += (int) event.getY() - mOriginY; + event.setLocation(event.getX(), mDispatchY); + return mNotesListView.dispatchTouchEvent(event); + } + break; + } + default: { + // 如果事件被分派,则将事件的位置恢复,并将事件分派给列表视图 + if (mDispatch) { + event.setLocation(event.getX(), mDispatchY); + mDispatch = false; + return mNotesListView.dispatchTouchEvent(event); + } + break; + } + } + // 返回false,表示事件未被处理 + return false; + } + + }; + + 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 { + // 构造函数,传入ContentResolver对象 + public BackgroundQueryHandler(ContentResolver contentResolver) { + // 调用父类的构造函数,传入ContentResolver对象 + super(contentResolver); + } + + @Override + protected void onQueryComplete(int token, Object cookie, Cursor cursor) { + // 根据token的值,执行不同的操作 + switch (token) { + // 如果token的值为FOLDER_NOTE_LIST_QUERY_TOKEN,则执行以下操作 + case FOLDER_NOTE_LIST_QUERY_TOKEN: + // 将查询结果传递给mNotesListAdapter + mNotesListAdapter.changeCursor(cursor); + break; + // 如果token的值为FOLDER_LIST_QUERY_TOKEN,则执行以下操作 + case FOLDER_LIST_QUERY_TOKEN: + // 如果查询结果不为空且结果数量大于0,则执行以下操作 + if (cursor != null && cursor.getCount() > 0) { + // 显示文件夹列表菜单 + showFolderListMenu(cursor); + } else { + // 否则,输出错误日志 + Log.e(TAG, "Query folder failed"); + } + break; + // 如果token的值不匹配以上两种情况,则直接返回 + default: + return; + } + } + } + + // 显示文件夹列表菜单 + private void showFolderListMenu(Cursor cursor) { + // 创建一个AlertDialog.Builder对象 + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + // 设置对话框标题 + builder.setTitle(R.string.menu_title_select_folder); + // 创建一个FoldersListAdapter对象,用于显示文件夹列表 + 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(); + // 结束ActionMode + mModeCallBack.finishActionMode(); + } + }); + // 显示对话框 + builder.show(); + } + + // 创建新笔记 + private void createNewNote() { + // 创建一个Intent对象,指定目标Activity为NoteEditActivity + Intent intent = new Intent(this, NoteEditActivity.class); + // 设置Intent的动作,为插入或编辑 + intent.setAction(Intent.ACTION_INSERT_OR_EDIT); + // 将当前文件夹ID作为参数传递给目标Activity + intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mCurrentFolderId); + // 启动目标Activity,并设置请求码为REQUEST_CODE_NEW_NODE + this.startActivityForResult(intent, REQUEST_CODE_NEW_NODE); + } + + private void batchDelete() { + // 创建一个异步任务,用于批量删除 + new AsyncTask>() { + // 在后台执行批量删除操作 + protected HashSet doInBackground(Void... unused) { + // 获取选中的widget + HashSet widgets = mNotesListAdapter.getSelectedWidget(); + // 如果不是同步模式,直接删除笔记 + if (!isSyncMode()) { + // if not synced, delete notes directly + if (DataUtils.batchDeleteNotes(mContentResolver, mNotesListAdapter + .getSelectedItemIds())) { + } else { + Log.e(TAG, "Delete notes error, should not happens"); + } + } else { + // 在同步模式下,将删除的笔记移动到垃圾桶文件夹 + // in sync mode, we'll move the deleted note into the trash + // folder + if (!DataUtils.batchMoveToFolder(mContentResolver, mNotesListAdapter + .getSelectedItemIds(), Notes.ID_TRASH_FOLER)) { + Log.e(TAG, "Move notes to trash folder error, should not happens"); + } + } + return widgets; + } + + // 在异步任务执行完毕后更新widget + @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); + } + } + } + // 结束ActionMode + mModeCallBack.finishActionMode(); + } + }.execute(); + } + + private void deleteFolder(long folderId) { + // 删除文件夹 + if (folderId == Notes.ID_ROOT_FOLDER) { + // 如果文件夹ID为根文件夹,则抛出异常 + Log.e(TAG, "Wrong folder id, should not happen " + folderId); + return; + } + + HashSet ids = new HashSet(); + // 创建一个HashSet,用于存储要删除的文件夹ID + ids.add(folderId); + // 将要删除的文件夹ID添加到HashSet中 + HashSet widgets = DataUtils.getFolderNoteWidget(mContentResolver, + folderId); + // 获取要删除的文件夹对应的widget + if (!isSyncMode()) { + // if not synced, delete folder directly + DataUtils.batchDeleteNotes(mContentResolver, ids); + } else { + // in sync mode, we'll move the deleted folder into the trash folder + DataUtils.batchMoveToFolder(mContentResolver, ids, Notes.ID_TRASH_FOLER); + } + if (widgets != null) { + for (AppWidgetAttribute widget : widgets) { + if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID + && widget.widgetType != Notes.TYPE_WIDGET_INVALIDE) { + updateWidget(widget.widgetId, widget.widgetType); + } + } + } + } + + // 打开节点 + private void openNode(NoteItemData data) { + // 创建一个Intent对象,指定要启动的Activity为NoteEditActivity + Intent intent = new Intent(this, NoteEditActivity.class); + // 设置Intent的Action为查看 + intent.setAction(Intent.ACTION_VIEW); + // 将节点的ID作为附加数据传递给Intent + intent.putExtra(Intent.EXTRA_UID, data.getId()); + // 启动Activity,并指定请求码为REQUEST_CODE_OPEN_NODE + this.startActivityForResult(intent, REQUEST_CODE_OPEN_NODE); + } + + // 打开文件夹 + private void openFolder(NoteItemData data) { + // 设置当前文件夹ID + mCurrentFolderId = data.getId(); + // 异步查询笔记列表 + startAsyncNotesListQuery(); + // 如果打开的是通话记录文件夹 + if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { + // 设置状态为通话记录文件夹 + mState = ListEditState.CALL_RECORD_FOLDER; + // 隐藏添加新笔记按钮 + mAddNewNote.setVisibility(View.GONE); + } else { + // 设置状态为子文件夹 + mState = ListEditState.SUB_FOLDER; + } + // 如果打开的是通话记录文件夹 + if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { + // 设置标题栏为通话记录文件夹名称 + mTitleBar.setText(R.string.call_record_folder_name); + } else { + // 设置标题栏为文件夹名称 + mTitleBar.setText(data.getSnippet()); + } + // 显示标题栏 + mTitleBar.setVisibility(View.VISIBLE); + } + + // 点击事件 + public void onClick(View v) { + // 根据点击的View的id进行判断 + switch (v.getId()) { + // 如果点击的是新建笔记按钮 + case R.id.btn_new_note: + // 调用创建新笔记的方法 + createNewNote(); + break; + // 默认情况 + default: + break; + } + } + + // 显示软键盘 + private void showSoftInput() { + // 获取输入法管理器 + InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + // 如果输入法管理器不为空 + if (inputMethodManager != null) { + // 显示软键盘 + inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); + } + } + + // 隐藏软键盘 + 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对象 + ContentValues values = new ContentValues(); + // 设置文件夹名称 + values.put(NoteColumns.SNIPPET, name); + // 设置文件夹类型 + values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); + // 设置本地修改标志 + values.put(NoteColumns.LOCAL_MODIFIED, 1); + // 更新文件夹数据 + mContentResolver.update(Notes.CONTENT_NOTE_URI, values, NoteColumns.ID + + "=?", new String[] { + String.valueOf(mFocusNoteDataItem.getId()) + }); + } + } else if (!TextUtils.isEmpty(name)) { + // 如果是创建文件夹,并且文件夹名称不为空 + ContentValues values = new ContentValues(); + // 设置文件夹名称 + values.put(NoteColumns.SNIPPET, name); + // 设置文件夹类型 + values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); + // 插入文件夹数据 + mContentResolver.insert(Notes.CONTENT_NOTE_URI, values); + } + // 关闭对话框 + dialog.dismiss(); + } + }); + + // 如果文件夹名称为空,禁用确定按钮 + if (TextUtils.isEmpty(etName.getText())) { + positive.setEnabled(false); + } + /** + * When the name edit text is null, disable the positive button + */ + etName.addTextChangedListener(new TextWatcher() { + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // TODO Auto-generated method stub + + } + + public void onTextChanged(CharSequence s, int start, int before, int count) { + if (TextUtils.isEmpty(etName.getText())) { + positive.setEnabled(false); + } else { + positive.setEnabled(true); + } + } + + public void afterTextChanged(Editable s) { + // TODO Auto-generated method stub + + } + }); + } + + @Override + public void onBackPressed() { + // 根据当前状态执行不同的操作 + switch (mState) { + case SUB_FOLDER: + // 如果当前状态是子文件夹,则将当前文件夹ID设置为根文件夹,状态设置为笔记列表,并开始异步查询笔记列表 + mCurrentFolderId = Notes.ID_ROOT_FOLDER; + mState = ListEditState.NOTE_LIST; + startAsyncNotesListQuery(); + // 隐藏标题栏 + mTitleBar.setVisibility(View.GONE); + break; + case CALL_RECORD_FOLDER: + // 如果当前状态是通话记录文件夹,则将当前文件夹ID设置为根文件夹,状态设置为笔记列表,并开始异步查询笔记列表 + mCurrentFolderId = Notes.ID_ROOT_FOLDER; + mState = ListEditState.NOTE_LIST; + // 显示添加新笔记按钮 + mAddNewNote.setVisibility(View.VISIBLE); + // 隐藏标题栏 + mTitleBar.setVisibility(View.GONE); + startAsyncNotesListQuery(); + break; + case NOTE_LIST: + // 如果当前状态是笔记列表,则调用父类的onBackPressed方法 + super.onBackPressed(); + break; + default: + // 默认情况下,不执行任何操作 + break; + } + } + + // 更新小部件 + private void updateWidget(int appWidgetId, int appWidgetType) { + // 创建一个Intent,用于更新小部件 + Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); + // 根据小部件类型,设置Intent的Class + 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; + } + + // 将小部件ID添加到Intent中 + 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); + } + } + }; + + // 重写onContextMenuClosed方法 + @Override + public void onContextMenuClosed(Menu menu) { + // 如果mNotesListView不为空 + if (mNotesListView != null) { + // 将mNotesListView的onCreateContextMenuListener设置为null + mNotesListView.setOnCreateContextMenuListener(null); + } + // 调用父类的onContextMenuClosed方法 + super.onContextMenuClosed(menu); + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + // 如果长按的数据项为空,则打印错误日志并返回false + if (mFocusNoteDataItem == null) { + Log.e(TAG, "The long click data item is null"); + return false; + } + // 根据菜单项的id执行相应的操作 + 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(); + // 加载note_list菜单 + if (mState == ListEditState.NOTE_LIST) { + getMenuInflater().inflate(R.menu.note_list, menu); + // set sync or sync_cancel + menu.findItem(R.id.menu_sync).setTitle( + GTaskSyncService.isSyncing() ? R.string.menu_sync_cancel : R.string.menu_sync); + } else if (mState == ListEditState.SUB_FOLDER) { + getMenuInflater().inflate(R.menu.sub_folder, menu); + } else if (mState == ListEditState.CALL_RECORD_FOLDER) { + getMenuInflater().inflate(R.menu.call_record_folder, menu); + } else { + Log.e(TAG, "Wrong state:" + mState); + } + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // 根据菜单项的ID执行相应的操作 + 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; + } + + // 重写onSearchRequested()方法 + @Override + public boolean onSearchRequested() { + // 启动搜索,参数分别为:搜索字符串、是否显示搜索框、应用数据、是否显示搜索结果 + startSearch(null, false, null /* appData */, false); + // 返回true,表示搜索请求已处理 + return true; + } + + // 导出笔记到文本 + private void exportNoteToText() { + // 获取BackupUtils实例 + final BackupUtils backup = BackupUtils.getInstance(NotesListActivity.this); + // 创建异步任务 + new AsyncTask() { + + // 在后台执行导出操作 + @Override + protected Integer doInBackground(Void... unused) { + return backup.exportToText(); + } + + // 在主线程中处理结果 + @Override + protected void onPostExecute(Integer result) { + // 如果SD卡未挂载 + 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) { + // 判断点击的view是否为NotesListItem类型 + if (view instanceof NotesListItem) { + // 获取点击的item数据 + NoteItemData item = ((NotesListItem) view).getItemData(); + // 判断是否处于选择模式 + if (mNotesListAdapter.isInChoiceMode()) { + // 判断点击的item类型是否为Notes.TYPE_NOTE + if (item.getType() == Notes.TYPE_NOTE) { + // 获取点击的item位置,减去ListView的headerViewsCount + position = position - mNotesListView.getHeaderViewsCount(); + // 调用回调函数,改变item的选中状态 + mModeCallBack.onItemCheckedStateChanged(null, position, id, + !mNotesListAdapter.isSelectedItem(position)); + } + return; + } + + // 判断当前状态 + switch (mState) { + // 如果当前状态为NOTE_LIST + case NOTE_LIST: + // 判断点击的item类型是否为Notes.TYPE_FOLDER或Notes.TYPE_SYSTEM + if (item.getType() == Notes.TYPE_FOLDER + || item.getType() == Notes.TYPE_SYSTEM) { + // 打开文件夹 + openFolder(item); + // 判断点击的item类型是否为Notes.TYPE_NOTE + } else if (item.getType() == Notes.TYPE_NOTE) { + // 打开节点 + openNode(item); + // 如果点击的item类型不是Notes.TYPE_FOLDER、Notes.TYPE_SYSTEM或Notes.TYPE_NOTE,则输出错误日志 + } else { + Log.e(TAG, "Wrong note type in NOTE_LIST"); + } + break; + // 如果当前状态为SUB_FOLDER或CALL_RECORD_FOLDER + case SUB_FOLDER: + case CALL_RECORD_FOLDER: + // 判断点击的item类型是否为Notes.TYPE_NOTE + if (item.getType() == Notes.TYPE_NOTE) { + // 打开节点 + openNode(item); + // 如果点击的item类型不是Notes.TYPE_NOTE,则输出错误日志 + } else { + Log.e(TAG, "Wrong note type in SUB_FOLDER"); + } + break; + // 默认情况 + default: + break; + } + } + } + + } + + // 开始查询目标文件夹 + private void startQueryDestinationFolders() { + // 查询条件:类型为文件夹,父ID不为垃圾桶ID,ID不为当前文件夹ID + String selection = NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>? AND " + NoteColumns.ID + "<>?"; + // 如果当前状态为笔记列表,则不查询根文件夹 + selection = (mState == ListEditState.NOTE_LIST) ? selection: + "(" + selection + ") OR (" + NoteColumns.ID + "=" + Notes.ID_ROOT_FOLDER + ")"; + + // 启动后台查询 + mBackgroundQueryHandler.startQuery(FOLDER_LIST_QUERY_TOKEN, + null, + Notes.CONTENT_NOTE_URI, + FoldersListAdapter.PROJECTION, + selection, + new String[] { + String.valueOf(Notes.TYPE_FOLDER), + String.valueOf(Notes.ID_TRASH_FOLER), + String.valueOf(mCurrentFolderId) + }, + NoteColumns.MODIFIED_DATE + " DESC"); + } + + public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { + if (view instanceof NotesListItem) { + mFocusNoteDataItem = ((NotesListItem) view).getItemData(); + if (mFocusNoteDataItem.getType() == Notes.TYPE_NOTE && !mNotesListAdapter.isInChoiceMode()) { + if (mNotesListView.startActionMode(mModeCallBack) != null) { + mModeCallBack.onItemCheckedStateChanged(null, position, id, true); + mNotesListView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + } else { + Log.e(TAG, "startActionMode fails"); + } + } else if (mFocusNoteDataItem.getType() == Notes.TYPE_FOLDER) { + mNotesListView.setOnCreateContextMenuListener(mFolderOnCreateContextMenuListener); + } + } + return false; + } + } + /* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + package net.micode.notes.ui; + + import android.content.Context; + import android.database.Cursor; + import android.util.Log; + import android.view.View; + import android.view.ViewGroup; + import android.widget.CursorAdapter; + + import net.micode.notes.data.Notes; + + import java.util.Collection; + import java.util.HashMap; + import java.util.HashSet; + import java.util.Iterator; + + + public class NotesListAdapter extends CursorAdapter { + private static final String TAG = "NotesListAdapter"; + private Context mContext; + private HashMap mSelectedIndex; + private int mNotesCount; + private boolean mChoiceMode; + + public static class AppWidgetAttribute { + public int widgetId; + public int widgetType; + }; + + // 构造函数,用于初始化NotesListAdapter对象 + public NotesListAdapter(Context context) { + // 调用父类的构造函数,传入上下文和null + super(context, null); + // 初始化mSelectedIndex,用于存储选中的索引 + mSelectedIndex = new HashMap(); + // 保存上下文 + mContext = context; + // 初始化mNotesCount,用于存储笔记数量 + mNotesCount = 0; + } + + // 重写newView方法,用于创建新的视图 + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + // 创建一个新的NotesListItem视图 + return new NotesListItem(context); + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + // 判断view是否为NotesListItem类型 + if (view instanceof NotesListItem) { + // 创建NoteItemData对象 + NoteItemData itemData = new NoteItemData(context, cursor); + // 将NoteItemData对象绑定到NotesListItem对象上 + ((NotesListItem) view).bind(context, itemData, mChoiceMode, + isSelectedItem(cursor.getPosition())); + } + } + + // 设置指定位置的项是否被选中 + public void setCheckedItem(final int position, final boolean checked) { + // 将指定位置的项设置为选中状态 + mSelectedIndex.put(position, checked); + // 通知数据集发生变化 + notifyDataSetChanged(); + } + + // 判断是否处于选择模式 + public boolean isInChoiceMode() { + // 返回mChoiceMode的值 + return mChoiceMode; + } + + // 设置选择模式 + public void setChoiceMode(boolean mode) { + // 清空已选择项 + mSelectedIndex.clear(); + // 设置选择模式 + mChoiceMode = mode; + } + + public void selectAll(boolean checked) { + Cursor cursor = getCursor(); + for (int i = 0; i < getCount(); i++) { + if (cursor.moveToPosition(i)) { + if (NoteItemData.getNoteType(cursor) == Notes.TYPE_NOTE) { + setCheckedItem(i, checked); + } + } + } + } + + // 获取选中的项的ID + public HashSet getSelectedItemIds() { + // 创建一个HashSet来存储选中的项的ID + HashSet itemSet = new HashSet(); + // 遍历mSelectedIndex中的所有键值对 + for (Integer position : mSelectedIndex.keySet()) { + // 如果该位置的值为true,表示该项被选中 + if (mSelectedIndex.get(position) == true) { + // 获取该项的ID + Long id = getItemId(position); + // 如果ID为Notes.ID_ROOT_FOLDER,表示该项为根文件夹,不应该发生 + if (id == Notes.ID_ROOT_FOLDER) { + Log.d(TAG, "Wrong item id, should not happen"); + } else { + // 否则,将该ID添加到itemSet中 + itemSet.add(id); + } + } + } + + // 返回itemSet + return itemSet; + } + + // 获取选中的小部件 + public HashSet getSelectedWidget() { + // 创建一个HashSet来存储选中的小部件 + HashSet itemSet = new HashSet(); + // 遍历选中的小部件索引 + for (Integer position : mSelectedIndex.keySet()) { + // 如果该小部件被选中 + if (mSelectedIndex.get(position) == true) { + // 获取该小部件的Cursor + Cursor c = (Cursor) getItem(position); + // 如果Cursor不为空 + if (c != null) { + // 创建一个AppWidgetAttribute对象 + AppWidgetAttribute widget = new AppWidgetAttribute(); + // 获取NoteItemData对象 + NoteItemData item = new NoteItemData(mContext, c); + // 设置小部件的ID和类型 + widget.widgetId = item.getWidgetId(); + widget.widgetType = item.getWidgetType(); + // 将小部件添加到HashSet中 + itemSet.add(widget); + /** + * Don't close cursor here, only the adapter could close it + */ + } else { + Log.e(TAG, "Invalid cursor"); + return null; + } + } + } + return itemSet; + } + + // 获取被选中的数量 + public int getSelectedCount() { + // 获取被选中的索引集合 + Collection values = mSelectedIndex.values(); + // 如果集合为空,返回0 + if (null == values) { + return 0; + } + // 获取集合的迭代器 + Iterator iter = values.iterator(); + // 初始化计数器 + int count = 0; + // 遍历集合 + while (iter.hasNext()) { + // 如果当前索引被选中,计数器加1 + if (true == iter.next()) { + count++; + } + } + // 返回计数器的值 + return count; + } + + // 判断是否所有选项都被选中 + public boolean isAllSelected() { + // 获取被选中的选项数量 + int checkedCount = getSelectedCount(); + // 如果被选中的选项数量不为0且等于总选项数量,则返回true,否则返回false + return (checkedCount != 0 && checkedCount == mNotesCount); + } + + // 判断指定位置的项是否被选中 + public boolean isSelectedItem(final int position) { + // 如果指定位置的项未被选中,则返回false + if (null == mSelectedIndex.get(position)) { + return false; + } + // 否则返回true + return mSelectedIndex.get(position); + } + + // 重写onContentChanged方法 + @Override + protected void onContentChanged() { + // 调用父类的onContentChanged方法 + super.onContentChanged(); + // 计算笔记数量 + calcNotesCount(); + } + + @Override + public void changeCursor(Cursor cursor) { + super.changeCursor(cursor); + calcNotesCount(); + } + + // 计算笔记数量 + private void calcNotesCount() { + // 初始化笔记数量为0 + mNotesCount = 0; + // 遍历所有笔记 + for (int i = 0; i < getCount(); i++) { + // 获取当前笔记的游标 + Cursor c = (Cursor) getItem(i); + // 如果游标不为空 + if (c != null) { + // 如果笔记类型为笔记 + if (NoteItemData.getNoteType(c) == Notes.TYPE_NOTE) { + // 笔记数量加1 + mNotesCount++; + } + } else { + // 如果游标为空,打印错误日志并返回 + Log.e(TAG, "Invalid cursor"); + return; + } + } + } + } + /* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + package net.micode.notes.ui; + + import android.content.Context; + import android.text.format.DateUtils; + import android.view.View; + import android.widget.CheckBox; + import android.widget.ImageView; + import android.widget.LinearLayout; + import android.widget.TextView; + + import net.micode.notes.R; + import net.micode.notes.data.Notes; + import net.micode.notes.tool.DataUtils; + import net.micode.notes.tool.ResourceParser.NoteItemBgResources; + + + public class NotesListItem extends LinearLayout { + private ImageView mAlert; + private TextView mTitle; + private TextView mTime; + private TextView mCallName; + private NoteItemData mItemData; + private CheckBox mCheckBox; + + // 构造函数,用于初始化NotesListItem对象 + public NotesListItem(Context context) { + // 调用父类的构造函数 + super(context); + // 使用传入的context对象加载布局文件note_item.xml,并将布局文件中的视图添加到当前视图中 + inflate(context, R.layout.note_item, this); + // 通过findViewById方法获取布局文件中的视图,并赋值给相应的成员变量 + mAlert = (ImageView) findViewById(R.id.iv_alert_icon); + mTitle = (TextView) findViewById(R.id.tv_title); + mTime = (TextView) findViewById(R.id.tv_time); + mCallName = (TextView) findViewById(R.id.tv_name); + mCheckBox = (CheckBox) findViewById(android.R.id.checkbox); + } + + // 绑定数据到视图 + public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked) { + // 如果是选择模式且数据类型为笔记 + if (choiceMode && data.getType() == Notes.TYPE_NOTE) { + // 显示复选框 + mCheckBox.setVisibility(View.VISIBLE); + // 设置复选框状态 + mCheckBox.setChecked(checked); + } else { + // 否则隐藏复选框 + mCheckBox.setVisibility(View.GONE); + } + + // 保存数据 + mItemData = data; + // 如果数据ID为通话记录文件夹 + if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { + // 隐藏通话名称 + mCallName.setVisibility(View.GONE); + // 显示提醒 + mAlert.setVisibility(View.VISIBLE); + // 设置标题样式 + mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); + // 设置标题文本 + mTitle.setText(context.getString(R.string.call_record_folder_name) + + context.getString(R.string.format_folder_files_count, data.getNotesCount())); + // 设置提醒图标 + mAlert.setImageResource(R.drawable.call_record); + // 如果数据父ID为通话记录文件夹 + } else if (data.getParentId() == Notes.ID_CALL_RECORD_FOLDER) { + // 显示通话名称 + mCallName.setVisibility(View.VISIBLE); + // 设置通话名称文本 + mCallName.setText(data.getCallName()); + // 设置标题样式 + mTitle.setTextAppearance(context,R.style.TextAppearanceSecondaryItem); + // 设置标题文本 + mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); + // 如果有提醒 + if (data.hasAlert()) { + // 设置提醒图标 + mAlert.setImageResource(R.drawable.clock); + // 显示提醒 + mAlert.setVisibility(View.VISIBLE); + } else { + // 否则隐藏提醒 + mAlert.setVisibility(View.GONE); + } + // 否则 + } else { + // 隐藏通话名称 + mCallName.setVisibility(View.GONE); + // 设置标题样式 + mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); + + // 如果数据类型为文件夹 + if (data.getType() == Notes.TYPE_FOLDER) { + // 设置标题文本 + mTitle.setText(data.getSnippet() + + context.getString(R.string.format_folder_files_count, + data.getNotesCount())); + // 隐藏提醒 + mAlert.setVisibility(View.GONE); + // 否则 + } else { + // 设置标题文本 + mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); + // 如果有提醒 + if (data.hasAlert()) { + // 设置提醒图标 + mAlert.setImageResource(R.drawable.clock); + // 显示提醒 + mAlert.setVisibility(View.VISIBLE); + // 否则 + } else { + // 隐藏提醒 + mAlert.setVisibility(View.GONE); + } + } + } + // 设置修改时间 + mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate())); + + // 设置背景 + setBackground(data); + } + + // 设置背景颜色 + private void setBackground(NoteItemData data) { + // 获取背景颜色id + int id = data.getBgColorId(); + // 判断类型是否为笔记 + if (data.getType() == Notes.TYPE_NOTE) { + // 判断是否为单个笔记或单个文件夹 + if (data.isSingle() || data.isOneFollowingFolder()) { + // 设置背景资源为单个笔记或单个文件夹的背景资源 + setBackgroundResource(NoteItemBgResources.getNoteBgSingleRes(id)); + // 判断是否为最后一个笔记 + } else if (data.isLast()) { + // 设置背景资源为最后一个笔记的背景资源 + setBackgroundResource(NoteItemBgResources.getNoteBgLastRes(id)); + // 判断是否为第一个笔记或多个文件夹 + } else if (data.isFirst() || data.isMultiFollowingFolder()) { + // 设置背景资源为第一个笔记或多个文件夹的背景资源 + setBackgroundResource(NoteItemBgResources.getNoteBgFirstRes(id)); + // 其他情况 + } else { + // 设置背景资源为普通笔记的背景资源 + setBackgroundResource(NoteItemBgResources.getNoteBgNormalRes(id)); + } + // 其他情况 + } else { + // 设置背景资源为文件夹的背景资源 + setBackgroundResource(NoteItemBgResources.getFolderBgRes()); + } + } + + // 获取NoteItemData对象 + public NoteItemData getItemData() { + // 返回mItemData对象 + return mItemData; + } + } + /* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + package net.micode.notes.ui; + + import android.accounts.Account; + import android.accounts.AccountManager; + import android.app.ActionBar; + import android.app.AlertDialog; + import android.content.BroadcastReceiver; + import android.content.ContentValues; + import android.content.Context; + import android.content.DialogInterface; + import android.content.Intent; + import android.content.IntentFilter; + import android.content.SharedPreferences; + import android.os.Bundle; + import android.preference.Preference; + import android.preference.Preference.OnPreferenceClickListener; + import android.preference.PreferenceActivity; + import android.preference.PreferenceCategory; + import android.text.TextUtils; + import android.text.format.DateFormat; + import android.view.LayoutInflater; + import android.view.Menu; + import android.view.MenuItem; + import android.view.View; + import android.widget.Button; + import android.widget.TextView; + import android.widget.Toast; + + import net.micode.notes.R; + import net.micode.notes.data.Notes; + import net.micode.notes.data.Notes.NoteColumns; + import net.micode.notes.gtask.remote.GTaskSyncService; + + + public class NotesPreferenceActivity extends PreferenceActivity { + // 定义偏好设置名称 + public static final String PREFERENCE_NAME = "notes_preferences"; + + // 定义同步账户名称的偏好设置键 + public static final String PREFERENCE_SYNC_ACCOUNT_NAME = "pref_key_account_name"; + + // 定义最后同步时间的偏好设置键 + public static final String PREFERENCE_LAST_SYNC_TIME = "pref_last_sync_time"; + + // 定义设置背景颜色的偏好设置键 + public static final String PREFERENCE_SET_BG_COLOR_KEY = "pref_key_bg_random_appear"; + + // 定义同步账户的偏好设置键 + private static final String PREFERENCE_SYNC_ACCOUNT_KEY = "pref_sync_account_key"; + + // 定义账户类型过滤器的键 + private static final String AUTHORITIES_FILTER_KEY = "authorities"; + + // 定义账户类别 + private PreferenceCategory mAccountCategory; + + // 定义广播接收器 + private GTaskReceiver mReceiver; + + // 定义原始账户 + private Account[] mOriAccounts; + + // 定义是否添加了账户的标记 + private boolean mHasAddedAccount; + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + + /* using the app icon for navigation */ + getActionBar().setDisplayHomeAsUpEnabled(true); + + // 从资源文件中添加偏好设置 + addPreferencesFromResource(R.xml.preferences); + // 获取偏好设置中的账户类别 + mAccountCategory = (PreferenceCategory) findPreference(PREFERENCE_SYNC_ACCOUNT_KEY); + // 创建一个广播接收器 + mReceiver = new GTaskReceiver(); + // 创建一个意图过滤器 + IntentFilter filter = new IntentFilter(); + // 添加广播接收器的动作 + filter.addAction(GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME); + // 注册广播接收器 + registerReceiver(mReceiver, filter); + + // 初始化原始账户 + mOriAccounts = null; + // 从布局文件中获取头部视图 + View header = LayoutInflater.from(this).inflate(R.layout.settings_header, null); + // 将头部视图添加到列表视图中 + getListView().addHeaderView(header, null, true); + } + + @Override + protected void onResume() { + super.onResume(); + + // need to set sync account automatically if user has added a new + // account + if (mHasAddedAccount) { + // 获取Google账户 + 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(); + } + + // 重写onDestroy方法 + @Override + protected void onDestroy() { + // 如果mReceiver不为空,则取消注册 + if (mReceiver != null) { + unregisterReceiver(mReceiver); + } + // 调用父类的onDestroy方法 + super.onDestroy(); + } + + // 加载账户偏好设置 + private void loadAccountPreference() { + // 移除所有账户偏好设置 + mAccountCategory.removeAll(); + + // 创建一个新的账户偏好设置 + Preference accountPref = new Preference(this); + // 获取默认账户名称 + final String defaultAccount = getSyncAccountName(this); + // 设置账户偏好设置的标题 + accountPref.setTitle(getString(R.string.preferences_account_title)); + // 设置账户偏好设置的摘要 + accountPref.setSummary(getString(R.string.preferences_account_summary)); + // 设置账户偏好设置的点击事件监听器 + accountPref.setOnPreferenceClickListener(new OnPreferenceClickListener() { + public boolean onPreferenceClick(Preference preference) { + // 如果没有正在同步,则执行以下操作 + if (!GTaskSyncService.isSyncing()) { + // 如果默认账户为空,则执行以下操作 + if (TextUtils.isEmpty(defaultAccount)) { + // the first time to set account + showSelectAccountAlertDialog(); + } else { + // if the account has already been set, we need to promp + // user about the risk + showChangeAccountConfirmAlertDialog(); + } + } else { + Toast.makeText(NotesPreferenceActivity.this, + R.string.preferences_toast_cannot_change_account, Toast.LENGTH_SHORT) + .show(); + } + return true; + } + }); + + mAccountCategory.addPreference(accountPref); + } + + private void loadSyncButton() { + // 获取同步按钮和上次同步时间文本框 + Button syncButton = (Button) findViewById(R.id.preference_sync_button); + TextView lastSyncTimeView = (TextView) findViewById(R.id.prefenerece_sync_status_textview); + + // set button state + if (GTaskSyncService.isSyncing()) { + syncButton.setText(getString(R.string.preferences_button_sync_cancel)); + syncButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + GTaskSyncService.cancelSync(NotesPreferenceActivity.this); + } + }); + } else { + syncButton.setText(getString(R.string.preferences_button_sync_immediately)); + syncButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + GTaskSyncService.startSync(NotesPreferenceActivity.this); + } + }); + } + syncButton.setEnabled(!TextUtils.isEmpty(getSyncAccountName(this))); + + // set last sync time + if (GTaskSyncService.isSyncing()) { + lastSyncTimeView.setText(GTaskSyncService.getProgressString()); + lastSyncTimeView.setVisibility(View.VISIBLE); + } else { + long lastSyncTime = getLastSyncTime(this); + if (lastSyncTime != 0) { + lastSyncTimeView.setText(getString(R.string.preferences_last_sync_time, + DateFormat.format(getString(R.string.preferences_last_sync_time_format), + lastSyncTime))); + lastSyncTimeView.setVisibility(View.VISIBLE); + } else { + lastSyncTimeView.setVisibility(View.GONE); + } + } + } + + // 刷新UI + private void refreshUI() { + // 加载账户偏好设置 + loadAccountPreference(); + // 加载同步按钮 + loadSyncButton(); + } + + private void showSelectAccountAlertDialog() { + // 创建一个AlertDialog.Builder对象 + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); + + // 从布局文件中加载标题视图 + View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null); + // 获取标题文本视图 + TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title); + // 设置标题文本 + titleTextView.setText(getString(R.string.preferences_dialog_select_account_title)); + // 获取副标题文本视图 + TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle); + // 设置副标题文本 + subtitleTextView.setText(getString(R.string.preferences_dialog_select_account_tips)); + + // 设置自定义标题 + dialogBuilder.setCustomTitle(titleView); + // 设置确定按钮为空 + dialogBuilder.setPositiveButton(null, null); + + // 获取Google账户 + Account[] accounts = getGoogleAccounts(); + // 获取默认账户 + String defAccount = getSyncAccountName(this); + + // 保存原始账户 + mOriAccounts = accounts; + // 标记是否添加了账户 + mHasAddedAccount = false; + + // 如果有账户 + if (accounts.length > 0) { + // 创建一个字符序列数组 + CharSequence[] items = new CharSequence[accounts.length]; + // 保存字符序列数组 + final CharSequence[] itemMapping = items; + // 默认选中项 + int checkedItem = -1; + // 索引 + int index = 0; + // 遍历账户 + for (Account account : accounts) { + // 如果账户名与默认账户名相同,则选中该项 + if (TextUtils.equals(account.name, defAccount)) { + checkedItem = index; + } + // 将账户名添加到字符序列数组中 + items[index++] = account.name; + } + // 设置单选列表项 + dialogBuilder.setSingleChoiceItems(items, checkedItem, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + // 设置同步账户 + setSyncAccount(itemMapping[which].toString()); + // 关闭对话框 + dialog.dismiss(); + // 刷新UI + refreshUI(); + } + }); + } + + // 从布局文件中加载添加账户视图 + View addAccountView = LayoutInflater.from(this).inflate(R.layout.add_account_text, null); + // 设置对话框内容视图 + dialogBuilder.setView(addAccountView); + + // 显示对话框 + final AlertDialog dialog = dialogBuilder.show(); + // 添加账户视图点击事件 + addAccountView.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + // 标记已添加账户 + mHasAddedAccount = true; + // 创建一个Intent对象,跳转到添加账户设置界面 + Intent intent = new Intent("android.settings.ADD_ACCOUNT_SETTINGS"); + // 设置账户类型过滤器 + intent.putExtra(AUTHORITIES_FILTER_KEY, new String[] { + "gmail-ls" + }); + // 启动Activity + startActivityForResult(intent, -1); + // 关闭对话框 + dialog.dismiss(); + } + }); + } + + // 显示更改账户确认对话框 + private void showChangeAccountConfirmAlertDialog() { + // 创建对话框构建器 + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); + + // 加载对话框标题布局 + View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null); + // 获取标题文本视图 + TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title); + // 设置标题文本 + titleTextView.setText(getString(R.string.preferences_dialog_change_account_title, + getSyncAccountName(this))); + // 获取副标题文本视图 + TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle); + // 设置副标题文本 + subtitleTextView.setText(getString(R.string.preferences_dialog_change_account_warn_msg)); + // 设置自定义标题 + dialogBuilder.setCustomTitle(titleView); + + // 创建菜单项数组 + CharSequence[] menuItemArray = new CharSequence[] { + getString(R.string.preferences_menu_change_account), + getString(R.string.preferences_menu_remove_account), + getString(R.string.preferences_menu_cancel) + }; + // 设置对话框菜单项 + dialogBuilder.setItems(menuItemArray, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + // 如果点击了更改账户菜单项 + if (which == 0) { + // 显示选择账户对话框 + showSelectAccountAlertDialog(); + // 如果点击了移除账户菜单项 + } else if (which == 1) { + // 移除同步账户 + removeSyncAccount(); + // 刷新UI + refreshUI(); + } + } + }); + // 显示对话框 + dialogBuilder.show(); + } + + // 获取Google账户 + private Account[] getGoogleAccounts() { + // 获取AccountManager实例 + AccountManager accountManager = AccountManager.get(this); + // 获取类型为com.google的账户 + return accountManager.getAccountsByType("com.google"); + } + + // 设置同步账户 + private void setSyncAccount(String account) { + // 如果当前同步账户与传入的账户不一致,则进行以下操作 + if (!getSyncAccountName(this).equals(account)) { + // 获取SharedPreferences对象 + SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); + // 获取SharedPreferences.Editor对象 + SharedPreferences.Editor editor = settings.edit(); + // 如果传入的账户不为空,则将账户名保存到SharedPreferences中 + if (account != null) { + editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, account); + } else { + // 如果传入的账户为空,则将账户名保存为空字符串 + editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, ""); + } + // 提交修改 + editor.commit(); + + // clean up last sync time + setLastSyncTime(this, 0); + + // clean up local gtask related info + new Thread(new Runnable() { + public void run() { + ContentValues values = new ContentValues(); + values.put(NoteColumns.GTASK_ID, ""); + values.put(NoteColumns.SYNC_ID, 0); + getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null); + } + }).start(); + + Toast.makeText(NotesPreferenceActivity.this, + getString(R.string.preferences_toast_success_set_accout, account), + Toast.LENGTH_SHORT).show(); + } + } + + // 移除同步账户 + private void removeSyncAccount() { + // 获取SharedPreferences对象 + SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); + // 获取SharedPreferences.Editor对象 + SharedPreferences.Editor editor = settings.edit(); + // 如果SharedPreferences中包含PREFERENCE_SYNC_ACCOUNT_NAME键,则移除该键值对 + if (settings.contains(PREFERENCE_SYNC_ACCOUNT_NAME)) { + editor.remove(PREFERENCE_SYNC_ACCOUNT_NAME); + } + // 如果SharedPreferences中包含PREFERENCE_LAST_SYNC_TIME键,则移除该键值对 + if (settings.contains(PREFERENCE_LAST_SYNC_TIME)) { + editor.remove(PREFERENCE_LAST_SYNC_TIME); + } + // 提交修改 + editor.commit(); + + // clean up local gtask related info + new Thread(new Runnable() { + public void run() { + ContentValues values = new ContentValues(); + values.put(NoteColumns.GTASK_ID, ""); + values.put(NoteColumns.SYNC_ID, 0); + getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null); + } + }).start(); + } + + // 获取同步账户名称 + public static String getSyncAccountName(Context context) { + // 获取SharedPreferences对象 + 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对象 + SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, + Context.MODE_PRIVATE); + // 获取SharedPreferences.Editor对象 + SharedPreferences.Editor editor = settings.edit(); + // 将最后同步时间存入SharedPreferences对象 + editor.putLong(PREFERENCE_LAST_SYNC_TIME, time); + // 提交修改 + editor.commit(); + } + + // 获取最后一次同步时间 + public static long getLastSyncTime(Context context) { + // 获取SharedPreferences对象 + SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, + Context.MODE_PRIVATE); + // 获取最后一次同步时间 + return settings.getLong(PREFERENCE_LAST_SYNC_TIME, 0); + } + + private class GTaskReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + // 刷新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) { + // 根据菜单项的ID进行判断 + switch (item.getItemId()) { + // 如果点击的是返回键 + case android.R.id.home: + // 创建一个新的Intent,指向NotesListActivity + Intent intent = new Intent(this, NotesListActivity.class); + // 设置Intent的标志,清除栈顶的Activity + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + // 启动新的Activity + startActivity(intent); + // 返回true,表示已经处理了该菜单项 + return true; + // 如果点击的是其他菜单项 + default: + // 返回false,表示没有处理该菜单项 + return false; + } + } + } + \ No newline at end of file -- 2.34.1