From 517e5ba82ad2db96d99f4f3f07abf636c1266ff3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=AE=B6=E5=84=92?= <2494326140@qq.com> Date: Fri, 14 Apr 2023 15:31:27 +0800 Subject: [PATCH 1/8] CJRreading --- doc/CJR_codereading/data/Contact.java | 73 ++++ doc/CJR_codereading/data/Notes.java | 279 +++++++++++++ .../data/NotesDatabaseHelper.java | 362 +++++++++++++++++ doc/CJR_codereading/data/NotesProvider.java | 305 +++++++++++++++ doc/CJR_codereading/model/Note.java | 253 ++++++++++++ doc/CJR_codereading/model/WorkingNote.java | 368 ++++++++++++++++++ doc/CJR_codereading/tool/BackupUtils.java | 344 ++++++++++++++++ doc/CJR_codereading/tool/DataUtils.java | 295 ++++++++++++++ .../tool/GTaskStringUtils.java | 113 ++++++ doc/CJR_codereading/tool/ResourceParser.java | 181 +++++++++ .../widget/NoteWidgetProvider.java | 132 +++++++ .../widget/NoteWidgetProvider_2x.java | 47 +++ .../widget/NoteWidgetProvider_4x.java | 46 +++ 13 files changed, 2798 insertions(+) create mode 100644 doc/CJR_codereading/data/Contact.java create mode 100644 doc/CJR_codereading/data/Notes.java create mode 100644 doc/CJR_codereading/data/NotesDatabaseHelper.java create mode 100644 doc/CJR_codereading/data/NotesProvider.java create mode 100644 doc/CJR_codereading/model/Note.java create mode 100644 doc/CJR_codereading/model/WorkingNote.java create mode 100644 doc/CJR_codereading/tool/BackupUtils.java create mode 100644 doc/CJR_codereading/tool/DataUtils.java create mode 100644 doc/CJR_codereading/tool/GTaskStringUtils.java create mode 100644 doc/CJR_codereading/tool/ResourceParser.java create mode 100644 doc/CJR_codereading/widget/NoteWidgetProvider.java create mode 100644 doc/CJR_codereading/widget/NoteWidgetProvider_2x.java create mode 100644 doc/CJR_codereading/widget/NoteWidgetProvider_4x.java diff --git a/doc/CJR_codereading/data/Contact.java b/doc/CJR_codereading/data/Contact.java new file mode 100644 index 0000000..d97ac5d --- /dev/null +++ b/doc/CJR_codereading/data/Contact.java @@ -0,0 +1,73 @@ +/* + * 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.data; + +import android.content.Context; +import android.database.Cursor; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.Data; +import android.telephony.PhoneNumberUtils; +import android.util.Log; + +import java.util.HashMap; + +public class Contact { + private static HashMap sContactCache; + private static final String TAG = "Contact"; + + private static final String CALLER_ID_SELECTION = "PHONE_NUMBERS_EQUAL(" + Phone.NUMBER + + ",?) AND " + Data.MIMETYPE + "='" + Phone.CONTENT_ITEM_TYPE + "'" + + " AND " + Data.RAW_CONTACT_ID + " IN " + + "(SELECT raw_contact_id " + + " FROM phone_lookup" + + " WHERE min_match = '+')"; + + public static String getContact(Context context, String phoneNumber) { + if(sContactCache == null) { + sContactCache = new HashMap(); + } + + if(sContactCache.containsKey(phoneNumber)) { + return sContactCache.get(phoneNumber); + } + + String selection = CALLER_ID_SELECTION.replace("+", + PhoneNumberUtils.toCallerIDMinMatch(phoneNumber)); + Cursor cursor = context.getContentResolver().query( + Data.CONTENT_URI, + new String [] { Phone.DISPLAY_NAME }, + selection, + new String[] { phoneNumber }, + null); + + if (cursor != null && cursor.moveToFirst()) { + try { + String name = cursor.getString(0); + sContactCache.put(phoneNumber, name); + return name; + } catch (IndexOutOfBoundsException e) { + Log.e(TAG, " Cursor get string error " + e.toString()); + return null; + } finally { + cursor.close(); + } + } else { + Log.d(TAG, "No contact matched with number:" + phoneNumber); + return null; + } + } +} diff --git a/doc/CJR_codereading/data/Notes.java b/doc/CJR_codereading/data/Notes.java new file mode 100644 index 0000000..f240604 --- /dev/null +++ b/doc/CJR_codereading/data/Notes.java @@ -0,0 +1,279 @@ +/* + * 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.data; + +import android.net.Uri; +public class Notes { + public static final String AUTHORITY = "micode_notes"; + public static final String TAG = "Notes"; + public static final int TYPE_NOTE = 0; + public static final int TYPE_FOLDER = 1; + public static final int TYPE_SYSTEM = 2; + + /** + * Following IDs are system folders' identifiers + * {@link Notes#ID_ROOT_FOLDER } is default folder + * {@link Notes#ID_TEMPARAY_FOLDER } is for notes belonging no folder + * {@link Notes#ID_CALL_RECORD_FOLDER} is to store call records + */ + public static final int ID_ROOT_FOLDER = 0; + public static final int ID_TEMPARAY_FOLDER = -1; + public static final int ID_CALL_RECORD_FOLDER = -2; + public static final int ID_TRASH_FOLER = -3; + + public static final String INTENT_EXTRA_ALERT_DATE = "net.micode.notes.alert_date"; + public static final String INTENT_EXTRA_BACKGROUND_ID = "net.micode.notes.background_color_id"; + public static final String INTENT_EXTRA_WIDGET_ID = "net.micode.notes.widget_id"; + public static final String INTENT_EXTRA_WIDGET_TYPE = "net.micode.notes.widget_type"; + public static final String INTENT_EXTRA_FOLDER_ID = "net.micode.notes.folder_id"; + public static final String INTENT_EXTRA_CALL_DATE = "net.micode.notes.call_date"; + + public static final int TYPE_WIDGET_INVALIDE = -1; + public static final int TYPE_WIDGET_2X = 0; + public static final int TYPE_WIDGET_4X = 1; + + public static class DataConstants { + public static final String NOTE = TextNote.CONTENT_ITEM_TYPE; + public static final String CALL_NOTE = CallNote.CONTENT_ITEM_TYPE; + } + + /** + * Uri to query all notes and folders + */ + public static final Uri CONTENT_NOTE_URI = Uri.parse("content://" + AUTHORITY + "/note"); + + /** + * Uri to query data + */ + public static final Uri CONTENT_DATA_URI = Uri.parse("content://" + AUTHORITY + "/data"); + + public interface NoteColumns { + /** + * The unique ID for a row + *

Type: INTEGER (long)

+ */ + public static final String ID = "_id"; + + /** + * The parent's id for note or folder + *

Type: INTEGER (long)

+ */ + public static final String PARENT_ID = "parent_id"; + + /** + * Created data for note or folder + *

Type: INTEGER (long)

+ */ + public static final String CREATED_DATE = "created_date"; + + /** + * Latest modified date + *

Type: INTEGER (long)

+ */ + public static final String MODIFIED_DATE = "modified_date"; + + + /** + * Alert date + *

Type: INTEGER (long)

+ */ + public static final String ALERTED_DATE = "alert_date"; + + /** + * Folder's name or text content of note + *

Type: TEXT

+ */ + public static final String SNIPPET = "snippet"; + + /** + * Note's widget id + *

Type: INTEGER (long)

+ */ + public static final String WIDGET_ID = "widget_id"; + + /** + * Note's widget type + *

Type: INTEGER (long)

+ */ + public static final String WIDGET_TYPE = "widget_type"; + + /** + * Note's background color's id + *

Type: INTEGER (long)

+ */ + public static final String BG_COLOR_ID = "bg_color_id"; + + /** + * For text note, it doesn't has attachment, for multi-media + * note, it has at least one attachment + *

Type: INTEGER

+ */ + public static final String HAS_ATTACHMENT = "has_attachment"; + + /** + * Folder's count of notes + *

Type: INTEGER (long)

+ */ + public static final String NOTES_COUNT = "notes_count"; + + /** + * The file type: folder or note + *

Type: INTEGER

+ */ + public static final String TYPE = "type"; + + /** + * The last sync id + *

Type: INTEGER (long)

+ */ + public static final String SYNC_ID = "sync_id"; + + /** + * Sign to indicate local modified or not + *

Type: INTEGER

+ */ + public static final String LOCAL_MODIFIED = "local_modified"; + + /** + * Original parent id before moving into temporary folder + *

Type : INTEGER

+ */ + public static final String ORIGIN_PARENT_ID = "origin_parent_id"; + + /** + * The gtask id + *

Type : TEXT

+ */ + public static final String GTASK_ID = "gtask_id"; + + /** + * The version code + *

Type : INTEGER (long)

+ */ + public static final String VERSION = "version"; + } + + public interface DataColumns { + /** + * The unique ID for a row + *

Type: INTEGER (long)

+ */ + public static final String ID = "_id"; + + /** + * The MIME type of the item represented by this row. + *

Type: Text

+ */ + public static final String MIME_TYPE = "mime_type"; + + /** + * The reference id to note that this data belongs to + *

Type: INTEGER (long)

+ */ + public static final String NOTE_ID = "note_id"; + + /** + * Created data for note or folder + *

Type: INTEGER (long)

+ */ + public static final String CREATED_DATE = "created_date"; + + /** + * Latest modified date + *

Type: INTEGER (long)

+ */ + public static final String MODIFIED_DATE = "modified_date"; + + /** + * Data's content + *

Type: TEXT

+ */ + public static final String CONTENT = "content"; + + + /** + * Generic data column, the meaning is {@link #MIMETYPE} specific, used for + * integer data type + *

Type: INTEGER

+ */ + public static final String DATA1 = "data1"; + + /** + * Generic data column, the meaning is {@link #MIMETYPE} specific, used for + * integer data type + *

Type: INTEGER

+ */ + public static final String DATA2 = "data2"; + + /** + * Generic data column, the meaning is {@link #MIMETYPE} specific, used for + * TEXT data type + *

Type: TEXT

+ */ + public static final String DATA3 = "data3"; + + /** + * Generic data column, the meaning is {@link #MIMETYPE} specific, used for + * TEXT data type + *

Type: TEXT

+ */ + public static final String DATA4 = "data4"; + + /** + * Generic data column, the meaning is {@link #MIMETYPE} specific, used for + * TEXT data type + *

Type: TEXT

+ */ + public static final String DATA5 = "data5"; + } + + public static final class TextNote implements DataColumns { + /** + * Mode to indicate the text in check list mode or not + *

Type: Integer 1:check list mode 0: normal mode

+ */ + public static final String MODE = DATA1; + + public static final int MODE_CHECK_LIST = 1; + + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/text_note"; + + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/text_note"; + + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/text_note"); + } + + public static final class CallNote implements DataColumns { + /** + * Call date for this record + *

Type: INTEGER (long)

+ */ + public static final String CALL_DATE = DATA1; + + /** + * Phone number for this record + *

Type: TEXT

+ */ + public static final String PHONE_NUMBER = DATA3; + + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/call_note"; + + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/call_note"; + + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/call_note"); + } +} diff --git a/doc/CJR_codereading/data/NotesDatabaseHelper.java b/doc/CJR_codereading/data/NotesDatabaseHelper.java new file mode 100644 index 0000000..ffe5d57 --- /dev/null +++ b/doc/CJR_codereading/data/NotesDatabaseHelper.java @@ -0,0 +1,362 @@ +/* + * 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.data; + +import android.content.ContentValues; +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.util.Log; + +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.DataConstants; +import net.micode.notes.data.Notes.NoteColumns; + + +public class NotesDatabaseHelper extends SQLiteOpenHelper { + private static final String DB_NAME = "note.db"; + + private static final int DB_VERSION = 4; + + public interface TABLE { + public static final String NOTE = "note"; + + public static final String DATA = "data"; + } + + private static final String TAG = "NotesDatabaseHelper"; + + private static NotesDatabaseHelper mInstance; + + private static final String CREATE_NOTE_TABLE_SQL = + "CREATE TABLE " + TABLE.NOTE + "(" + + NoteColumns.ID + " INTEGER PRIMARY KEY," + + NoteColumns.PARENT_ID + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.ALERTED_DATE + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.BG_COLOR_ID + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + + NoteColumns.HAS_ATTACHMENT + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + + NoteColumns.NOTES_COUNT + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.SNIPPET + " TEXT NOT NULL DEFAULT ''," + + NoteColumns.TYPE + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.WIDGET_ID + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.WIDGET_TYPE + " INTEGER NOT NULL DEFAULT -1," + + NoteColumns.SYNC_ID + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.LOCAL_MODIFIED + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.ORIGIN_PARENT_ID + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''," + + NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0" + + ")"; + + private static final String CREATE_DATA_TABLE_SQL = + "CREATE TABLE " + TABLE.DATA + "(" + + DataColumns.ID + " INTEGER PRIMARY KEY," + + DataColumns.MIME_TYPE + " TEXT NOT NULL," + + DataColumns.NOTE_ID + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + + NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + + DataColumns.CONTENT + " TEXT NOT NULL DEFAULT ''," + + DataColumns.DATA1 + " INTEGER," + + DataColumns.DATA2 + " INTEGER," + + DataColumns.DATA3 + " TEXT NOT NULL DEFAULT ''," + + DataColumns.DATA4 + " TEXT NOT NULL DEFAULT ''," + + DataColumns.DATA5 + " TEXT NOT NULL DEFAULT ''" + + ")"; + + private static final String CREATE_DATA_NOTE_ID_INDEX_SQL = + "CREATE INDEX IF NOT EXISTS note_id_index ON " + + TABLE.DATA + "(" + DataColumns.NOTE_ID + ");"; + + /** + * Increase folder's note count when move note to the folder + */ + private static final String NOTE_INCREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER = + "CREATE TRIGGER increase_folder_count_on_update "+ + " AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE + + " BEGIN " + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" + + " WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" + + " END"; + + /** + * Decrease folder's note count when move note from folder + */ + private static final String NOTE_DECREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER = + "CREATE TRIGGER decrease_folder_count_on_update " + + " AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE + + " BEGIN " + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" + + " WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID + + " AND " + NoteColumns.NOTES_COUNT + ">0" + ";" + + " END"; + + /** + * Increase folder's note count when insert new note to the folder + */ + private static final String NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER = + "CREATE TRIGGER increase_folder_count_on_insert " + + " AFTER INSERT ON " + TABLE.NOTE + + " BEGIN " + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" + + " WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" + + " END"; + + /** + * Decrease folder's note count when delete note from the folder + */ + private static final String NOTE_DECREASE_FOLDER_COUNT_ON_DELETE_TRIGGER = + "CREATE TRIGGER decrease_folder_count_on_delete " + + " AFTER DELETE ON " + TABLE.NOTE + + " BEGIN " + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" + + " WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID + + " AND " + NoteColumns.NOTES_COUNT + ">0;" + + " END"; + + /** + * Update note's content when insert data with type {@link DataConstants#NOTE} + */ + private static final String DATA_UPDATE_NOTE_CONTENT_ON_INSERT_TRIGGER = + "CREATE TRIGGER update_note_content_on_insert " + + " AFTER INSERT ON " + TABLE.DATA + + " WHEN new." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" + + " BEGIN" + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT + + " WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" + + " END"; + + /** + * Update note's content when data with {@link DataConstants#NOTE} type has changed + */ + private static final String DATA_UPDATE_NOTE_CONTENT_ON_UPDATE_TRIGGER = + "CREATE TRIGGER update_note_content_on_update " + + " AFTER UPDATE ON " + TABLE.DATA + + " WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" + + " BEGIN" + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT + + " WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" + + " END"; + + /** + * Update note's content when data with {@link DataConstants#NOTE} type has deleted + */ + private static final String DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER = + "CREATE TRIGGER update_note_content_on_delete " + + " AFTER delete ON " + TABLE.DATA + + " WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" + + " BEGIN" + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.SNIPPET + "=''" + + " WHERE " + NoteColumns.ID + "=old." + DataColumns.NOTE_ID + ";" + + " END"; + + /** + * Delete datas belong to note which has been deleted + */ + private static final String NOTE_DELETE_DATA_ON_DELETE_TRIGGER = + "CREATE TRIGGER delete_data_on_delete " + + " AFTER DELETE ON " + TABLE.NOTE + + " BEGIN" + + " DELETE FROM " + TABLE.DATA + + " WHERE " + DataColumns.NOTE_ID + "=old." + NoteColumns.ID + ";" + + " END"; + + /** + * Delete notes belong to folder which has been deleted + */ + private static final String FOLDER_DELETE_NOTES_ON_DELETE_TRIGGER = + "CREATE TRIGGER folder_delete_notes_on_delete " + + " AFTER DELETE ON " + TABLE.NOTE + + " BEGIN" + + " DELETE FROM " + TABLE.NOTE + + " WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" + + " END"; + + /** + * Move notes belong to folder which has been moved to trash folder + */ + private static final String FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER = + "CREATE TRIGGER folder_move_notes_on_trash " + + " AFTER UPDATE ON " + TABLE.NOTE + + " WHEN new." + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER + + " BEGIN" + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER + + " WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" + + " END"; + + public NotesDatabaseHelper(Context context) { + super(context, DB_NAME, null, DB_VERSION); + } + + public void createNoteTable(SQLiteDatabase db) { + db.execSQL(CREATE_NOTE_TABLE_SQL); + reCreateNoteTableTriggers(db); + createSystemFolder(db); + Log.d(TAG, "note table has been created"); + } + + private void reCreateNoteTableTriggers(SQLiteDatabase db) { + db.execSQL("DROP TRIGGER IF EXISTS increase_folder_count_on_update"); + db.execSQL("DROP TRIGGER IF EXISTS decrease_folder_count_on_update"); + db.execSQL("DROP TRIGGER IF EXISTS decrease_folder_count_on_delete"); + db.execSQL("DROP TRIGGER IF EXISTS delete_data_on_delete"); + db.execSQL("DROP TRIGGER IF EXISTS increase_folder_count_on_insert"); + db.execSQL("DROP TRIGGER IF EXISTS folder_delete_notes_on_delete"); + db.execSQL("DROP TRIGGER IF EXISTS folder_move_notes_on_trash"); + + db.execSQL(NOTE_INCREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER); + db.execSQL(NOTE_DECREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER); + db.execSQL(NOTE_DECREASE_FOLDER_COUNT_ON_DELETE_TRIGGER); + db.execSQL(NOTE_DELETE_DATA_ON_DELETE_TRIGGER); + db.execSQL(NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER); + db.execSQL(FOLDER_DELETE_NOTES_ON_DELETE_TRIGGER); + db.execSQL(FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER); + } + + private void createSystemFolder(SQLiteDatabase db) { + ContentValues values = new ContentValues(); + + /** + * call record foler for call notes + */ + values.put(NoteColumns.ID, Notes.ID_CALL_RECORD_FOLDER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + + /** + * root folder which is default folder + */ + values.clear(); + values.put(NoteColumns.ID, Notes.ID_ROOT_FOLDER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + + /** + * temporary folder which is used for moving note + */ + values.clear(); + values.put(NoteColumns.ID, Notes.ID_TEMPARAY_FOLDER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + + /** + * create trash folder + */ + values.clear(); + values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + } + + public void createDataTable(SQLiteDatabase db) { + db.execSQL(CREATE_DATA_TABLE_SQL); + reCreateDataTableTriggers(db); + db.execSQL(CREATE_DATA_NOTE_ID_INDEX_SQL); + Log.d(TAG, "data table has been created"); + } + + private void reCreateDataTableTriggers(SQLiteDatabase db) { + db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_insert"); + db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_update"); + db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_delete"); + + db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_INSERT_TRIGGER); + db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_UPDATE_TRIGGER); + db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER); + } + + static synchronized NotesDatabaseHelper getInstance(Context context) { + if (mInstance == null) { + mInstance = new NotesDatabaseHelper(context); + } + return mInstance; + } + + @Override + public void onCreate(SQLiteDatabase db) { + createNoteTable(db); + createDataTable(db); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + boolean reCreateTriggers = false; + boolean skipV2 = false; + + if (oldVersion == 1) { + upgradeToV2(db); + skipV2 = true; // this upgrade including the upgrade from v2 to v3 + oldVersion++; + } + + if (oldVersion == 2 && !skipV2) { + upgradeToV3(db); + reCreateTriggers = true; + oldVersion++; + } + + if (oldVersion == 3) { + upgradeToV4(db); + oldVersion++; + } + + if (reCreateTriggers) { + reCreateNoteTableTriggers(db); + reCreateDataTableTriggers(db); + } + + if (oldVersion != newVersion) { + throw new IllegalStateException("Upgrade notes database to version " + newVersion + + "fails"); + } + } + + private void upgradeToV2(SQLiteDatabase db) { + db.execSQL("DROP TABLE IF EXISTS " + TABLE.NOTE); + db.execSQL("DROP TABLE IF EXISTS " + TABLE.DATA); + createNoteTable(db); + createDataTable(db); + } + + private void upgradeToV3(SQLiteDatabase db) { + // drop unused triggers + db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_insert"); + db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_delete"); + db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_update"); + // add a column for gtask id + db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.GTASK_ID + + " TEXT NOT NULL DEFAULT ''"); + // add a trash system folder + ContentValues values = new ContentValues(); + values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + } + + private void upgradeToV4(SQLiteDatabase db) { + db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.VERSION + + " INTEGER NOT NULL DEFAULT 0"); + } +} diff --git a/doc/CJR_codereading/data/NotesProvider.java b/doc/CJR_codereading/data/NotesProvider.java new file mode 100644 index 0000000..edb0a60 --- /dev/null +++ b/doc/CJR_codereading/data/NotesProvider.java @@ -0,0 +1,305 @@ +/* + * 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.data; + + +import android.app.SearchManager; +import android.content.ContentProvider; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Intent; +import android.content.UriMatcher; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; +import android.text.TextUtils; +import android.util.Log; + +import net.micode.notes.R; +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.data.NotesDatabaseHelper.TABLE; + + +public class NotesProvider extends ContentProvider { + private static final UriMatcher mMatcher; + + private NotesDatabaseHelper mHelper; + + private static final String TAG = "NotesProvider"; + + private static final int URI_NOTE = 1; + private static final int URI_NOTE_ITEM = 2; + private static final int URI_DATA = 3; + private static final int URI_DATA_ITEM = 4; + + private static final int URI_SEARCH = 5; + private static final int URI_SEARCH_SUGGEST = 6; + + static { + mMatcher = new UriMatcher(UriMatcher.NO_MATCH); + mMatcher.addURI(Notes.AUTHORITY, "note", URI_NOTE); + mMatcher.addURI(Notes.AUTHORITY, "note/#", URI_NOTE_ITEM); + mMatcher.addURI(Notes.AUTHORITY, "data", URI_DATA); + mMatcher.addURI(Notes.AUTHORITY, "data/#", URI_DATA_ITEM); + mMatcher.addURI(Notes.AUTHORITY, "search", URI_SEARCH); + mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, URI_SEARCH_SUGGEST); + mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", URI_SEARCH_SUGGEST); + } + + /** + * x'0A' represents the '\n' character in sqlite. For title and content in the search result, + * we will trim '\n' and white space in order to show more information. + */ + private static final String NOTES_SEARCH_PROJECTION = NoteColumns.ID + "," + + NoteColumns.ID + " AS " + SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA + "," + + "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_1 + "," + + "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_2 + "," + + R.drawable.search_result + " AS " + SearchManager.SUGGEST_COLUMN_ICON_1 + "," + + "'" + Intent.ACTION_VIEW + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_ACTION + "," + + "'" + Notes.TextNote.CONTENT_TYPE + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA; + + private static String NOTES_SNIPPET_SEARCH_QUERY = "SELECT " + NOTES_SEARCH_PROJECTION + + " FROM " + TABLE.NOTE + + " WHERE " + NoteColumns.SNIPPET + " LIKE ?" + + " AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER + + " AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE; + + @Override + public boolean onCreate() { + mHelper = NotesDatabaseHelper.getInstance(getContext()); + return true; + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, + String sortOrder) { + Cursor c = null; + SQLiteDatabase db = mHelper.getReadableDatabase(); + String id = null; + switch (mMatcher.match(uri)) { + case URI_NOTE: + c = db.query(TABLE.NOTE, projection, selection, selectionArgs, null, null, + sortOrder); + break; + case URI_NOTE_ITEM: + id = uri.getPathSegments().get(1); + c = db.query(TABLE.NOTE, projection, NoteColumns.ID + "=" + id + + parseSelection(selection), selectionArgs, null, null, sortOrder); + break; + case URI_DATA: + c = db.query(TABLE.DATA, projection, selection, selectionArgs, null, null, + sortOrder); + break; + case URI_DATA_ITEM: + id = uri.getPathSegments().get(1); + c = db.query(TABLE.DATA, projection, DataColumns.ID + "=" + id + + parseSelection(selection), selectionArgs, null, null, sortOrder); + break; + case URI_SEARCH: + case URI_SEARCH_SUGGEST: + if (sortOrder != null || projection != null) { + throw new IllegalArgumentException( + "do not specify sortOrder, selection, selectionArgs, or projection" + "with this query"); + } + + String searchString = null; + if (mMatcher.match(uri) == URI_SEARCH_SUGGEST) { + if (uri.getPathSegments().size() > 1) { + searchString = uri.getPathSegments().get(1); + } + } else { + searchString = uri.getQueryParameter("pattern"); + } + + if (TextUtils.isEmpty(searchString)) { + return null; + } + + try { + searchString = String.format("%%%s%%", searchString); + c = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY, + new String[] { searchString }); + } catch (IllegalStateException ex) { + Log.e(TAG, "got exception: " + ex.toString()); + } + break; + default: + throw new IllegalArgumentException("Unknown URI " + uri); + } + if (c != null) { + c.setNotificationUri(getContext().getContentResolver(), uri); + } + return c; + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + SQLiteDatabase db = mHelper.getWritableDatabase(); + long dataId = 0, noteId = 0, insertedId = 0; + switch (mMatcher.match(uri)) { + case URI_NOTE: + insertedId = noteId = db.insert(TABLE.NOTE, null, values); + break; + case URI_DATA: + if (values.containsKey(DataColumns.NOTE_ID)) { + noteId = values.getAsLong(DataColumns.NOTE_ID); + } else { + Log.d(TAG, "Wrong data format without note id:" + values.toString()); + } + insertedId = dataId = db.insert(TABLE.DATA, null, values); + break; + default: + throw new IllegalArgumentException("Unknown URI " + uri); + } + // Notify the note uri + if (noteId > 0) { + getContext().getContentResolver().notifyChange( + ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null); + } + + // Notify the data uri + if (dataId > 0) { + getContext().getContentResolver().notifyChange( + ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), null); + } + + return ContentUris.withAppendedId(uri, insertedId); + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + int count = 0; + String id = null; + SQLiteDatabase db = mHelper.getWritableDatabase(); + boolean deleteData = false; + switch (mMatcher.match(uri)) { + case URI_NOTE: + selection = "(" + selection + ") AND " + NoteColumns.ID + ">0 "; + count = db.delete(TABLE.NOTE, selection, selectionArgs); + break; + case URI_NOTE_ITEM: + id = uri.getPathSegments().get(1); + /** + * ID that smaller than 0 is system folder which is not allowed to + * trash + */ + long noteId = Long.valueOf(id); + if (noteId <= 0) { + break; + } + count = db.delete(TABLE.NOTE, + NoteColumns.ID + "=" + id + parseSelection(selection), selectionArgs); + break; + case URI_DATA: + count = db.delete(TABLE.DATA, selection, selectionArgs); + deleteData = true; + break; + case URI_DATA_ITEM: + id = uri.getPathSegments().get(1); + count = db.delete(TABLE.DATA, + DataColumns.ID + "=" + id + parseSelection(selection), selectionArgs); + deleteData = true; + break; + default: + throw new IllegalArgumentException("Unknown URI " + uri); + } + if (count > 0) { + if (deleteData) { + getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null); + } + getContext().getContentResolver().notifyChange(uri, null); + } + return count; + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + int count = 0; + String id = null; + SQLiteDatabase db = mHelper.getWritableDatabase(); + boolean updateData = false; + switch (mMatcher.match(uri)) { + case URI_NOTE: + increaseNoteVersion(-1, selection, selectionArgs); + count = db.update(TABLE.NOTE, values, selection, selectionArgs); + break; + case URI_NOTE_ITEM: + id = uri.getPathSegments().get(1); + increaseNoteVersion(Long.valueOf(id), selection, selectionArgs); + count = db.update(TABLE.NOTE, values, NoteColumns.ID + "=" + id + + parseSelection(selection), selectionArgs); + break; + case URI_DATA: + count = db.update(TABLE.DATA, values, selection, selectionArgs); + updateData = true; + break; + case URI_DATA_ITEM: + id = uri.getPathSegments().get(1); + count = db.update(TABLE.DATA, values, DataColumns.ID + "=" + id + + parseSelection(selection), selectionArgs); + updateData = true; + break; + default: + throw new IllegalArgumentException("Unknown URI " + uri); + } + + if (count > 0) { + if (updateData) { + getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null); + } + getContext().getContentResolver().notifyChange(uri, null); + } + return count; + } + + private String parseSelection(String selection) { + return (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : ""); + } + + private void increaseNoteVersion(long id, String selection, String[] selectionArgs) { + StringBuilder sql = new StringBuilder(120); + sql.append("UPDATE "); + sql.append(TABLE.NOTE); + sql.append(" SET "); + sql.append(NoteColumns.VERSION); + sql.append("=" + NoteColumns.VERSION + "+1 "); + + if (id > 0 || !TextUtils.isEmpty(selection)) { + sql.append(" WHERE "); + } + if (id > 0) { + sql.append(NoteColumns.ID + "=" + String.valueOf(id)); + } + if (!TextUtils.isEmpty(selection)) { + String selectString = id > 0 ? parseSelection(selection) : selection; + for (String args : selectionArgs) { + selectString = selectString.replaceFirst("\\?", args); + } + sql.append(selectString); + } + + mHelper.getWritableDatabase().execSQL(sql.toString()); + } + + @Override + public String getType(Uri uri) { + // TODO Auto-generated method stub + return null; + } + +} diff --git a/doc/CJR_codereading/model/Note.java b/doc/CJR_codereading/model/Note.java new file mode 100644 index 0000000..6706cf6 --- /dev/null +++ b/doc/CJR_codereading/model/Note.java @@ -0,0 +1,253 @@ +/* + * 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.model; +import android.content.ContentProviderOperation; +import android.content.ContentProviderResult; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.OperationApplicationException; +import android.net.Uri; +import android.os.RemoteException; +import android.util.Log; + +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.CallNote; +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.data.Notes.TextNote; + +import java.util.ArrayList; + + +public class Note { + private ContentValues mNoteDiffValues; + private NoteData mNoteData; + private static final String TAG = "Note"; + /** + * Create a new note id for adding a new note to databases + */ + public static synchronized long getNewNoteId(Context context, long folderId) { + // Create a new note in the database + ContentValues values = new ContentValues(); + long createdTime = System.currentTimeMillis(); + values.put(NoteColumns.CREATED_DATE, createdTime); + values.put(NoteColumns.MODIFIED_DATE, createdTime); + values.put(NoteColumns.TYPE, Notes.TYPE_NOTE); + values.put(NoteColumns.LOCAL_MODIFIED, 1); + values.put(NoteColumns.PARENT_ID, folderId); + Uri uri = context.getContentResolver().insert(Notes.CONTENT_NOTE_URI, values); + + long noteId = 0; + try { + noteId = Long.valueOf(uri.getPathSegments().get(1)); + } catch (NumberFormatException e) { + Log.e(TAG, "Get note id error :" + e.toString()); + noteId = 0; + } + if (noteId == -1) { + throw new IllegalStateException("Wrong note id:" + noteId); + } + return noteId; + } + + public Note() { + mNoteDiffValues = new ContentValues(); + mNoteData = new NoteData(); + } + + public void setNoteValue(String key, String value) { + mNoteDiffValues.put(key, value); + mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); + mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); + } + + public void setTextData(String key, String value) { + mNoteData.setTextData(key, value); + } + + public void setTextDataId(long id) { + mNoteData.setTextDataId(id); + } + + public long getTextDataId() { + return mNoteData.mTextDataId; + } + + public void setCallDataId(long id) { + mNoteData.setCallDataId(id); + } + + public void setCallData(String key, String value) { + mNoteData.setCallData(key, value); + } + + public boolean isLocalModified() { + return mNoteDiffValues.size() > 0 || mNoteData.isLocalModified(); + } + + public boolean syncNote(Context context, long noteId) { + if (noteId <= 0) { + throw new IllegalArgumentException("Wrong note id:" + noteId); + } + + if (!isLocalModified()) { + return true; + } + + /** + * In theory, once data changed, the note should be updated on {@link NoteColumns#LOCAL_MODIFIED} and + * {@link NoteColumns#MODIFIED_DATE}. For data safety, though update note fails, we also update the + * note data info + */ + if (context.getContentResolver().update( + ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), mNoteDiffValues, null, + null) == 0) { + Log.e(TAG, "Update note error, should not happen"); + // Do not return, fall through + } + mNoteDiffValues.clear(); + + if (mNoteData.isLocalModified() + && (mNoteData.pushIntoContentResolver(context, noteId) == null)) { + return false; + } + + return true; + } + + private class NoteData { + private long mTextDataId; + + private ContentValues mTextDataValues; + + private long mCallDataId; + + private ContentValues mCallDataValues; + + private static final String TAG = "NoteData"; + + public NoteData() { + mTextDataValues = new ContentValues(); + mCallDataValues = new ContentValues(); + mTextDataId = 0; + mCallDataId = 0; + } + + boolean isLocalModified() { + return mTextDataValues.size() > 0 || mCallDataValues.size() > 0; + } + + void setTextDataId(long id) { + if(id <= 0) { + throw new IllegalArgumentException("Text data id should larger than 0"); + } + mTextDataId = id; + } + + void setCallDataId(long id) { + if (id <= 0) { + throw new IllegalArgumentException("Call data id should larger than 0"); + } + mCallDataId = id; + } + + void setCallData(String key, String value) { + mCallDataValues.put(key, value); + mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); + mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); + } + + void setTextData(String key, String value) { + mTextDataValues.put(key, value); + mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); + mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); + } + + Uri pushIntoContentResolver(Context context, long noteId) { + /** + * Check for safety + */ + if (noteId <= 0) { + throw new IllegalArgumentException("Wrong note id:" + noteId); + } + + ArrayList operationList = new ArrayList(); + ContentProviderOperation.Builder builder = null; + + if(mTextDataValues.size() > 0) { + mTextDataValues.put(DataColumns.NOTE_ID, noteId); + if (mTextDataId == 0) { + mTextDataValues.put(DataColumns.MIME_TYPE, TextNote.CONTENT_ITEM_TYPE); + Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI, + mTextDataValues); + try { + setTextDataId(Long.valueOf(uri.getPathSegments().get(1))); + } catch (NumberFormatException e) { + Log.e(TAG, "Insert new text data fail with noteId" + noteId); + mTextDataValues.clear(); + return null; + } + } else { + builder = ContentProviderOperation.newUpdate(ContentUris.withAppendedId( + Notes.CONTENT_DATA_URI, mTextDataId)); + builder.withValues(mTextDataValues); + operationList.add(builder.build()); + } + mTextDataValues.clear(); + } + + if(mCallDataValues.size() > 0) { + mCallDataValues.put(DataColumns.NOTE_ID, noteId); + if (mCallDataId == 0) { + mCallDataValues.put(DataColumns.MIME_TYPE, CallNote.CONTENT_ITEM_TYPE); + Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI, + mCallDataValues); + try { + setCallDataId(Long.valueOf(uri.getPathSegments().get(1))); + } catch (NumberFormatException e) { + Log.e(TAG, "Insert new call data fail with noteId" + noteId); + mCallDataValues.clear(); + return null; + } + } else { + builder = ContentProviderOperation.newUpdate(ContentUris.withAppendedId( + Notes.CONTENT_DATA_URI, mCallDataId)); + builder.withValues(mCallDataValues); + operationList.add(builder.build()); + } + mCallDataValues.clear(); + } + + if (operationList.size() > 0) { + try { + ContentProviderResult[] results = context.getContentResolver().applyBatch( + Notes.AUTHORITY, operationList); + return (results == null || results.length == 0 || results[0] == null) ? null + : ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId); + } catch (RemoteException e) { + Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); + return null; + } catch (OperationApplicationException e) { + Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); + return null; + } + } + return null; + } + } +} diff --git a/doc/CJR_codereading/model/WorkingNote.java b/doc/CJR_codereading/model/WorkingNote.java new file mode 100644 index 0000000..be081e4 --- /dev/null +++ b/doc/CJR_codereading/model/WorkingNote.java @@ -0,0 +1,368 @@ +/* + * 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.model; + +import android.appwidget.AppWidgetManager; +import android.content.ContentUris; +import android.content.Context; +import android.database.Cursor; +import android.text.TextUtils; +import android.util.Log; + +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.CallNote; +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.DataConstants; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.data.Notes.TextNote; +import net.micode.notes.tool.ResourceParser.NoteBgResources; + + +public class WorkingNote { + // Note for the working note + private Note mNote; + // Note Id + private long mNoteId; + // Note content + private String mContent; + // Note mode + private int mMode; + + private long mAlertDate; + + private long mModifiedDate; + + private int mBgColorId; + + private int mWidgetId; + + private int mWidgetType; + + private long mFolderId; + + private Context mContext; + + private static final String TAG = "WorkingNote"; + + private boolean mIsDeleted; + + private NoteSettingChangedListener mNoteSettingStatusListener; + + public static final String[] DATA_PROJECTION = new String[] { + DataColumns.ID, + DataColumns.CONTENT, + DataColumns.MIME_TYPE, + DataColumns.DATA1, + DataColumns.DATA2, + DataColumns.DATA3, + DataColumns.DATA4, + }; + + public static final String[] NOTE_PROJECTION = new String[] { + NoteColumns.PARENT_ID, + NoteColumns.ALERTED_DATE, + NoteColumns.BG_COLOR_ID, + NoteColumns.WIDGET_ID, + NoteColumns.WIDGET_TYPE, + NoteColumns.MODIFIED_DATE + }; + + private static final int DATA_ID_COLUMN = 0; + + private static final int DATA_CONTENT_COLUMN = 1; + + private static final int DATA_MIME_TYPE_COLUMN = 2; + + private static final int DATA_MODE_COLUMN = 3; + + private static final int NOTE_PARENT_ID_COLUMN = 0; + + private static final int NOTE_ALERTED_DATE_COLUMN = 1; + + private static final int NOTE_BG_COLOR_ID_COLUMN = 2; + + private static final int NOTE_WIDGET_ID_COLUMN = 3; + + private static final int NOTE_WIDGET_TYPE_COLUMN = 4; + + private static final int NOTE_MODIFIED_DATE_COLUMN = 5; + + // New note construct + private WorkingNote(Context context, long folderId) { + mContext = context; + mAlertDate = 0; + mModifiedDate = System.currentTimeMillis(); + mFolderId = folderId; + mNote = new Note(); + mNoteId = 0; + mIsDeleted = false; + mMode = 0; + mWidgetType = Notes.TYPE_WIDGET_INVALIDE; + } + + // Existing note construct + private WorkingNote(Context context, long noteId, long folderId) { + mContext = context; + mNoteId = noteId; + mFolderId = folderId; + mIsDeleted = false; + mNote = new Note(); + loadNote(); + } + + private void loadNote() { + Cursor cursor = mContext.getContentResolver().query( + ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mNoteId), NOTE_PROJECTION, null, + null, null); + + if (cursor != null) { + if (cursor.moveToFirst()) { + mFolderId = cursor.getLong(NOTE_PARENT_ID_COLUMN); + mBgColorId = cursor.getInt(NOTE_BG_COLOR_ID_COLUMN); + mWidgetId = cursor.getInt(NOTE_WIDGET_ID_COLUMN); + mWidgetType = cursor.getInt(NOTE_WIDGET_TYPE_COLUMN); + mAlertDate = cursor.getLong(NOTE_ALERTED_DATE_COLUMN); + mModifiedDate = cursor.getLong(NOTE_MODIFIED_DATE_COLUMN); + } + cursor.close(); + } else { + Log.e(TAG, "No note with id:" + mNoteId); + throw new IllegalArgumentException("Unable to find note with id " + mNoteId); + } + loadNoteData(); + } + + private void loadNoteData() { + Cursor cursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI, DATA_PROJECTION, + DataColumns.NOTE_ID + "=?", new String[] { + String.valueOf(mNoteId) + }, null); + + if (cursor != null) { + if (cursor.moveToFirst()) { + do { + String type = cursor.getString(DATA_MIME_TYPE_COLUMN); + if (DataConstants.NOTE.equals(type)) { + mContent = cursor.getString(DATA_CONTENT_COLUMN); + mMode = cursor.getInt(DATA_MODE_COLUMN); + mNote.setTextDataId(cursor.getLong(DATA_ID_COLUMN)); + } else if (DataConstants.CALL_NOTE.equals(type)) { + mNote.setCallDataId(cursor.getLong(DATA_ID_COLUMN)); + } else { + Log.d(TAG, "Wrong note type with type:" + type); + } + } while (cursor.moveToNext()); + } + cursor.close(); + } else { + Log.e(TAG, "No data with id:" + mNoteId); + throw new IllegalArgumentException("Unable to find note's data with id " + mNoteId); + } + } + + public static WorkingNote createEmptyNote(Context context, long folderId, int widgetId, + int widgetType, int defaultBgColorId) { + WorkingNote note = new WorkingNote(context, folderId); + note.setBgColorId(defaultBgColorId); + note.setWidgetId(widgetId); + note.setWidgetType(widgetType); + return note; + } + + public static WorkingNote load(Context context, long id) { + return new WorkingNote(context, id, 0); + } + + public synchronized boolean saveNote() { + if (isWorthSaving()) { + if (!existInDatabase()) { + if ((mNoteId = Note.getNewNoteId(mContext, mFolderId)) == 0) { + Log.e(TAG, "Create new note fail with id:" + mNoteId); + return false; + } + } + + mNote.syncNote(mContext, mNoteId); + + /** + * Update widget content if there exist any widget of this note + */ + if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID + && mWidgetType != Notes.TYPE_WIDGET_INVALIDE + && mNoteSettingStatusListener != null) { + mNoteSettingStatusListener.onWidgetChanged(); + } + return true; + } else { + return false; + } + } + + public boolean existInDatabase() { + return mNoteId > 0; + } + + private boolean isWorthSaving() { + if (mIsDeleted || (!existInDatabase() && TextUtils.isEmpty(mContent)) + || (existInDatabase() && !mNote.isLocalModified())) { + return false; + } else { + return true; + } + } + + public void setOnSettingStatusChangedListener(NoteSettingChangedListener l) { + mNoteSettingStatusListener = l; + } + + public void setAlertDate(long date, boolean set) { + if (date != mAlertDate) { + mAlertDate = date; + mNote.setNoteValue(NoteColumns.ALERTED_DATE, String.valueOf(mAlertDate)); + } + if (mNoteSettingStatusListener != null) { + mNoteSettingStatusListener.onClockAlertChanged(date, set); + } + } + + public void markDeleted(boolean mark) { + mIsDeleted = mark; + if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID + && mWidgetType != Notes.TYPE_WIDGET_INVALIDE && mNoteSettingStatusListener != null) { + mNoteSettingStatusListener.onWidgetChanged(); + } + } + + public void setBgColorId(int id) { + if (id != mBgColorId) { + mBgColorId = id; + if (mNoteSettingStatusListener != null) { + mNoteSettingStatusListener.onBackgroundColorChanged(); + } + mNote.setNoteValue(NoteColumns.BG_COLOR_ID, String.valueOf(id)); + } + } + + public void setCheckListMode(int mode) { + if (mMode != mode) { + if (mNoteSettingStatusListener != null) { + mNoteSettingStatusListener.onCheckListModeChanged(mMode, mode); + } + mMode = mode; + mNote.setTextData(TextNote.MODE, String.valueOf(mMode)); + } + } + + public void setWidgetType(int type) { + if (type != mWidgetType) { + mWidgetType = type; + mNote.setNoteValue(NoteColumns.WIDGET_TYPE, String.valueOf(mWidgetType)); + } + } + + public void setWidgetId(int id) { + if (id != mWidgetId) { + mWidgetId = id; + mNote.setNoteValue(NoteColumns.WIDGET_ID, String.valueOf(mWidgetId)); + } + } + + public void setWorkingText(String text) { + if (!TextUtils.equals(mContent, text)) { + mContent = text; + mNote.setTextData(DataColumns.CONTENT, mContent); + } + } + + public void convertToCallNote(String phoneNumber, long callDate) { + mNote.setCallData(CallNote.CALL_DATE, String.valueOf(callDate)); + mNote.setCallData(CallNote.PHONE_NUMBER, phoneNumber); + mNote.setNoteValue(NoteColumns.PARENT_ID, String.valueOf(Notes.ID_CALL_RECORD_FOLDER)); + } + + public boolean hasClockAlert() { + return (mAlertDate > 0 ? true : false); + } + + public String getContent() { + return mContent; + } + + public long getAlertDate() { + return mAlertDate; + } + + public long getModifiedDate() { + return mModifiedDate; + } + + public int getBgColorResId() { + return NoteBgResources.getNoteBgResource(mBgColorId); + } + + public int getBgColorId() { + return mBgColorId; + } + + public int getTitleBgResId() { + return NoteBgResources.getNoteTitleBgResource(mBgColorId); + } + + public int getCheckListMode() { + return mMode; + } + + public long getNoteId() { + return mNoteId; + } + + public long getFolderId() { + return mFolderId; + } + + public int getWidgetId() { + return mWidgetId; + } + + public int getWidgetType() { + return mWidgetType; + } + + public interface NoteSettingChangedListener { + /** + * Called when the background color of current note has just changed + */ + void onBackgroundColorChanged(); + + /** + * Called when user set clock + */ + void onClockAlertChanged(long date, boolean set); + + /** + * Call when user create note from widget + */ + void onWidgetChanged(); + + /** + * Call when switch between check list mode and normal mode + * @param oldMode is previous mode before change + * @param newMode is new mode + */ + void onCheckListModeChanged(int oldMode, int newMode); + } +} diff --git a/doc/CJR_codereading/tool/BackupUtils.java b/doc/CJR_codereading/tool/BackupUtils.java new file mode 100644 index 0000000..39f6ec4 --- /dev/null +++ b/doc/CJR_codereading/tool/BackupUtils.java @@ -0,0 +1,344 @@ +/* + * 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.tool; + +import android.content.Context; +import android.database.Cursor; +import android.os.Environment; +import android.text.TextUtils; +import android.text.format.DateFormat; +import android.util.Log; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.DataConstants; +import net.micode.notes.data.Notes.NoteColumns; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintStream; + + +public class BackupUtils { + private static final String TAG = "BackupUtils"; + // Singleton stuff + private static BackupUtils sInstance; + + public static synchronized BackupUtils getInstance(Context context) { + if (sInstance == null) { + sInstance = new BackupUtils(context); + } + return sInstance; + } + + /** + * Following states are signs to represents backup or restore + * status + */ + // Currently, the sdcard is not mounted + public static final int STATE_SD_CARD_UNMOUONTED = 0; + // The backup file not exist + public static final int STATE_BACKUP_FILE_NOT_EXIST = 1; + // The data is not well formated, may be changed by other programs + public static final int STATE_DATA_DESTROIED = 2; + // Some run-time exception which causes restore or backup fails + public static final int STATE_SYSTEM_ERROR = 3; + // Backup or restore success + public static final int STATE_SUCCESS = 4; + + private TextExport mTextExport; + + private BackupUtils(Context context) { + mTextExport = new TextExport(context); + } + + private static boolean externalStorageAvailable() { + return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()); + } + + public int exportToText() { + return mTextExport.exportToText(); + } + + public String getExportedTextFileName() { + return mTextExport.mFileName; + } + + public String getExportedTextFileDir() { + return mTextExport.mFileDirectory; + } + + private static class TextExport { + private static final String[] NOTE_PROJECTION = { + NoteColumns.ID, + NoteColumns.MODIFIED_DATE, + NoteColumns.SNIPPET, + NoteColumns.TYPE + }; + + private static final int NOTE_COLUMN_ID = 0; + + private static final int NOTE_COLUMN_MODIFIED_DATE = 1; + + private static final int NOTE_COLUMN_SNIPPET = 2; + + private static final String[] DATA_PROJECTION = { + DataColumns.CONTENT, + DataColumns.MIME_TYPE, + DataColumns.DATA1, + DataColumns.DATA2, + DataColumns.DATA3, + DataColumns.DATA4, + }; + + private static final int DATA_COLUMN_CONTENT = 0; + + private static final int DATA_COLUMN_MIME_TYPE = 1; + + private static final int DATA_COLUMN_CALL_DATE = 2; + + private static final int DATA_COLUMN_PHONE_NUMBER = 4; + + private final String [] TEXT_FORMAT; + private static final int FORMAT_FOLDER_NAME = 0; + private static final int FORMAT_NOTE_DATE = 1; + private static final int FORMAT_NOTE_CONTENT = 2; + + private Context mContext; + private String mFileName; + private String mFileDirectory; + + public TextExport(Context context) { + TEXT_FORMAT = context.getResources().getStringArray(R.array.format_for_exported_note); + mContext = context; + mFileName = ""; + mFileDirectory = ""; + } + + private String getFormat(int id) { + return TEXT_FORMAT[id]; + } + + /** + * Export the folder identified by folder id to text + */ + private void exportFolderToText(String folderId, PrintStream ps) { + // Query notes belong to this folder + Cursor notesCursor = mContext.getContentResolver().query(Notes.CONTENT_NOTE_URI, + NOTE_PROJECTION, NoteColumns.PARENT_ID + "=?", new String[] { + folderId + }, null); + + if (notesCursor != null) { + if (notesCursor.moveToFirst()) { + do { + // Print note's last modified date + ps.println(String.format(getFormat(FORMAT_NOTE_DATE), DateFormat.format( + mContext.getString(R.string.format_datetime_mdhm), + notesCursor.getLong(NOTE_COLUMN_MODIFIED_DATE)))); + // Query data belong to this note + String noteId = notesCursor.getString(NOTE_COLUMN_ID); + exportNoteToText(noteId, ps); + } while (notesCursor.moveToNext()); + } + notesCursor.close(); + } + } + + /** + * Export note identified by id to a print stream + */ + private void exportNoteToText(String noteId, PrintStream ps) { + Cursor dataCursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI, + DATA_PROJECTION, DataColumns.NOTE_ID + "=?", new String[] { + noteId + }, null); + + if (dataCursor != null) { + if (dataCursor.moveToFirst()) { + do { + String mimeType = dataCursor.getString(DATA_COLUMN_MIME_TYPE); + if (DataConstants.CALL_NOTE.equals(mimeType)) { + // Print phone number + String phoneNumber = dataCursor.getString(DATA_COLUMN_PHONE_NUMBER); + long callDate = dataCursor.getLong(DATA_COLUMN_CALL_DATE); + String location = dataCursor.getString(DATA_COLUMN_CONTENT); + + if (!TextUtils.isEmpty(phoneNumber)) { + ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT), + phoneNumber)); + } + // Print call date + ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT), DateFormat + .format(mContext.getString(R.string.format_datetime_mdhm), + callDate))); + // Print call attachment location + if (!TextUtils.isEmpty(location)) { + ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT), + location)); + } + } else if (DataConstants.NOTE.equals(mimeType)) { + String content = dataCursor.getString(DATA_COLUMN_CONTENT); + if (!TextUtils.isEmpty(content)) { + ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT), + content)); + } + } + } while (dataCursor.moveToNext()); + } + dataCursor.close(); + } + // print a line separator between note + try { + ps.write(new byte[] { + Character.LINE_SEPARATOR, Character.LETTER_NUMBER + }); + } catch (IOException e) { + Log.e(TAG, e.toString()); + } + } + + /** + * Note will be exported as text which is user readable + */ + public int exportToText() { + if (!externalStorageAvailable()) { + Log.d(TAG, "Media was not mounted"); + return STATE_SD_CARD_UNMOUONTED; + } + + PrintStream ps = getExportToTextPrintStream(); + if (ps == null) { + Log.e(TAG, "get print stream error"); + return STATE_SYSTEM_ERROR; + } + // First export folder and its notes + Cursor folderCursor = mContext.getContentResolver().query( + Notes.CONTENT_NOTE_URI, + NOTE_PROJECTION, + "(" + NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER + " AND " + + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER + ") OR " + + NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER, null, null); + + if (folderCursor != null) { + if (folderCursor.moveToFirst()) { + do { + // Print folder's name + String folderName = ""; + if(folderCursor.getLong(NOTE_COLUMN_ID) == Notes.ID_CALL_RECORD_FOLDER) { + folderName = mContext.getString(R.string.call_record_folder_name); + } else { + folderName = folderCursor.getString(NOTE_COLUMN_SNIPPET); + } + if (!TextUtils.isEmpty(folderName)) { + ps.println(String.format(getFormat(FORMAT_FOLDER_NAME), folderName)); + } + String folderId = folderCursor.getString(NOTE_COLUMN_ID); + exportFolderToText(folderId, ps); + } while (folderCursor.moveToNext()); + } + folderCursor.close(); + } + + // Export notes in root's folder + Cursor noteCursor = mContext.getContentResolver().query( + Notes.CONTENT_NOTE_URI, + NOTE_PROJECTION, + NoteColumns.TYPE + "=" + +Notes.TYPE_NOTE + " AND " + NoteColumns.PARENT_ID + + "=0", null, null); + + if (noteCursor != null) { + if (noteCursor.moveToFirst()) { + do { + ps.println(String.format(getFormat(FORMAT_NOTE_DATE), DateFormat.format( + mContext.getString(R.string.format_datetime_mdhm), + noteCursor.getLong(NOTE_COLUMN_MODIFIED_DATE)))); + // Query data belong to this note + String noteId = noteCursor.getString(NOTE_COLUMN_ID); + exportNoteToText(noteId, ps); + } while (noteCursor.moveToNext()); + } + noteCursor.close(); + } + ps.close(); + + return STATE_SUCCESS; + } + + /** + * Get a print stream pointed to the file {@generateExportedTextFile} + */ + private PrintStream getExportToTextPrintStream() { + File file = generateFileMountedOnSDcard(mContext, R.string.file_path, + R.string.file_name_txt_format); + if (file == null) { + Log.e(TAG, "create file to exported failed"); + return null; + } + mFileName = file.getName(); + mFileDirectory = mContext.getString(R.string.file_path); + PrintStream ps = null; + try { + FileOutputStream fos = new FileOutputStream(file); + ps = new PrintStream(fos); + } catch (FileNotFoundException e) { + e.printStackTrace(); + return null; + } catch (NullPointerException e) { + e.printStackTrace(); + return null; + } + return ps; + } + } + + /** + * Generate the text file to store imported data + */ + private static File generateFileMountedOnSDcard(Context context, int filePathResId, int fileNameFormatResId) { + StringBuilder sb = new StringBuilder(); + sb.append(Environment.getExternalStorageDirectory()); + sb.append(context.getString(filePathResId)); + File filedir = new File(sb.toString()); + sb.append(context.getString( + fileNameFormatResId, + DateFormat.format(context.getString(R.string.format_date_ymd), + System.currentTimeMillis()))); + File file = new File(sb.toString()); + + try { + if (!filedir.exists()) { + filedir.mkdir(); + } + if (!file.exists()) { + file.createNewFile(); + } + return file; + } catch (SecurityException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + + return null; + } +} + + diff --git a/doc/CJR_codereading/tool/DataUtils.java b/doc/CJR_codereading/tool/DataUtils.java new file mode 100644 index 0000000..2a14982 --- /dev/null +++ b/doc/CJR_codereading/tool/DataUtils.java @@ -0,0 +1,295 @@ +/* + * 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.tool; + +import android.content.ContentProviderOperation; +import android.content.ContentProviderResult; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.OperationApplicationException; +import android.database.Cursor; +import android.os.RemoteException; +import android.util.Log; + +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.CallNote; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute; + +import java.util.ArrayList; +import java.util.HashSet; + + +public class DataUtils { + public static final String TAG = "DataUtils"; + public static boolean batchDeleteNotes(ContentResolver resolver, HashSet ids) { + if (ids == null) { + Log.d(TAG, "the ids is null"); + return true; + } + if (ids.size() == 0) { + Log.d(TAG, "no id is in the hashset"); + return true; + } + + ArrayList operationList = new ArrayList(); + for (long id : ids) { + if(id == Notes.ID_ROOT_FOLDER) { + Log.e(TAG, "Don't delete system folder root"); + continue; + } + ContentProviderOperation.Builder builder = ContentProviderOperation + .newDelete(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id)); + operationList.add(builder.build()); + } + try { + ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operationList); + if (results == null || results.length == 0 || results[0] == null) { + Log.d(TAG, "delete notes failed, ids:" + ids.toString()); + return false; + } + return true; + } catch (RemoteException e) { + Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); + } catch (OperationApplicationException e) { + Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); + } + return false; + } + + public static void moveNoteToFoler(ContentResolver resolver, long id, long srcFolderId, long desFolderId) { + ContentValues values = new ContentValues(); + values.put(NoteColumns.PARENT_ID, desFolderId); + values.put(NoteColumns.ORIGIN_PARENT_ID, srcFolderId); + values.put(NoteColumns.LOCAL_MODIFIED, 1); + resolver.update(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id), values, null, null); + } + + public static boolean batchMoveToFolder(ContentResolver resolver, HashSet ids, + long folderId) { + if (ids == null) { + Log.d(TAG, "the ids is null"); + return true; + } + + ArrayList operationList = new ArrayList(); + for (long id : ids) { + ContentProviderOperation.Builder builder = ContentProviderOperation + .newUpdate(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id)); + builder.withValue(NoteColumns.PARENT_ID, folderId); + builder.withValue(NoteColumns.LOCAL_MODIFIED, 1); + operationList.add(builder.build()); + } + + try { + ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operationList); + if (results == null || results.length == 0 || results[0] == null) { + Log.d(TAG, "delete notes failed, ids:" + ids.toString()); + return false; + } + return true; + } catch (RemoteException e) { + Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); + } catch (OperationApplicationException e) { + Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); + } + return false; + } + + /** + * Get the all folder count except system folders {@link Notes#TYPE_SYSTEM}} + */ + public static int getUserFolderCount(ContentResolver resolver) { + Cursor cursor =resolver.query(Notes.CONTENT_NOTE_URI, + new String[] { "COUNT(*)" }, + NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>?", + new String[] { String.valueOf(Notes.TYPE_FOLDER), String.valueOf(Notes.ID_TRASH_FOLER)}, + null); + + int count = 0; + if(cursor != null) { + if(cursor.moveToFirst()) { + try { + count = cursor.getInt(0); + } catch (IndexOutOfBoundsException e) { + Log.e(TAG, "get folder count failed:" + e.toString()); + } finally { + cursor.close(); + } + } + } + return count; + } + + public static boolean visibleInNoteDatabase(ContentResolver resolver, long noteId, int type) { + Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), + null, + NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER, + new String [] {String.valueOf(type)}, + null); + + boolean exist = false; + if (cursor != null) { + if (cursor.getCount() > 0) { + exist = true; + } + cursor.close(); + } + return exist; + } + + public static boolean existInNoteDatabase(ContentResolver resolver, long noteId) { + Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), + null, null, null, null); + + boolean exist = false; + if (cursor != null) { + if (cursor.getCount() > 0) { + exist = true; + } + cursor.close(); + } + return exist; + } + + public static boolean existInDataDatabase(ContentResolver resolver, long dataId) { + Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), + null, null, null, null); + + boolean exist = false; + if (cursor != null) { + if (cursor.getCount() > 0) { + exist = true; + } + cursor.close(); + } + return exist; + } + + public static boolean checkVisibleFolderName(ContentResolver resolver, String name) { + Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI, null, + NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER + + " AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER + + " AND " + NoteColumns.SNIPPET + "=?", + new String[] { name }, null); + boolean exist = false; + if(cursor != null) { + if(cursor.getCount() > 0) { + exist = true; + } + cursor.close(); + } + return exist; + } + + public static HashSet getFolderNoteWidget(ContentResolver resolver, long folderId) { + Cursor c = resolver.query(Notes.CONTENT_NOTE_URI, + new String[] { NoteColumns.WIDGET_ID, NoteColumns.WIDGET_TYPE }, + NoteColumns.PARENT_ID + "=?", + new String[] { String.valueOf(folderId) }, + null); + + HashSet set = null; + if (c != null) { + if (c.moveToFirst()) { + set = new HashSet(); + do { + try { + AppWidgetAttribute widget = new AppWidgetAttribute(); + widget.widgetId = c.getInt(0); + widget.widgetType = c.getInt(1); + set.add(widget); + } catch (IndexOutOfBoundsException e) { + Log.e(TAG, e.toString()); + } + } while (c.moveToNext()); + } + c.close(); + } + return set; + } + + public static String getCallNumberByNoteId(ContentResolver resolver, long noteId) { + Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI, + new String [] { CallNote.PHONE_NUMBER }, + CallNote.NOTE_ID + "=? AND " + CallNote.MIME_TYPE + "=?", + new String [] { String.valueOf(noteId), CallNote.CONTENT_ITEM_TYPE }, + null); + + if (cursor != null && cursor.moveToFirst()) { + try { + return cursor.getString(0); + } catch (IndexOutOfBoundsException e) { + Log.e(TAG, "Get call number fails " + e.toString()); + } finally { + cursor.close(); + } + } + return ""; + } + + public static long getNoteIdByPhoneNumberAndCallDate(ContentResolver resolver, String phoneNumber, long callDate) { + Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI, + new String [] { CallNote.NOTE_ID }, + CallNote.CALL_DATE + "=? AND " + CallNote.MIME_TYPE + "=? AND PHONE_NUMBERS_EQUAL(" + + CallNote.PHONE_NUMBER + ",?)", + new String [] { String.valueOf(callDate), CallNote.CONTENT_ITEM_TYPE, phoneNumber }, + null); + + if (cursor != null) { + if (cursor.moveToFirst()) { + try { + return cursor.getLong(0); + } catch (IndexOutOfBoundsException e) { + Log.e(TAG, "Get call note id fails " + e.toString()); + } + } + cursor.close(); + } + return 0; + } + + public static String getSnippetById(ContentResolver resolver, long noteId) { + Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI, + new String [] { NoteColumns.SNIPPET }, + NoteColumns.ID + "=?", + new String [] { String.valueOf(noteId)}, + null); + + if (cursor != null) { + String snippet = ""; + if (cursor.moveToFirst()) { + snippet = cursor.getString(0); + } + cursor.close(); + return snippet; + } + throw new IllegalArgumentException("Note is not found with id: " + noteId); + } + + public static String getFormattedSnippet(String snippet) { + if (snippet != null) { + snippet = snippet.trim(); + int index = snippet.indexOf('\n'); + if (index != -1) { + snippet = snippet.substring(0, index); + } + } + return snippet; + } +} diff --git a/doc/CJR_codereading/tool/GTaskStringUtils.java b/doc/CJR_codereading/tool/GTaskStringUtils.java new file mode 100644 index 0000000..666b729 --- /dev/null +++ b/doc/CJR_codereading/tool/GTaskStringUtils.java @@ -0,0 +1,113 @@ +/* + * 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.tool; + +public class GTaskStringUtils { + + public final static String GTASK_JSON_ACTION_ID = "action_id"; + + public final static String GTASK_JSON_ACTION_LIST = "action_list"; + + public final static String GTASK_JSON_ACTION_TYPE = "action_type"; + + public final static String GTASK_JSON_ACTION_TYPE_CREATE = "create"; + + public final static String GTASK_JSON_ACTION_TYPE_GETALL = "get_all"; + + public final static String GTASK_JSON_ACTION_TYPE_MOVE = "move"; + + public final static String GTASK_JSON_ACTION_TYPE_UPDATE = "update"; + + public final static String GTASK_JSON_CREATOR_ID = "creator_id"; + + public final static String GTASK_JSON_CHILD_ENTITY = "child_entity"; + + public final static String GTASK_JSON_CLIENT_VERSION = "client_version"; + + public final static String GTASK_JSON_COMPLETED = "completed"; + + public final static String GTASK_JSON_CURRENT_LIST_ID = "current_list_id"; + + public final static String GTASK_JSON_DEFAULT_LIST_ID = "default_list_id"; + + public final static String GTASK_JSON_DELETED = "deleted"; + + public final static String GTASK_JSON_DEST_LIST = "dest_list"; + + public final static String GTASK_JSON_DEST_PARENT = "dest_parent"; + + public final static String GTASK_JSON_DEST_PARENT_TYPE = "dest_parent_type"; + + public final static String GTASK_JSON_ENTITY_DELTA = "entity_delta"; + + public final static String GTASK_JSON_ENTITY_TYPE = "entity_type"; + + public final static String GTASK_JSON_GET_DELETED = "get_deleted"; + + public final static String GTASK_JSON_ID = "id"; + + public final static String GTASK_JSON_INDEX = "index"; + + public final static String GTASK_JSON_LAST_MODIFIED = "last_modified"; + + public final static String GTASK_JSON_LATEST_SYNC_POINT = "latest_sync_point"; + + public final static String GTASK_JSON_LIST_ID = "list_id"; + + public final static String GTASK_JSON_LISTS = "lists"; + + public final static String GTASK_JSON_NAME = "name"; + + public final static String GTASK_JSON_NEW_ID = "new_id"; + + public final static String GTASK_JSON_NOTES = "notes"; + + public final static String GTASK_JSON_PARENT_ID = "parent_id"; + + public final static String GTASK_JSON_PRIOR_SIBLING_ID = "prior_sibling_id"; + + public final static String GTASK_JSON_RESULTS = "results"; + + public final static String GTASK_JSON_SOURCE_LIST = "source_list"; + + public final static String GTASK_JSON_TASKS = "tasks"; + + public final static String GTASK_JSON_TYPE = "type"; + + public final static String GTASK_JSON_TYPE_GROUP = "GROUP"; + + public final static String GTASK_JSON_TYPE_TASK = "TASK"; + + public final static String GTASK_JSON_USER = "user"; + + public final static String MIUI_FOLDER_PREFFIX = "[MIUI_Notes]"; + + public final static String FOLDER_DEFAULT = "Default"; + + public final static String FOLDER_CALL_NOTE = "Call_Note"; + + public final static String FOLDER_META = "METADATA"; + + public final static String META_HEAD_GTASK_ID = "meta_gid"; + + public final static String META_HEAD_NOTE = "meta_note"; + + public final static String META_HEAD_DATA = "meta_data"; + + public final static String META_NOTE_NAME = "[META INFO] DON'T UPDATE AND DELETE"; + +} diff --git a/doc/CJR_codereading/tool/ResourceParser.java b/doc/CJR_codereading/tool/ResourceParser.java new file mode 100644 index 0000000..1ad3ad6 --- /dev/null +++ b/doc/CJR_codereading/tool/ResourceParser.java @@ -0,0 +1,181 @@ +/* + * 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.tool; + +import android.content.Context; +import android.preference.PreferenceManager; + +import net.micode.notes.R; +import net.micode.notes.ui.NotesPreferenceActivity; + +public class ResourceParser { + + public static final int YELLOW = 0; + public static final int BLUE = 1; + public static final int WHITE = 2; + public static final int GREEN = 3; + public static final int RED = 4; + + public static final int BG_DEFAULT_COLOR = YELLOW; + + public static final int TEXT_SMALL = 0; + public static final int TEXT_MEDIUM = 1; + public static final int TEXT_LARGE = 2; + public static final int TEXT_SUPER = 3; + + public static final int BG_DEFAULT_FONT_SIZE = TEXT_MEDIUM; + + public static class NoteBgResources { + private final static int [] BG_EDIT_RESOURCES = new int [] { + R.drawable.edit_yellow, + R.drawable.edit_blue, + R.drawable.edit_white, + R.drawable.edit_green, + R.drawable.edit_red + }; + + private final static int [] BG_EDIT_TITLE_RESOURCES = new int [] { + R.drawable.edit_title_yellow, + R.drawable.edit_title_blue, + R.drawable.edit_title_white, + R.drawable.edit_title_green, + R.drawable.edit_title_red + }; + + public static int getNoteBgResource(int id) { + return BG_EDIT_RESOURCES[id]; + } + + public static int getNoteTitleBgResource(int id) { + return BG_EDIT_TITLE_RESOURCES[id]; + } + } + + public static int getDefaultBgId(Context context) { + if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean( + NotesPreferenceActivity.PREFERENCE_SET_BG_COLOR_KEY, false)) { + return (int) (Math.random() * NoteBgResources.BG_EDIT_RESOURCES.length); + } else { + return BG_DEFAULT_COLOR; + } + } + + public static class NoteItemBgResources { + private final static int [] BG_FIRST_RESOURCES = new int [] { + R.drawable.list_yellow_up, + R.drawable.list_blue_up, + R.drawable.list_white_up, + R.drawable.list_green_up, + R.drawable.list_red_up + }; + + private final static int [] BG_NORMAL_RESOURCES = new int [] { + R.drawable.list_yellow_middle, + R.drawable.list_blue_middle, + R.drawable.list_white_middle, + R.drawable.list_green_middle, + R.drawable.list_red_middle + }; + + private final static int [] BG_LAST_RESOURCES = new int [] { + R.drawable.list_yellow_down, + R.drawable.list_blue_down, + R.drawable.list_white_down, + R.drawable.list_green_down, + R.drawable.list_red_down, + }; + + private final static int [] BG_SINGLE_RESOURCES = new int [] { + R.drawable.list_yellow_single, + R.drawable.list_blue_single, + R.drawable.list_white_single, + R.drawable.list_green_single, + R.drawable.list_red_single + }; + + public static int getNoteBgFirstRes(int id) { + return BG_FIRST_RESOURCES[id]; + } + + public static int getNoteBgLastRes(int id) { + return BG_LAST_RESOURCES[id]; + } + + public static int getNoteBgSingleRes(int id) { + return BG_SINGLE_RESOURCES[id]; + } + + public static int getNoteBgNormalRes(int id) { + return BG_NORMAL_RESOURCES[id]; + } + + public static int getFolderBgRes() { + return R.drawable.list_folder; + } + } + + public static class WidgetBgResources { + private final static int [] BG_2X_RESOURCES = new int [] { + R.drawable.widget_2x_yellow, + R.drawable.widget_2x_blue, + R.drawable.widget_2x_white, + R.drawable.widget_2x_green, + R.drawable.widget_2x_red, + }; + + public static int getWidget2xBgResource(int id) { + return BG_2X_RESOURCES[id]; + } + + private final static int [] BG_4X_RESOURCES = new int [] { + R.drawable.widget_4x_yellow, + R.drawable.widget_4x_blue, + R.drawable.widget_4x_white, + R.drawable.widget_4x_green, + R.drawable.widget_4x_red + }; + + public static int getWidget4xBgResource(int id) { + return BG_4X_RESOURCES[id]; + } + } + + public static class TextAppearanceResources { + private final static int [] TEXTAPPEARANCE_RESOURCES = new int [] { + R.style.TextAppearanceNormal, + R.style.TextAppearanceMedium, + R.style.TextAppearanceLarge, + R.style.TextAppearanceSuper + }; + + public static int getTexAppearanceResource(int id) { + /** + * HACKME: Fix bug of store the resource id in shared preference. + * The id may larger than the length of resources, in this case, + * return the {@link ResourceParser#BG_DEFAULT_FONT_SIZE} + */ + if (id >= TEXTAPPEARANCE_RESOURCES.length) { + return BG_DEFAULT_FONT_SIZE; + } + return TEXTAPPEARANCE_RESOURCES[id]; + } + + public static int getResourcesSize() { + return TEXTAPPEARANCE_RESOURCES.length; + } + } +} diff --git a/doc/CJR_codereading/widget/NoteWidgetProvider.java b/doc/CJR_codereading/widget/NoteWidgetProvider.java new file mode 100644 index 0000000..ec6f819 --- /dev/null +++ b/doc/CJR_codereading/widget/NoteWidgetProvider.java @@ -0,0 +1,132 @@ +/* + * 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; + +public abstract class NoteWidgetProvider extends AppWidgetProvider { + public static final String [] PROJECTION = new String [] { + NoteColumns.ID, + NoteColumns.BG_COLOR_ID, + NoteColumns.SNIPPET + }; + + 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"; + + @Override + public void onDeleted(Context context, int[] appWidgetIds) { + ContentValues values = new ContentValues(); + values.put(NoteColumns.WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); + for (int i = 0; i < appWidgetIds.length; i++) { + context.getContentResolver().update(Notes.CONTENT_NOTE_URI, + values, + NoteColumns.WIDGET_ID + "=?", + new String[] { String.valueOf(appWidgetIds[i])}); + } + } + + private Cursor getNoteWidgetInfo(Context context, int widgetId) { + 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) { + for (int i = 0; i < appWidgetIds.length; i++) { + if (appWidgetIds[i] != AppWidgetManager.INVALID_APPWIDGET_ID) { + int bgId = ResourceParser.getDefaultBgId(context); + String snippet = ""; + Intent intent = new Intent(context, NoteEditActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + intent.putExtra(Notes.INTENT_EXTRA_WIDGET_ID, appWidgetIds[i]); + 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; + } + 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 rv = new RemoteViews(context.getPackageName(), getLayoutId()); + rv.setImageViewResource(R.id.widget_bg_image, getBgResourceId(bgId)); + intent.putExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, bgId); + /** + * Generate the pending intent to start host for the widget + */ + PendingIntent pendingIntent = null; + if (privacyMode) { + 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 { + rv.setTextViewText(R.id.widget_text, snippet); + pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], intent, + PendingIntent.FLAG_UPDATE_CURRENT); + } + + rv.setOnClickPendingIntent(R.id.widget_text, pendingIntent); + appWidgetManager.updateAppWidget(appWidgetIds[i], rv); + } + } + } + + protected abstract int getBgResourceId(int bgId); + + protected abstract int getLayoutId(); + + protected abstract int getWidgetType(); +} diff --git a/doc/CJR_codereading/widget/NoteWidgetProvider_2x.java b/doc/CJR_codereading/widget/NoteWidgetProvider_2x.java new file mode 100644 index 0000000..adcb2f7 --- /dev/null +++ b/doc/CJR_codereading/widget/NoteWidgetProvider_2x.java @@ -0,0 +1,47 @@ +/* + * 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; + + +public class NoteWidgetProvider_2x extends NoteWidgetProvider { + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + super.update(context, appWidgetManager, appWidgetIds); + } + + @Override + protected int getLayoutId() { + return R.layout.widget_2x; + } + + @Override + protected int getBgResourceId(int bgId) { + return ResourceParser.WidgetBgResources.getWidget2xBgResource(bgId); + } + + @Override + protected int getWidgetType() { + return Notes.TYPE_WIDGET_2X; + } +} diff --git a/doc/CJR_codereading/widget/NoteWidgetProvider_4x.java b/doc/CJR_codereading/widget/NoteWidgetProvider_4x.java new file mode 100644 index 0000000..c12a02e --- /dev/null +++ b/doc/CJR_codereading/widget/NoteWidgetProvider_4x.java @@ -0,0 +1,46 @@ +/* + * 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; + + +public class NoteWidgetProvider_4x extends NoteWidgetProvider { + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + super.update(context, appWidgetManager, appWidgetIds); + } + + protected int getLayoutId() { + return R.layout.widget_4x; + } + + @Override + protected int getBgResourceId(int bgId) { + return ResourceParser.WidgetBgResources.getWidget4xBgResource(bgId); + } + + @Override + protected int getWidgetType() { + return Notes.TYPE_WIDGET_4X; + } +} -- 2.34.1 From 0cef3324dd49fd6ed715e01ca64330ea43bfe22e Mon Sep 17 00:00:00 2001 From: zheyu Date: Fri, 14 Apr 2023 15:51:23 +0800 Subject: [PATCH 2/8] coderead --- .../ui/AlarmAlertActivity.java | 207 ++++ doc/cyw.readcode.ui/ui/AlarmInitReceiver.java | 71 ++ doc/cyw.readcode.ui/ui/AlarmReceiver.java | 34 + doc/cyw.readcode.ui/ui/DateTimePicker.java | 504 ++++++++ .../ui/DateTimePickerDialog.java | 96 ++ doc/cyw.readcode.ui/ui/DropdownMenu.java | 65 + .../ui/FoldersListAdapter.java | 87 ++ doc/cyw.readcode.ui/ui/NoteEditActivity.java | 1083 +++++++++++++++++ doc/cyw.readcode.ui/ui/NoteEditText.java | 286 +++++ doc/cyw.readcode.ui/ui/NoteItemData.java | 230 ++++ doc/cyw.readcode.ui/ui/NotesListActivity.java | 1018 ++++++++++++++++ doc/cyw.readcode.ui/ui/NotesListAdapter.java | 273 +++++ doc/cyw.readcode.ui/ui/NotesListItem.java | 132 ++ .../ui/NotesPreferenceActivity.java | 530 ++++++++ .../.gradle/7.5/fileHashes/fileHashes.bin | Bin 42547 -> 42647 bytes .../.gradle/7.5/fileHashes/fileHashes.lock | Bin 17 -> 17 bytes .../micode/notes/ui/AlarmAlertActivity.java | 65 +- .../micode/notes/ui/AlarmInitReceiver.java | 8 +- .../net/micode/notes/ui/AlarmReceiver.java | 6 +- .../net/micode/notes/ui/DateTimePicker.java | 71 +- .../micode/notes/ui/DateTimePickerDialog.java | 30 +- .../net/micode/notes/ui/DropdownMenu.java | 12 +- .../micode/notes/ui/FoldersListAdapter.java | 25 +- .../net/micode/notes/ui/NoteEditActivity.java | 284 ++++- .../net/micode/notes/ui/NoteEditText.java | 77 +- .../net/micode/notes/ui/NoteItemData.java | 58 +- .../micode/notes/ui/NotesListActivity.java | 168 ++- .../net/micode/notes/ui/NotesListAdapter.java | 97 +- .../net/micode/notes/ui/NotesListItem.java | 52 +- .../notes/ui/NotesPreferenceActivity.java | 186 ++- src/Notes-master1/local.properties | 7 +- 31 files changed, 5532 insertions(+), 230 deletions(-) create mode 100644 doc/cyw.readcode.ui/ui/AlarmAlertActivity.java create mode 100644 doc/cyw.readcode.ui/ui/AlarmInitReceiver.java create mode 100644 doc/cyw.readcode.ui/ui/AlarmReceiver.java create mode 100644 doc/cyw.readcode.ui/ui/DateTimePicker.java create mode 100644 doc/cyw.readcode.ui/ui/DateTimePickerDialog.java create mode 100644 doc/cyw.readcode.ui/ui/DropdownMenu.java create mode 100644 doc/cyw.readcode.ui/ui/FoldersListAdapter.java create mode 100644 doc/cyw.readcode.ui/ui/NoteEditActivity.java create mode 100644 doc/cyw.readcode.ui/ui/NoteEditText.java create mode 100644 doc/cyw.readcode.ui/ui/NoteItemData.java create mode 100644 doc/cyw.readcode.ui/ui/NotesListActivity.java create mode 100644 doc/cyw.readcode.ui/ui/NotesListAdapter.java create mode 100644 doc/cyw.readcode.ui/ui/NotesListItem.java create mode 100644 doc/cyw.readcode.ui/ui/NotesPreferenceActivity.java diff --git a/doc/cyw.readcode.ui/ui/AlarmAlertActivity.java b/doc/cyw.readcode.ui/ui/AlarmAlertActivity.java new file mode 100644 index 0000000..e9fdb28 --- /dev/null +++ b/doc/cyw.readcode.ui/ui/AlarmAlertActivity.java @@ -0,0 +1,207 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.micode.notes.ui; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.DialogInterface.OnDismissListener; +import android.content.Intent; +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.media.RingtoneManager; +import android.net.Uri; +import android.os.Bundle; +import android.os.PowerManager; +import android.provider.Settings; +import android.view.Window; +import android.view.WindowManager; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.tool.DataUtils; + +import java.io.IOException; + +public class AlarmAlertActivity extends Activity implements OnClickListener, OnDismissListener { + private long mNoteId; //文本在数据库存储中的ID号 + private String mSnippet; //闹钟提示时出现的文本片段 + private static final int SNIPPET_PREW_MAX_LEN = 60; + MediaPlayer mPlayer; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + //Bundle类型的数据与Map类型的数据相似,都是以key-value的形式存储数据的 + //onsaveInstanceState方法是用来保存Activity的状态的 + //能从onCreate的参数savedInsanceState中获得状态数据 + requestWindowFeature(Window.FEATURE_NO_TITLE); + //界面显示——无标题 + + final Window win = getWindow(); + win.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); + + if (!isScreenOn()) { + win.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON + //保持窗体点亮 + | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON + //将窗体点亮 + | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON + //允许窗体点亮时锁屏 + | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR); + }//在手机锁屏后如果到了闹钟提示时间,点亮屏幕 + + Intent intent = getIntent(); + + try { + mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1)); + mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId); + //根据ID从数据库中获取标签的内容; + //getContentResolver()是实现数据共享,实例存储。 + 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; + } + /* + try + { + // 代码区 + } + catch(Exception e) + { + // 异常处理 + } + 代码区如果有错误,就会返回所写异常的处理。*/ + mPlayer = new MediaPlayer(); + if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) { + showActionDialog(); + //弹出对话框 + playAlarmSound(); + //闹钟提示音激发 + } else { + finish(); + //完成闹钟动作 + } + } + + private boolean isScreenOn() { + //判断屏幕是否锁屏,调用系统函数判断,最后返回值是布尔类型 + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + return pm.isScreenOn(); + } + + private void playAlarmSound() { + //闹钟提示音激发 + Uri url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM); + //调用系统的铃声管理URI,得到闹钟提示音 + int silentModeStreams = Settings.System.getInt(getContentResolver(), + Settings.System.MODE_RINGER_STREAMS_AFFECTED, 0); + + if ((silentModeStreams & (1 << AudioManager.STREAM_ALARM)) != 0) { + mPlayer.setAudioStreamType(silentModeStreams); + } else { + mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM); + } + try { + mPlayer.setDataSource(this, url); + //方法:setDataSource(Context context, Uri uri) + //解释:无返回值,设置多媒体数据来源【根据 Uri】 + mPlayer.prepare(); + //准备同步 + mPlayer.setLooping(true); + //设置是否循环播放 + mPlayer.start(); + //开始播放 + } catch (IllegalArgumentException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + //e.printStackTrace()函数功能是抛出异常, 还将显示出更深的调用信息 + //System.out.println(e),这个方法打印出异常,并且输出在哪里出现的异常 + } catch (SecurityException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IllegalStateException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + private void showActionDialog() { + AlertDialog.Builder dialog = new AlertDialog.Builder(this); + //AlertDialog的构造方法全部是Protected的 + //所以不能直接通过new一个AlertDialog来创建出一个AlertDialog。 + //要创建一个AlertDialog,就要用到AlertDialog.Builder中的create()方法 + //如这里的dialog就是新建了一个AlertDialog + dialog.setTitle(R.string.app_name); + //为对话框设置标题 + dialog.setMessage(mSnippet); + //为对话框设置内容 + dialog.setPositiveButton(R.string.notealert_ok, this); + //给对话框添加"Yes"按钮 + if (isScreenOn()) { + dialog.setNegativeButton(R.string.notealert_enter, this); + }//对话框添加"No"按钮 + dialog.show().setOnDismissListener(this); + } + + public void onClick(DialogInterface dialog, int which) { + switch (which) { + //用which来选择click后下一步的操作 + case DialogInterface.BUTTON_NEGATIVE: + //这是取消操作 + Intent intent = new Intent(this, NoteEditActivity.class); + //实现两个类间的数据传输 + intent.setAction(Intent.ACTION_VIEW); + //设置动作属性 + intent.putExtra(Intent.EXTRA_UID, mNoteId); + //实现key-value对 + //EXTRA_UID为key;mNoteId为键 + startActivity(intent); + //开始动作 + break; + default: + //这是确定操作 + break; + } + } + + public void onDismiss(DialogInterface dialog) { + //忽略 + stopAlarmSound(); + //停止闹钟声音 + finish(); + //完成该动作 + } + + private void stopAlarmSound() { + if (mPlayer != null) { + mPlayer.stop(); + //停止播放 + mPlayer.release(); + //释放MediaPlayer对象 + mPlayer = null; + } + } +} diff --git a/doc/cyw.readcode.ui/ui/AlarmInitReceiver.java b/doc/cyw.readcode.ui/ui/AlarmInitReceiver.java new file mode 100644 index 0000000..7f03f69 --- /dev/null +++ b/doc/cyw.readcode.ui/ui/AlarmInitReceiver.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.ContentUris; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; + +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; + + +public class AlarmInitReceiver extends BroadcastReceiver { + + private static final String [] PROJECTION = new String [] { + NoteColumns.ID, + NoteColumns.ALERTED_DATE + }; + //对数据库的操作,调用标签ID和闹钟时间 + private static final int COLUMN_ID = 0; + private static final int COLUMN_ALERTED_DATE = 1; + + @Override + public void onReceive(Context context, Intent intent) { + long currentDate = System.currentTimeMillis(); + //System.currentTimeMillis()产生一个当前的毫秒 + //这个毫秒其实就是自1970年1月1日0时起的毫秒数 + Cursor c = context.getContentResolver().query(Notes.CONTENT_NOTE_URI, + PROJECTION, + NoteColumns.ALERTED_DATE + ">? AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE, + new String[] { String.valueOf(currentDate) }, + //将long变量currentDate转化为字符串 + null); + + if (c != null) { + if (c.moveToFirst()) { + do { + long alertDate = c.getLong(COLUMN_ALERTED_DATE); + Intent sender = new Intent(context, AlarmReceiver.class); + sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, c.getLong(COLUMN_ID))); + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, 0); + AlarmManager alermManager = (AlarmManager) context + .getSystemService(Context.ALARM_SERVICE); + alermManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent); + } while (c.moveToNext()); + } + c.close(); + } + //然而通过网上查找资料发现,对于闹钟机制的启动,通常需要上面的几个步骤 + //如新建Intent、PendingIntent以及AlarmManager等 + //这里就是根据数据库里的闹钟时间创建一个闹钟机制 + } +} diff --git a/doc/cyw.readcode.ui/ui/AlarmReceiver.java b/doc/cyw.readcode.ui/ui/AlarmReceiver.java new file mode 100644 index 0000000..8d7492d --- /dev/null +++ b/doc/cyw.readcode.ui/ui/AlarmReceiver.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +public class AlarmReceiver extends BroadcastReceiver {//类:闹钟消息接收器,从BroadcastReceiver类继承而来 + @Override + public void onReceive(Context context, Intent intent) { + intent.setClass(context, AlarmAlertActivity.class); + //启动AlarmAlertActivity + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + //activity要存在于activity的栈中,而非activity的途径启动activity时必然不存在一个activity的栈 + //所以要新起一个栈装入启动的activity + context.startActivity(intent); + } +} +//这是实现alarm这个功能最接近用户层的包,基于上面的两个包, \ No newline at end of file diff --git a/doc/cyw.readcode.ui/ui/DateTimePicker.java b/doc/cyw.readcode.ui/ui/DateTimePicker.java new file mode 100644 index 0000000..11a8469 --- /dev/null +++ b/doc/cyw.readcode.ui/ui/DateTimePicker.java @@ -0,0 +1,504 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import java.text.DateFormatSymbols; +import java.util.Calendar; + +import net.micode.notes.R; + + +import android.content.Context; +import android.text.format.DateFormat; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.NumberPicker; + +public class DateTimePicker extends FrameLayout {//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; + 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; + private final NumberPicker mHourSpinner; + private final NumberPicker mMinuteSpinner; + private final NumberPicker mAmPmSpinner; + //NumberPicker是数字选择器 + //这里定义的四个变量全部是在设置闹钟时需要选择的变量(如日期、时、分、上午或者下午) + + private Calendar mDate; + //定义了Calendar类型的变量mDate,用于操作时间 + private String[] mDateDisplayValues = new String[DAYS_IN_ALL_WEEK]; + + private boolean mIsAm; + + private boolean mIs24HourView; + + private boolean mIsEnabled = DEFAULT_ENABLE_STATE; + + private boolean mInitialising; + + private OnDateTimeChangedListener mOnDateTimeChangedListener; + + 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(); + } + };//OnValueChangeListener,这是时间改变监听器,这里主要是对日期的监听 + //将现在日期的值传递给mDate;updateDateControl是同步操作 + + private NumberPicker.OnValueChangeListener mOnHourChangedListener = new NumberPicker.OnValueChangeListener() { + //这里是对 小时(Hour) 的监听 + @Override + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + boolean isDateChanged = false; + Calendar cal = Calendar.getInstance(); + //声明一个Calendar的变量cal,便于后续的操作 + if (!mIs24HourView) { + if (!mIsAm && oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) { + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, 1); + isDateChanged = true; + //这里是对于12小时制时,晚上11点和12点交替时对日期的更改 + } 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; + }//这里是对于12小时制时,凌晨11点和12点交替时对日期的更改 + 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(); + }//这里是对于12小时制时,中午11点和12点交替时对AM和PM的更改 + } else { + if (oldVal == HOURS_IN_ALL_DAY - 1 && newVal == 0) { + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, 1); + isDateChanged = true; + //这里是对于24小时制时,晚上11点和12点交替时对日期的更改 + } else if (oldVal == 0 && newVal == HOURS_IN_ALL_DAY - 1) { + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, -1); + isDateChanged = true; + } + }//这里是对于12小时制时,凌晨11点和12点交替时对日期的更改 + int newHour = mHourSpinner.getValue() % HOURS_IN_HALF_DAY + (mIsAm ? 0 : HOURS_IN_HALF_DAY); + //通过数字选择器对newHour的赋值 + mDate.set(Calendar.HOUR_OF_DAY, newHour); + //通过set函数将新的Hour值传给mDate + 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 + //这里是对 分钟(Minute)改变的监听 + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + int minValue = mMinuteSpinner.getMinValue(); + int maxValue = mMinuteSpinner.getMaxValue(); + int offset = 0; + //设置offset,作为小时改变的一个记录数据 + if (oldVal == maxValue && newVal == minValue) { + offset += 1; + } else if (oldVal == minValue && newVal == maxValue) { + offset -= 1; + }//如果原值为59,新值为0,则offset加1 + //如果原值为0,新值为59,则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() { + //对AM和PM的监听 + @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); + }//接口:日期变化监听器 + + 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); + //如果当前Activity里用到别的layout,比如对话框layout + //还要设置这个layout上的其他组件的内容,就必须用inflate()方法先将对话框的layout找出来 + //然后再用findViewById()找到它上面的其它组件 + mDateSpinner = (NumberPicker) findViewById(R.id.date); + mDateSpinner.setMinValue(DATE_SPINNER_MIN_VAL); + mDateSpinner.setMaxValue(DATE_SPINNER_MAX_VAL); + mDateSpinner.setOnValueChangedListener(mOnDateChangedListener); + + mHourSpinner = (NumberPicker) findViewById(R.id.hour); + mHourSpinner.setOnValueChangedListener(mOnHourChangedListener); + mMinuteSpinner = (NumberPicker) findViewById(R.id.minute); + mMinuteSpinner.setMinValue(MINUT_SPINNER_MIN_VAL); + mMinuteSpinner.setMaxValue(MINUT_SPINNER_MAX_VAL); + mMinuteSpinner.setOnLongPressUpdateInterval(100); + mMinuteSpinner.setOnValueChangedListener(mOnMinuteChangedListener); + + 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); + + // update controls to initial state + updateDateControl(); + updateHourControl(); + updateAmPmControl(); + + set24HourView(is24HourView); + + // set to current time + setCurrentDate(date); + + setEnabled(isEnabled()); + + // set the content descriptions + 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; + }//函数:判断当前的时间日期选择器是否处于启用的的状态 + + /** + * Get the current date in millis + * + * @return the current date in millis + */ + public long getCurrentDateInTimeMillis() { + return mDate.getTimeInMillis(); + } +//实现函数——得到当前的秒数 + /** + * Set the current date + * + * @param date The current date in millis + */ + 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)); + }//实现函数功能——设置当前的时间,参数是date + + /** + * Set the current date + * + * @param year The current year + * @param month The current month + * @param dayOfMonth The current dayOfMonth + * @param hourOfDay The current hourOfDay + * @param minute The current minute + */ + public void setCurrentDate(int year, int month, + int dayOfMonth, int hourOfDay, int minute) { + setCurrentYear(year); + setCurrentMonth(month); + setCurrentDay(dayOfMonth); + setCurrentHour(hourOfDay); + setCurrentMinute(minute); + }//实现函数功能——设置当前的时间,参数是各详细的变量 + + /** + * Get current year + * + * @return The current year + */ + //下面是得到year、month、day等值 + public int getCurrentYear() { + return mDate.get(Calendar.YEAR); + } + + /** + * Set current year + * + * @param year The current year + */ + public void setCurrentYear(int year) { + if (!mInitialising && year == getCurrentYear()) { + return; + } + mDate.set(Calendar.YEAR, year); + updateDateControl(); + onDateTimeChanged(); + } + + /** + * Get current month in the year + * + * @return The current month in the year + */ + public int getCurrentMonth() { + return mDate.get(Calendar.MONTH); + } + + /** + * Set current month in the year + * + * @param month The month in the year + */ + public void setCurrentMonth(int month) { + if (!mInitialising && month == getCurrentMonth()) { + return; + } + mDate.set(Calendar.MONTH, month); + updateDateControl(); + onDateTimeChanged(); + } + + /** + * Get current day of the month + * + * @return The day of the month + */ + public int getCurrentDay() { + return mDate.get(Calendar.DAY_OF_MONTH); + } + + /** + * Set current day of the month + * + * @param dayOfMonth The day of the month + */ + public void setCurrentDay(int dayOfMonth) { + if (!mInitialising && dayOfMonth == getCurrentDay()) { + return; + } + mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); + updateDateControl(); + onDateTimeChanged(); + } + + /** + * Get current hour in 24 hour mode, in the range (0~23) + * @return The current hour in 24 hour mode + */ + 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; + } + } + } + + /** + * Set current hour in 24 hour mode, in the range (0~23) + * + * @param hourOfDay + */ + 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(); + } + + /** + * Get currentMinute + * + * @return The Current Minute + */ + public int getCurrentMinute() { + return mDate.get(Calendar.MINUTE); + } + + /** + * Set current minute + */ + public void setCurrentMinute(int minute) { + if (!mInitialising && minute == getCurrentMinute()) { + return; + } + mMinuteSpinner.setValue(minute); + mDate.set(Calendar.MINUTE, minute); + onDateTimeChanged(); + } + + /** + * @return true if this is in 24 hour view else false. + */ + public boolean is24HourView () { + return mIs24HourView; + } + + /** + * Set whether in 24 hour or AM/PM mode. + * + * @param is24HourView True for 24 hour mode. False for AM/PM mode. + */ + public void set24HourView(boolean is24HourView) {//函数:设置24小时下的视图 + if (mIs24HourView == is24HourView) { + return; + } + mIs24HourView = is24HourView; + mAmPmSpinner.setVisibility(is24HourView ? View.GONE : View.VISIBLE);// 语句:如果为12小时视图则显示上下午选择 + 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); + } else { + int index = mIsAm ? Calendar.AM : Calendar.PM; + mAmPmSpinner.setValue(index); + mAmPmSpinner.setVisibility(View.VISIBLE); + }// 对于上下午操作的算法 + } + + private void updateHourControl() { + if (mIs24HourView) { + mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW); + mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW); + } else { + mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW); + mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW); + }// 对与小时的算法 + } + + /** + * Set the callback that indicates the 'Set' button has been pressed. + * @param callback the callback, if null will do nothing + */ + public void setOnDateTimeChangedListener(OnDateTimeChangedListener callback) { + //函数:根据参数设置时间日期监听器 + mOnDateTimeChangedListener = callback; + } + + private void onDateTimeChanged() {//函数:监听日期时间的变化 + if (mOnDateTimeChangedListener != null) { + mOnDateTimeChangedListener.onDateTimeChanged(this, getCurrentYear(), + getCurrentMonth(), getCurrentDay(), getCurrentHourOfDay(), getCurrentMinute()); + } + } +} diff --git a/doc/cyw.readcode.ui/ui/DateTimePickerDialog.java b/doc/cyw.readcode.ui/ui/DateTimePickerDialog.java new file mode 100644 index 0000000..b35a4cf --- /dev/null +++ b/doc/cyw.readcode.ui/ui/DateTimePickerDialog.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import java.util.Calendar; + +import net.micode.notes.R; +import net.micode.notes.ui.DateTimePicker; +import net.micode.notes.ui.DateTimePicker.OnDateTimeChangedListener; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.text.format.DateFormat; +import android.text.format.DateUtils; + +public class DateTimePickerDialog extends AlertDialog implements OnClickListener { + + private Calendar mDate = Calendar.getInstance(); + //创建一个Calendar类型的变量 mDate,方便时间的操作 + private boolean mIs24HourView; + private OnDateTimeSetListener mOnDateTimeSetListener; + //声明一个时间日期滚动选择控件 mOnDateTimeSetListener + private DateTimePicker mDateTimePicker; + //DateTimePicker控件,控件一般用于让用户可以从日期列表中选择单个值。 + //运行时,单击控件边上的下拉箭头,会显示为两个部分:一个下拉列表,一个用于选择日期的 + public interface OnDateTimeSetListener { + //设置一个接口当时期时间设置时进行的操作 + void OnDateTimeSet(AlertDialog dialog, long date); + } + + public DateTimePickerDialog(Context context, long date) { + //对该界面对话框的实例化 + super(context); + //对数据库的操作 + mDateTimePicker = new DateTimePicker(context); + setView(mDateTimePicker);//添加一个子视图 + mDateTimePicker.setOnDateTimeChangedListener(new OnDateTimeChangedListener() { + public void onDateTimeChanged(DateTimePicker view, int year, int month, + int dayOfMonth, int hourOfDay, int minute) { + mDate.set(Calendar.YEAR, year); + mDate.set(Calendar.MONTH, month); + mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); + mDate.set(Calendar.HOUR_OF_DAY, hourOfDay); + mDate.set(Calendar.MINUTE, minute);//将视图中的各选项设置为系统当前时间 + updateTitle(mDate.getTimeInMillis()); + } + }); + mDate.setTimeInMillis(date);//得到系统时间 + mDate.set(Calendar.SECOND, 0);//将秒数设置为0 + mDateTimePicker.setCurrentDate(mDate.getTimeInMillis()); + setButton(context.getString(R.string.datetime_dialog_ok), this); + setButton2(context.getString(R.string.datetime_dialog_cancel), (OnClickListener)null);//设置按钮 + set24HourView(DateFormat.is24HourFormat(this.getContext()));//时间标准化打印 + updateTitle(mDate.getTimeInMillis()); + }//将时间日期滚动选择控件实例化 + + public void set24HourView(boolean is24HourView) { + mIs24HourView = is24HourView; + } + + public void setOnDateTimeSetListener(OnDateTimeSetListener callBack) { + mOnDateTimeSetListener = callBack; + }//将时间日期滚动选择控件实例化 + + private void updateTitle(long date) { + int flag = + DateUtils.FORMAT_SHOW_YEAR | + DateUtils.FORMAT_SHOW_DATE | + DateUtils.FORMAT_SHOW_TIME; + flag |= mIs24HourView ? DateUtils.FORMAT_24HOUR : DateUtils.FORMAT_24HOUR; + setTitle(DateUtils.formatDateTime(this.getContext(), date, flag)); + }//android开发中常见日期管理工具类(API)——DateUtils:按照上下午显示时间 + + public void onClick(DialogInterface arg0, int arg1) { + if (mOnDateTimeSetListener != null) { + mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis()); + } + }//第一个参数arg0是接收到点击事件的对话框 + //第二个参数arg1是该对话框上的按钮 +} \ No newline at end of file diff --git a/doc/cyw.readcode.ui/ui/DropdownMenu.java b/doc/cyw.readcode.ui/ui/DropdownMenu.java new file mode 100644 index 0000000..0c4b307 --- /dev/null +++ b/doc/cyw.readcode.ui/ui/DropdownMenu.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.Context; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.PopupMenu; +import android.widget.PopupMenu.OnMenuItemClickListener; + +import net.micode.notes.R; + +public class DropdownMenu { + private Button mButton; + private PopupMenu mPopupMenu; + //声明一个下拉菜单 + private Menu mMenu; + + public DropdownMenu(Context context, Button button, int menuId) { + mButton = button; + mButton.setBackgroundResource(R.drawable.dropdown_icon); + //设置这个view的背景 + mPopupMenu = new PopupMenu(context, mButton); + mMenu = mPopupMenu.getMenu(); + mPopupMenu.getMenuInflater().inflate(menuId, mMenu); + //MenuInflater是用来实例化Menu目录下的Menu布局文件 + //根据ID来确认menu的内容选项 + mButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + mPopupMenu.show(); + } + }); + } + + public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) { + if (mPopupMenu != null) { + mPopupMenu.setOnMenuItemClickListener(listener); + }//设置菜单的监听 + } + + public MenuItem findItem(int id) { + return mMenu.findItem(id); + }//对于菜单选项的初始化,根据索引搜索菜单需要的选项 + + public void setTitle(CharSequence title) { + mButton.setText(title); + }//布局文件,设置标题 +} \ No newline at end of file diff --git a/doc/cyw.readcode.ui/ui/FoldersListAdapter.java b/doc/cyw.readcode.ui/ui/FoldersListAdapter.java new file mode 100644 index 0000000..2c5853d --- /dev/null +++ b/doc/cyw.readcode.ui/ui/FoldersListAdapter.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.Context; +import android.database.Cursor; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CursorAdapter; +import android.widget.LinearLayout; +import android.widget.TextView; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; + + +public class FoldersListAdapter extends CursorAdapter { + //CursorAdapter是Cursor和ListView的接口 + //FoldersListAdapter继承了CursorAdapter的类 + //主要作用是便签数据库和用户的交互 + //这里就是用folder(文件夹)的形式展现给用户 + public static final String [] PROJECTION = { + NoteColumns.ID, + NoteColumns.SNIPPET + };//调用数据库中便签的ID和片段 + + public static final int ID_COLUMN = 0; + public static final int NAME_COLUMN = 1; + + public FoldersListAdapter(Context context, Cursor c) { + super(context, c); + // TODO Auto-generated constructor stub + }//数据库操作 + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + //ViewGroup是容器 + return new FolderListItem(context); + }//创建一个文件夹,对于各文件夹中子标签的初始化 + + @Override + public void bindView(View view, Context context, Cursor cursor) { + if (view instanceof FolderListItem) { + String folderName = (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context + .getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN); + ((FolderListItem) view).bind(folderName); + } + }//将各个布局文件绑定起来 + + public String getFolderName(Context context, int position) { + Cursor cursor = (Cursor) getItem(position); + return (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context + .getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN); + }//根据数据库中标签的ID得到标签的各项内容 + + private class FolderListItem extends LinearLayout { + private TextView mName; + + public FolderListItem(Context context) { + super(context); + //操作数据库 + inflate(context, R.layout.folder_list_item, this); + //根据布局文件的名字等信息将其找出来 + mName = (TextView) findViewById(R.id.tv_folder_name); + } + + public void bind(String name) { + mName.setText(name);//设置名字 + } + } + +} \ No newline at end of file diff --git a/doc/cyw.readcode.ui/ui/NoteEditActivity.java b/doc/cyw.readcode.ui/ui/NoteEditActivity.java new file mode 100644 index 0000000..9bbbb97 --- /dev/null +++ b/doc/cyw.readcode.ui/ui/NoteEditActivity.java @@ -0,0 +1,1083 @@ +/* + * 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; + + +public class NoteEditActivity extends Activity implements OnClickListener, + NoteSettingChangedListener, OnTextViewChangeListener { + //该类主要是针对标签的编辑 + //继承了系统内部许多和监听有关的类 + private class HeadViewHolder { + public TextView tvModified; + + public ImageView ivAlertIcon; + + public TextView tvAlertDate; + + public ImageView ibSetBgColor; + } + //使用Map实现数据存储 + 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); + //put函数是将指定值和指定键相连 + } + + 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); + //put函数是将指定值和指定键相连 + } + + 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); + //put函数是将指定值和指定键相连 + } + + 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); + //put函数是将指定值和指定键相连 + } + + private static final String TAG = "NoteEditActivity"; + + private HeadViewHolder mNoteHeaderHolder; + + private View mHeadViewPanel; + //私有化一个界面操作mHeadViewPanel,对表头的操作 + private View mNoteBgColorSelector; + //私有化一个界面操作mNoteBgColorSelector,对背景颜色的操作 + private View mFontSizeSelector; + //私有化一个界面操作mFontSizeSelector,对标签字体的操作 + private EditText mNoteEditor; + //声明编辑控件,对文本操作 + private View mNoteEditorPanel; + //私有化一个界面操作mNoteEditorPanel,文本编辑的控制板 + //private WorkingNote mWorkingNote; + public WorkingNote mWorkingNote; + //对模板WorkingNote的初始化 + private SharedPreferences mSharedPrefs; + //私有化SharedPreferences的数据存储方式 + //它的本质是基于XML文件存储key-value键值对数据 + private int mFontSizeId; + //用于操作字体的大小 + 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; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + this.setContentView(R.layout.note_edit); + //对数据库的访问操作 + if (savedInstanceState == null && !initActivityState(getIntent())) { + finish(); + return; + } + initResources(); + } + + /** + * Current activity may be killed when the memory is low. Once it is killed, for another time + * user load this activity, we should restore the former state + */ + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + 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"); + }//为防止内存不足时程序的终止,在这里有一个保存现场的函数 + } + + private boolean initActivityState(Intent intent) { + /** + * If the user specified the {@link Intent#ACTION_VIEW} but not provided with id, + * then jump to the NotesListActivity + */ + mWorkingNote = null; + if (TextUtils.equals(Intent.ACTION_VIEW, intent.getAction())) { + long noteId = intent.getLongExtra(Intent.EXTRA_UID, 0); + mUserQuery = ""; + //如果用户实例化标签时,系统并未给出标签ID + /** + * Starting from the searched result + */ + //根据键值查找ID + if (intent.hasExtra(SearchManager.EXTRA_DATA_KEY)) { + noteId = Long.parseLong(intent.getStringExtra(SearchManager.EXTRA_DATA_KEY)); + mUserQuery = intent.getStringExtra(SearchManager.USER_QUERY); + } + //如果ID在数据库中未找到 + if (!DataUtils.visibleInNoteDatabase(getContentResolver(), noteId, Notes.TYPE_NOTE)) { + Intent jump = new Intent(this, NotesListActivity.class); + startActivity(jump); + //程序将跳转到上面声明的intent——jump + showToast(R.string.error_note_not_exist); + finish(); + return false; + } + //ID在数据库中找到 + else { + mWorkingNote = WorkingNote.load(this, noteId); + if (mWorkingNote == null) { + Log.e(TAG, "load note failed with note id" + noteId); + //打印出红色的错误信息 + finish(); + return false; + } + } + //setSoftInputMode——软键盘输入模式 + 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())) { + // intent.getAction() + // 大多用于broadcast发送广播时给机制(intent)设置一个action,就是一个字符串 + // 用户可以通过receive(接受)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)); + // intent.getInt(Long、String)Extra是对各变量的语法分析 + // Parse call-record note + 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); + }//创建一个新的WorkingNote + + 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; + } + + @Override + protected void onResume() { + super.onResume(); + initNoteScreen(); + } + + private void initNoteScreen() { + //对界面的初始化操作 + mNoteEditor.setTextAppearance(this, TextAppearanceResources + .getTexAppearanceResource(mFontSizeId)); + //设置外观 + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + switchToListMode(mWorkingNote.getContent()); + } else { + mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery)); + mNoteEditor.setSelection(mNoteEditor.getText().length()); + } + for (Integer id : sBgSelectorSelectionMap.keySet()) { + findViewById(sBgSelectorSelectionMap.get(id)).setVisibility(View.GONE); + } + mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId()); + mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId()); + + mNoteHeaderHolder.tvModified.setText(DateUtils.formatDateTime(this, + mWorkingNote.getModifiedDate(), DateUtils.FORMAT_SHOW_DATE + | DateUtils.FORMAT_NUMERIC_DATE | DateUtils.FORMAT_SHOW_TIME + | DateUtils.FORMAT_SHOW_YEAR)); + + /** + * TODO: Add the menu for setting alert. Currently disable it because the DateTimePicker + * is not ready + */ + 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); + }; + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + initActivityState(intent); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + /** + * For new note without note id, we should firstly save it to + * generate a id. If the editing note is not worth saving, there + * is no id which is equivalent to create new note + */ + if (!mWorkingNote.existInDatabase()) { + saveNote(); + } + //在创建一个新的标签时,先在数据库中匹配 + //如果不存在,那么先在数据库中存储 + outState.putLong(Intent.EXTRA_UID, mWorkingNote.getNoteId()); + Log.d(TAG, "Save working note id: " + mWorkingNote.getNoteId() + " onSaveInstanceState"); + } + + @Override + //MotionEvent是对屏幕触控的传递机制 + 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())) + //如果触控的位置超出了给定的范围,返回false + { + 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); + /** + * HACKME: Fix bug of store the resource id in shared preference. + * The id may larger than the length of resources, in this case, + * return the {@link ResourceParser#BG_DEFAULT_FONT_SIZE} + */ + if(mFontSizeId >= TextAppearanceResources.getResourcesSize()) { + mFontSizeId = ResourceParser.BG_DEFAULT_FONT_SIZE; + } + mEditTextList = (LinearLayout) findViewById(R.id.note_edit_list); + } + + @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)) { + findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( + View.GONE); + mWorkingNote.setBgColorId(sBgSelectorBtnsMap.get(id)); + mNoteBgColorSelector.setVisibility(View.GONE); + } else if (sFontSizeBtnsMap.containsKey(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(); + super.onBackPressed(); + } + + private boolean clearSettingState() { + if (mNoteBgColorSelector.getVisibility() == View.VISIBLE) { + mNoteBgColorSelector.setVisibility(View.GONE); + return true; + } else if (mFontSizeSelector.getVisibility() == View.VISIBLE) { + mFontSizeSelector.setVisibility(View.GONE); + return true; + } + 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) { + if (isFinishing()) { + return true; + } + clearSettingState(); + menu.clear(); + if (mWorkingNote.getFolderId() == Notes.ID_CALL_RECORD_FOLDER) { + getMenuInflater().inflate(R.menu.call_note_edit, menu); + // MenuInflater是用来实例化Menu目录下的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); + } + return true; + } + + @Override + /* + * 函数功能:动态改变菜单选项内容 + * 函数实现:如下注释 + */ + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + //根据菜单的id来编剧相关项目 + 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)); + // 设置标签的标题为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) { + //点击所触发事件 + deleteCurrentNote(); + // 删除单签便签 + finish(); + } + }); + //添加“YES”按钮 + builder.setNegativeButton(android.R.string.cancel, null); + //添加“NO”的按钮 + builder.show(); + //显示对话框 + break; + case R.id.menu_font_size: + //字体大小的编辑 + mFontSizeSelector.setVisibility(View.VISIBLE); + // 将字体选择器置为可见 + findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.VISIBLE); + // 通过id找到相应的大小 + break; + case R.id.menu_list_mode: + //选择列表模式 + mWorkingNote.setCheckListMode(mWorkingNote.getCheckListMode() == 0 ? + TextNote.MODE_CHECK_LIST : 0); + break; + case R.id.menu_share: + //菜单共享 + getWorkingText(); + sendTo(this, mWorkingNote.getContent()); + // 用sendto函数将运行文本发送到遍历的本文内 + 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; + } + 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 = new Intent(Intent.ACTION_SEND); + //建立intent链接选项 + intent.putExtra(Intent.EXTRA_TEXT, info); + //将需要传递的便签信息放入text文件中 + intent.setType("text/plain"); + //编辑连接器的类型 + context.startActivity(intent); + //在acti中进行链接 + } + + /* + * 函数功能:创建一个新的便签 + * 函数实现:如下注释 + */ + private void createNewNote() { + // Firstly, save current editing notes + //保存当前便签 + saveNote(); + + // For safety, start a new NoteEditActivity + finish(); + Intent intent = new Intent(this, NoteEditActivity.class); + //设置链接器 + intent.setAction(Intent.ACTION_INSERT_OR_EDIT); + //该活动定义为创建或编辑 + intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mWorkingNote.getFolderId()); + //将运行便签的id添加到INTENT_EXTRA_FOLDER_ID标记中 + startActivity(intent); + //开始activity并链接 + } + + /* + * 函数功能:删除当前便签 + * 函数实现:如下注释 + */ + private void deleteCurrentNote() { + if (mWorkingNote.existInDatabase()) { + //假如当前运行的便签内存有数据 + HashSet ids = new HashSet(); + long id = mWorkingNote.getNoteId(); + if (id != Notes.ID_ROOT_FOLDER) { + ids.add(id); + //如果不是头文件夹建立一个hash表把便签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); + //将这些标签的删除标记置为true + } + + /* + * 函数功能:判断是否为同步模式 + * 函数实现:直接看NotesPreferenceActivity中同步名称是否为空 + */ + private boolean isSyncMode() { + 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(); + } + if (mWorkingNote.getNoteId() > 0) { + Intent intent = new Intent(this, AlarmReceiver.class); + intent.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mWorkingNote.getNoteId())); + //若有有运行的便签就是建立一个链接器将标签id都存在uri中 + PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0); + AlarmManager alarmManager = ((AlarmManager) getSystemService(ALARM_SERVICE)); + //设置提醒管理器 + showAlertHeader(); + if(!set) { + alarmManager.cancel(pendingIntent); + } else { + alarmManager.set(AlarmManager.RTC_WAKEUP, date, pendingIntent); + } + //如果用户设置了时间,就通过提醒管理器设置一个监听事项 + } else { + /** + * There is the condition that user has input nothing (the note is + * not worthy saving), we have no note id, remind the user that he + * should input something + */ + //没有运行的便签就报错 + Log.e(TAG, "Clock alert setting error"); + showToast(R.string.error_note_empty_for_clock); + } + } + + /* + * 函数功能:Widget发生改变的所触发的事件 + */ + public void onWidgetChanged() { + updateWidget();//更新Widget + } + + /* + * 函数功能: 删除编辑文本框所触发的事件 + * 函数实现:如下注释 + */ + 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); + //通过id把编辑框存在便签编辑框中 + } + + 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); + } + //通过id把编辑框存在空的NoteEditText中 + int length = edit.length(); + edit.append(text); + edit.requestFocus();//请求优先完成该此 编辑 + edit.setSelection(length);//定位到length位置处的条目 + } + + /* + * 函数功能:进入编辑文本框所触发的事件 + * 函数实现:如下注释 + */ + public void onEditTextEnter(int index, String text) { + /** + * Should not happen, check for debug + */ + 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); + //将文本编辑框置为可见 + } + + /* + * 函数功能:获取高亮效果的反馈情况 + * 函数实现:如下注释 + */ + 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); + //建立一个状态机检查Pattern并进行匹配 + 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; + } + + /* + * 函数功能:获取列表项 + * 函数实现:如下注释 + */ + 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; + //初始化check标记 + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + // 若模式为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; + //扩展字符串为已打钩并把标记置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(); + //运行 getWorkingText()之后保存 + if (saved) { + /** + * There are two modes from List view to edit view, open one note, + * create/edit a node. Opening node requires to the original + * position in the list when back from edit view, while creating a + * new node requires to the top of the list. This code + * {@link #RESULT_OK} is used to identify the create/edit state + */ + //如英文注释所说链接RESULT_OK是为了识别保存的2种情况,一是创建后保存,二是修改后保存 + setResult(RESULT_OK); + } + return saved; + } + + /* + * 函数功能:将便签发送至桌面 + * 函数实现:如下注释 + */ + private void sendToDesktop() { + /** + * Before send message to home, we should make sure that current + * editing note is exists in databases. So, for new note, firstly + * save it + */ + if (!mWorkingNote.existInDatabase()) { + saveNote(); + //若不存在数据也就是新的标签就保存起来先 + } + + if (mWorkingNote.getNoteId() > 0) { + //若是有内容 + Intent sender = new Intent(); + Intent shortcutIntent = new Intent(this, NoteEditActivity.class); + //建立发送到桌面的连接器 + shortcutIntent.setAction(Intent.ACTION_VIEW); + //链接为一个视图 + shortcutIntent.putExtra(Intent.EXTRA_UID, mWorkingNote.getNoteId()); + sender.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent); + 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"); + //设置sneder的行为是发送 + showToast(R.string.info_note_enter_desktop); + sendBroadcast(sender); + //显示到桌面 + } else { + /** + * There is the condition that user has input nothing (the note is + * not worthy saving), we have no note id, remind the user that he + * should input something + */ + 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; + //直接设置为content中的内容并返回,有勾选和未勾选2种 + } + + /* + * 函数功能:显示提示的视图 + * 函数实现:根据下标显示对应的提示 + */ + private void showToast(int resId) { + showToast(resId, Toast.LENGTH_SHORT); + } + + /* + * 函数功能:持续显示提示的视图 + * 函数实现:根据下标和持续的时间(duration)编辑提示视图并显示 + */ + private void showToast(int resId, int duration) { + Toast.makeText(this, resId, duration).show(); + } +} diff --git a/doc/cyw.readcode.ui/ui/NoteEditText.java b/doc/cyw.readcode.ui/ui/NoteEditText.java new file mode 100644 index 0000000..334ebd6 --- /dev/null +++ b/doc/cyw.readcode.ui/ui/NoteEditText.java @@ -0,0 +1,286 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.Context; +import android.graphics.Rect; +import android.text.Layout; +import android.text.Selection; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.style.URLSpan; +import android.util.AttributeSet; +import android.util.Log; +import android.view.ContextMenu; +import android.view.KeyEvent; +import android.view.MenuItem; +import android.view.MenuItem.OnMenuItemClickListener; +import android.view.MotionEvent; +import android.widget.EditText; + +import net.micode.notes.R; + +import java.util.HashMap; +import java.util.Map; + +//继承edittext,设置便签设置文本框 +public class NoteEditText extends EditText { + private static final String TAG = "NoteEditText"; + private int mIndex; + private int mSelectionStartBeforeDelete; + + private static final String SCHEME_TEL = "tel:" ; + private static final String SCHEME_HTTP = "http:" ; + private static final String SCHEME_EMAIL = "mailto:" ; + + ///建立一个字符和整数的hash表,用于链接电话,网站,还有邮箱 + private static final Map sSchemaActionResMap = new HashMap(); + static { + sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel); + sSchemaActionResMap.put(SCHEME_HTTP, R.string.note_link_web); + sSchemaActionResMap.put(SCHEME_EMAIL, R.string.note_link_email); + } + + /** + * Call by the {@link NoteEditActivity} to delete or add edit text + */ + //在NoteEditActivity中删除或添加文本的操作,可以看做是一个文本是否被变的标记,英文注释已说明的很清楚 + public interface OnTextViewChangeListener { + /** + * Delete current edit text when {@link KeyEvent#KEYCODE_DEL} happens + * and the text is null + */ + //处理删除按键时的操作 + void onEditTextDelete(int index, String text); + + /** + * Add edit text after current edit text when {@link KeyEvent#KEYCODE_ENTER} + * happen + */ + //处理进入按键时的操作 + void onEditTextEnter(int index, String text); + + /** + * Hide or show item option when text change + */ + void onTextChange(int index, boolean hasText); + } + + private OnTextViewChangeListener mOnTextViewChangeListener; + + //根据context设置文本 + public NoteEditText(Context context) { + super(context, null);//用super引用父类变量 + mIndex = 0; + } + + //设置当前光标 + public void setIndex(int index) { + mIndex = index; + } + + //初始化文本修改标记 + public void setOnTextViewChangeListener(OnTextViewChangeListener listener) { + mOnTextViewChangeListener = listener; + } + + //AttributeSet 百度了一下是自定义空控件属性,用于维护便签动态变化的属性 + //初始化便签 + public NoteEditText(Context context, AttributeSet attrs) { + super(context, attrs, android.R.attr.editTextStyle); + } + + // 根据defstyle自动初始化 + public NoteEditText(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + // TODO Auto-generated construct or stub + } + + @Override + //view里的函数,处理手机屏幕的所有事件 + /*参数event为手机屏幕触摸事件封装类的对象,其中封装了该事件的所有信息, + 例如触摸的位置、触摸的类型以及触摸的时间等。该对象会在用户触摸手机屏幕时被创建。*/ + public boolean onTouchEvent(MotionEvent event) { + switch (event.getAction()) { + //重写了需要处理屏幕被按下的事件 + case MotionEvent.ACTION_DOWN: + //跟新当前坐标值 + int x = (int) event.getX(); + int y = (int) event.getY(); + x -= getTotalPaddingLeft(); + y -= getTotalPaddingTop(); + x += getScrollX(); + y += getScrollY(); + + //用布局控件layout根据x,y的新值设置新的位置 + Layout layout = getLayout(); + int line = layout.getLineForVertical(y); + int off = layout.getOffsetForHorizontal(line, x); + + //更新光标新的位置 + Selection.setSelection(getText(), off); + break; + } + + return super.onTouchEvent(event); + } + + @Override + /* + * 函数功能:处理用户按下一个键盘按键时会触发 的事件 + * 实现过程:如下注释 + */ + public boolean onKeyDown(int keyCode, KeyEvent event) { + switch (keyCode) { + //根据按键的 Unicode 编码值来处理 + case KeyEvent.KEYCODE_ENTER: + //“进入”按键 + if (mOnTextViewChangeListener != null) { + return false; + } + break; + case KeyEvent.KEYCODE_DEL: + //“删除”按键 + mSelectionStartBeforeDelete = getSelectionStart(); + break; + default: + break; + } + //继续执行父类的其他点击事件 + return super.onKeyDown(keyCode, event); + } + + @Override + /* + * 函数功能:处理用户松开一个键盘按键时会触发 的事件 + * 实现方式:如下注释 + */ + public boolean onKeyUp(int keyCode, KeyEvent event) { + switch(keyCode) { + //根据按键的 Unicode 编码值来处理,有删除和进入2种操作 + case KeyEvent.KEYCODE_DEL: + if (mOnTextViewChangeListener != null) { + //若是被修改过 + if (0 == mSelectionStartBeforeDelete && mIndex != 0) { + //若之前有被修改并且文档不为空 + mOnTextViewChangeListener.onEditTextDelete(mIndex, getText().toString()); + //利用上文OnTextViewChangeListener对KEYCODE_DEL按键情况的删除函数进行删除 + return true; + } + } else { + Log.d(TAG, "OnTextViewChangeListener was not seted"); + //其他情况报错,文档的改动监听器并没有建立 + } + break; + case KeyEvent.KEYCODE_ENTER: + //同上也是分为监听器是否建立2种情况 + if (mOnTextViewChangeListener != null) { + int selectionStart = getSelectionStart(); + //获取当前位置 + String text = getText().subSequence(selectionStart, length()).toString(); + //获取当前文本 + setText(getText().subSequence(0, selectionStart)); + //根据获取的文本设置当前文本 + mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, text); + //当{@link KeyEvent#KEYCODE_ENTER}添加新文本 + } else { + Log.d(TAG, "OnTextViewChangeListener was not seted"); + //其他情况报错,文档的改动监听器并没有建立 + } + break; + default: + break; + } + //继续执行父类的其他按键弹起的事件 + return super.onKeyUp(keyCode, event); + } + + @Override + /* + * 函数功能:当焦点发生变化时,会自动调用该方法来处理焦点改变的事件 + * 实现方式:如下注释 + * 参数:focused表示触发该事件的View是否获得了焦点,当该控件获得焦点时,Focused等于true,否则等于false。 + direction表示焦点移动的方向,用数值表示 + Rect:表示在触发事件的View的坐标系中,前一个获得焦点的矩形区域,即表示焦点是从哪里来的。如果不可用则为null + */ + protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { + if (mOnTextViewChangeListener != null) { + //若监听器已经建立 + if (!focused && TextUtils.isEmpty(getText())) { + //获取到焦点并且文本不为空 + mOnTextViewChangeListener.onTextChange(mIndex, false); + //mOnTextViewChangeListener子函数,置false隐藏事件选项 + } else { + mOnTextViewChangeListener.onTextChange(mIndex, true); + //mOnTextViewChangeListener子函数,置true显示事件选项 + } + } + //继续执行父类的其他焦点变化的事件 + super.onFocusChanged(focused, direction, previouslyFocusedRect); + } + + @Override + /* + * 函数功能:生成上下文菜单 + * 函数实现:如下注释 + */ + protected void onCreateContextMenu(ContextMenu menu) { + if (getText() instanceof Spanned) { + //有文本存在 + int selStart = getSelectionStart(); + int selEnd = getSelectionEnd(); + //获取文本开始和结尾位置 + + int min = Math.min(selStart, selEnd); + int max = Math.max(selStart, selEnd); + //获取开始到结尾的最大值和最小值 + + final URLSpan[] urls = ((Spanned) getText()).getSpans(min, max, URLSpan.class); + //设置url的信息的范围值 + if (urls.length == 1) { + int defaultResId = 0; + for(String schema: sSchemaActionResMap.keySet()) { + //获取计划表中所有的key值 + if(urls[0].getURL().indexOf(schema) >= 0) { + //若url可以添加则在添加后将defaultResId置为key所映射的值 + defaultResId = sSchemaActionResMap.get(schema); + break; + } + } + + if (defaultResId == 0) { + //defaultResId == 0则说明url并没有添加任何东西,所以置为连接其他SchemaActionResMap的值 + defaultResId = R.string.note_link_other; + } + + //建立菜单 + menu.add(0, 0, 0, defaultResId).setOnMenuItemClickListener( + new OnMenuItemClickListener() { + //新建按键监听器 + public boolean onMenuItemClick(MenuItem item) { + // goto a new intent + urls[0].onClick(NoteEditText.this); + //根据相应的文本设置菜单的按键 + return true; + } + }); + } + } + //继续执行父类的其他菜单创建的事件 + super.onCreateContextMenu(menu); + } +} diff --git a/doc/cyw.readcode.ui/ui/NoteItemData.java b/doc/cyw.readcode.ui/ui/NoteItemData.java new file mode 100644 index 0000000..ff52c1f --- /dev/null +++ b/doc/cyw.readcode.ui/ui/NoteItemData.java @@ -0,0 +1,230 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.Context; +import android.database.Cursor; +import android.text.TextUtils; + +import net.micode.notes.data.Contact; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.tool.DataUtils; + + +public class NoteItemData { + static final String [] PROJECTION = new String [] { + NoteColumns.ID, + NoteColumns.ALERTED_DATE, + NoteColumns.BG_COLOR_ID, + NoteColumns.CREATED_DATE, + NoteColumns.HAS_ATTACHMENT, + NoteColumns.MODIFIED_DATE, + NoteColumns.NOTES_COUNT, + NoteColumns.PARENT_ID, + NoteColumns.SNIPPET, + NoteColumns.TYPE, + NoteColumns.WIDGET_ID, + NoteColumns.WIDGET_TYPE, + }; + //常量标记和数据就不一一标记了,意义翻译基本就知道 + private static final int ID_COLUMN = 0; + private static final int ALERTED_DATE_COLUMN = 1; + private static final int BG_COLOR_ID_COLUMN = 2; + private static final int CREATED_DATE_COLUMN = 3; + private static final int HAS_ATTACHMENT_COLUMN = 4; + private static final int MODIFIED_DATE_COLUMN = 5; + private static final int NOTES_COUNT_COLUMN = 6; + private static final int PARENT_ID_COLUMN = 7; + private static final int SNIPPET_COLUMN = 8; + private static final int TYPE_COLUMN = 9; + private static final int WIDGET_ID_COLUMN = 10; + private static final int WIDGET_TYPE_COLUMN = 11; + + private long mId; + private long mAlertDate; + private int mBgColorId; + private long mCreatedDate; + private boolean mHasAttachment; + private long mModifiedDate; + private int mNotesCount; + private long mParentId; + private String mSnippet; + private int mType; + private int mWidgetId; + private int mWidgetType; + private String mName; + private String mPhoneNumber; + + private boolean mIsLastItem; + private boolean mIsFirstItem; + private boolean mIsOnlyOneItem; + private boolean mIsOneNoteFollowingFolder; + private boolean mIsMultiNotesFollowingFolder; + //初始化NoteItemData,主要利用光标cursor获取的东西 + public NoteItemData(Context context, Cursor cursor) { + //getxxx为转换格式 + mId = cursor.getLong(ID_COLUMN); + mAlertDate = cursor.getLong(ALERTED_DATE_COLUMN); + mBgColorId = cursor.getInt(BG_COLOR_ID_COLUMN); + mCreatedDate = cursor.getLong(CREATED_DATE_COLUMN); + mHasAttachment = (cursor.getInt(HAS_ATTACHMENT_COLUMN) > 0) ? true : false; + mModifiedDate = cursor.getLong(MODIFIED_DATE_COLUMN); + mNotesCount = cursor.getInt(NOTES_COUNT_COLUMN); + mParentId = cursor.getLong(PARENT_ID_COLUMN); + mSnippet = cursor.getString(SNIPPET_COLUMN); + mSnippet = mSnippet.replace(NoteEditActivity.TAG_CHECKED, "").replace( + NoteEditActivity.TAG_UNCHECKED, ""); + mType = cursor.getInt(TYPE_COLUMN); + mWidgetId = cursor.getInt(WIDGET_ID_COLUMN); + mWidgetType = cursor.getInt(WIDGET_TYPE_COLUMN); + + //初始化电话号码的信息 + mPhoneNumber = ""; + if (mParentId == Notes.ID_CALL_RECORD_FOLDER) { + mPhoneNumber = DataUtils.getCallNumberByNoteId(context.getContentResolver(), mId); + if (!TextUtils.isEmpty(mPhoneNumber)) {//mphonenumber里有符合字符串,则用contart功能连接 + mName = Contact.getContact(context, mPhoneNumber); + if (mName == null) { + mName = mPhoneNumber; + } + } + } + + if (mName == null) { + mName = ""; + } + checkPostion(cursor); + } + ///根据鼠标的位置设置标记,和位置 + private void checkPostion(Cursor cursor) { + //初始化几个标记,cursor具体功能笔记中已提到,不一一叙述 + mIsLastItem = cursor.isLast() ? true : false; + mIsFirstItem = cursor.isFirst() ? true : false; + mIsOnlyOneItem = (cursor.getCount() == 1); + //初始化“多重子文件”“单一子文件”2个标记 + mIsMultiNotesFollowingFolder = false; + mIsOneNoteFollowingFolder = false; + + //主要是设置上诉2标记 + if (mType == Notes.TYPE_NOTE && !mIsFirstItem) {//若是note格式并且不是第一个元素 + int position = cursor.getPosition(); + if (cursor.moveToPrevious()) {//获取光标位置后看上一行 + if (cursor.getInt(TYPE_COLUMN) == Notes.TYPE_FOLDER + || cursor.getInt(TYPE_COLUMN) == Notes.TYPE_SYSTEM) {//若光标满足系统或note格式 + if (cursor.getCount() > (position + 1)) { + mIsMultiNotesFollowingFolder = true;//若是数据行数大于但前位置+1则设置成正确 + } else { + mIsOneNoteFollowingFolder = true;//否则单一文件夹标记为true + } + } + if (!cursor.moveToNext()) {//若不能再往下走则报错 + throw new IllegalStateException("cursor move to previous but can't move back"); + } + } + } + } + ///下面的代码的作用均是声明获取属性的方法,下面一系列函数都是返回状态值等,用于判断状态 + public boolean isOneFollowingFolder() { + return mIsOneNoteFollowingFolder; + } + + public boolean isMultiFollowingFolder() { + return mIsMultiNotesFollowingFolder; + } + + public boolean isLast() { + return mIsLastItem; + } + + public String getCallName() { + return mName; + } + + public boolean isFirst() { + return mIsFirstItem; + } + + public boolean isSingle() { + return mIsOnlyOneItem; + } + + public long getId() { + return mId; + } + + public long getAlertDate() { + return mAlertDate; + } + + public long getCreatedDate() { + return mCreatedDate; + } + + public boolean hasAttachment() { + return mHasAttachment; + } + + public long getModifiedDate() { + return mModifiedDate; + } + + public int getBgColorId() { + return mBgColorId; + } + + public long getParentId() { + return mParentId; + } + + public int getNotesCount() { + return mNotesCount; + } + + public long getFolderId () { + return mParentId; + } + + public int getType() { + return mType; + } + + public int getWidgetType() { + return mWidgetType; + } + + public int getWidgetId() { + return mWidgetId; + } + + public String getSnippet() { + return mSnippet; + } + + public boolean hasAlert() { + return (mAlertDate > 0); + } + + //若数据父id为保存至文件夹模式的id且满足电话号码单元不为空,则isCallRecord为true + public boolean isCallRecord() { + return (mParentId == Notes.ID_CALL_RECORD_FOLDER && !TextUtils.isEmpty(mPhoneNumber)); + } + + public static int getNoteType(Cursor cursor) {//获得便签的类型 + return cursor.getInt(TYPE_COLUMN); + } +} \ No newline at end of file diff --git a/doc/cyw.readcode.ui/ui/NotesListActivity.java b/doc/cyw.readcode.ui/ui/NotesListActivity.java new file mode 100644 index 0000000..9992129 --- /dev/null +++ b/doc/cyw.readcode.ui/ui/NotesListActivity.java @@ -0,0 +1,1018 @@ +/* + * 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; +//主界面,一进入就是这个界面 +/** + * @author k + * + */ +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"; //单行超过80个字符 + + 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(final Bundle savedInstanceState) { //需要是final类型 根据程序上下文环境,Java关键字final有“这是无法改变的”或者“终态的”含义,它可以修饰非抽象类、非抽象类成员方法和变量。你可能出于两种理解而需要阻止改变:设计或效率。 + // final类不能被继承,没有子类,final类中的方法默认是final的。 + //final方法不能被子类的方法覆盖,但可以被继承。 + //final成员变量表示常量,只能被赋值一次,赋值后值不再改变。 + //final不能用于修饰构造方法。 + super.onCreate(savedInstanceState); // 调用父类的onCreate函数 + setContentView(R.layout.note_list); + initResources(); + + /** + * Insert an introduction when user firstly use this application + */ + setAppInfoFromRawRes(); + } + + @Override + // 返回一些子模块完成的数据交给主Activity处理 + 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); + // 调用 Activity 的onActivityResult() + } + } + + private void setAppInfoFromRawRes() { + // Android平台给我们提供了一个SharedPreferences类,它是一个轻量级的存储类,特别适合用于保存软件配置参数。 + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); + if (!sp.getBoolean(PREFERENCE_ADD_INTRODUCTION, false)) { + StringBuilder sb = new StringBuilder(); + InputStream in = null; + try { + // 把资源文件放到应用程序的/raw/raw下,那么就可以在应用中使用getResources获取资源后, + // 以openRawResource方法(不带后缀的资源文件名)打开这个文件。 + 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 + 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()) { + // 更新保存note的信息 + sp.edit().putBoolean(PREFERENCE_ADD_INTRODUCTION, true).commit(); + } else { + Log.e(TAG, "Save introduction note error"); + return; + } + } + } + + @Override + protected void onStart() { + super.onStart(); + startAsyncNotesListQuery(); + } + + // 初始化资源 + private void initResources() { + mContentResolver = this.getContentResolver(); // 获取应用程序的数据,得到类似数据表的东西 + mBackgroundQueryHandler = new BackgroundQueryHandler(this.getContentResolver()); + mCurrentFolderId = Notes.ID_ROOT_FOLDER; + + // findViewById 是安卓编程的定位函数,主要是引用.R文件里的引用名 + mNotesListView = (ListView) findViewById(R.id.notes_list); // 绑定XML中的ListView,作为Item的容器 + 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);// 在activity中要获取该按钮 + mAddNewNote.setOnClickListener(this); + mAddNewNote.setOnTouchListener(new NewNoteOnTouchListener()); + mDispatch = false; + mDispatchY = 0; + mOriginY = 0; + mTitleBar = (TextView) findViewById(R.id.tv_title_bar); + mState = ListEditState.NOTE_LIST; + mModeCallBack = new ModeCallback(); + } + + // 继承自ListView.MultiChoiceModeListener 和 OnMenuItemClickListener + private class ModeCallback implements ListView.MultiChoiceModeListener, OnMenuItemClickListener { + private DropdownMenu mDropDownMenu; + private ActionMode mActionMode; + private MenuItem mMoveMenu; + + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + getMenuInflater().inflate(R.menu.note_list_options, menu); + menu.findItem(R.id.delete).setOnMenuItemClickListener(this); + mMoveMenu = menu.findItem(R.id.move); + if (mFocusNoteDataItem.getParentId() == Notes.ID_CALL_RECORD_FOLDER + || DataUtils.getUserFolderCount(mContentResolver) == 0) { + mMoveMenu.setVisible(false); + } else { + mMoveMenu.setVisible(true); + mMoveMenu.setOnMenuItemClickListener(this); + } + mActionMode = mode; + mNotesListAdapter.setChoiceMode(true); + mNotesListView.setLongClickable(false); + mAddNewNote.setVisibility(View.GONE); + + View customView = LayoutInflater.from(NotesListActivity.this).inflate( + R.layout.note_list_dropdown_menu, null); + mode.setCustomView(customView); + mDropDownMenu = new DropdownMenu(NotesListActivity.this, + (Button) customView.findViewById(R.id.selection_menu), + R.menu.note_list_dropdown); + mDropDownMenu.setOnDropdownMenuItemClickListener(new PopupMenu.OnMenuItemClickListener(){ + public boolean onMenuItemClick(final MenuItem item) { + mNotesListAdapter.selectAll(!mNotesListAdapter.isAllSelected()); + updateMenu(); + return true; + } + + }); + return true; + } + + // 更新菜单 + private void updateMenu() { + int selectedCount = mNotesListAdapter.getSelectedCount(); + // Update dropdown menu + String format = getResources().getString(R.string.menu_select_title, selectedCount); + mDropDownMenu.setTitle(format); // 更改标题 + MenuItem item = mDropDownMenu.findItem(R.id.action_select_all); + if (item != null) { + if (mNotesListAdapter.isAllSelected()) { + item.setChecked(true); + item.setTitle(R.string.menu_deselect_all); + } else { + item.setChecked(false); + item.setTitle(R.string.menu_select_all); + } + } + } + + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + // TODO Auto-generated method stub + return false; + } + + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + // TODO Auto-generated method stub + return false; + } + + public void onDestroyActionMode(ActionMode mode) { + mNotesListAdapter.setChoiceMode(false); + mNotesListView.setLongClickable(true); + mAddNewNote.setVisibility(View.VISIBLE); + } + + public void finishActionMode() { + mActionMode.finish(); + } + + public void onItemCheckedStateChanged(ActionMode mode, int position, long id, + boolean checked) { + mNotesListAdapter.setCheckedItem(position, checked); + updateMenu(); + } + + public boolean onMenuItemClick(MenuItem item) { + if (mNotesListAdapter.getSelectedCount() == 0) { + Toast.makeText(NotesListActivity.this, getString(R.string.menu_select_none), + Toast.LENGTH_SHORT).show(); + return true; + } + + switch (item.getItemId()) { + case R.id.delete: + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + builder.setTitle(getString(R.string.alert_title_delete)); + builder.setIcon(android.R.drawable.ic_dialog_alert); + builder.setMessage(getString(R.string.alert_message_delete_notes, + mNotesListAdapter.getSelectedCount())); + builder.setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, + int which) { + batchDelete(); + } + }); + builder.setNegativeButton(android.R.string.cancel, null); + builder.show(); + break; + case R.id.move: + startQueryDestinationFolders(); + break; + default: + return false; + } + return true; + } + } + + private class NewNoteOnTouchListener implements OnTouchListener { + + public boolean onTouch(View v, MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: { + Display display = getWindowManager().getDefaultDisplay(); + int screenHeight = display.getHeight(); + int newNoteViewHeight = mAddNewNote.getHeight(); + int start = screenHeight - newNoteViewHeight; + int eventY = start + (int) event.getY(); + /** + * Minus TitleBar's height + */ + if (mState == ListEditState.SUB_FOLDER) { + eventY -= mTitleBar.getHeight(); + start -= mTitleBar.getHeight(); + } + /** + * HACKME:When click the transparent part of "New Note" button, dispatch + * the event to the list view behind this button. The transparent part of + * "New Note" button could be expressed by formula y=-0.12x+94锛圲nit:pixel锛� + * and the line top of the button. The coordinate based on left of the "New + * Note" button. The 94 represents maximum height of the transparent part. + * Notice that, if the background of the button changes, the formula should + * also change. This is very bad, just for the UI designer's strong requirement. + */ + if (event.getY() < (event.getX() * (-0.12) + 94)) { + View view = mNotesListView.getChildAt(mNotesListView.getChildCount() - 1 + - mNotesListView.getFooterViewsCount()); + if (view != null && view.getBottom() > start + && (view.getTop() < (start + 94))) { + mOriginY = (int) event.getY(); + mDispatchY = eventY; + event.setLocation(event.getX(), mDispatchY); + mDispatch = true; + return mNotesListView.dispatchTouchEvent(event); + } + } + break; + } + case MotionEvent.ACTION_MOVE: { + if (mDispatch) { + mDispatchY += (int) event.getY() - mOriginY; + event.setLocation(event.getX(), mDispatchY); + return mNotesListView.dispatchTouchEvent(event); + } + break; + } + default: { + if (mDispatch) { + event.setLocation(event.getX(), mDispatchY); + mDispatch = false; + return mNotesListView.dispatchTouchEvent(event); + } + break; + } + } + return false; + } + + }; + + private void startAsyncNotesListQuery() { + String selection = (mCurrentFolderId == Notes.ID_ROOT_FOLDER) ? ROOT_FOLDER_SELECTION + : NORMAL_SELECTION; + mBackgroundQueryHandler.startQuery(FOLDER_NOTE_LIST_QUERY_TOKEN, null, + Notes.CONTENT_NOTE_URI, NoteItemData.PROJECTION, selection, new String[] { + String.valueOf(mCurrentFolderId) + }, NoteColumns.TYPE + " DESC," + NoteColumns.MODIFIED_DATE + " DESC"); + } + + private final class BackgroundQueryHandler extends AsyncQueryHandler { + public BackgroundQueryHandler(ContentResolver contentResolver) { + super(contentResolver); + } + + @Override + protected void onQueryComplete(int token, Object cookie, Cursor cursor) { + switch (token) { + case FOLDER_NOTE_LIST_QUERY_TOKEN: + mNotesListAdapter.changeCursor(cursor); + break; + case FOLDER_LIST_QUERY_TOKEN: + if (cursor != null && cursor.getCount() > 0) { + showFolderListMenu(cursor); + } else { + Log.e(TAG, "Query folder failed"); + } + break; + default: + return; + } + } + } + + private void showFolderListMenu(Cursor cursor) { + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + builder.setTitle(R.string.menu_title_select_folder); + final FoldersListAdapter adapter = new FoldersListAdapter(this, cursor); + builder.setAdapter(adapter, new DialogInterface.OnClickListener() { + + public void onClick(DialogInterface dialog, int which) { + DataUtils.batchMoveToFolder(mContentResolver, + mNotesListAdapter.getSelectedItemIds(), adapter.getItemId(which)); + Toast.makeText( + NotesListActivity.this, + getString(R.string.format_move_notes_to_folder, + mNotesListAdapter.getSelectedCount(), + adapter.getFolderName(NotesListActivity.this, which)), + Toast.LENGTH_SHORT).show(); + mModeCallBack.finishActionMode(); + } + }); + builder.show(); + } + + private void createNewNote() { + Intent intent = new Intent(this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_INSERT_OR_EDIT); + intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mCurrentFolderId); + this.startActivityForResult(intent, REQUEST_CODE_NEW_NODE); + } + + private void batchDelete() { + new AsyncTask>() { + protected HashSet doInBackground(Void... unused) { + HashSet widgets = mNotesListAdapter.getSelectedWidget(); + if (!isSyncMode()) { + // if not synced, delete notes directly + if (DataUtils.batchDeleteNotes(mContentResolver, mNotesListAdapter + .getSelectedItemIds())) { + } else { + Log.e(TAG, "Delete notes error, should not happens"); + } + } else { + // in sync mode, we'll move the deleted note into the trash + // folder + if (!DataUtils.batchMoveToFolder(mContentResolver, mNotesListAdapter + .getSelectedItemIds(), Notes.ID_TRASH_FOLER)) { + Log.e(TAG, "Move notes to trash folder error, should not happens"); + } + } + return widgets; + } + + @Override + protected void onPostExecute(HashSet widgets) { + if (widgets != null) { + for (AppWidgetAttribute widget : widgets) { + if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID + && widget.widgetType != Notes.TYPE_WIDGET_INVALIDE) { + updateWidget(widget.widgetId, widget.widgetType); + } + } + } + mModeCallBack.finishActionMode(); + } + }.execute(); + } + + private void deleteFolder(long folderId) { + if (folderId == Notes.ID_ROOT_FOLDER) { + Log.e(TAG, "Wrong folder id, should not happen " + folderId); + return; + } + + HashSet ids = new HashSet(); + ids.add(folderId); + HashSet widgets = DataUtils.getFolderNoteWidget(mContentResolver, + folderId); + if (!isSyncMode()) { + // if not synced, delete folder directly + DataUtils.batchDeleteNotes(mContentResolver, ids); + } else { + // in sync mode, we'll move the deleted folder into the trash folder + DataUtils.batchMoveToFolder(mContentResolver, ids, Notes.ID_TRASH_FOLER); + } + if (widgets != null) { + for (AppWidgetAttribute widget : widgets) { + if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID + && widget.widgetType != Notes.TYPE_WIDGET_INVALIDE) { + updateWidget(widget.widgetId, widget.widgetType); + } + } + } + } + + private void openNode(NoteItemData data) { + Intent intent = new Intent(this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.putExtra(Intent.EXTRA_UID, data.getId()); + this.startActivityForResult(intent, REQUEST_CODE_OPEN_NODE); + } + + private void openFolder(NoteItemData data) { + mCurrentFolderId = data.getId(); + startAsyncNotesListQuery(); + if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { + mState = ListEditState.CALL_RECORD_FOLDER; + mAddNewNote.setVisibility(View.GONE); + } else { + mState = ListEditState.SUB_FOLDER; + } + if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { + mTitleBar.setText(R.string.call_record_folder_name); + } else { + mTitleBar.setText(data.getSnippet()); + } + mTitleBar.setVisibility(View.VISIBLE); + } + + public void onClick(View v) { + switch (v.getId()) { + case R.id.btn_new_note: + createNewNote(); + break; + default: + break; + } + } + + private void showSoftInput() { + InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + if (inputMethodManager != null) { + inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); + } + } + + private void hideSoftInput(View view) { + InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + + private void showCreateOrModifyFolderDialog(final boolean create) { + final AlertDialog.Builder builder = new AlertDialog.Builder(this); + View view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_text, null); + final EditText etName = (EditText) view.findViewById(R.id.et_foler_name); + showSoftInput(); + if (!create) { + if (mFocusNoteDataItem != null) { + etName.setText(mFocusNoteDataItem.getSnippet()); + builder.setTitle(getString(R.string.menu_folder_change_name)); + } else { + Log.e(TAG, "The long click data item is null"); + return; + } + } else { + etName.setText(""); + builder.setTitle(this.getString(R.string.menu_create_folder)); + } + + builder.setPositiveButton(android.R.string.ok, null); + builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + hideSoftInput(etName); + } + }); + + final Dialog dialog = builder.setView(view).show(); + final Button positive = (Button)dialog.findViewById(android.R.id.button1); + positive.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + hideSoftInput(etName); + String name = etName.getText().toString(); + if (DataUtils.checkVisibleFolderName(mContentResolver, name)) { + Toast.makeText(NotesListActivity.this, getString(R.string.folder_exist, name), + Toast.LENGTH_LONG).show(); + etName.setSelection(0, etName.length()); + return; + } + if (!create) { + if (!TextUtils.isEmpty(name)) { + ContentValues values = new ContentValues(); + values.put(NoteColumns.SNIPPET, name); + values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); + values.put(NoteColumns.LOCAL_MODIFIED, 1); + mContentResolver.update(Notes.CONTENT_NOTE_URI, values, NoteColumns.ID + + "=?", new String[] { + String.valueOf(mFocusNoteDataItem.getId()) + }); + } + } else if (!TextUtils.isEmpty(name)) { + ContentValues values = new ContentValues(); + values.put(NoteColumns.SNIPPET, name); + values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); + mContentResolver.insert(Notes.CONTENT_NOTE_URI, values); + } + dialog.dismiss(); + } + }); + + if (TextUtils.isEmpty(etName.getText())) { + positive.setEnabled(false); + } + /** + * When the name edit text is null, disable the positive button + */ + etName.addTextChangedListener(new TextWatcher() { + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // TODO Auto-generated method stub + + } + + public void onTextChanged(CharSequence s, int start, int before, int count) { + if (TextUtils.isEmpty(etName.getText())) { + positive.setEnabled(false); + } else { + positive.setEnabled(true); + } + } + + public void afterTextChanged(Editable s) { + // TODO Auto-generated method stub + + } + }); + } + + /* (non-Javadoc) + * @see android.app.Activity#onBackPressed() + * 按返回键时根据情况更改类中的数据 + */ + @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; + } + } + + /** + * @param appWidgetId + * @param appWidgetType + * 根据不同类型的widget更新插件,通过intent传送数据 + */ + 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); + } + + /* (non-Javadoc) + * @see android.app.Activity#onContextItemSelected(android.view.MenuItem) + * 针对menu中不同的选择进行不同的处理,里面详细注释 + */ + @Override + public boolean onContextItemSelected(MenuItem item) { + if (mFocusNoteDataItem == null) { + Log.e(TAG, "The long click data item is null"); + return false; + } + switch (item.getItemId()) { + case MENU_FOLDER_VIEW: + openFolder(mFocusNoteDataItem);//打开对应文件 + break; + case MENU_FOLDER_DELETE: + AlertDialog.Builder builder = new AlertDialog.Builder(this);//设置确认是否删除的对话框 + builder.setTitle(getString(R.string.alert_title_delete)); + builder.setIcon(android.R.drawable.ic_dialog_alert); + builder.setMessage(getString(R.string.alert_message_delete_folder)); + builder.setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + deleteFolder(mFocusNoteDataItem.getId()); + } + }); + builder.setNegativeButton(android.R.string.cancel, null); + builder.show();//显示对话框 + break; + case MENU_FOLDER_CHANGE_NAME: + showCreateOrModifyFolderDialog(false); + break; + default: + break; + } + + return true; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + menu.clear(); + if (mState == ListEditState.NOTE_LIST) { + getMenuInflater().inflate(R.menu.note_list, menu); + // set sync or sync_cancel + menu.findItem(R.id.menu_sync).setTitle( + GTaskSyncService.isSyncing() ? R.string.menu_sync_cancel : R.string.menu_sync); + } else if (mState == ListEditState.SUB_FOLDER) { + getMenuInflater().inflate(R.menu.sub_folder, menu); + } else if (mState == ListEditState.CALL_RECORD_FOLDER) { + getMenuInflater().inflate(R.menu.call_record_folder, menu); + } else { + Log.e(TAG, "Wrong state:" + mState); + } + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_new_folder: { + showCreateOrModifyFolderDialog(true); + break; + } + case R.id.menu_export_text: { + exportNoteToText(); + break; + } + case R.id.menu_sync: { + if (isSyncMode()) { + if (TextUtils.equals(item.getTitle(), getString(R.string.menu_sync))) { + GTaskSyncService.startSync(this); + } else { + GTaskSyncService.cancelSync(this); + } + } else { + startPreferenceActivity(); + } + break; + } + case R.id.menu_setting: { + startPreferenceActivity(); + break; + } + case R.id.menu_new_note: { + createNewNote(); + break; + } + case R.id.menu_search: + onSearchRequested(); + break; + default: + break; + } + return true; + } + + /* (non-Javadoc) + * @see android.app.Activity#onSearchRequested() + * 直接调用startSearch函数 + */ + @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(); + } + + /** + * @return + * 功能:判断是否正在同步 + */ + private boolean isSyncMode() { + return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0; + } + + /** + * 功能:跳转到PreferenceActivity界面 + */ + private void startPreferenceActivity() { + Activity from = getParent() != null ? getParent() : this; + Intent intent = new Intent(from, NotesPreferenceActivity.class); + from.startActivityIfNeeded(intent, -1); + } + + /** + * @author k + * 函数功能:实现对便签列表项的点击事件(短按) + */ + 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"); + } + + /* (non-Javadoc) + * @see android.widget.AdapterView.OnItemLongClickListener#onItemLongClick(android.widget.AdapterView, android.view.View, int, long) + * 长按某一项时进行的操作 + * 如果长按的是便签,则通过ActionMode菜单实现;如果长按的是文件夹,则通过ContextMenu菜单实现; + * 具体ActionMOde菜单和ContextMenu菜单的详细见精度笔记 + */ + public boolean onItemLongClick(AdapterView parent, View view, int position, long id) {// 函数:实现长按项目的点击事件。如果长按的是便签,则通过ActionMode菜单实现;如果长按的是文件夹,则通过ContextMenu菜单实现。 + 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/doc/cyw.readcode.ui/ui/NotesListAdapter.java b/doc/cyw.readcode.ui/ui/NotesListAdapter.java new file mode 100644 index 0000000..5c5d3ee --- /dev/null +++ b/doc/cyw.readcode.ui/ui/NotesListAdapter.java @@ -0,0 +1,273 @@ +/* + * 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; + + +/* + * 功能:直译为便签表连接器,继承了CursorAdapter,它为cursor和ListView提供了连接的桥梁。 + * 所以NotesListAdapter实现的是鼠标和编辑便签链接的桥梁 + */ +public class NotesListAdapter extends CursorAdapter { + private static final String TAG = "NotesListAdapter"; + private Context mContext; + private HashMap mSelectedIndex; + private int mNotesCount; //便签数 + private boolean mChoiceMode; //选择模式标记 + + /* + * 桌面widget的属性,包括编号和类型 + */ + public static class AppWidgetAttribute { + public int widgetId; + public int widgetType; + }; + + /* + * 函数功能:初始化便签链接器 + * 函数实现:根据传进来的内容设置相关变量 + */ + public NotesListAdapter(Context context) { + super(context, null); //父类对象置空 + mSelectedIndex = new HashMap(); //新建选项下标的hash表 + mContext = context; + mNotesCount = 0; + } + + @Override + /* + * 函数功能:新建一个视图来存储光标所指向的数据 + * 函数实现:使用兄弟类NotesListItem新建一个项目选项 + */ + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return new NotesListItem(context); + } + + /* + * 函数功能:将已经存在的视图和鼠标指向的数据进行捆绑 + * 函数实现:如下注释 + */ + @Override + public void bindView(View view, Context context, Cursor cursor) { + if (view instanceof NotesListItem) { + //若view是NotesListItem的一个实例 + NoteItemData itemData = new NoteItemData(context, cursor); + ((NotesListItem) view).bind(context, itemData, mChoiceMode, + isSelectedItem(cursor.getPosition())); + //则新建一个项目选项并且用bind跟将view和鼠标,内容,便签数据捆绑在一起 + } + } + + /* + * 函数功能:设置勾选框 + * 函数实现:如下注释 + */ + public void setCheckedItem(final int position, final boolean checked) { + mSelectedIndex.put(position, checked); + //根据定位和是否勾选设置下标 + notifyDataSetChanged(); + //在修改后刷新activity + } + + /* + * 函数功能:判断单选按钮是否勾选 + */ + public boolean isInChoiceMode() { + return mChoiceMode; + } + + /* + * 函数功能:设置单项选项框 + * 函数实现:重置下标并且根据参数mode设置选项 + */ + public void setChoiceMode(boolean mode) { + mSelectedIndex.clear(); + mChoiceMode = mode; + } + + /* + * 函数功能:选择全部选项 + * 函数实现:如下注释 + */ + public void selectAll(boolean checked) { + Cursor cursor = getCursor(); + //获取光标位置 + for (int i = 0; i < getCount(); i++) { + if (cursor.moveToPosition(i)) { + if (NoteItemData.getNoteType(cursor) == Notes.TYPE_NOTE) { + setCheckedItem(i, checked); + } + } + } + //遍历所有光标可用的位置在判断为便签类型之后勾选单项框 + } + + /* + * 函数功能:建立选择项的下标列表 + * 函数实现:如下注释 + */ + public HashSet getSelectedItemIds() { + HashSet itemSet = new HashSet(); + //建立hash表 + for (Integer position : mSelectedIndex.keySet()) { + //遍历所有的关键 + if (mSelectedIndex.get(position) == true) { + //若光标位置可用 + Long id = getItemId(position); + if (id == Notes.ID_ROOT_FOLDER) { + //原文件不需要添加 + Log.d(TAG, "Wrong item id, should not happen"); + } else { + itemSet.add(id); + } + //则将id该下标假如选项集合中 + + } + } + + return itemSet; + } + + /* + * 函数功能:建立桌面Widget的选项表 + * 函数实现:如下注释 + */ + public HashSet getSelectedWidget() { + HashSet itemSet = new HashSet(); + for (Integer position : mSelectedIndex.keySet()) { + if (mSelectedIndex.get(position) == true) { + Cursor c = (Cursor) getItem(position); + //以上4句和getSelectedItemIds一样,不再重复 + if (c != null) { + //光标位置可用的话就建立新的Widget属性并编辑下标和类型,最后添加到选项集中 + AppWidgetAttribute widget = new AppWidgetAttribute(); + NoteItemData item = new NoteItemData(mContext, c); + widget.widgetId = item.getWidgetId(); + widget.widgetType = item.getWidgetType(); + itemSet.add(widget); + /** + * Don't close cursor here, only the adapter could close it + */ + } else { + Log.e(TAG, "Invalid cursor"); + return null; + } + } + } + return itemSet; + } + + /* + * 函数功能:获取选项个数 + * 函数实现:如下注释 + */ + public int getSelectedCount() { + Collection values = mSelectedIndex.values(); + //首先获取选项下标的值 + if (null == values) { + return 0; + } + Iterator iter = values.iterator(); + //初始化叠加器 + int count = 0; + while (iter.hasNext()) { + if (true == iter.next()) { + //若value值为真计数+1 + count++; + } + } + return count; + } + + /* + * 函数功能:判断是否全部选中 + * 函数实现:如下注释 + */ + public boolean isAllSelected() { + int checkedCount = getSelectedCount(); + return (checkedCount != 0 && checkedCount == mNotesCount); + //获取选项数看是否等于便签的个数 + } + + /* + * 函数功能:判断是否为选项表 + * 函数实现:通过传递的下标来确定 + */ + public boolean isSelectedItem(final int position) { + if (null == mSelectedIndex.get(position)) { + return false; + } + return mSelectedIndex.get(position); + } + + @Override + /* + * 函数功能:在activity内容发生局部变动的时候回调该函数计算便签的数量 + * 函数实现:如下注释 + */ + protected void onContentChanged() { + super.onContentChanged(); + //执行基类函数 + calcNotesCount(); + } + + @Override + /* + * 函数功能:在activity光标发生局部变动的时候回调该函数计算便签的数量 + */ + public void changeCursor(Cursor cursor) { + super.changeCursor(cursor); + //执行基类函数 + calcNotesCount(); + } + + /* + * 函数功能:计算便签数量 + * + */ + private void calcNotesCount() { + mNotesCount = 0; + for (int i = 0; i < getCount(); i++) { + //获取总数同时遍历 + Cursor c = (Cursor) getItem(i); + if (c != null) { + if (NoteItemData.getNoteType(c) == Notes.TYPE_NOTE) { + mNotesCount++; + //若该位置不为空并且文本类型为便签就+1 + } + } else { + Log.e(TAG, "Invalid cursor"); + return; + } + //否则报错 + } + } +} diff --git a/doc/cyw.readcode.ui/ui/NotesListItem.java b/doc/cyw.readcode.ui/ui/NotesListItem.java new file mode 100644 index 0000000..b89acf7 --- /dev/null +++ b/doc/cyw.readcode.ui/ui/NotesListItem.java @@ -0,0 +1,132 @@ +/* + * 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; //打钩框 + + /*初始化基本信息*/ + public NotesListItem(Context context) { + super(context); //super()它的主要作用是调整调用父类构造函数的顺序 + inflate(context, R.layout.note_item, this);//Inflate可用于将一个xml中定义的布局控件找出来,这里的xml是r。layout + //findViewById用于从contentView中查找指定ID的View,转换出来的形式根据需要而定; + 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); + } + ///根据data的属性对各个控件的属性的控制,主要是可见性Visibility,内容setText,格式setTextAppearance + 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; + ///设置控件属性,一共三种情况,由data的id和父id是否与保存到文件夹的id一致来决定 + if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { + mCallName.setVisibility(View.GONE); + mAlert.setVisibility(View.VISIBLE); + //设置该textview的style + mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); + //settext为设置内容 + mTitle.setText(context.getString(R.string.call_record_folder_name) + + context.getString(R.string.format_folder_files_count, data.getNotesCount())); + mAlert.setImageResource(R.drawable.call_record); + } else if (data.getParentId() == Notes.ID_CALL_RECORD_FOLDER) { + mCallName.setVisibility(View.VISIBLE); + mCallName.setText(data.getCallName()); + mTitle.setTextAppearance(context,R.style.TextAppearanceSecondaryItem); + mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); + ///关于闹钟的设置 + if (data.hasAlert()) { + mAlert.setImageResource(R.drawable.clock);//图片来源的设置 + mAlert.setVisibility(View.VISIBLE); + } else { + mAlert.setVisibility(View.GONE); + } + } else { + mCallName.setVisibility(View.GONE); + mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); + ///设置title格式 + 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); + } + } + } + ///设置内容,获取相关时间,从data里编辑的日期中获取 + mTime. setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate())); + + setBackground(data); + } + //根据data的文件属性来设置背景 + private void setBackground(NoteItemData data) { + int id = data.getBgColorId(); + //,若是note型文件,则4种情况,对于4种不同情况的背景来源 + 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 { + //若不是note直接调用文件夹的背景来源 + setBackgroundResource(NoteItemBgResources.getFolderBgRes()); + } + } + public NoteItemData getItemData() { + return mItemData; + }//返回当前便签的数据信息 +} diff --git a/doc/cyw.readcode.ui/ui/NotesPreferenceActivity.java b/doc/cyw.readcode.ui/ui/NotesPreferenceActivity.java new file mode 100644 index 0000000..75cfd3d --- /dev/null +++ b/doc/cyw.readcode.ui/ui/NotesPreferenceActivity.java @@ -0,0 +1,530 @@ +/* + * 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; + +/* + *该类功能:NotesPreferenceActivity,在小米便签中主要实现的是对背景颜色和字体大小的数据储存。 + * 继承了PreferenceActivity主要功能为对系统信息和配置进行自动保存的Activity + */ +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; + //账户的hash标记 + + @Override + /* + *函数功能:创建一个activity,在函数里要完成所有的正常静态设置 + *参数:Bundle icicle:存放了 activity 当前的状态 + *函数实现:如下注释 + */ + protected void onCreate(Bundle icicle) { + //先执行父类的创建函数 + super.onCreate(icicle); + + /* using the app icon for navigation */ + getActionBar().setDisplayHomeAsUpEnabled(true); + //给左上角图标的左边加上一个返回的图标 + + addPreferencesFromResource(R.xml.preferences); + //添加xml来源并显示 xml + 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); + //获取listvivew,ListView的作用:用于列出所有选择 + getListView().addHeaderView(header, null, true); + //在listview组件上方添加其他组件 + } + + @Override + /* + * 函数功能:activity交互功能的实现,用于接受用户的输入 + * 函数实现:如下注释 + */ + protected void onResume() { + //先执行父类 的交互实现 + super.onResume(); + + // need to set sync account automatically if user has added a new + // account + if (mHasAddedAccount) { + //若用户新加了账户则自动设置同步账户 + Account[] accounts = getGoogleAccounts(); + //获取google同步账户 + if (mOriAccounts != null && accounts.length > mOriAccounts.length) { + //若原账户不为空且当前账户有增加 + for (Account accountNew : accounts) { + boolean found = false; + for (Account accountOld : mOriAccounts) { + if (TextUtils.equals(accountOld.name, accountNew.name)) { + //更新账户 + found = true; + break; + } + } + if (!found) { + setSyncAccount(accountNew.name); + //若是没有找到旧的账户,那么同步账号中就只添加新账户 + break; + } + } + } + } + + refreshUI(); + //刷新标签界面 + } + + @Override + /* + * 函数功能:销毁一个activity + * 函数实现:如下注释 + */ + protected void onDestroy() { + if (mReceiver != null) { + unregisterReceiver(mReceiver); + //注销接收器 + } + super.onDestroy(); + //执行父类的销毁动作 + } + + /* + * 函数功能:重新设置账户信息 + * 函数实现:如下注释 + */ + private void loadAccountPreference() { + mAccountCategory.removeAll(); + //销毁所有的分组 + Preference accountPref = new Preference(this); + //建立首选项 + final String defaultAccount = getSyncAccountName(this); + accountPref.setTitle(getString(R.string.preferences_account_title)); + accountPref.setSummary(getString(R.string.preferences_account_summary)); + //设置首选项的大标题和小标题 + accountPref.setOnPreferenceClickListener(new OnPreferenceClickListener() { + public boolean onPreferenceClick(Preference preference) { + //建立监听器 + if (!GTaskSyncService.isSyncing()) { + if (TextUtils.isEmpty(defaultAccount)) { + // the first time to set account + //若是第一次建立账户显示选择账户提示对话框 + showSelectAccountAlertDialog(); + } else { + // if the account has already been set, we need to promp + // user about the risk + //若是已经建立则显示修改对话框并进行修改操作 + showChangeAccountConfirmAlertDialog(); + } + } else { + //若在没有同步的情况下,则在toast中显示不能修改 + Toast.makeText(NotesPreferenceActivity.this, + R.string.preferences_toast_cannot_change_account, Toast.LENGTH_SHORT) + .show(); + } + return true; + } + }); + + //根据新建首选项编辑新的账户分组 + mAccountCategory.addPreference(accountPref); + } + + /* + *函数功能:设置按键的状态和最后同步的时间 + *函数实现:如下注释 + */ + private void loadSyncButton() { + Button syncButton = (Button) findViewById(R.id.preference_sync_button); + TextView lastSyncTimeView = (TextView) findViewById(R.id.prefenerece_sync_status_textview); + //获取同步按钮控件和最终同步时间的的窗口 + // set button state + //设置按钮的状态 + if (GTaskSyncService.isSyncing()) { + //若是在同步状态下 + syncButton.setText(getString(R.string.preferences_button_sync_cancel)); + syncButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + GTaskSyncService.cancelSync(NotesPreferenceActivity.this); + } + }); + //设置按钮显示的文本为“取消同步”以及监听器 + } else { + syncButton.setText(getString(R.string.preferences_button_sync_immediately)); + syncButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + GTaskSyncService.startSync(NotesPreferenceActivity.this); + } + }); + //若是不同步则设置按钮显示的文本为“立即同步”以及对应监听器 + } + syncButton.setEnabled(!TextUtils.isEmpty(getSyncAccountName(this))); + //设置按键可用还是不可用 + + // set last sync time + // 设置最终同步时间 + if (GTaskSyncService.isSyncing()) { + //若是在同步的情况下 + lastSyncTimeView.setText(GTaskSyncService.getProgressString()); + lastSyncTimeView.setVisibility(View.VISIBLE); + // 根据当前同步服务器设置时间显示框的文本以及可见性 + } else { + //若是非同步情况 + long lastSyncTime = getLastSyncTime(this); + if (lastSyncTime != 0) { + lastSyncTimeView.setText(getString(R.string.preferences_last_sync_time, + DateFormat.format(getString(R.string.preferences_last_sync_time_format), + lastSyncTime))); + lastSyncTimeView.setVisibility(View.VISIBLE); + //则根据最后同步时间的信息来编辑时间显示框的文本内容和可见性 + } else { + //若时间为空直接设置为不可见状态 + lastSyncTimeView.setVisibility(View.GONE); + } + } + } + /* + *函数功能:刷新标签界面 + *函数实现:调用上文设置账号和设置按键两个函数来实现 + */ + private void refreshUI() { + loadAccountPreference(); + loadSyncButton(); + } + + /* + * 函数功能:显示账户选择的对话框并进行账户的设置 + * 函数实现:如下注释 + */ + private void showSelectAccountAlertDialog() { + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); + //创建一个新的对话框 + + View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null); + TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title); + titleTextView.setText(getString(R.string.preferences_dialog_select_account_title)); + TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle); + subtitleTextView.setText(getString(R.string.preferences_dialog_select_account_tips)); + //设置标题以及子标题的内容 + dialogBuilder.setCustomTitle(titleView); + dialogBuilder.setPositiveButton(null, null); + //设置对话框的自定义标题,建立一个YES的按钮 + Account[] accounts = getGoogleAccounts(); + String defAccount = getSyncAccountName(this); + //获取同步账户信息 + mOriAccounts = accounts; + mHasAddedAccount = false; + + if (accounts.length > 0) { + //若账户不为空 + CharSequence[] items = new CharSequence[accounts.length]; + final CharSequence[] itemMapping = items; + int checkedItem = -1; + int index = 0; + for (Account account : accounts) { + if (TextUtils.equals(account.name, defAccount)) { + checkedItem = index; + //在账户列表中查询到所需账户 + } + items[index++] = account.name; + } + dialogBuilder.setSingleChoiceItems(items, checkedItem, + //在对话框建立一个单选的复选框 + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + setSyncAccount(itemMapping[which].toString()); + dialog.dismiss(); + //取消对话框 + refreshUI(); + } + //设置点击后执行的事件,包括检录新同步账户和刷新标签界面 + }); + //建立对话框网络版的监听器 + } + + View addAccountView = LayoutInflater.from(this).inflate(R.layout.add_account_text, null); + dialogBuilder.setView(addAccountView); + //给新加账户对话框设置自定义样式 + + final AlertDialog dialog = dialogBuilder.show(); + //显示对话框 + addAccountView.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + mHasAddedAccount = true; + //将新加账户的hash置true + Intent intent = new Intent("android.settings.ADD_ACCOUNT_SETTINGS"); + //建立网络建立组件 + intent.putExtra(AUTHORITIES_FILTER_KEY, new String[] { + "gmail-ls" + }); + startActivityForResult(intent, -1); + //跳回上一个选项 + dialog.dismiss(); + } + }); + //建立新加账户对话框的监听器 + } + + /* + * 函数功能:显示账户选择对话框和相关账户操作 + * 函数实现:如下注释 + */ + private void showChangeAccountConfirmAlertDialog() { + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); + //创建一个新的对话框 + View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null); + TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title); + titleTextView.setText(getString(R.string.preferences_dialog_change_account_title, + getSyncAccountName(this))); + TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle); + subtitleTextView.setText(getString(R.string.preferences_dialog_change_account_warn_msg)); + //根据同步修改的账户信息设置标题以及子标题的内容 + dialogBuilder.setCustomTitle(titleView); + //设置对话框的自定义标题 + CharSequence[] menuItemArray = new CharSequence[] { + getString(R.string.preferences_menu_change_account), + getString(R.string.preferences_menu_remove_account), + getString(R.string.preferences_menu_cancel) + }; + //定义一些标记字符串 + dialogBuilder.setItems(menuItemArray, new DialogInterface.OnClickListener() { + //设置对话框要显示的一个list,用于显示几个命令时,即change,remove,cancel + public void onClick(DialogInterface dialog, int which) { + //按键功能,由which来决定 + if (which == 0) { + //进入账户选择对话框 + showSelectAccountAlertDialog(); + } else if (which == 1) { + //删除账户并且跟新便签界面 + removeSyncAccount(); + refreshUI(); + } + } + }); + dialogBuilder.show(); + //显示对话框 + } + + /* + *函数功能:获取谷歌账户 + *函数实现:通过账户管理器直接获取 + */ + private Account[] getGoogleAccounts() { + AccountManager accountManager = AccountManager.get(this); + return accountManager.getAccountsByType("com.google"); + } + + /* + * 函数功能:设置同步账户 + * 函数实现:如下注释: + */ + private void setSyncAccount(String account) { + if (!getSyncAccountName(this).equals(account)) { + //假如该账号不在同步账号列表中 + SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = settings.edit(); + //编辑共享的首选项 + if (account != null) { + editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, account); + } else { + editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, ""); + } + //将该账号加入到首选项中 + + editor.commit(); + //提交修改的数据 + + + 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(); + //将toast的文本信息置为“设置账户成功”并显示出来 + } + } + /* + * 函数功能:删除同步账户 + * 函数实现:如下注释: + */ + private void removeSyncAccount() { + SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = settings.edit(); + //设置共享首选项 + + if (settings.contains(PREFERENCE_SYNC_ACCOUNT_NAME)) { + editor.remove(PREFERENCE_SYNC_ACCOUNT_NAME); + //假如当前首选项中有账户就删除 + } + if (settings.contains(PREFERENCE_LAST_SYNC_TIME)) { + editor.remove(PREFERENCE_LAST_SYNC_TIME); + //删除当前首选项中有账户时间 + } + editor.commit(); + //提交更新后的数据 + + // clean up local gtask related info + new Thread(new Runnable() { + public void run() { + ContentValues values = new ContentValues(); + values.put(NoteColumns.GTASK_ID, ""); + values.put(NoteColumns.SYNC_ID, 0); + getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null); + } + }).start(); + //重置当地同步任务的信息 + } + + /* + * 函数功能:获取同步账户名称 + * 函数实现:通过共享的首选项里的信息直接获取 + */ + public static String getSyncAccountName(Context context) { + SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, + Context.MODE_PRIVATE); + return settings.getString(PREFERENCE_SYNC_ACCOUNT_NAME, ""); + } + + /* + * 函数功能:设置最终同步的时间 + * 函数实现:如下注释 + */ + public static void setLastSyncTime(Context context, long time) { + SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, + Context.MODE_PRIVATE); + SharedPreferences.Editor editor = settings.edit(); + // 从共享首选项中找到相关账户并获取其编辑器 + editor.putLong(PREFERENCE_LAST_SYNC_TIME, time); + editor.commit(); + //编辑最终同步时间并提交更新 + } + /* + * 函数功能:获取最终同步时间 + * 函数实现:通过共享的首选项里的信息直接获取 + */ + public static long getLastSyncTime(Context context) { + SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, + Context.MODE_PRIVATE); + return settings.getLong(PREFERENCE_LAST_SYNC_TIME, 0); + } + + /* + * 函数功能:接受同步信息 + * 函数实现:继承BroadcastReceiver + */ + private class GTaskReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + refreshUI(); + if (intent.getBooleanExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_IS_SYNCING, false)) { + //获取随广播而来的Intent中的同步服务的数据 + TextView syncStatus = (TextView) findViewById(R.id.prefenerece_sync_status_textview); + syncStatus.setText(intent + .getStringExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_PROGRESS_MSG)); + //通过获取的数据在设置系统的状态 + } + + } + } + + /* + * 函数功能:处理菜单的选项 + * 函数实现:如下注释 + * 参数:MenuItem菜单选项 + */ + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + //根据选项的id选择,这里只有一个主页 + case android.R.id.home: + Intent intent = new Intent(this, NotesListActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + return true; + //在主页情况下在创建连接组件intent,发出清空的信号并开始一个相应的activity + default: + return false; + } + } +} + diff --git a/src/Notes-master1/.gradle/7.5/fileHashes/fileHashes.bin b/src/Notes-master1/.gradle/7.5/fileHashes/fileHashes.bin index 189ef4ff341ab7e8ba72bd214ea8f19913770948..29d469c015b59ea40c640db51c38dfc37fe8af75 100644 GIT binary patch delta 103 zcmdmdhH3g)rVS<%jE^^)N)#&aH)MA^`Mvzh00zs9C(pH*Az-HQO7TF*tGQ6Y)Xln< v+B}Sr8x?mjZFcnd6g&B81OMaz4`G(izQ!z*(>?SAK09x_2~o`;2Bh@>Y0M#3 delta 36 scmbP!mTB`DrVS<%j1M=PN)#$g-e=LjIn`2`hf#B*;tQtDjvk+40T2)lU;qFB diff --git a/src/Notes-master1/.gradle/7.5/fileHashes/fileHashes.lock b/src/Notes-master1/.gradle/7.5/fileHashes/fileHashes.lock index 14ec67ec871ce5f7fb018e6218b8245696b5a56d..c67971fbc12771ec1465c055cf95cdc1d4ef4bdf 100644 GIT binary patch literal 17 VcmZQB7jQf@V;lD|1~6c<001nW1Lpt$ literal 17 VcmZQB7jQf@V;lD|1~6bU0RSwT1LFVy diff --git a/src/Notes-master1/app/src/main/java/net/micode/notes/ui/AlarmAlertActivity.java b/src/Notes-master1/app/src/main/java/net/micode/notes/ui/AlarmAlertActivity.java index 85723be..e9fdb28 100644 --- a/src/Notes-master1/app/src/main/java/net/micode/notes/ui/AlarmAlertActivity.java +++ b/src/Notes-master1/app/src/main/java/net/micode/notes/ui/AlarmAlertActivity.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package net.micode.notes.ui; import android.app.Activity; @@ -39,58 +38,81 @@ import net.micode.notes.tool.DataUtils; import java.io.IOException; - public class AlarmAlertActivity extends Activity implements OnClickListener, OnDismissListener { - private long mNoteId; - private String mSnippet; + private long mNoteId; //文本在数据库存储中的ID号 + private String mSnippet; //闹钟提示时出现的文本片段 private static final int SNIPPET_PREW_MAX_LEN = 60; MediaPlayer mPlayer; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + //Bundle类型的数据与Map类型的数据相似,都是以key-value的形式存储数据的 + //onsaveInstanceState方法是用来保存Activity的状态的 + //能从onCreate的参数savedInsanceState中获得状态数据 requestWindowFeature(Window.FEATURE_NO_TITLE); + //界面显示——无标题 final Window win = getWindow(); win.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); if (!isScreenOn()) { win.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON + //保持窗体点亮 | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON + //将窗体点亮 | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON + //允许窗体点亮时锁屏 | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR); - } + }//在手机锁屏后如果到了闹钟提示时间,点亮屏幕 Intent intent = getIntent(); try { mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1)); mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId); + //根据ID从数据库中获取标签的内容; + //getContentResolver()是实现数据共享,实例存储。 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; } - + /* + try + { + // 代码区 + } + catch(Exception e) + { + // 异常处理 + } + 代码区如果有错误,就会返回所写异常的处理。*/ mPlayer = new MediaPlayer(); if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) { showActionDialog(); + //弹出对话框 playAlarmSound(); + //闹钟提示音激发 } else { finish(); + //完成闹钟动作 } } private boolean isScreenOn() { + //判断屏幕是否锁屏,调用系统函数判断,最后返回值是布尔类型 PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); return pm.isScreenOn(); } private void playAlarmSound() { + //闹钟提示音激发 Uri url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM); - + //调用系统的铃声管理URI,得到闹钟提示音 int silentModeStreams = Settings.System.getInt(getContentResolver(), Settings.System.MODE_RINGER_STREAMS_AFFECTED, 0); @@ -101,12 +123,19 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD } try { mPlayer.setDataSource(this, url); + //方法:setDataSource(Context context, Uri uri) + //解释:无返回值,设置多媒体数据来源【根据 Uri】 mPlayer.prepare(); + //准备同步 mPlayer.setLooping(true); + //设置是否循环播放 mPlayer.start(); + //开始播放 } catch (IllegalArgumentException e) { // TODO Auto-generated catch block e.printStackTrace(); + //e.printStackTrace()函数功能是抛出异常, 还将显示出更深的调用信息 + //System.out.println(e),这个方法打印出异常,并且输出在哪里出现的异常 } catch (SecurityException e) { // TODO Auto-generated catch block e.printStackTrace(); @@ -121,37 +150,57 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD private void showActionDialog() { AlertDialog.Builder dialog = new AlertDialog.Builder(this); + //AlertDialog的构造方法全部是Protected的 + //所以不能直接通过new一个AlertDialog来创建出一个AlertDialog。 + //要创建一个AlertDialog,就要用到AlertDialog.Builder中的create()方法 + //如这里的dialog就是新建了一个AlertDialog dialog.setTitle(R.string.app_name); + //为对话框设置标题 dialog.setMessage(mSnippet); + //为对话框设置内容 dialog.setPositiveButton(R.string.notealert_ok, this); + //给对话框添加"Yes"按钮 if (isScreenOn()) { dialog.setNegativeButton(R.string.notealert_enter, this); - } + }//对话框添加"No"按钮 dialog.show().setOnDismissListener(this); } public void onClick(DialogInterface dialog, int which) { switch (which) { + //用which来选择click后下一步的操作 case DialogInterface.BUTTON_NEGATIVE: + //这是取消操作 Intent intent = new Intent(this, NoteEditActivity.class); + //实现两个类间的数据传输 intent.setAction(Intent.ACTION_VIEW); + //设置动作属性 intent.putExtra(Intent.EXTRA_UID, mNoteId); + //实现key-value对 + //EXTRA_UID为key;mNoteId为键 startActivity(intent); + //开始动作 break; default: + //这是确定操作 break; } } public void onDismiss(DialogInterface dialog) { + //忽略 stopAlarmSound(); + //停止闹钟声音 finish(); + //完成该动作 } private void stopAlarmSound() { if (mPlayer != null) { mPlayer.stop(); + //停止播放 mPlayer.release(); + //释放MediaPlayer对象 mPlayer = null; } } diff --git a/src/Notes-master1/app/src/main/java/net/micode/notes/ui/AlarmInitReceiver.java b/src/Notes-master1/app/src/main/java/net/micode/notes/ui/AlarmInitReceiver.java index f221202..7f03f69 100644 --- a/src/Notes-master1/app/src/main/java/net/micode/notes/ui/AlarmInitReceiver.java +++ b/src/Notes-master1/app/src/main/java/net/micode/notes/ui/AlarmInitReceiver.java @@ -34,17 +34,20 @@ public class AlarmInitReceiver extends BroadcastReceiver { NoteColumns.ID, NoteColumns.ALERTED_DATE }; - + //对数据库的操作,调用标签ID和闹钟时间 private static final int COLUMN_ID = 0; private static final int COLUMN_ALERTED_DATE = 1; @Override public void onReceive(Context context, Intent intent) { long currentDate = System.currentTimeMillis(); + //System.currentTimeMillis()产生一个当前的毫秒 + //这个毫秒其实就是自1970年1月1日0时起的毫秒数 Cursor c = context.getContentResolver().query(Notes.CONTENT_NOTE_URI, PROJECTION, NoteColumns.ALERTED_DATE + ">? AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE, new String[] { String.valueOf(currentDate) }, + //将long变量currentDate转化为字符串 null); if (c != null) { @@ -61,5 +64,8 @@ public class AlarmInitReceiver extends BroadcastReceiver { } c.close(); } + //然而通过网上查找资料发现,对于闹钟机制的启动,通常需要上面的几个步骤 + //如新建Intent、PendingIntent以及AlarmManager等 + //这里就是根据数据库里的闹钟时间创建一个闹钟机制 } } diff --git a/src/Notes-master1/app/src/main/java/net/micode/notes/ui/AlarmReceiver.java b/src/Notes-master1/app/src/main/java/net/micode/notes/ui/AlarmReceiver.java index 54e503b..8d7492d 100644 --- a/src/Notes-master1/app/src/main/java/net/micode/notes/ui/AlarmReceiver.java +++ b/src/Notes-master1/app/src/main/java/net/micode/notes/ui/AlarmReceiver.java @@ -20,11 +20,15 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -public class AlarmReceiver extends BroadcastReceiver { +public class AlarmReceiver extends BroadcastReceiver {//类:闹钟消息接收器,从BroadcastReceiver类继承而来 @Override public void onReceive(Context context, Intent intent) { intent.setClass(context, AlarmAlertActivity.class); + //启动AlarmAlertActivity intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + //activity要存在于activity的栈中,而非activity的途径启动activity时必然不存在一个activity的栈 + //所以要新起一个栈装入启动的activity context.startActivity(intent); } } +//这是实现alarm这个功能最接近用户层的包,基于上面的两个包, \ No newline at end of file diff --git a/src/Notes-master1/app/src/main/java/net/micode/notes/ui/DateTimePicker.java b/src/Notes-master1/app/src/main/java/net/micode/notes/ui/DateTimePicker.java index 496b0cd..11a8469 100644 --- a/src/Notes-master1/app/src/main/java/net/micode/notes/ui/DateTimePicker.java +++ b/src/Notes-master1/app/src/main/java/net/micode/notes/ui/DateTimePicker.java @@ -28,7 +28,8 @@ import android.view.View; import android.widget.FrameLayout; import android.widget.NumberPicker; -public class DateTimePicker extends FrameLayout { +public class DateTimePicker extends FrameLayout {//FrameLayout是布局模板之一 + //所有的子元素全部在屏幕的右上方 private static final boolean DEFAULT_ENABLE_STATE = true; @@ -45,13 +46,16 @@ public class DateTimePicker extends FrameLayout { 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; private final NumberPicker mHourSpinner; private final NumberPicker mMinuteSpinner; private final NumberPicker mAmPmSpinner; - private Calendar mDate; + //NumberPicker是数字选择器 + //这里定义的四个变量全部是在设置闹钟时需要选择的变量(如日期、时、分、上午或者下午) + private Calendar mDate; + //定义了Calendar类型的变量mDate,用于操作时间 private String[] mDateDisplayValues = new String[DAYS_IN_ALL_WEEK]; private boolean mIsAm; @@ -71,41 +75,48 @@ public class DateTimePicker extends FrameLayout { updateDateControl(); onDateTimeChanged(); } - }; + };//OnValueChangeListener,这是时间改变监听器,这里主要是对日期的监听 + //将现在日期的值传递给mDate;updateDateControl是同步操作 private NumberPicker.OnValueChangeListener mOnHourChangedListener = new NumberPicker.OnValueChangeListener() { + //这里是对 小时(Hour) 的监听 @Override public void onValueChange(NumberPicker picker, int oldVal, int newVal) { boolean isDateChanged = false; Calendar cal = Calendar.getInstance(); + //声明一个Calendar的变量cal,便于后续的操作 if (!mIs24HourView) { if (!mIsAm && oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) { cal.setTimeInMillis(mDate.getTimeInMillis()); cal.add(Calendar.DAY_OF_YEAR, 1); isDateChanged = true; + //这里是对于12小时制时,晚上11点和12点交替时对日期的更改 } 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; - } + }//这里是对于12小时制时,凌晨11点和12点交替时对日期的更改 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(); - } + }//这里是对于12小时制时,中午11点和12点交替时对AM和PM的更改 } else { if (oldVal == HOURS_IN_ALL_DAY - 1 && newVal == 0) { cal.setTimeInMillis(mDate.getTimeInMillis()); cal.add(Calendar.DAY_OF_YEAR, 1); isDateChanged = true; + //这里是对于24小时制时,晚上11点和12点交替时对日期的更改 } else if (oldVal == 0 && newVal == HOURS_IN_ALL_DAY - 1) { cal.setTimeInMillis(mDate.getTimeInMillis()); cal.add(Calendar.DAY_OF_YEAR, -1); isDateChanged = true; } - } + }//这里是对于12小时制时,凌晨11点和12点交替时对日期的更改 int newHour = mHourSpinner.getValue() % HOURS_IN_HALF_DAY + (mIsAm ? 0 : HOURS_IN_HALF_DAY); + //通过数字选择器对newHour的赋值 mDate.set(Calendar.HOUR_OF_DAY, newHour); + //通过set函数将新的Hour值传给mDate onDateTimeChanged(); if (isDateChanged) { setCurrentYear(cal.get(Calendar.YEAR)); @@ -117,15 +128,18 @@ public class DateTimePicker extends FrameLayout { private NumberPicker.OnValueChangeListener mOnMinuteChangedListener = new NumberPicker.OnValueChangeListener() { @Override + //这里是对 分钟(Minute)改变的监听 public void onValueChange(NumberPicker picker, int oldVal, int newVal) { int minValue = mMinuteSpinner.getMinValue(); int maxValue = mMinuteSpinner.getMaxValue(); int offset = 0; + //设置offset,作为小时改变的一个记录数据 if (oldVal == maxValue && newVal == minValue) { offset += 1; } else if (oldVal == minValue && newVal == maxValue) { offset -= 1; - } + }//如果原值为59,新值为0,则offset加1 + //如果原值为0,新值为59,则offset减1 if (offset != 0) { mDate.add(Calendar.HOUR_OF_DAY, offset); mHourSpinner.setValue(getCurrentHour()); @@ -145,6 +159,7 @@ public class DateTimePicker extends FrameLayout { }; private NumberPicker.OnValueChangeListener mOnAmPmChangedListener = new NumberPicker.OnValueChangeListener() { + //对AM和PM的监听 @Override public void onValueChange(NumberPicker picker, int oldVal, int newVal) { mIsAm = !mIsAm; @@ -161,23 +176,25 @@ public class DateTimePicker extends FrameLayout { public interface OnDateTimeChangedListener { void onDateTimeChanged(DateTimePicker view, int year, int month, int dayOfMonth, int hourOfDay, int minute); - } + }//接口:日期变化监听器 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); + super(context);//获取系统时间 mDate = Calendar.getInstance(); mInitialising = true; mIsAm = getCurrentHourOfDay() >= HOURS_IN_HALF_DAY; inflate(context, R.layout.datetime_picker, this); - + //如果当前Activity里用到别的layout,比如对话框layout + //还要设置这个layout上的其他组件的内容,就必须用inflate()方法先将对话框的layout找出来 + //然后再用findViewById()找到它上面的其它组件 mDateSpinner = (NumberPicker) findViewById(R.id.date); mDateSpinner.setMinValue(DATE_SPINNER_MIN_VAL); mDateSpinner.setMaxValue(DATE_SPINNER_MAX_VAL); @@ -215,22 +232,22 @@ public class DateTimePicker extends FrameLayout { } @Override - public void setEnabled(boolean enabled) { + public void setEnabled(boolean enabled) {//方法:将转轮设置为使能状态 if (mIsEnabled == enabled) { return; } super.setEnabled(enabled); - mDateSpinner.setEnabled(enabled); + mDateSpinner.setEnabled(enabled);//语句:分别对四个分部件设置使能 mMinuteSpinner.setEnabled(enabled); mHourSpinner.setEnabled(enabled); mAmPmSpinner.setEnabled(enabled); - mIsEnabled = enabled; + mIsEnabled = enabled;//修改标志变量 } @Override public boolean isEnabled() { return mIsEnabled; - } + }//函数:判断当前的时间日期选择器是否处于启用的的状态 /** * Get the current date in millis @@ -240,7 +257,7 @@ public class DateTimePicker extends FrameLayout { public long getCurrentDateInTimeMillis() { return mDate.getTimeInMillis(); } - +//实现函数——得到当前的秒数 /** * Set the current date * @@ -251,7 +268,7 @@ public class DateTimePicker extends FrameLayout { 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)); - } + }//实现函数功能——设置当前的时间,参数是date /** * Set the current date @@ -269,13 +286,14 @@ public class DateTimePicker extends FrameLayout { setCurrentDay(dayOfMonth); setCurrentHour(hourOfDay); setCurrentMinute(minute); - } + }//实现函数功能——设置当前的时间,参数是各详细的变量 /** * Get current year * * @return The current year */ + //下面是得到year、month、day等值 public int getCurrentYear() { return mDate.get(Calendar.YEAR); } @@ -422,12 +440,12 @@ public class DateTimePicker extends FrameLayout { * * @param is24HourView True for 24 hour mode. False for AM/PM mode. */ - public void set24HourView(boolean is24HourView) { + public void set24HourView(boolean is24HourView) {//函数:设置24小时下的视图 if (mIs24HourView == is24HourView) { return; } mIs24HourView = is24HourView; - mAmPmSpinner.setVisibility(is24HourView ? View.GONE : View.VISIBLE); + mAmPmSpinner.setVisibility(is24HourView ? View.GONE : View.VISIBLE);// 语句:如果为12小时视图则显示上下午选择 int hour = getCurrentHourOfDay(); updateHourControl(); setCurrentHour(hour); @@ -446,7 +464,7 @@ public class DateTimePicker extends FrameLayout { mDateSpinner.setDisplayedValues(mDateDisplayValues); mDateSpinner.setValue(DAYS_IN_ALL_WEEK / 2); mDateSpinner.invalidate(); - } + }// 对于星期几的算法 private void updateAmPmControl() { if (mIs24HourView) { @@ -455,7 +473,7 @@ public class DateTimePicker extends FrameLayout { int index = mIsAm ? Calendar.AM : Calendar.PM; mAmPmSpinner.setValue(index); mAmPmSpinner.setVisibility(View.VISIBLE); - } + }// 对于上下午操作的算法 } private void updateHourControl() { @@ -465,7 +483,7 @@ public class DateTimePicker extends FrameLayout { } else { mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW); mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW); - } + }// 对与小时的算法 } /** @@ -473,10 +491,11 @@ public class DateTimePicker extends FrameLayout { * @param callback the callback, if null will do nothing */ public void setOnDateTimeChangedListener(OnDateTimeChangedListener callback) { + //函数:根据参数设置时间日期监听器 mOnDateTimeChangedListener = callback; } - private void onDateTimeChanged() { + private void onDateTimeChanged() {//函数:监听日期时间的变化 if (mOnDateTimeChangedListener != null) { mOnDateTimeChangedListener.onDateTimeChanged(this, getCurrentYear(), getCurrentMonth(), getCurrentDay(), getCurrentHourOfDay(), getCurrentMinute()); diff --git a/src/Notes-master1/app/src/main/java/net/micode/notes/ui/DateTimePickerDialog.java b/src/Notes-master1/app/src/main/java/net/micode/notes/ui/DateTimePickerDialog.java index 2c47ba4..b35a4cf 100644 --- a/src/Notes-master1/app/src/main/java/net/micode/notes/ui/DateTimePickerDialog.java +++ b/src/Notes-master1/app/src/main/java/net/micode/notes/ui/DateTimePickerDialog.java @@ -32,18 +32,24 @@ import android.text.format.DateUtils; public class DateTimePickerDialog extends AlertDialog implements OnClickListener { private Calendar mDate = Calendar.getInstance(); + //创建一个Calendar类型的变量 mDate,方便时间的操作 private boolean mIs24HourView; private OnDateTimeSetListener mOnDateTimeSetListener; + //声明一个时间日期滚动选择控件 mOnDateTimeSetListener private DateTimePicker mDateTimePicker; - + //DateTimePicker控件,控件一般用于让用户可以从日期列表中选择单个值。 + //运行时,单击控件边上的下拉箭头,会显示为两个部分:一个下拉列表,一个用于选择日期的 public interface OnDateTimeSetListener { + //设置一个接口当时期时间设置时进行的操作 void OnDateTimeSet(AlertDialog dialog, long date); } public DateTimePickerDialog(Context context, long date) { + //对该界面对话框的实例化 super(context); + //对数据库的操作 mDateTimePicker = new DateTimePicker(context); - setView(mDateTimePicker); + setView(mDateTimePicker);//添加一个子视图 mDateTimePicker.setOnDateTimeChangedListener(new OnDateTimeChangedListener() { public void onDateTimeChanged(DateTimePicker view, int year, int month, int dayOfMonth, int hourOfDay, int minute) { @@ -51,18 +57,18 @@ public class DateTimePickerDialog extends AlertDialog implements OnClickListener mDate.set(Calendar.MONTH, month); mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); mDate.set(Calendar.HOUR_OF_DAY, hourOfDay); - mDate.set(Calendar.MINUTE, minute); + mDate.set(Calendar.MINUTE, minute);//将视图中的各选项设置为系统当前时间 updateTitle(mDate.getTimeInMillis()); } }); - mDate.setTimeInMillis(date); - mDate.set(Calendar.SECOND, 0); + mDate.setTimeInMillis(date);//得到系统时间 + mDate.set(Calendar.SECOND, 0);//将秒数设置为0 mDateTimePicker.setCurrentDate(mDate.getTimeInMillis()); setButton(context.getString(R.string.datetime_dialog_ok), this); - setButton2(context.getString(R.string.datetime_dialog_cancel), (OnClickListener)null); - set24HourView(DateFormat.is24HourFormat(this.getContext())); + setButton2(context.getString(R.string.datetime_dialog_cancel), (OnClickListener)null);//设置按钮 + set24HourView(DateFormat.is24HourFormat(this.getContext()));//时间标准化打印 updateTitle(mDate.getTimeInMillis()); - } + }//将时间日期滚动选择控件实例化 public void set24HourView(boolean is24HourView) { mIs24HourView = is24HourView; @@ -70,7 +76,7 @@ public class DateTimePickerDialog extends AlertDialog implements OnClickListener public void setOnDateTimeSetListener(OnDateTimeSetListener callBack) { mOnDateTimeSetListener = callBack; - } + }//将时间日期滚动选择控件实例化 private void updateTitle(long date) { int flag = @@ -79,12 +85,12 @@ public class DateTimePickerDialog extends AlertDialog implements OnClickListener DateUtils.FORMAT_SHOW_TIME; flag |= mIs24HourView ? DateUtils.FORMAT_24HOUR : DateUtils.FORMAT_24HOUR; setTitle(DateUtils.formatDateTime(this.getContext(), date, flag)); - } + }//android开发中常见日期管理工具类(API)——DateUtils:按照上下午显示时间 public void onClick(DialogInterface arg0, int arg1) { if (mOnDateTimeSetListener != null) { mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis()); } - } - + }//第一个参数arg0是接收到点击事件的对话框 + //第二个参数arg1是该对话框上的按钮 } \ No newline at end of file diff --git a/src/Notes-master1/app/src/main/java/net/micode/notes/ui/DropdownMenu.java b/src/Notes-master1/app/src/main/java/net/micode/notes/ui/DropdownMenu.java index 613dc74..0c4b307 100644 --- a/src/Notes-master1/app/src/main/java/net/micode/notes/ui/DropdownMenu.java +++ b/src/Notes-master1/app/src/main/java/net/micode/notes/ui/DropdownMenu.java @@ -30,14 +30,18 @@ import net.micode.notes.R; public class DropdownMenu { private Button mButton; private PopupMenu mPopupMenu; + //声明一个下拉菜单 private Menu mMenu; public DropdownMenu(Context context, Button button, int menuId) { mButton = button; mButton.setBackgroundResource(R.drawable.dropdown_icon); + //设置这个view的背景 mPopupMenu = new PopupMenu(context, mButton); mMenu = mPopupMenu.getMenu(); mPopupMenu.getMenuInflater().inflate(menuId, mMenu); + //MenuInflater是用来实例化Menu目录下的Menu布局文件 + //根据ID来确认menu的内容选项 mButton.setOnClickListener(new OnClickListener() { public void onClick(View v) { mPopupMenu.show(); @@ -48,14 +52,14 @@ public class DropdownMenu { public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) { if (mPopupMenu != null) { mPopupMenu.setOnMenuItemClickListener(listener); - } + }//设置菜单的监听 } public MenuItem findItem(int id) { return mMenu.findItem(id); - } + }//对于菜单选项的初始化,根据索引搜索菜单需要的选项 public void setTitle(CharSequence title) { mButton.setText(title); - } -} + }//布局文件,设置标题 +} \ No newline at end of file diff --git a/src/Notes-master1/app/src/main/java/net/micode/notes/ui/FoldersListAdapter.java b/src/Notes-master1/app/src/main/java/net/micode/notes/ui/FoldersListAdapter.java index 96b77da..2c5853d 100644 --- a/src/Notes-master1/app/src/main/java/net/micode/notes/ui/FoldersListAdapter.java +++ b/src/Notes-master1/app/src/main/java/net/micode/notes/ui/FoldersListAdapter.java @@ -30,10 +30,14 @@ import net.micode.notes.data.Notes.NoteColumns; public class FoldersListAdapter extends CursorAdapter { + //CursorAdapter是Cursor和ListView的接口 + //FoldersListAdapter继承了CursorAdapter的类 + //主要作用是便签数据库和用户的交互 + //这里就是用folder(文件夹)的形式展现给用户 public static final String [] PROJECTION = { - NoteColumns.ID, - NoteColumns.SNIPPET - }; + NoteColumns.ID, + NoteColumns.SNIPPET + };//调用数据库中便签的ID和片段 public static final int ID_COLUMN = 0; public static final int NAME_COLUMN = 1; @@ -41,12 +45,13 @@ public class FoldersListAdapter extends CursorAdapter { public FoldersListAdapter(Context context, Cursor c) { super(context, c); // TODO Auto-generated constructor stub - } + }//数据库操作 @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { + //ViewGroup是容器 return new FolderListItem(context); - } + }//创建一个文件夹,对于各文件夹中子标签的初始化 @Override public void bindView(View view, Context context, Cursor cursor) { @@ -55,26 +60,28 @@ public class FoldersListAdapter extends CursorAdapter { .getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN); ((FolderListItem) view).bind(folderName); } - } + }//将各个布局文件绑定起来 public String getFolderName(Context context, int position) { Cursor cursor = (Cursor) getItem(position); return (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context .getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN); - } + }//根据数据库中标签的ID得到标签的各项内容 private class FolderListItem extends LinearLayout { private TextView mName; public FolderListItem(Context context) { super(context); + //操作数据库 inflate(context, R.layout.folder_list_item, this); + //根据布局文件的名字等信息将其找出来 mName = (TextView) findViewById(R.id.tv_folder_name); } public void bind(String name) { - mName.setText(name); + mName.setText(name);//设置名字 } } -} +} \ No newline at end of file diff --git a/src/Notes-master1/app/src/main/java/net/micode/notes/ui/NoteEditActivity.java b/src/Notes-master1/app/src/main/java/net/micode/notes/ui/NoteEditActivity.java index 96a9ff8..9bbbb97 100644 --- a/src/Notes-master1/app/src/main/java/net/micode/notes/ui/NoteEditActivity.java +++ b/src/Notes-master1/app/src/main/java/net/micode/notes/ui/NoteEditActivity.java @@ -74,6 +74,8 @@ import java.util.regex.Pattern; public class NoteEditActivity extends Activity implements OnClickListener, NoteSettingChangedListener, OnTextViewChangeListener { + //该类主要是针对标签的编辑 + //继承了系统内部许多和监听有关的类 private class HeadViewHolder { public TextView tvModified; @@ -83,7 +85,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, public ImageView ibSetBgColor; } - + //使用Map实现数据存储 private static final Map sBgSelectorBtnsMap = new HashMap(); static { sBgSelectorBtnsMap.put(R.id.iv_bg_yellow, ResourceParser.YELLOW); @@ -91,6 +93,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, 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); + //put函数是将指定值和指定键相连 } private static final Map sBgSelectorSelectionMap = new HashMap(); @@ -100,6 +103,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, 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); + //put函数是将指定值和指定键相连 } private static final Map sFontSizeBtnsMap = new HashMap(); @@ -108,6 +112,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, 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); + //put函数是将指定值和指定键相连 } private static final Map sFontSelectorSelectionMap = new HashMap(); @@ -116,6 +121,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, 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); + //put函数是将指定值和指定键相连 } private static final String TAG = "NoteEditActivity"; @@ -123,20 +129,23 @@ public class NoteEditActivity extends Activity implements OnClickListener, private HeadViewHolder mNoteHeaderHolder; private View mHeadViewPanel; - + //私有化一个界面操作mHeadViewPanel,对表头的操作 private View mNoteBgColorSelector; - + //私有化一个界面操作mNoteBgColorSelector,对背景颜色的操作 private View mFontSizeSelector; - + //私有化一个界面操作mFontSizeSelector,对标签字体的操作 private EditText mNoteEditor; - + //声明编辑控件,对文本操作 private View mNoteEditorPanel; - - private WorkingNote mWorkingNote; - + //私有化一个界面操作mNoteEditorPanel,文本编辑的控制板 + //private WorkingNote mWorkingNote; + public WorkingNote mWorkingNote; + //对模板WorkingNote的初始化 private SharedPreferences mSharedPrefs; + //私有化SharedPreferences的数据存储方式 + //它的本质是基于XML文件存储key-value键值对数据 private int mFontSizeId; - + //用于操作字体的大小 private static final String PREFERENCE_FONT_SIZE = "pref_font_size"; private static final int SHORTCUT_ICON_TITLE_MAX_LEN = 10; @@ -145,7 +154,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, public static final String TAG_UNCHECKED = String.valueOf('\u25A1'); private LinearLayout mEditTextList; - + //线性布局 private String mUserQuery; private Pattern mPattern; @@ -153,7 +162,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); this.setContentView(R.layout.note_edit); - + //对数据库的访问操作 if (savedInstanceState == null && !initActivityState(getIntent())) { finish(); return; @@ -176,7 +185,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, return; } Log.d(TAG, "Restoring from killed activity"); - } + }//为防止内存不足时程序的终止,在这里有一个保存现场的函数 } private boolean initActivityState(Intent intent) { @@ -188,34 +197,42 @@ public class NoteEditActivity extends Activity implements OnClickListener, if (TextUtils.equals(Intent.ACTION_VIEW, intent.getAction())) { long noteId = intent.getLongExtra(Intent.EXTRA_UID, 0); mUserQuery = ""; - + //如果用户实例化标签时,系统并未给出标签ID /** * Starting from the searched result */ + //根据键值查找ID if (intent.hasExtra(SearchManager.EXTRA_DATA_KEY)) { noteId = Long.parseLong(intent.getStringExtra(SearchManager.EXTRA_DATA_KEY)); mUserQuery = intent.getStringExtra(SearchManager.USER_QUERY); } - + //如果ID在数据库中未找到 if (!DataUtils.visibleInNoteDatabase(getContentResolver(), noteId, Notes.TYPE_NOTE)) { Intent jump = new Intent(this, NotesListActivity.class); startActivity(jump); + //程序将跳转到上面声明的intent——jump showToast(R.string.error_note_not_exist); finish(); return false; - } else { + } + //ID在数据库中找到 + else { mWorkingNote = WorkingNote.load(this, noteId); if (mWorkingNote == null) { Log.e(TAG, "load note failed with note id" + noteId); + //打印出红色的错误信息 finish(); return false; } } + //setSoftInputMode——软键盘输入模式 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())) { - // New note + // intent.getAction() + // 大多用于broadcast发送广播时给机制(intent)设置一个action,就是一个字符串 + // 用户可以通过receive(接受)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); @@ -223,7 +240,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, Notes.TYPE_WIDGET_INVALIDE); int bgResId = intent.getIntExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, ResourceParser.getDefaultBgId(this)); - + // intent.getInt(Long、String)Extra是对各变量的语法分析 // Parse call-record note String phoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER); long callDate = intent.getLongExtra(Notes.INTENT_EXTRA_CALL_DATE, 0); @@ -240,15 +257,17 @@ public class NoteEditActivity extends Activity implements OnClickListener, 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); - } + }//创建一个新的WorkingNote getWindow().setSoftInputMode( WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE @@ -269,8 +288,10 @@ public class NoteEditActivity extends Activity implements OnClickListener, } private void initNoteScreen() { + //对界面的初始化操作 mNoteEditor.setTextAppearance(this, TextAppearanceResources .getTexAppearanceResource(mFontSizeId)); + //设置外观 if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { switchToListMode(mWorkingNote.getContent()); } else { @@ -294,18 +315,21 @@ public class NoteEditActivity extends Activity implements OnClickListener, */ showAlertHeader(); } - + //设置闹钟的显示 private void showAlertHeader() { if (mWorkingNote.hasClockAlert()) { long time = System.currentTimeMillis(); if (time > mWorkingNote.getAlertDate()) { mNoteHeaderHolder.tvAlertDate.setText(R.string.note_alert_expired); - } else { + } + //如果系统时间大于了闹钟设置的时间,那么闹钟失效 + 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); @@ -329,26 +353,29 @@ public class NoteEditActivity extends Activity implements OnClickListener, if (!mWorkingNote.existInDatabase()) { saveNote(); } + //在创建一个新的标签时,先在数据库中匹配 + //如果不存在,那么先在数据库中存储 outState.putLong(Intent.EXTRA_UID, mWorkingNote.getNoteId()); Log.d(TAG, "Save working note id: " + mWorkingNote.getNoteId() + " onSaveInstanceState"); } @Override + //MotionEvent是对屏幕触控的传递机制 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); @@ -357,9 +384,11 @@ public class NoteEditActivity extends Activity implements OnClickListener, if (ev.getX() < x || ev.getX() > (x + view.getWidth()) || ev.getY() < y - || ev.getY() > (y + view.getHeight())) { - return false; - } + || ev.getY() > (y + view.getHeight())) + //如果触控的位置超出了给定的范围,返回false + { + return false; + } return true; } @@ -377,13 +406,13 @@ public class NoteEditActivity extends Activity implements OnClickListener, 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); /** @@ -405,7 +434,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, } clearSettingState(); } - + //和桌面小工具的同步 private void updateWidget() { Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); if (mWorkingNote.getWidgetType() == Notes.TYPE_WIDGET_2X) { @@ -418,7 +447,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, } intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] { - mWorkingNote.getWidgetId() + mWorkingNote.getWidgetId() }); sendBroadcast(intent); @@ -450,7 +479,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, } mFontSizeSelector.setVisibility(View.GONE); } - } + }//************************存在问题 @Override public void onBackPressed() { @@ -481,6 +510,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, } @Override + //对选择菜单的准备 public boolean onPrepareOptionsMenu(Menu menu) { if (isFinishing()) { return true; @@ -489,6 +519,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, menu.clear(); if (mWorkingNote.getFolderId() == Notes.ID_CALL_RECORD_FOLDER) { getMenuInflater().inflate(R.menu.call_note_edit, menu); + // MenuInflater是用来实例化Menu目录下的Menu布局文件的 } else { getMenuInflater().inflate(R.menu.note_edit, menu); } @@ -506,45 +537,71 @@ public class NoteEditActivity extends Activity implements OnClickListener, } @Override + /* + * 函数功能:动态改变菜单选项内容 + * 函数实现:如下注释 + */ public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { + //根据菜单的id来编剧相关项目 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)); + // 设置标签的标题为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) { + //点击所触发事件 deleteCurrentNote(); + // 删除单签便签 finish(); } }); + //添加“YES”按钮 builder.setNegativeButton(android.R.string.cancel, null); + //添加“NO”的按钮 builder.show(); + //显示对话框 break; case R.id.menu_font_size: + //字体大小的编辑 mFontSizeSelector.setVisibility(View.VISIBLE); + // 将字体选择器置为可见 findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.VISIBLE); + // 通过id找到相应的大小 break; case R.id.menu_list_mode: + //选择列表模式 mWorkingNote.setCheckListMode(mWorkingNote.getCheckListMode() == 0 ? TextNote.MODE_CHECK_LIST : 0); break; case R.id.menu_share: + //菜单共享 getWorkingText(); sendTo(this, mWorkingNote.getContent()); + // 用sendto函数将运行文本发送到遍历的本文内 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: @@ -553,111 +610,170 @@ public class NoteEditActivity extends Activity implements OnClickListener, 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 = new Intent(Intent.ACTION_SEND); + //建立intent链接选项 intent.putExtra(Intent.EXTRA_TEXT, info); + //将需要传递的便签信息放入text文件中 intent.setType("text/plain"); + //编辑连接器的类型 context.startActivity(intent); + //在acti中进行链接 } + /* + * 函数功能:创建一个新的便签 + * 函数实现:如下注释 + */ private void createNewNote() { // Firstly, save current editing notes + //保存当前便签 saveNote(); // For safety, start a new NoteEditActivity finish(); Intent intent = new Intent(this, NoteEditActivity.class); + //设置链接器 intent.setAction(Intent.ACTION_INSERT_OR_EDIT); + //该活动定义为创建或编辑 intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mWorkingNote.getFolderId()); + //将运行便签的id添加到INTENT_EXTRA_FOLDER_ID标记中 startActivity(intent); + //开始activity并链接 } + /* + * 函数功能:删除当前便签 + * 函数实现:如下注释 + */ private void deleteCurrentNote() { if (mWorkingNote.existInDatabase()) { + //假如当前运行的便签内存有数据 HashSet ids = new HashSet(); long id = mWorkingNote.getNoteId(); if (id != Notes.ID_ROOT_FOLDER) { ids.add(id); + //如果不是头文件夹建立一个hash表把便签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); + //将这些标签的删除标记置为true } + /* + * 函数功能:判断是否为同步模式 + * 函数实现:直接看NotesPreferenceActivity中同步名称是否为空 + */ private boolean isSyncMode() { 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(); } if (mWorkingNote.getNoteId() > 0) { Intent intent = new Intent(this, AlarmReceiver.class); intent.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mWorkingNote.getNoteId())); + //若有有运行的便签就是建立一个链接器将标签id都存在uri中 PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0); AlarmManager alarmManager = ((AlarmManager) getSystemService(ALARM_SERVICE)); + //设置提醒管理器 showAlertHeader(); if(!set) { alarmManager.cancel(pendingIntent); } else { alarmManager.set(AlarmManager.RTC_WAKEUP, date, pendingIntent); } + //如果用户设置了时间,就通过提醒管理器设置一个监听事项 } else { /** * There is the condition that user has input nothing (the note is * not worthy saving), we have no note id, remind the user that he * should input something */ + //没有运行的便签就报错 Log.e(TAG, "Clock alert setting error"); showToast(R.string.error_note_empty_for_clock); } } + /* + * 函数功能:Widget发生改变的所触发的事件 + */ public void onWidgetChanged() { - updateWidget(); + updateWidget();//更新Widget } + /* + * 函数功能: 删除编辑文本框所触发的事件 + * 函数实现:如下注释 + */ 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); + //通过id把编辑框存在便签编辑框中 } mEditTextList.removeViewAt(index); + //删除特定位置的视图 NoteEditText edit = null; if(index == 0) { edit = (NoteEditText) mEditTextList.getChildAt(0).findViewById( @@ -666,69 +782,101 @@ public class NoteEditActivity extends Activity implements OnClickListener, edit = (NoteEditText) mEditTextList.getChildAt(index - 1).findViewById( R.id.et_edit_text); } + //通过id把编辑框存在空的NoteEditText中 int length = edit.length(); edit.append(text); - edit.requestFocus(); - edit.setSelection(length); + edit.requestFocus();//请求优先完成该此 编辑 + edit.setSelection(length);//定位到length位置处的条目 } + /* + * 函数功能:进入编辑文本框所触发的事件 + * 函数实现:如下注释 + */ public void onEditTextEnter(int index, String text) { /** * Should not happen, check for debug */ 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); + 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); + //将文本编辑框置为可见 } + /* + * 函数功能:获取高亮效果的反馈情况 + * 函数实现:如下注释 + */ 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); + //建立一个状态机检查Pattern并进行匹配 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; } + /* + * 函数功能:获取列表项 + * 函数实现:如下注释 + */ 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) { @@ -739,12 +887,15 @@ public class NoteEditActivity extends Activity implements OnClickListener, } } }); + //建立一个打钩框并设置监听器 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(); @@ -753,61 +904,94 @@ public class NoteEditActivity extends Activity implements OnClickListener, 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; + //初始化check标记 if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + // 若模式为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; + //扩展字符串为已打钩并把标记置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(); + //运行 getWorkingText()之后保存 if (saved) { /** * There are two modes from List view to edit view, open one note, @@ -816,11 +1000,16 @@ public class NoteEditActivity extends Activity implements OnClickListener, * new node requires to the top of the list. This code * {@link #RESULT_OK} is used to identify the create/edit state */ + //如英文注释所说链接RESULT_OK是为了识别保存的2种情况,一是创建后保存,二是修改后保存 setResult(RESULT_OK); } return saved; } + /* + * 函数功能:将便签发送至桌面 + * 函数实现:如下注释 + */ private void sendToDesktop() { /** * Before send message to home, we should make sure that current @@ -829,12 +1018,16 @@ public class NoteEditActivity extends Activity implements OnClickListener, */ if (!mWorkingNote.existInDatabase()) { saveNote(); + //若不存在数据也就是新的标签就保存起来先 } if (mWorkingNote.getNoteId() > 0) { + //若是有内容 Intent sender = new Intent(); Intent shortcutIntent = new Intent(this, NoteEditActivity.class); + //建立发送到桌面的连接器 shortcutIntent.setAction(Intent.ACTION_VIEW); + //链接为一个视图 shortcutIntent.putExtra(Intent.EXTRA_UID, mWorkingNote.getNoteId()); sender.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent); sender.putExtra(Intent.EXTRA_SHORTCUT_NAME, @@ -842,9 +1035,12 @@ public class NoteEditActivity extends Activity implements OnClickListener, 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"); + //设置sneder的行为是发送 showToast(R.string.info_note_enter_desktop); sendBroadcast(sender); + //显示到桌面 } else { /** * There is the condition that user has input nothing (the note is @@ -853,20 +1049,34 @@ public class NoteEditActivity extends Activity implements OnClickListener, */ 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; + //直接设置为content中的内容并返回,有勾选和未勾选2种 } + /* + * 函数功能:显示提示的视图 + * 函数实现:根据下标显示对应的提示 + */ private void showToast(int resId) { showToast(resId, Toast.LENGTH_SHORT); } + /* + * 函数功能:持续显示提示的视图 + * 函数实现:根据下标和持续的时间(duration)编辑提示视图并显示 + */ private void showToast(int resId, int duration) { Toast.makeText(this, resId, duration).show(); } diff --git a/src/Notes-master1/app/src/main/java/net/micode/notes/ui/NoteEditText.java b/src/Notes-master1/app/src/main/java/net/micode/notes/ui/NoteEditText.java index 2afe2a8..334ebd6 100644 --- a/src/Notes-master1/app/src/main/java/net/micode/notes/ui/NoteEditText.java +++ b/src/Notes-master1/app/src/main/java/net/micode/notes/ui/NoteEditText.java @@ -37,6 +37,7 @@ import net.micode.notes.R; import java.util.HashMap; import java.util.Map; +//继承edittext,设置便签设置文本框 public class NoteEditText extends EditText { private static final String TAG = "NoteEditText"; private int mIndex; @@ -46,6 +47,7 @@ public class NoteEditText extends EditText { private static final String SCHEME_HTTP = "http:" ; private static final String SCHEME_EMAIL = "mailto:" ; + ///建立一个字符和整数的hash表,用于链接电话,网站,还有邮箱 private static final Map sSchemaActionResMap = new HashMap(); static { sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel); @@ -56,17 +58,20 @@ public class NoteEditText extends EditText { /** * Call by the {@link NoteEditActivity} to delete or add edit text */ + //在NoteEditActivity中删除或添加文本的操作,可以看做是一个文本是否被变的标记,英文注释已说明的很清楚 public interface OnTextViewChangeListener { /** * Delete current edit text when {@link KeyEvent#KEYCODE_DEL} happens * and the text is null */ + //处理删除按键时的操作 void onEditTextDelete(int index, String text); /** * Add edit text after current edit text when {@link KeyEvent#KEYCODE_ENTER} * happen */ + //处理进入按键时的操作 void onEditTextEnter(int index, String text); /** @@ -77,33 +82,43 @@ public class NoteEditText extends EditText { private OnTextViewChangeListener mOnTextViewChangeListener; + //根据context设置文本 public NoteEditText(Context context) { - super(context, null); + super(context, null);//用super引用父类变量 mIndex = 0; } + //设置当前光标 public void setIndex(int index) { mIndex = index; } + //初始化文本修改标记 public void setOnTextViewChangeListener(OnTextViewChangeListener listener) { mOnTextViewChangeListener = listener; } - public NoteEditText(Context context, AttributeSet attrs) { + //AttributeSet 百度了一下是自定义空控件属性,用于维护便签动态变化的属性 + //初始化便签 + public NoteEditText(Context context, AttributeSet attrs) { super(context, attrs, android.R.attr.editTextStyle); } + // 根据defstyle自动初始化 public NoteEditText(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - // TODO Auto-generated constructor stub + // TODO Auto-generated construct or stub } @Override + //view里的函数,处理手机屏幕的所有事件 + /*参数event为手机屏幕触摸事件封装类的对象,其中封装了该事件的所有信息, + 例如触摸的位置、触摸的类型以及触摸的时间等。该对象会在用户触摸手机屏幕时被创建。*/ public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { + //重写了需要处理屏幕被按下的事件 case MotionEvent.ACTION_DOWN: - + //跟新当前坐标值 int x = (int) event.getX(); int y = (int) event.getY(); x -= getTotalPaddingLeft(); @@ -111,9 +126,12 @@ public class NoteEditText extends EditText { x += getScrollX(); y += getScrollY(); + //用布局控件layout根据x,y的新值设置新的位置 Layout layout = getLayout(); int line = layout.getLineForVertical(y); int off = layout.getOffsetForHorizontal(line, x); + + //更新光标新的位置 Selection.setSelection(getText(), off); break; } @@ -122,96 +140,147 @@ public class NoteEditText extends EditText { } @Override + /* + * 函数功能:处理用户按下一个键盘按键时会触发 的事件 + * 实现过程:如下注释 + */ public boolean onKeyDown(int keyCode, KeyEvent event) { switch (keyCode) { + //根据按键的 Unicode 编码值来处理 case KeyEvent.KEYCODE_ENTER: + //“进入”按键 if (mOnTextViewChangeListener != null) { return false; } break; case KeyEvent.KEYCODE_DEL: + //“删除”按键 mSelectionStartBeforeDelete = getSelectionStart(); break; default: break; } + //继续执行父类的其他点击事件 return super.onKeyDown(keyCode, event); } @Override + /* + * 函数功能:处理用户松开一个键盘按键时会触发 的事件 + * 实现方式:如下注释 + */ public boolean onKeyUp(int keyCode, KeyEvent event) { switch(keyCode) { + //根据按键的 Unicode 编码值来处理,有删除和进入2种操作 case KeyEvent.KEYCODE_DEL: if (mOnTextViewChangeListener != null) { + //若是被修改过 if (0 == mSelectionStartBeforeDelete && mIndex != 0) { + //若之前有被修改并且文档不为空 mOnTextViewChangeListener.onEditTextDelete(mIndex, getText().toString()); + //利用上文OnTextViewChangeListener对KEYCODE_DEL按键情况的删除函数进行删除 return true; } } else { Log.d(TAG, "OnTextViewChangeListener was not seted"); + //其他情况报错,文档的改动监听器并没有建立 } break; case KeyEvent.KEYCODE_ENTER: + //同上也是分为监听器是否建立2种情况 if (mOnTextViewChangeListener != null) { int selectionStart = getSelectionStart(); + //获取当前位置 String text = getText().subSequence(selectionStart, length()).toString(); + //获取当前文本 setText(getText().subSequence(0, selectionStart)); + //根据获取的文本设置当前文本 mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, text); + //当{@link KeyEvent#KEYCODE_ENTER}添加新文本 } else { Log.d(TAG, "OnTextViewChangeListener was not seted"); + //其他情况报错,文档的改动监听器并没有建立 } break; default: break; } + //继续执行父类的其他按键弹起的事件 return super.onKeyUp(keyCode, event); } @Override + /* + * 函数功能:当焦点发生变化时,会自动调用该方法来处理焦点改变的事件 + * 实现方式:如下注释 + * 参数:focused表示触发该事件的View是否获得了焦点,当该控件获得焦点时,Focused等于true,否则等于false。 + direction表示焦点移动的方向,用数值表示 + Rect:表示在触发事件的View的坐标系中,前一个获得焦点的矩形区域,即表示焦点是从哪里来的。如果不可用则为null + */ protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { if (mOnTextViewChangeListener != null) { + //若监听器已经建立 if (!focused && TextUtils.isEmpty(getText())) { + //获取到焦点并且文本不为空 mOnTextViewChangeListener.onTextChange(mIndex, false); + //mOnTextViewChangeListener子函数,置false隐藏事件选项 } else { mOnTextViewChangeListener.onTextChange(mIndex, true); + //mOnTextViewChangeListener子函数,置true显示事件选项 } } + //继续执行父类的其他焦点变化的事件 super.onFocusChanged(focused, direction, previouslyFocusedRect); } @Override + /* + * 函数功能:生成上下文菜单 + * 函数实现:如下注释 + */ protected void onCreateContextMenu(ContextMenu menu) { if (getText() instanceof Spanned) { + //有文本存在 int selStart = getSelectionStart(); int selEnd = getSelectionEnd(); + //获取文本开始和结尾位置 int min = Math.min(selStart, selEnd); int max = Math.max(selStart, selEnd); + //获取开始到结尾的最大值和最小值 final URLSpan[] urls = ((Spanned) getText()).getSpans(min, max, URLSpan.class); + //设置url的信息的范围值 if (urls.length == 1) { int defaultResId = 0; for(String schema: sSchemaActionResMap.keySet()) { + //获取计划表中所有的key值 if(urls[0].getURL().indexOf(schema) >= 0) { + //若url可以添加则在添加后将defaultResId置为key所映射的值 defaultResId = sSchemaActionResMap.get(schema); break; } } if (defaultResId == 0) { + //defaultResId == 0则说明url并没有添加任何东西,所以置为连接其他SchemaActionResMap的值 defaultResId = R.string.note_link_other; } + //建立菜单 menu.add(0, 0, 0, defaultResId).setOnMenuItemClickListener( new OnMenuItemClickListener() { + //新建按键监听器 public boolean onMenuItemClick(MenuItem item) { // goto a new intent urls[0].onClick(NoteEditText.this); + //根据相应的文本设置菜单的按键 return true; } }); } } + //继续执行父类的其他菜单创建的事件 super.onCreateContextMenu(menu); } } diff --git a/src/Notes-master1/app/src/main/java/net/micode/notes/ui/NoteItemData.java b/src/Notes-master1/app/src/main/java/net/micode/notes/ui/NoteItemData.java index 0f5a878..ff52c1f 100644 --- a/src/Notes-master1/app/src/main/java/net/micode/notes/ui/NoteItemData.java +++ b/src/Notes-master1/app/src/main/java/net/micode/notes/ui/NoteItemData.java @@ -28,20 +28,20 @@ import net.micode.notes.tool.DataUtils; public class NoteItemData { static final String [] PROJECTION = new String [] { - NoteColumns.ID, - NoteColumns.ALERTED_DATE, - NoteColumns.BG_COLOR_ID, - NoteColumns.CREATED_DATE, - NoteColumns.HAS_ATTACHMENT, - NoteColumns.MODIFIED_DATE, - NoteColumns.NOTES_COUNT, - NoteColumns.PARENT_ID, - NoteColumns.SNIPPET, - NoteColumns.TYPE, - NoteColumns.WIDGET_ID, - NoteColumns.WIDGET_TYPE, + NoteColumns.ID, + NoteColumns.ALERTED_DATE, + NoteColumns.BG_COLOR_ID, + NoteColumns.CREATED_DATE, + NoteColumns.HAS_ATTACHMENT, + NoteColumns.MODIFIED_DATE, + NoteColumns.NOTES_COUNT, + NoteColumns.PARENT_ID, + NoteColumns.SNIPPET, + NoteColumns.TYPE, + NoteColumns.WIDGET_ID, + NoteColumns.WIDGET_TYPE, }; - + //常量标记和数据就不一一标记了,意义翻译基本就知道 private static final int ID_COLUMN = 0; private static final int ALERTED_DATE_COLUMN = 1; private static final int BG_COLOR_ID_COLUMN = 2; @@ -75,8 +75,9 @@ public class NoteItemData { private boolean mIsOnlyOneItem; private boolean mIsOneNoteFollowingFolder; private boolean mIsMultiNotesFollowingFolder; - - public NoteItemData(Context context, Cursor cursor) { + //初始化NoteItemData,主要利用光标cursor获取的东西 + public NoteItemData(Context context, Cursor cursor) { + //getxxx为转换格式 mId = cursor.getLong(ID_COLUMN); mAlertDate = cursor.getLong(ALERTED_DATE_COLUMN); mBgColorId = cursor.getInt(BG_COLOR_ID_COLUMN); @@ -92,10 +93,11 @@ public class NoteItemData { mWidgetId = cursor.getInt(WIDGET_ID_COLUMN); mWidgetType = cursor.getInt(WIDGET_TYPE_COLUMN); + //初始化电话号码的信息 mPhoneNumber = ""; if (mParentId == Notes.ID_CALL_RECORD_FOLDER) { mPhoneNumber = DataUtils.getCallNumberByNoteId(context.getContentResolver(), mId); - if (!TextUtils.isEmpty(mPhoneNumber)) { + if (!TextUtils.isEmpty(mPhoneNumber)) {//mphonenumber里有符合字符串,则用contart功能连接 mName = Contact.getContact(context, mPhoneNumber); if (mName == null) { mName = mPhoneNumber; @@ -108,32 +110,35 @@ public class NoteItemData { } checkPostion(cursor); } - + ///根据鼠标的位置设置标记,和位置 private void checkPostion(Cursor cursor) { + //初始化几个标记,cursor具体功能笔记中已提到,不一一叙述 mIsLastItem = cursor.isLast() ? true : false; mIsFirstItem = cursor.isFirst() ? true : false; mIsOnlyOneItem = (cursor.getCount() == 1); + //初始化“多重子文件”“单一子文件”2个标记 mIsMultiNotesFollowingFolder = false; mIsOneNoteFollowingFolder = false; - if (mType == Notes.TYPE_NOTE && !mIsFirstItem) { + //主要是设置上诉2标记 + if (mType == Notes.TYPE_NOTE && !mIsFirstItem) {//若是note格式并且不是第一个元素 int position = cursor.getPosition(); - if (cursor.moveToPrevious()) { + if (cursor.moveToPrevious()) {//获取光标位置后看上一行 if (cursor.getInt(TYPE_COLUMN) == Notes.TYPE_FOLDER - || cursor.getInt(TYPE_COLUMN) == Notes.TYPE_SYSTEM) { + || cursor.getInt(TYPE_COLUMN) == Notes.TYPE_SYSTEM) {//若光标满足系统或note格式 if (cursor.getCount() > (position + 1)) { - mIsMultiNotesFollowingFolder = true; + mIsMultiNotesFollowingFolder = true;//若是数据行数大于但前位置+1则设置成正确 } else { - mIsOneNoteFollowingFolder = true; + mIsOneNoteFollowingFolder = true;//否则单一文件夹标记为true } } - if (!cursor.moveToNext()) { + if (!cursor.moveToNext()) {//若不能再往下走则报错 throw new IllegalStateException("cursor move to previous but can't move back"); } } } } - + ///下面的代码的作用均是声明获取属性的方法,下面一系列函数都是返回状态值等,用于判断状态 public boolean isOneFollowingFolder() { return mIsOneNoteFollowingFolder; } @@ -214,11 +219,12 @@ public class NoteItemData { return (mAlertDate > 0); } + //若数据父id为保存至文件夹模式的id且满足电话号码单元不为空,则isCallRecord为true public boolean isCallRecord() { return (mParentId == Notes.ID_CALL_RECORD_FOLDER && !TextUtils.isEmpty(mPhoneNumber)); } - public static int getNoteType(Cursor cursor) { + public static int getNoteType(Cursor cursor) {//获得便签的类型 return cursor.getInt(TYPE_COLUMN); } -} +} \ No newline at end of file diff --git a/src/Notes-master1/app/src/main/java/net/micode/notes/ui/NotesListActivity.java b/src/Notes-master1/app/src/main/java/net/micode/notes/ui/NotesListActivity.java index e843aec..9992129 100644 --- a/src/Notes-master1/app/src/main/java/net/micode/notes/ui/NotesListActivity.java +++ b/src/Notes-master1/app/src/main/java/net/micode/notes/ui/NotesListActivity.java @@ -77,8 +77,12 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.HashSet; - -public class NotesListActivity extends Activity implements OnClickListener, OnItemLongClickListener { +//主界面,一进入就是这个界面 +/** + * @author k + * + */ +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; @@ -89,7 +93,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt private static final int MENU_FOLDER_CHANGE_NAME = 2; - private static final String PREFERENCE_ADD_INTRODUCTION = "net.micode.notes.introduction"; + private static final String PREFERENCE_ADD_INTRODUCTION = "net.micode.notes.introduction"; //单行超过80个字符 private enum ListEditState { NOTE_LIST, SUB_FOLDER, CALL_RECORD_FOLDER @@ -136,8 +140,13 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt private final static int REQUEST_CODE_NEW_NODE = 103; @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); + // 创建类 + protected void onCreate(final Bundle savedInstanceState) { //需要是final类型 根据程序上下文环境,Java关键字final有“这是无法改变的”或者“终态的”含义,它可以修饰非抽象类、非抽象类成员方法和变量。你可能出于两种理解而需要阻止改变:设计或效率。 + // final类不能被继承,没有子类,final类中的方法默认是final的。 + //final方法不能被子类的方法覆盖,但可以被继承。 + //final成员变量表示常量,只能被赋值一次,赋值后值不再改变。 + //final不能用于修饰构造方法。 + super.onCreate(savedInstanceState); // 调用父类的onCreate函数 setContentView(R.layout.note_list); initResources(); @@ -148,26 +157,32 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } @Override + // 返回一些子模块完成的数据交给主Activity处理 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); + // 调用 Activity 的onActivityResult() } } private void setAppInfoFromRawRes() { + // Android平台给我们提供了一个SharedPreferences类,它是一个轻量级的存储类,特别适合用于保存软件配置参数。 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); + // 把资源文件放到应用程序的/raw/raw下,那么就可以在应用中使用getResources获取资源后, + // 以openRawResource方法(不带后缀的资源文件名)打开这个文件。 + in = getResources().openRawResource(R.raw.introduction); if (in != null) { InputStreamReader isr = new InputStreamReader(in); BufferedReader br = new BufferedReader(isr); - char [] buf = new char[1024]; + char [] buf = new char[1024]; // 自行定义的数值,使用者不知道有什么意义 int len = 0; while ((len = br.read(buf)) > 0) { sb.append(buf, 0, len); @@ -180,7 +195,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt e.printStackTrace(); return; } finally { - if(in != null) { + if (in != null) { try { in.close(); } catch (IOException e) { @@ -190,11 +205,13 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } } + // 创建空的WorkingNote 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()) { + // 更新保存note的信息 sp.edit().putBoolean(PREFERENCE_ADD_INTRODUCTION, true).commit(); } else { Log.e(TAG, "Save introduction note error"); @@ -209,18 +226,21 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt startAsyncNotesListQuery(); } + // 初始化资源 private void initResources() { - mContentResolver = this.getContentResolver(); + mContentResolver = this.getContentResolver(); // 获取应用程序的数据,得到类似数据表的东西 mBackgroundQueryHandler = new BackgroundQueryHandler(this.getContentResolver()); mCurrentFolderId = Notes.ID_ROOT_FOLDER; - mNotesListView = (ListView) findViewById(R.id.notes_list); + + // findViewById 是安卓编程的定位函数,主要是引用.R文件里的引用名 + mNotesListView = (ListView) findViewById(R.id.notes_list); // 绑定XML中的ListView,作为Item的容器 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 = (Button) findViewById(R.id.btn_new_note);// 在activity中要获取该按钮 mAddNewNote.setOnClickListener(this); mAddNewNote.setOnTouchListener(new NewNoteOnTouchListener()); mDispatch = false; @@ -231,6 +251,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt mModeCallBack = new ModeCallback(); } + // 继承自ListView.MultiChoiceModeListener 和 OnMenuItemClickListener private class ModeCallback implements ListView.MultiChoiceModeListener, OnMenuItemClickListener { private DropdownMenu mDropDownMenu; private ActionMode mActionMode; @@ -259,7 +280,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt (Button) customView.findViewById(R.id.selection_menu), R.menu.note_list_dropdown); mDropDownMenu.setOnDropdownMenuItemClickListener(new PopupMenu.OnMenuItemClickListener(){ - public boolean onMenuItemClick(MenuItem item) { + public boolean onMenuItemClick(final MenuItem item) { mNotesListAdapter.selectAll(!mNotesListAdapter.isAllSelected()); updateMenu(); return true; @@ -269,11 +290,12 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt 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); + mDropDownMenu.setTitle(format); // 更改标题 MenuItem item = mDropDownMenu.findItem(R.id.action_select_all); if (item != null) { if (mNotesListAdapter.isAllSelected()) { @@ -307,7 +329,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } public void onItemCheckedStateChanged(ActionMode mode, int position, long id, - boolean checked) { + boolean checked) { mNotesListAdapter.setCheckedItem(position, checked); updateMenu(); } @@ -325,14 +347,14 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt 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())); + mNotesListAdapter.getSelectedCount())); builder.setPositiveButton(android.R.string.ok, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, - int which) { - batchDelete(); - } - }); + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, + int which) { + batchDelete(); + } + }); builder.setNegativeButton(android.R.string.cancel, null); builder.show(); break; @@ -366,7 +388,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt /** * 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) + * "New Note" button could be expressed by formula y=-0.12x+94锛圲nit: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 @@ -413,7 +435,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt : NORMAL_SELECTION; mBackgroundQueryHandler.startQuery(FOLDER_NOTE_LIST_QUERY_TOKEN, null, Notes.CONTENT_NOTE_URI, NoteItemData.PROJECTION, selection, new String[] { - String.valueOf(mCurrentFolderId) + String.valueOf(mCurrentFolderId) }, NoteColumns.TYPE + " DESC," + NoteColumns.MODIFIED_DATE + " DESC"); } @@ -624,7 +646,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt values.put(NoteColumns.LOCAL_MODIFIED, 1); mContentResolver.update(Notes.CONTENT_NOTE_URI, values, NoteColumns.ID + "=?", new String[] { - String.valueOf(mFocusNoteDataItem.getId()) + String.valueOf(mFocusNoteDataItem.getId()) }); } } else if (!TextUtils.isEmpty(name)) { @@ -664,30 +686,38 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt }); } + /* (non-Javadoc) + * @see android.app.Activity#onBackPressed() + * 按返回键时根据情况更改类中的数据 + */ @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; - } + 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; + } } + /** + * @param appWidgetId + * @param appWidgetType + * 根据不同类型的widget更新插件,通过intent传送数据 + */ private void updateWidget(int appWidgetId, int appWidgetType) { Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); if (appWidgetType == Notes.TYPE_WIDGET_2X) { @@ -700,13 +730,16 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] { - appWidgetId + 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) { @@ -726,6 +759,10 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt super.onContextMenuClosed(menu); } + /* (non-Javadoc) + * @see android.app.Activity#onContextItemSelected(android.view.MenuItem) + * 针对menu中不同的选择进行不同的处理,里面详细注释 + */ @Override public boolean onContextItemSelected(MenuItem item) { if (mFocusNoteDataItem == null) { @@ -734,10 +771,10 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } switch (item.getItemId()) { case MENU_FOLDER_VIEW: - openFolder(mFocusNoteDataItem); + openFolder(mFocusNoteDataItem);//打开对应文件 break; case MENU_FOLDER_DELETE: - AlertDialog.Builder builder = new AlertDialog.Builder(this); + 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)); @@ -748,7 +785,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } }); builder.setNegativeButton(android.R.string.cancel, null); - builder.show(); + builder.show();//显示对话框 break; case MENU_FOLDER_CHANGE_NAME: showCreateOrModifyFolderDialog(false); @@ -818,12 +855,19 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt return true; } + /* (non-Javadoc) + * @see android.app.Activity#onSearchRequested() + * 直接调用startSearch函数 + */ @Override public boolean onSearchRequested() { startSearch(null, false, null /* appData */, false); return true; } + /** + * 函数功能:实现将便签导出到文本功能 + */ private void exportNoteToText() { final BackupUtils backup = BackupUtils.getInstance(NotesListActivity.this); new AsyncTask() { @@ -866,16 +910,27 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt }.execute(); } + /** + * @return + * 功能:判断是否正在同步 + */ private boolean isSyncMode() { return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0; } + /** + * 功能:跳转到PreferenceActivity界面 + */ private void startPreferenceActivity() { Activity from = getParent() != null ? getParent() : this; Intent intent = new Intent(from, NotesPreferenceActivity.class); from.startActivityIfNeeded(intent, -1); } + /** + * @author k + * 函数功能:实现对便签列表项的点击事件(短按) + */ private class OnListItemClickListener implements OnItemClickListener { public void onItemClick(AdapterView parent, View view, int position, long id) { @@ -917,10 +972,13 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } + /** + * 查询目标文件 + */ 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 + ")"; + "(" + selection + ") OR (" + NoteColumns.ID + "=" + Notes.ID_ROOT_FOLDER + ")"; mBackgroundQueryHandler.startQuery(FOLDER_LIST_QUERY_TOKEN, null, @@ -935,7 +993,13 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt NoteColumns.MODIFIED_DATE + " DESC"); } - public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { + /* (non-Javadoc) + * @see android.widget.AdapterView.OnItemLongClickListener#onItemLongClick(android.widget.AdapterView, android.view.View, int, long) + * 长按某一项时进行的操作 + * 如果长按的是便签,则通过ActionMode菜单实现;如果长按的是文件夹,则通过ContextMenu菜单实现; + * 具体ActionMOde菜单和ContextMenu菜单的详细见精度笔记 + */ + public boolean onItemLongClick(AdapterView parent, View view, int position, long id) {// 函数:实现长按项目的点击事件。如果长按的是便签,则通过ActionMode菜单实现;如果长按的是文件夹,则通过ContextMenu菜单实现。 if (view instanceof NotesListItem) { mFocusNoteDataItem = ((NotesListItem) view).getItemData(); if (mFocusNoteDataItem.getType() == Notes.TYPE_NOTE && !mNotesListAdapter.isInChoiceMode()) { @@ -951,4 +1015,4 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } return false; } -} +} \ No newline at end of file diff --git a/src/Notes-master1/app/src/main/java/net/micode/notes/ui/NotesListAdapter.java b/src/Notes-master1/app/src/main/java/net/micode/notes/ui/NotesListAdapter.java index 51c9cb9..5c5d3ee 100644 --- a/src/Notes-master1/app/src/main/java/net/micode/notes/ui/NotesListAdapter.java +++ b/src/Notes-master1/app/src/main/java/net/micode/notes/ui/NotesListAdapter.java @@ -23,6 +23,7 @@ import android.view.View; import android.view.ViewGroup; import android.widget.CursorAdapter; + import net.micode.notes.data.Notes; import java.util.Collection; @@ -31,55 +32,94 @@ import java.util.HashSet; import java.util.Iterator; +/* + * 功能:直译为便签表连接器,继承了CursorAdapter,它为cursor和ListView提供了连接的桥梁。 + * 所以NotesListAdapter实现的是鼠标和编辑便签链接的桥梁 + */ public class NotesListAdapter extends CursorAdapter { private static final String TAG = "NotesListAdapter"; private Context mContext; private HashMap mSelectedIndex; - private int mNotesCount; - private boolean mChoiceMode; + private int mNotesCount; //便签数 + private boolean mChoiceMode; //选择模式标记 + /* + * 桌面widget的属性,包括编号和类型 + */ public static class AppWidgetAttribute { public int widgetId; public int widgetType; }; + /* + * 函数功能:初始化便签链接器 + * 函数实现:根据传进来的内容设置相关变量 + */ public NotesListAdapter(Context context) { - super(context, null); - mSelectedIndex = new HashMap(); + super(context, null); //父类对象置空 + mSelectedIndex = new HashMap(); //新建选项下标的hash表 mContext = context; mNotesCount = 0; } @Override + /* + * 函数功能:新建一个视图来存储光标所指向的数据 + * 函数实现:使用兄弟类NotesListItem新建一个项目选项 + */ public View newView(Context context, Cursor cursor, ViewGroup parent) { return new NotesListItem(context); } + /* + * 函数功能:将已经存在的视图和鼠标指向的数据进行捆绑 + * 函数实现:如下注释 + */ @Override public void bindView(View view, Context context, Cursor cursor) { if (view instanceof NotesListItem) { + //若view是NotesListItem的一个实例 NoteItemData itemData = new NoteItemData(context, cursor); ((NotesListItem) view).bind(context, itemData, mChoiceMode, isSelectedItem(cursor.getPosition())); + //则新建一个项目选项并且用bind跟将view和鼠标,内容,便签数据捆绑在一起 } } + /* + * 函数功能:设置勾选框 + * 函数实现:如下注释 + */ public void setCheckedItem(final int position, final boolean checked) { mSelectedIndex.put(position, checked); + //根据定位和是否勾选设置下标 notifyDataSetChanged(); + //在修改后刷新activity } + /* + * 函数功能:判断单选按钮是否勾选 + */ public boolean isInChoiceMode() { return mChoiceMode; } + /* + * 函数功能:设置单项选项框 + * 函数实现:重置下标并且根据参数mode设置选项 + */ 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) { @@ -87,30 +127,47 @@ public class NotesListAdapter extends CursorAdapter { } } } + //遍历所有光标可用的位置在判断为便签类型之后勾选单项框 } + /* + * 函数功能:建立选择项的下标列表 + * 函数实现:如下注释 + */ public HashSet getSelectedItemIds() { HashSet itemSet = new HashSet(); + //建立hash表 for (Integer position : mSelectedIndex.keySet()) { + //遍历所有的关键 if (mSelectedIndex.get(position) == true) { + //若光标位置可用 Long id = getItemId(position); if (id == Notes.ID_ROOT_FOLDER) { + //原文件不需要添加 Log.d(TAG, "Wrong item id, should not happen"); } else { itemSet.add(id); } + //则将id该下标假如选项集合中 + } } return itemSet; } + /* + * 函数功能:建立桌面Widget的选项表 + * 函数实现:如下注释 + */ public HashSet getSelectedWidget() { HashSet itemSet = new HashSet(); for (Integer position : mSelectedIndex.keySet()) { if (mSelectedIndex.get(position) == true) { Cursor c = (Cursor) getItem(position); + //以上4句和getSelectedItemIds一样,不再重复 if (c != null) { + //光标位置可用的话就建立新的Widget属性并编辑下标和类型,最后添加到选项集中 AppWidgetAttribute widget = new AppWidgetAttribute(); NoteItemData item = new NoteItemData(mContext, c); widget.widgetId = item.getWidgetId(); @@ -128,26 +185,42 @@ public class NotesListAdapter extends CursorAdapter { return itemSet; } + /* + * 函数功能:获取选项个数 + * 函数实现:如下注释 + */ public int getSelectedCount() { Collection values = mSelectedIndex.values(); + //首先获取选项下标的值 if (null == values) { return 0; } Iterator iter = values.iterator(); + //初始化叠加器 int count = 0; while (iter.hasNext()) { if (true == iter.next()) { + //若value值为真计数+1 count++; } } return count; } + /* + * 函数功能:判断是否全部选中 + * 函数实现:如下注释 + */ public boolean isAllSelected() { int checkedCount = getSelectedCount(); return (checkedCount != 0 && checkedCount == mNotesCount); + //获取选项数看是否等于便签的个数 } + /* + * 函数功能:判断是否为选项表 + * 函数实现:通过传递的下标来确定 + */ public boolean isSelectedItem(final int position) { if (null == mSelectedIndex.get(position)) { return false; @@ -156,29 +229,45 @@ public class NotesListAdapter extends CursorAdapter { } @Override + /* + * 函数功能:在activity内容发生局部变动的时候回调该函数计算便签的数量 + * 函数实现:如下注释 + */ protected void onContentChanged() { super.onContentChanged(); + //执行基类函数 calcNotesCount(); } @Override + /* + * 函数功能:在activity光标发生局部变动的时候回调该函数计算便签的数量 + */ public void changeCursor(Cursor cursor) { super.changeCursor(cursor); + //执行基类函数 calcNotesCount(); } + /* + * 函数功能:计算便签数量 + * + */ private void calcNotesCount() { mNotesCount = 0; for (int i = 0; i < getCount(); i++) { + //获取总数同时遍历 Cursor c = (Cursor) getItem(i); if (c != null) { if (NoteItemData.getNoteType(c) == Notes.TYPE_NOTE) { mNotesCount++; + //若该位置不为空并且文本类型为便签就+1 } } else { Log.e(TAG, "Invalid cursor"); return; } + //否则报错 } } } diff --git a/src/Notes-master1/app/src/main/java/net/micode/notes/ui/NotesListItem.java b/src/Notes-master1/app/src/main/java/net/micode/notes/ui/NotesListItem.java index 1221e80..b89acf7 100644 --- a/src/Notes-master1/app/src/main/java/net/micode/notes/ui/NotesListItem.java +++ b/src/Notes-master1/app/src/main/java/net/micode/notes/ui/NotesListItem.java @@ -30,37 +30,43 @@ 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; + private ImageView mAlert;//闹钟图片 + private TextView mTitle; //标题 + private TextView mTime; //时间 + private TextView mCallName; // + private NoteItemData mItemData; //标签数据 + private CheckBox mCheckBox; //打钩框 + /*初始化基本信息*/ public NotesListItem(Context context) { - super(context); - inflate(context, R.layout.note_item, this); + super(context); //super()它的主要作用是调整调用父类构造函数的顺序 + inflate(context, R.layout.note_item, this);//Inflate可用于将一个xml中定义的布局控件找出来,这里的xml是r。layout + //findViewById用于从contentView中查找指定ID的View,转换出来的形式根据需要而定; 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); } - + ///根据data的属性对各个控件的属性的控制,主要是可见性Visibility,内容setText,格式setTextAppearance 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); + mCheckBox.setVisibility(View.VISIBLE); ///设置可见行为可见 + mCheckBox.setChecked(checked); ///格子打钩 } else { mCheckBox.setVisibility(View.GONE); } mItemData = data; + ///设置控件属性,一共三种情况,由data的id和父id是否与保存到文件夹的id一致来决定 if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { mCallName.setVisibility(View.GONE); mAlert.setVisibility(View.VISIBLE); + //设置该textview的style mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); + //settext为设置内容 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); @@ -69,8 +75,9 @@ public class NotesListItem extends LinearLayout { 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.setImageResource(R.drawable.clock);//图片来源的设置 mAlert.setVisibility(View.VISIBLE); } else { mAlert.setVisibility(View.GONE); @@ -78,45 +85,48 @@ public class NotesListItem extends LinearLayout { } else { mCallName.setVisibility(View.GONE); mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); - + ///设置title格式 if (data.getType() == Notes.TYPE_FOLDER) { mTitle.setText(data.getSnippet() + context.getString(R.string.format_folder_files_count, - data.getNotesCount())); + data.getNotesCount())); mAlert.setVisibility(View.GONE); } else { mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); if (data.hasAlert()) { - mAlert.setImageResource(R.drawable.clock); + mAlert.setImageResource(R.drawable.clock);///设置图片来源 mAlert.setVisibility(View.VISIBLE); } else { mAlert.setVisibility(View.GONE); } } } - mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate())); + ///设置内容,获取相关时间,从data里编辑的日期中获取 + mTime. setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate())); setBackground(data); } - + //根据data的文件属性来设置背景 private void setBackground(NoteItemData data) { int id = data.getBgColorId(); + //,若是note型文件,则4种情况,对于4种不同情况的背景来源 if (data.getType() == Notes.TYPE_NOTE) { + //单个数据并且只有一个子文件夹 if (data.isSingle() || data.isOneFollowingFolder()) { setBackgroundResource(NoteItemBgResources.getNoteBgSingleRes(id)); - } else if (data.isLast()) { + } else if (data.isLast()) {//是最后一个数据 setBackgroundResource(NoteItemBgResources.getNoteBgLastRes(id)); - } else if (data.isFirst() || data.isMultiFollowingFolder()) { + } else if (data.isFirst() || data.isMultiFollowingFolder()) {//是一个数据并有多个子文件夹 setBackgroundResource(NoteItemBgResources.getNoteBgFirstRes(id)); } else { setBackgroundResource(NoteItemBgResources.getNoteBgNormalRes(id)); } } else { + //若不是note直接调用文件夹的背景来源 setBackgroundResource(NoteItemBgResources.getFolderBgRes()); } } - public NoteItemData getItemData() { return mItemData; - } + }//返回当前便签的数据信息 } diff --git a/src/Notes-master1/app/src/main/java/net/micode/notes/ui/NotesPreferenceActivity.java b/src/Notes-master1/app/src/main/java/net/micode/notes/ui/NotesPreferenceActivity.java index 07c5f7e..75cfd3d 100644 --- a/src/Notes-master1/app/src/main/java/net/micode/notes/ui/NotesPreferenceActivity.java +++ b/src/Notes-master1/app/src/main/java/net/micode/notes/ui/NotesPreferenceActivity.java @@ -47,66 +47,92 @@ import net.micode.notes.data.Notes; import net.micode.notes.data.Notes.NoteColumns; import net.micode.notes.gtask.remote.GTaskSyncService; - +/* + *该类功能:NotesPreferenceActivity,在小米便签中主要实现的是对背景颜色和字体大小的数据储存。 + * 继承了PreferenceActivity主要功能为对系统信息和配置进行自动保存的Activity + */ public class NotesPreferenceActivity extends PreferenceActivity { 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; + //账户的hash标记 @Override + /* + *函数功能:创建一个activity,在函数里要完成所有的正常静态设置 + *参数:Bundle icicle:存放了 activity 当前的状态 + *函数实现:如下注释 + */ protected void onCreate(Bundle icicle) { + //先执行父类的创建函数 super.onCreate(icicle); /* using the app icon for navigation */ getActionBar().setDisplayHomeAsUpEnabled(true); + //给左上角图标的左边加上一个返回的图标 addPreferencesFromResource(R.xml.preferences); + //添加xml来源并显示 xml 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); + //获取listvivew,ListView的作用:用于列出所有选择 getListView().addHeaderView(header, null, true); + //在listview组件上方添加其他组件 } @Override + /* + * 函数功能:activity交互功能的实现,用于接受用户的输入 + * 函数实现:如下注释 + */ protected void onResume() { + //先执行父类 的交互实现 super.onResume(); // need to set sync account automatically if user has added a new // account if (mHasAddedAccount) { + //若用户新加了账户则自动设置同步账户 Account[] accounts = getGoogleAccounts(); + //获取google同步账户 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; } } @@ -114,58 +140,83 @@ public class NotesPreferenceActivity extends PreferenceActivity { } refreshUI(); + //刷新标签界面 } @Override + /* + * 函数功能:销毁一个activity + * 函数实现:如下注释 + */ protected void onDestroy() { if (mReceiver != null) { unregisterReceiver(mReceiver); + //注销接收器 } super.onDestroy(); + //执行父类的销毁动作 } + /* + * 函数功能:重新设置账户信息 + * 函数实现:如下注释 + */ private void loadAccountPreference() { mAccountCategory.removeAll(); - + //销毁所有的分组 Preference accountPref = new Preference(this); + //建立首选项 final String defaultAccount = getSyncAccountName(this); accountPref.setTitle(getString(R.string.preferences_account_title)); accountPref.setSummary(getString(R.string.preferences_account_summary)); + //设置首选项的大标题和小标题 accountPref.setOnPreferenceClickListener(new OnPreferenceClickListener() { public boolean onPreferenceClick(Preference preference) { + //建立监听器 if (!GTaskSyncService.isSyncing()) { if (TextUtils.isEmpty(defaultAccount)) { // the first time to set account + //若是第一次建立账户显示选择账户提示对话框 showSelectAccountAlertDialog(); } else { // if the account has already been set, we need to promp // user about the risk + //若是已经建立则显示修改对话框并进行修改操作 showChangeAccountConfirmAlertDialog(); } } else { + //若在没有同步的情况下,则在toast中显示不能修改 Toast.makeText(NotesPreferenceActivity.this, - R.string.preferences_toast_cannot_change_account, Toast.LENGTH_SHORT) + 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() { @@ -173,50 +224,67 @@ public class NotesPreferenceActivity extends PreferenceActivity { GTaskSyncService.startSync(NotesPreferenceActivity.this); } }); + //若是不同步则设置按钮显示的文本为“立即同步”以及对应监听器 } syncButton.setEnabled(!TextUtils.isEmpty(getSyncAccountName(this))); + //设置按键可用还是不可用 // set last sync time + // 设置最终同步时间 if (GTaskSyncService.isSyncing()) { + //若是在同步的情况下 lastSyncTimeView.setText(GTaskSyncService.getProgressString()); lastSyncTimeView.setVisibility(View.VISIBLE); + // 根据当前同步服务器设置时间显示框的文本以及可见性 } else { + //若是非同步情况 long lastSyncTime = getLastSyncTime(this); if (lastSyncTime != 0) { lastSyncTimeView.setText(getString(R.string.preferences_last_sync_time, DateFormat.format(getString(R.string.preferences_last_sync_time_format), lastSyncTime))); lastSyncTimeView.setVisibility(View.VISIBLE); + //则根据最后同步时间的信息来编辑时间显示框的文本内容和可见性 } else { + //若时间为空直接设置为不可见状态 lastSyncTimeView.setVisibility(View.GONE); } } } - + /* + *函数功能:刷新标签界面 + *函数实现:调用上文设置账号和设置按键两个函数来实现 + */ private void refreshUI() { loadAccountPreference(); loadSyncButton(); } + /* + * 函数功能:显示账户选择的对话框并进行账户的设置 + * 函数实现:如下注释 + */ private void showSelectAccountAlertDialog() { AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); + //创建一个新的对话框 View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null); TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title); titleTextView.setText(getString(R.string.preferences_dialog_select_account_title)); TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle); subtitleTextView.setText(getString(R.string.preferences_dialog_select_account_tips)); - + //设置标题以及子标题的内容 dialogBuilder.setCustomTitle(titleView); dialogBuilder.setPositiveButton(null, null); - + //设置对话框的自定义标题,建立一个YES的按钮 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; @@ -224,83 +292,119 @@ public class NotesPreferenceActivity extends PreferenceActivity { for (Account account : accounts) { if (TextUtils.equals(account.name, defAccount)) { checkedItem = index; + //在账户列表中查询到所需账户 } items[index++] = account.name; } dialogBuilder.setSingleChoiceItems(items, checkedItem, + //在对话框建立一个单选的复选框 new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { setSyncAccount(itemMapping[which].toString()); dialog.dismiss(); + //取消对话框 refreshUI(); } + //设置点击后执行的事件,包括检录新同步账户和刷新标签界面 }); + //建立对话框网络版的监听器 } View addAccountView = LayoutInflater.from(this).inflate(R.layout.add_account_text, null); dialogBuilder.setView(addAccountView); + //给新加账户对话框设置自定义样式 final AlertDialog dialog = dialogBuilder.show(); + //显示对话框 addAccountView.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { mHasAddedAccount = true; + //将新加账户的hash置true Intent intent = new Intent("android.settings.ADD_ACCOUNT_SETTINGS"); + //建立网络建立组件 intent.putExtra(AUTHORITIES_FILTER_KEY, new String[] { - "gmail-ls" + "gmail-ls" }); startActivityForResult(intent, -1); + //跳回上一个选项 dialog.dismiss(); } }); + //建立新加账户对话框的监听器 } + /* + * 函数功能:显示账户选择对话框和相关账户操作 + * 函数实现:如下注释 + */ private void showChangeAccountConfirmAlertDialog() { AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); - + //创建一个新的对话框 View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null); TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title); titleTextView.setText(getString(R.string.preferences_dialog_change_account_title, getSyncAccountName(this))); TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle); subtitleTextView.setText(getString(R.string.preferences_dialog_change_account_warn_msg)); + //根据同步修改的账户信息设置标题以及子标题的内容 dialogBuilder.setCustomTitle(titleView); - + //设置对话框的自定义标题 CharSequence[] menuItemArray = new CharSequence[] { getString(R.string.preferences_menu_change_account), getString(R.string.preferences_menu_remove_account), getString(R.string.preferences_menu_cancel) }; + //定义一些标记字符串 dialogBuilder.setItems(menuItemArray, new DialogInterface.OnClickListener() { + //设置对话框要显示的一个list,用于显示几个命令时,即change,remove,cancel public void onClick(DialogInterface dialog, int which) { + //按键功能,由which来决定 if (which == 0) { + //进入账户选择对话框 showSelectAccountAlertDialog(); } else if (which == 1) { + //删除账户并且跟新便签界面 removeSyncAccount(); refreshUI(); } } }); dialogBuilder.show(); + //显示对话框 } + /* + *函数功能:获取谷歌账户 + *函数实现:通过账户管理器直接获取 + */ private Account[] getGoogleAccounts() { AccountManager accountManager = AccountManager.get(this); return accountManager.getAccountsByType("com.google"); } + /* + * 函数功能:设置同步账户 + * 函数实现:如下注释: + */ private void setSyncAccount(String account) { if (!getSyncAccountName(this).equals(account)) { + //假如该账号不在同步账号列表中 SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); SharedPreferences.Editor editor = settings.edit(); + //编辑共享的首选项 if (account != null) { editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, account); } else { editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, ""); } + //将该账号加入到首选项中 + editor.commit(); + //提交修改的数据 + - // clean up last sync time setLastSyncTime(this, 0); + //将最后同步时间清零 // clean up local gtask related info new Thread(new Runnable() { @@ -311,23 +415,33 @@ public class NotesPreferenceActivity extends PreferenceActivity { 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(); + //将toast的文本信息置为“设置账户成功”并显示出来 } } - + /* + * 函数功能:删除同步账户 + * 函数实现:如下注释: + */ private void removeSyncAccount() { SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); SharedPreferences.Editor editor = settings.edit(); + //设置共享首选项 + if (settings.contains(PREFERENCE_SYNC_ACCOUNT_NAME)) { editor.remove(PREFERENCE_SYNC_ACCOUNT_NAME); + //假如当前首选项中有账户就删除 } if (settings.contains(PREFERENCE_LAST_SYNC_TIME)) { editor.remove(PREFERENCE_LAST_SYNC_TIME); + //删除当前首选项中有账户时间 } editor.commit(); + //提交更新后的数据 // clean up local gtask related info new Thread(new Runnable() { @@ -338,51 +452,79 @@ public class NotesPreferenceActivity extends PreferenceActivity { 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); } + /* + * 函数功能:接受同步信息 + * 函数实现:继承BroadcastReceiver + */ private class GTaskReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { refreshUI(); if (intent.getBooleanExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_IS_SYNCING, false)) { + //获取随广播而来的Intent中的同步服务的数据 TextView syncStatus = (TextView) findViewById(R.id.prefenerece_sync_status_textview); syncStatus.setText(intent .getStringExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_PROGRESS_MSG)); + //通过获取的数据在设置系统的状态 } } } + /* + * 函数功能:处理菜单的选项 + * 函数实现:如下注释 + * 参数:MenuItem菜单选项 + */ public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { + //根据选项的id选择,这里只有一个主页 case android.R.id.home: Intent intent = new Intent(this, NotesListActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); return true; + //在主页情况下在创建连接组件intent,发出清空的信号并开始一个相应的activity default: return false; } } } + diff --git a/src/Notes-master1/local.properties b/src/Notes-master1/local.properties index 9cd754a..6290b01 100644 --- a/src/Notes-master1/local.properties +++ b/src/Notes-master1/local.properties @@ -2,6 +2,7 @@ # as it contains information specific to your local configuration. # # Location of the SDK. This is only used by Gradle. -# -#Fri Mar 24 15:00:12 CST 2023 -sdk.dir=C\:\\Users\\86147\\AppData\\Local\\Android\\Sdk +# For customization when using a Version Control System, please read the +# header note. +#Fri Mar 31 15:41:34 CST 2023 +sdk.dir=C\:\\Users\\86158\\AppData\\Local\\Android\\Sdk -- 2.34.1 From 66f88b663ac92f8f0dab73d6c1320c6bd3a96f25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=AE=B6=E5=84=92?= <2494326140@qq.com> Date: Fri, 21 Apr 2023 14:55:08 +0800 Subject: [PATCH 3/8] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E7=94=A8=E4=BE=8B?= =?UTF-8?q?=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- model/新需求用例图.png | Bin 0 -> 17591 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 model/新需求用例图.png diff --git a/model/新需求用例图.png b/model/新需求用例图.png new file mode 100644 index 0000000000000000000000000000000000000000..f76b5050b3554eb822a6ca66e3be30bc7182e3b8 GIT binary patch literal 17591 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 Date: Fri, 21 Apr 2023 14:58:29 +0800 Subject: [PATCH 4/8] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E9=9C=80=E6=B1=82?= =?UTF-8?q?=E5=88=86=E6=9E=90=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/新功能需求文档.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 doc/新功能需求文档.txt diff --git a/doc/新功能需求文档.txt b/doc/新功能需求文档.txt new file mode 100644 index 0000000..5ab58d4 --- /dev/null +++ b/doc/新功能需求文档.txt @@ -0,0 +1,3 @@ +1.写便签的时候加入一些当时的图片或者之前的照片,用来记录当时的情况或者情绪。 +2.小米便签背景太过简单,不够美观,想要加入自己的图片用作背景,满足个性化设计。 +3.有一些便签,比如密码或者私事怕忘记想要记录下来,但是又怕被别人看到,想要创建一个私密的空间,需要密码才能进入,极大保护了用户的个人隐私。 \ No newline at end of file -- 2.34.1 From 44ea8581d670578c3eb61ac1010b4bcd0d15c36e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=AE=B6=E5=84=92?= <2494326140@qq.com> Date: Fri, 21 Apr 2023 16:52:37 +0800 Subject: [PATCH 5/8] =?UTF-8?q?=E5=88=A0=E9=99=A4=E9=9C=80=E6=B1=82?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/新功能需求文档.txt | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 doc/新功能需求文档.txt diff --git a/doc/新功能需求文档.txt b/doc/新功能需求文档.txt deleted file mode 100644 index 5ab58d4..0000000 --- a/doc/新功能需求文档.txt +++ /dev/null @@ -1,3 +0,0 @@ -1.写便签的时候加入一些当时的图片或者之前的照片,用来记录当时的情况或者情绪。 -2.小米便签背景太过简单,不够美观,想要加入自己的图片用作背景,满足个性化设计。 -3.有一些便签,比如密码或者私事怕忘记想要记录下来,但是又怕被别人看到,想要创建一个私密的空间,需要密码才能进入,极大保护了用户的个人隐私。 \ No newline at end of file -- 2.34.1 From b8ea085e61250f7174c721220b8528d70cdbc6b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=AE=B6=E5=84=92?= <2494326140@qq.com> Date: Fri, 21 Apr 2023 16:53:45 +0800 Subject: [PATCH 6/8] =?UTF-8?q?=E5=88=A0=E9=99=A4=E7=94=A8=E4=BE=8B?= =?UTF-8?q?=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- model/新需求用例图.png | Bin 17591 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 model/新需求用例图.png diff --git a/model/新需求用例图.png b/model/新需求用例图.png deleted file mode 100644 index f76b5050b3554eb822a6ca66e3be30bc7182e3b8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17591 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 Date: Thu, 27 Apr 2023 20:27:36 +0800 Subject: [PATCH 7/8] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E7=94=A8=E4=BE=8B?= =?UTF-8?q?=E6=8F=8F=E8=BF=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/插入图片用例描述文档.txt | 23 +++++++++++++++++++++++ doc/隐私便签用例描述文档.txt | 21 +++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 doc/插入图片用例描述文档.txt create mode 100644 doc/隐私便签用例描述文档.txt diff --git a/doc/插入图片用例描述文档.txt b/doc/插入图片用例描述文档.txt new file mode 100644 index 0000000..d212e50 --- /dev/null +++ b/doc/插入图片用例描述文档.txt @@ -0,0 +1,23 @@ +用例名称:编辑便签插入图片 + +业务目标:编辑便签的时候进行插入图片 + +执行者:用户 + +前置条件: +- 用户需要登录到便签应用中 +- 用户已经创建了一个新的便签或者正在编辑一个已有的便签 + +基本交互动作: +1. 在当前编辑器中,用户点击“插入图片”按钮。 +2. 系统显示一个文件选择框,让用户选择待上传的图片文件。 +3. 用户选择并确认要上传的图片文件。 +4. 系统从本地设备上传选定的图片文件,并自动将其插入到当前编辑器中光标所在的位置。 +5. 在成功添加图片后,用户可以对添加的图片进行调整大小、旋转和定位等操作。 +6. 用户完成编辑后,可以保存或分享该便签,以确保所有内容(包括插入的图片)得以保存。 + + +后置条件: +- 图片成功地添加到了当前编辑器中的光标位置 +- 该图片将在用户保存或分享该便签时一起被保存或分享 + diff --git a/doc/隐私便签用例描述文档.txt b/doc/隐私便签用例描述文档.txt new file mode 100644 index 0000000..541a125 --- /dev/null +++ b/doc/隐私便签用例描述文档.txt @@ -0,0 +1,21 @@ +用例名称:便签中隐私功能 + +业务目标:用户可将已有便签或者正在编辑的便签设置为隐私便签 + +执行者:用户 + +前置条件: +- 用户需要登录到便签应用中 +- 用户已经创建一个新的便签或者正在编辑一个已有的便签 + + +基本交互动作: +1. 在当前编辑器中,用户点击“设置”按钮。 +2. 系统显示下拉菜单,其中包括“隐私设置”选项。 +3. 用户选择“隐私设置”选项并进入该页面。 +4. 在隐私设置页面,点击 “仅我可见”:仅允许当前用户查看和编辑此便签,其他用户无法访问。 +5. 用户完成设置后,可以保存更改并离开页面。从此时起,只有自己才能查看或编辑该便签。 + +后置条件: +- 该便签的隐私设置已被成功更改,并且只有当前用户才能查看或编辑该便签。 + -- 2.34.1 From abd7c0c95c2f3ba185f62cc668f1bd7a74d6512e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=AE=B6=E5=84=92?= <2494326140@qq.com> Date: Thu, 27 Apr 2023 20:46:46 +0800 Subject: [PATCH 8/8] =?UTF-8?q?=E4=B8=8A=E4=BC=A0UML=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- model/软工.png | Bin 0 -> 55771 bytes model/软工1.png | Bin 0 -> 24765 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 model/软工.png create mode 100644 model/软工1.png diff --git a/model/软工.png b/model/软工.png new file mode 100644 index 0000000000000000000000000000000000000000..e440feadb1118f9518b63bad67bcd09315826f04 GIT binary patch literal 55771 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�%@&Zz}iy?_?ijn5q?b_W^zXbMt~P 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 G{Qm%t`7mn$ literal 0 HcmV?d00001 diff --git a/model/软工1.png b/model/软工1.png new file mode 100644 index 0000000000000000000000000000000000000000..6dfe0f19c3d4a5af034cee467cc59062b9fbe563 GIT binary patch literal 24765 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